diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-02-24 21:09:05 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-02-24 21:09:05 +0300 |
commit | c2367afbf57ebc65d5b78a743b5d6a91f0aece9f (patch) | |
tree | 165c2c54bf72ab3a3a9417d97f63ece5c9eba9f5 | |
parent | 51a9512965d86e3094968fa514e4ae8a96d38cf3 (diff) |
Add latest changes from gitlab-org/gitlab@master
77 files changed, 963 insertions, 625 deletions
diff --git a/CHANGELOG-EE.md b/CHANGELOG-EE.md index 94863cfeecb..fe723f5d1ef 100644 --- a/CHANGELOG-EE.md +++ b/CHANGELOG-EE.md @@ -1,5 +1,12 @@ Please view this file on the master branch, on stable branches it's out of date. +## 12.8.1 + +### Performance (1 change) + +- Geo - Fix query to retrieve Job Artifacts when selective sync is disabled. !25388 + + ## 12.8.0 ### Removed (1 change) diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index 426cb9d6d8c..6fb2bffa71e 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -8.22.0 +8.23.0 diff --git a/app/assets/javascripts/grafana_integration/components/grafana_integration.vue b/app/assets/javascripts/grafana_integration/components/grafana_integration.vue index 41d83e45c52..22cafd50bf3 100644 --- a/app/assets/javascripts/grafana_integration/components/grafana_integration.vue +++ b/app/assets/javascripts/grafana_integration/components/grafana_integration.vue @@ -55,9 +55,9 @@ export default { <template> <section id="grafana" class="settings no-animate js-grafana-integration"> <div class="settings-header"> - <h4 class="js-section-header"> + <h3 class="js-section-header h4"> {{ s__('GrafanaIntegration|Grafana Authentication') }} - </h4> + </h3> <gl-button class="js-settings-toggle">{{ __('Expand') }}</gl-button> <p class="js-section-sub-header"> {{ s__('GrafanaIntegration|Embed Grafana charts in GitLab issues.') }} diff --git a/app/assets/javascripts/operation_settings/components/external_dashboard.vue b/app/assets/javascripts/operation_settings/components/external_dashboard.vue index e90e27a402a..8b6467bc0f6 100644 --- a/app/assets/javascripts/operation_settings/components/external_dashboard.vue +++ b/app/assets/javascripts/operation_settings/components/external_dashboard.vue @@ -33,9 +33,9 @@ export default { <template> <section class="settings no-animate"> <div class="settings-header"> - <h4 class="js-section-header"> + <h3 class="js-section-header h4"> {{ s__('ExternalMetrics|External Dashboard') }} - </h4> + </h3> <gl-button class="js-settings-toggle">{{ __('Expand') }}</gl-button> <p class="js-section-sub-header"> {{ diff --git a/app/assets/javascripts/pages/projects/pipelines/init_pipelines.js b/app/assets/javascripts/pages/projects/pipelines/init_pipelines.js index ade6908c4a5..5fd3fce88aa 100644 --- a/app/assets/javascripts/pages/projects/pipelines/init_pipelines.js +++ b/app/assets/javascripts/pages/projects/pipelines/init_pipelines.js @@ -13,19 +13,21 @@ export default () => { }); } + const pipelineTabLink = document.querySelector('.js-pipeline-tab-link a'); const { controllerAction } = document.querySelector('.js-pipeline-container').dataset; - const pipelineStatusUrl = `${document - .querySelector('.js-pipeline-tab-link a') - .getAttribute('href')}/status.json`; - // eslint-disable-next-line no-new - new Pipelines({ - initTabs: true, - pipelineStatusUrl, - tabsOptions: { - action: controllerAction, - defaultAction: 'pipelines', - parentEl: '.pipelines-tabs', - }, - }); + if (pipelineTabLink) { + const pipelineStatusUrl = `${pipelineTabLink.getAttribute('href')}/status.json`; + + // eslint-disable-next-line no-new + new Pipelines({ + initTabs: true, + pipelineStatusUrl, + tabsOptions: { + action: controllerAction, + defaultAction: 'pipelines', + parentEl: '.pipelines-tabs', + }, + }); + } }; diff --git a/app/assets/javascripts/pages/projects/settings/integrations/show/index.js b/app/assets/javascripts/pages/projects/settings/integrations/show/index.js new file mode 100644 index 00000000000..f2cf2eb9b28 --- /dev/null +++ b/app/assets/javascripts/pages/projects/settings/integrations/show/index.js @@ -0,0 +1,6 @@ +import PersistentUserCallout from '~/persistent_user_callout'; + +document.addEventListener('DOMContentLoaded', () => { + const callout = document.querySelector('.js-webhooks-moved-alert'); + PersistentUserCallout.factory(callout); +}); diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js index d9192d3d76b..ffcb0f24cc6 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js +++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js @@ -95,14 +95,14 @@ export default () => { }, }); + const tabsElement = document.querySelector('.pipelines-tabs'); const testReportsEnabled = window.gon && window.gon.features && window.gon.features.junitPipelineView; - if (testReportsEnabled) { + if (tabsElement && testReportsEnabled) { const fetchReportsAction = 'fetchReports'; testReportsStore.dispatch('setEndpoint', dataset.testReportEndpoint); - const tabsElmement = document.querySelector('.pipelines-tabs'); const isTestTabActive = Boolean( document.querySelector('.pipelines-tabs > li > a.test-tab.active'), ); @@ -113,11 +113,11 @@ export default () => { const tabClickHandler = e => { if (e.target.className === 'test-tab') { testReportsStore.dispatch(fetchReportsAction); - tabsElmement.removeEventListener('click', tabClickHandler); + tabsElement.removeEventListener('click', tabClickHandler); } }; - tabsElmement.addEventListener('click', tabClickHandler); + tabsElement.addEventListener('click', tabClickHandler); } // eslint-disable-next-line no-new diff --git a/app/assets/javascripts/releases/components/app_index.vue b/app/assets/javascripts/releases/components/app_index.vue index f602c9fdda2..b9e80899e25 100644 --- a/app/assets/javascripts/releases/components/app_index.vue +++ b/app/assets/javascripts/releases/components/app_index.vue @@ -76,7 +76,7 @@ export default { <div v-else-if="shouldRenderSuccessState" class="js-success-state"> <release-block v-for="(release, index) in releases" - :key="release.tag_name" + :key="release.tagName" :release="release" :class="{ 'linked-card': releases.length > 1 && index !== releases.length - 1 }" /> diff --git a/app/assets/javascripts/releases/components/evidence_block.vue b/app/assets/javascripts/releases/components/evidence_block.vue index d9abd195fee..edbea33f1d1 100644 --- a/app/assets/javascripts/releases/components/evidence_block.vue +++ b/app/assets/javascripts/releases/components/evidence_block.vue @@ -25,16 +25,16 @@ export default { }, computed: { evidenceTitle() { - return sprintf(__('%{tag}-evidence.json'), { tag: this.release.tag_name }); + return sprintf(__('%{tag}-evidence.json'), { tag: this.release.tagName }); }, evidenceUrl() { - return this.release.assets && this.release.assets.evidence_file_path; + return this.release.assets && this.release.assets.evidenceFilePath; }, shortSha() { return truncateSha(this.sha); }, sha() { - return this.release.evidence_sha; + return this.release.evidenceSha; }, }, }; diff --git a/app/assets/javascripts/releases/components/release_block.vue b/app/assets/javascripts/releases/components/release_block.vue index bc3f2c3bf30..f2cc36e38bb 100644 --- a/app/assets/javascripts/releases/components/release_block.vue +++ b/app/assets/javascripts/releases/components/release_block.vue @@ -38,13 +38,13 @@ export default { }, computed: { id() { - return slugify(this.release.tag_name); + return slugify(this.release.tagName); }, assets() { return this.release.assets || {}; }, hasEvidence() { - return Boolean(this.release.evidence_sha); + return Boolean(this.release.evidenceSha); }, milestones() { return this.release.milestones || []; @@ -102,7 +102,7 @@ export default { <evidence-block v-if="hasEvidence && shouldShowEvidence" :release="release" /> <div ref="gfm-content" class="card-text prepend-top-default"> - <div v-html="release.description_html"></div> + <div v-html="release.descriptionHtml"></div> </div> </div> @@ -110,11 +110,11 @@ export default { v-if="shouldShowFooter" class="card-footer" :commit="release.commit" - :commit-path="release.commit_path" - :tag-name="release.tag_name" - :tag-path="release.tag_path" + :commit-path="release.commitPath" + :tag-name="release.tagName" + :tag-path="release.tagPath" :author="release.author" - :released-at="release.released_at" + :released-at="release.releasedAt" /> </div> </template> diff --git a/app/assets/javascripts/releases/components/release_block_author.vue b/app/assets/javascripts/releases/components/release_block_author.vue index e7075d4d67a..0432d45b2dc 100644 --- a/app/assets/javascripts/releases/components/release_block_author.vue +++ b/app/assets/javascripts/releases/components/release_block_author.vue @@ -31,8 +31,8 @@ export default { <template #user> <user-avatar-link class="prepend-left-4" - :link-href="author.web_url" - :img-src="author.avatar_url" + :link-href="author.webUrl" + :img-src="author.avatarUrl" :img-alt="userImageAltDescription" :tooltip-text="author.username" /> diff --git a/app/assets/javascripts/releases/components/release_block_footer.vue b/app/assets/javascripts/releases/components/release_block_footer.vue index 8533fc17ffd..a95fbc0b373 100644 --- a/app/assets/javascripts/releases/components/release_block_footer.vue +++ b/app/assets/javascripts/releases/components/release_block_footer.vue @@ -66,9 +66,9 @@ export default { <icon ref="commitIcon" name="commit" class="mr-1" /> <div v-gl-tooltip.bottom :title="commit.title"> <gl-link v-if="commitPath" :href="commitPath"> - {{ commit.short_id }} + {{ commit.shortId }} </gl-link> - <span v-else>{{ commit.short_id }}</span> + <span v-else>{{ commit.shortId }}</span> </div> </div> @@ -100,8 +100,8 @@ export default { <div v-if="author" class="d-flex"> <span class="text-secondary">{{ __('by') }} </span> <user-avatar-link - :link-href="author.web_url" - :img-src="author.avatar_url" + :link-href="author.webUrl" + :img-src="author.avatarUrl" :img-alt="userImageAltDescription" :tooltip-text="author.username" tooltip-placement="bottom" diff --git a/app/assets/javascripts/releases/components/release_block_header.vue b/app/assets/javascripts/releases/components/release_block_header.vue index b459418aef2..f0d3f3f8c1d 100644 --- a/app/assets/javascripts/releases/components/release_block_header.vue +++ b/app/assets/javascripts/releases/components/release_block_header.vue @@ -20,10 +20,10 @@ export default { }, computed: { editLink() { - return this.release._links?.edit_url; + return this.release.Links?.editUrl; }, selfLink() { - return this.release._links?.self; + return this.release.Links?.self; }, }, }; @@ -36,7 +36,7 @@ export default { {{ release.name }} </gl-link> <template v-else>{{ release.name }}</template> - <gl-badge v-if="release.upcoming_release" variant="warning" class="align-middle">{{ + <gl-badge v-if="release.upcomingRelease" variant="warning" class="align-middle">{{ __('Upcoming Release') }}</gl-badge> </h2> diff --git a/app/assets/javascripts/releases/components/release_block_metadata.vue b/app/assets/javascripts/releases/components/release_block_metadata.vue index f0aad594062..052e4088a5f 100644 --- a/app/assets/javascripts/releases/components/release_block_metadata.vue +++ b/app/assets/javascripts/releases/components/release_block_metadata.vue @@ -32,21 +32,21 @@ export default { return this.release.commit || {}; }, commitUrl() { - return this.release.commit_path; + return this.release.commitPath; }, hasAuthor() { return Boolean(this.author); }, releasedTimeAgo() { return sprintf(__('released %{time}'), { - time: this.timeFormatted(this.release.released_at), + time: this.timeFormatted(this.release.releasedAt), }); }, shouldRenderMilestones() { return Boolean(this.release.milestones?.length); }, tagUrl() { - return this.release.tag_path; + return this.release.tagPath; }, }, }; @@ -57,24 +57,24 @@ export default { <div class="append-right-8"> <icon name="commit" class="align-middle" /> <gl-link v-if="commitUrl" v-gl-tooltip.bottom :title="commit.title" :href="commitUrl"> - {{ commit.short_id }} + {{ commit.shortId }} </gl-link> - <span v-else v-gl-tooltip.bottom :title="commit.title">{{ commit.short_id }}</span> + <span v-else v-gl-tooltip.bottom :title="commit.title">{{ commit.shortId }}</span> </div> <div class="append-right-8"> <icon name="tag" class="align-middle" /> <gl-link v-if="tagUrl" v-gl-tooltip.bottom :title="__('Tag')" :href="tagUrl"> - {{ release.tag_name }} + {{ release.tagName }} </gl-link> - <span v-else v-gl-tooltip.bottom :title="__('Tag')">{{ release.tag_name }}</span> + <span v-else v-gl-tooltip.bottom :title="__('Tag')">{{ release.tagName }}</span> </div> <release-block-milestones v-if="shouldRenderMilestones" :milestones="release.milestones" /> <div class="append-right-4"> • - <span v-gl-tooltip.bottom :title="tooltipTitle(release.released_at)"> + <span v-gl-tooltip.bottom :title="tooltipTitle(release.releasedAt)"> {{ releasedTimeAgo }} </span> </div> diff --git a/app/assets/javascripts/releases/components/release_block_milestone_info.vue b/app/assets/javascripts/releases/components/release_block_milestone_info.vue index d3e354d6157..50accf6b679 100644 --- a/app/assets/javascripts/releases/components/release_block_milestone_info.vue +++ b/app/assets/javascripts/releases/components/release_block_milestone_info.vue @@ -40,7 +40,7 @@ export default { return Number.isNaN(percent) ? 0 : percent; }, allIssueStats() { - return this.milestones.map(m => m.issue_stats || {}); + return this.milestones.map(m => m.issueStats || {}); }, openIssuesCount() { return this.allIssueStats.map(stats => stats.opened || 0).reduce(sumReducer); @@ -109,7 +109,7 @@ export default { :key="milestone.id" v-gl-tooltip :title="milestone.description" - :href="milestone.web_url" + :href="milestone.webUrl" class="append-right-4" > {{ milestone.title }} diff --git a/app/assets/javascripts/releases/components/release_block_milestones.vue b/app/assets/javascripts/releases/components/release_block_milestones.vue index a3dff75b828..9abd3345b22 100644 --- a/app/assets/javascripts/releases/components/release_block_milestones.vue +++ b/app/assets/javascripts/releases/components/release_block_milestones.vue @@ -38,7 +38,7 @@ export default { :key="milestone.id" v-gl-tooltip :title="milestone.description" - :href="milestone.web_url" + :href="milestone.webUrl" class="mx-1 js-milestone-link" > {{ milestone.title }} diff --git a/app/assets/javascripts/releases/stores/modules/detail/actions.js b/app/assets/javascripts/releases/stores/modules/detail/actions.js index c9749582f5c..f730af1c7dc 100644 --- a/app/assets/javascripts/releases/stores/modules/detail/actions.js +++ b/app/assets/javascripts/releases/stores/modules/detail/actions.js @@ -22,8 +22,7 @@ export const fetchRelease = ({ dispatch, state }) => { return api .release(state.projectId, state.tagName) .then(({ data: release }) => { - const camelCasedRelease = convertObjectPropsToCamelCase(release, { deep: true }); - dispatch('receiveReleaseSuccess', camelCasedRelease); + dispatch('receiveReleaseSuccess', convertObjectPropsToCamelCase(release, { deep: true })); }) .catch(error => { dispatch('receiveReleaseError', error); diff --git a/app/assets/javascripts/releases/stores/modules/list/actions.js b/app/assets/javascripts/releases/stores/modules/list/actions.js index b15fb69226f..06d13890a9d 100644 --- a/app/assets/javascripts/releases/stores/modules/list/actions.js +++ b/app/assets/javascripts/releases/stores/modules/list/actions.js @@ -2,7 +2,11 @@ import * as types from './mutation_types'; import createFlash from '~/flash'; import { __ } from '~/locale'; import api from '~/api'; -import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils'; +import { + normalizeHeaders, + parseIntPagination, + convertObjectPropsToCamelCase, +} from '~/lib/utils/common_utils'; /** * Commits a mutation to update the state while the main endpoint is being requested. @@ -28,7 +32,11 @@ export const fetchReleases = ({ dispatch }, { page = '1', projectId }) => { export const receiveReleasesSuccess = ({ commit }, { data, headers }) => { const pageInfo = parseIntPagination(normalizeHeaders(headers)); - commit(types.RECEIVE_RELEASES_SUCCESS, { data, pageInfo }); + const camelCasedReleases = convertObjectPropsToCamelCase(data, { deep: true }); + commit(types.RECEIVE_RELEASES_SUCCESS, { + data: camelCasedReleases, + pageInfo, + }); }; export const receiveReleasesError = ({ commit }) => { diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb index 5fa0339f44d..097a357889f 100644 --- a/app/controllers/projects/hooks_controller.rb +++ b/app/controllers/projects/hooks_controller.rb @@ -12,7 +12,8 @@ class Projects::HooksController < Projects::ApplicationController layout "project_settings" def index - redirect_to project_settings_integrations_path(@project) + @hooks = @project.hooks + @hook = ProjectHook.new end def create @@ -24,7 +25,7 @@ class Projects::HooksController < Projects::ApplicationController flash[:alert] = @hook.errors.full_messages.join.html_safe end - redirect_to project_settings_integrations_path(@project) + redirect_to action: :index end def edit @@ -33,7 +34,7 @@ class Projects::HooksController < Projects::ApplicationController def update if hook.update(hook_params) flash[:notice] = _('Hook was successfully updated.') - redirect_to project_settings_integrations_path(@project) + redirect_to action: :index else render 'edit' end @@ -44,13 +45,13 @@ class Projects::HooksController < Projects::ApplicationController set_hook_execution_notice(result) - redirect_back_or_default(default: { action: 'index' }) + redirect_back_or_default(default: { action: :index }) end def destroy hook.destroy - redirect_to project_settings_integrations_path(@project), status: :found + redirect_to action: :index, status: :found end private diff --git a/app/controllers/projects/settings/integrations_controller.rb b/app/controllers/projects/settings/integrations_controller.rb index 0c5cf01d912..a4a53676ec7 100644 --- a/app/controllers/projects/settings/integrations_controller.rb +++ b/app/controllers/projects/settings/integrations_controller.rb @@ -9,10 +9,6 @@ module Projects layout "project_settings" def show - @hooks = @project.hooks - @hook = ProjectHook.new - - # Services @services = @project.find_or_initialize_services(exceptions: service_exceptions) end diff --git a/app/graphql/resolvers/boards_resolver.rb b/app/graphql/resolvers/boards_resolver.rb index 45c03bf0bef..eceb5b38031 100644 --- a/app/graphql/resolvers/boards_resolver.rb +++ b/app/graphql/resolvers/boards_resolver.rb @@ -4,7 +4,11 @@ module Resolvers class BoardsResolver < BaseResolver type Types::BoardType, null: true - def resolve(**args) + argument :id, GraphQL::ID_TYPE, + required: false, + description: 'Find a board by its ID' + + def resolve(id: nil) # The project or group could have been loaded in batch by `BatchLoader`. # At this point we need the `id` of the project/group to query for boards, so # make sure it's loaded and not `nil` before continuing. @@ -12,7 +16,17 @@ module Resolvers return Board.none unless parent - Boards::ListService.new(parent, context[:current_user]).execute(create_default_board: false) + Boards::ListService.new(parent, context[:current_user], board_id: extract_board_id(id)).execute(create_default_board: false) + rescue ActiveRecord::RecordNotFound + Board.none + end + + private + + def extract_board_id(gid) + return unless gid.present? + + GitlabSchema.parse_gid(gid, expected_type: ::Board).model_id end end end diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb index 9f3905000b2..699aa51e6c8 100644 --- a/app/graphql/types/group_type.rb +++ b/app/graphql/types/group_type.rb @@ -52,6 +52,12 @@ module Types null: true, description: 'Boards of the group', resolver: Resolvers::BoardsResolver + + field :board, + Types::BoardType, + null: true, + description: 'A single board of the group', + resolver: Resolvers::BoardsResolver.single end end diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index f89bd5575a3..1142459f6eb 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -185,6 +185,12 @@ module Types null: true, description: 'Boards of the project', resolver: Resolvers::BoardsResolver + + field :board, + Types::BoardType, + null: true, + description: 'A single board of the project', + resolver: Resolvers::BoardsResolver.single end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 5aff12621b5..cf9f3b9e924 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -669,6 +669,9 @@ module ProjectsHelper project_members#index integrations#show services#edit + hooks#index + hooks#edit + hook_logs#show repository#show ci_cd#show operations#show diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb index ab691916706..841599abe81 100644 --- a/app/helpers/user_callouts_helper.rb +++ b/app/helpers/user_callouts_helper.rb @@ -5,6 +5,7 @@ module UserCalloutsHelper GCP_SIGNUP_OFFER = 'gcp_signup_offer' SUGGEST_POPOVER_DISMISSED = 'suggest_popover_dismissed' TABS_POSITION_HIGHLIGHT = 'tabs_position_highlight' + WEBHOOKS_MOVED = 'webhooks_moved' def show_gke_cluster_integration_callout?(project) can?(current_user, :create_cluster, project) && @@ -33,6 +34,10 @@ module UserCalloutsHelper current_user && !user_dismissed?(TABS_POSITION_HIGHLIGHT) && !Rails.env.test? end + def show_webhooks_moved_alert? + !user_dismissed?(WEBHOOKS_MOVED) + end + private def user_dismissed?(feature_name, ignore_dismissal_earlier_than = nil) diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb index a5f68831f34..bc480b14e67 100644 --- a/app/models/hooks/project_hook.rb +++ b/app/models/hooks/project_hook.rb @@ -21,7 +21,7 @@ class ProjectHook < WebHook validates :project, presence: true def pluralized_name - _('Project Hooks') + _('Webhooks') end end diff --git a/app/models/user_callout_enums.rb b/app/models/user_callout_enums.rb index ef0b2407e23..625cbb4fe5f 100644 --- a/app/models/user_callout_enums.rb +++ b/app/models/user_callout_enums.rb @@ -15,7 +15,8 @@ module UserCalloutEnums gcp_signup_offer: 2, cluster_security_warning: 3, suggest_popover_dismissed: 9, - tabs_position_highlight: 10 + tabs_position_highlight: 10, + webhooks_moved: 13 } end end diff --git a/app/services/resource_events/change_milestone_service.rb b/app/services/resource_events/change_milestone_service.rb index bad7d002c65..dd637bcc765 100644 --- a/app/services/resource_events/change_milestone_service.rb +++ b/app/services/resource_events/change_milestone_service.rb @@ -2,49 +2,35 @@ module ResourceEvents class ChangeMilestoneService - attr_reader :resource, :user, :event_created_at, :resource_args + attr_reader :resource, :user, :event_created_at, :milestone def initialize(resource:, user:, created_at: Time.now) @resource = resource @user = user @event_created_at = created_at - - @resource_args = { - user_id: user.id, - created_at: event_created_at - } + @milestone = resource&.milestone end def execute - args = build_resource_args - - action = if milestone.nil? - :remove - else - :add - end + ResourceMilestoneEvent.create(build_resource_args) - record = args.merge(milestone_id: milestone&.id, action: ResourceMilestoneEvent.actions[action]) - - create_event(record) + resource.expire_note_etag_cache end private - def milestone - resource&.milestone - end - - def create_event(record) - ResourceMilestoneEvent.create(record) - - resource.expire_note_etag_cache - end - def build_resource_args - key = resource.class.name.underscore.foreign_key + action = milestone.blank? ? :remove : :add + key = resource.class.name.foreign_key - resource_args.merge(key => resource.id, state: ResourceMilestoneEvent.states[resource.state]) + { + user_id: user.id, + created_at: event_created_at, + milestone_id: milestone&.id, + state: ResourceMilestoneEvent.states[resource.state], + action: ResourceMilestoneEvent.actions[action], + key => resource.id + } end end end diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index b9324f0596c..160e2a7c952 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -366,10 +366,14 @@ %span = _('Members') - if can_edit - = nav_link(controller: [:integrations, :services, :hooks, :hook_logs]) do + = nav_link(controller: [:integrations, :services]) do = link_to project_settings_integrations_path(@project), title: _('Integrations'), data: { qa_selector: 'integrations_settings_link' } do %span = _('Integrations') + = nav_link(controller: [:hooks, :hook_logs]) do + = link_to project_hooks_path(@project), title: _('Webhooks'), data: { qa_selector: 'webhooks_settings_link' } do + %span + = _('Webhooks') = nav_link(controller: :repository) do = link_to project_settings_repository_path(@project), title: _('Repository') do %span diff --git a/app/views/projects/hook_logs/show.html.haml b/app/views/projects/hook_logs/show.html.haml index a8796cd7b1c..873fb4d47b7 100644 --- a/app/views/projects/hook_logs/show.html.haml +++ b/app/views/projects/hook_logs/show.html.haml @@ -1,3 +1,7 @@ +- @content_class = 'limit-container-width' unless fluid_layout +- add_to_breadcrumbs _('Webhook Settings'), namespace_project_hooks_path +- page_title _('Webhook Logs') + .row.prepend-top-default.append-bottom-default .col-lg-3 %h4.prepend-top-0 diff --git a/app/views/projects/hooks/edit.html.haml b/app/views/projects/hooks/edit.html.haml index c1fdf619eb5..f7eae802dac 100644 --- a/app/views/projects/hooks/edit.html.haml +++ b/app/views/projects/hooks/edit.html.haml @@ -1,5 +1,6 @@ -- add_to_breadcrumbs _('ProjectService|Integrations'), namespace_project_settings_integrations_path -- page_title _('Edit Project Hook') +- @content_class = 'limit-container-width' unless fluid_layout +- add_to_breadcrumbs _('Webhook Settings'), namespace_project_hooks_path +- page_title _('Webhook') .row.prepend-top-default .col-lg-3 diff --git a/app/views/projects/hooks/_index.html.haml b/app/views/projects/hooks/index.html.haml index 70f2fa0e758..169a5cc9d6b 100644 --- a/app/views/projects/hooks/_index.html.haml +++ b/app/views/projects/hooks/index.html.haml @@ -1,3 +1,7 @@ +- @content_class = 'limit-container-width' unless fluid_layout +- breadcrumb_title _('Webhook Settings') +- page_title _('Webhooks') + .row.prepend-top-default .col-lg-4 = render 'shared/web_hooks/title_and_docs', hook: @hook diff --git a/app/views/projects/services/_index.html.haml b/app/views/projects/services/_index.html.haml index 3f33d72d3ec..a4041d09415 100644 --- a/app/views/projects/services/_index.html.haml +++ b/app/views/projects/services/_index.html.haml @@ -1,8 +1,8 @@ -.row.prepend-top-default.append-bottom-default +.row.prepend-top-default .col-lg-4 %h4.prepend-top-0 - = s_("ProjectService|Project services") - %p= s_("ProjectService|Project services allow you to integrate GitLab with other applications") + = _('Integrations') + %p= _('Integrations allow you to integrate GitLab with other applications') .col-lg-8 %table.table %colgroup diff --git a/app/views/projects/services/edit.html.haml b/app/views/projects/services/edit.html.haml index e3e8a312431..ef799d2c046 100644 --- a/app/views/projects/services/edit.html.haml +++ b/app/views/projects/services/edit.html.haml @@ -1,7 +1,6 @@ - breadcrumb_title @service.title +- add_to_breadcrumbs _('Integration Settings'), project_settings_integrations_path(@project) - page_title @service.title, s_("ProjectService|Services") -- add_to_breadcrumbs(s_("ProjectService|Settings"), edit_project_path(@project)) -- add_to_breadcrumbs(s_("ProjectService|Integrations"), project_settings_integrations_path(@project)) = render 'deprecated_message' if @service.deprecation_message diff --git a/app/views/projects/settings/integrations/show.html.haml b/app/views/projects/settings/integrations/show.html.haml index 76770290f36..f603f23a2c7 100644 --- a/app/views/projects/settings/integrations/show.html.haml +++ b/app/views/projects/settings/integrations/show.html.haml @@ -1,5 +1,15 @@ - @content_class = "limit-container-width" unless fluid_layout -- breadcrumb_title _("Integrations Settings") +- breadcrumb_title _('Integration Settings') - page_title _('Integrations') -= render 'projects/hooks/index' + +- if show_webhooks_moved_alert? + .gl-alert.gl-alert-info.js-webhooks-moved-alert.prepend-top-default{ role: 'alert', data: { feature_id: UserCalloutsHelper::WEBHOOKS_MOVED, dismiss_endpoint: user_callouts_path } } + = sprite_icon('information-o', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title') + %button.js-close.gl-alert-dismiss{ type: 'button', 'aria-label' => _('Dismiss') } + = sprite_icon('close', size: 16, css_class: 'gl-icon') + .gl-alert-body + = _('Webhooks have moved. They can now be found under the Settings menu.') + .gl-alert-actions + = link_to _('Go to Webhooks'), project_hooks_path(@project), class: 'btn gl-alert-action btn-info new-gl-button' + = render 'projects/services/index' diff --git a/app/views/projects/settings/operations/_error_tracking.html.haml b/app/views/projects/settings/operations/_error_tracking.html.haml index 06b5243dfd9..393b1f9d21a 100644 --- a/app/views/projects/settings/operations/_error_tracking.html.haml +++ b/app/views/projects/settings/operations/_error_tracking.html.haml @@ -4,7 +4,7 @@ %section.settings.no-animate.js-error-tracking-settings .settings-header - %h4 + %h3{ :class => "h4" } = _('Error Tracking') %button.btn.js-settings-toggle{ type: 'button' } = _('Expand') diff --git a/app/views/projects/settings/operations/_incidents.html.haml b/app/views/projects/settings/operations/_incidents.html.haml index 756d4042613..a96a41b78c2 100644 --- a/app/views/projects/settings/operations/_incidents.html.haml +++ b/app/views/projects/settings/operations/_incidents.html.haml @@ -4,7 +4,7 @@ %section.settings.no-animate.qa-incident-management-settings .settings-header - %h4= _('Incidents') + %h3{ :class => "h4" }= _('Incidents') %button.btn.js-settings-toggle{ type: 'button' } = _('Expand') %p diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml index 9c5b9593bba..ce85cbd7f07 100644 --- a/app/views/shared/web_hooks/_form.html.haml +++ b/app/views/shared/web_hooks/_form.html.haml @@ -15,62 +15,62 @@ = form.check_box :push_events, class: 'form-check-input' = form.label :push_events, class: 'list-label form-check-label ml-1' do %strong Push events - %p.light.ml-1 + = form.text_field :push_events_branch_filter, class: 'form-control', placeholder: 'Branch name or wildcard pattern to trigger on (leave blank for all)' + %p.text-muted.ml-1 This URL will be triggered by a push to the repository - = form.text_field :push_events_branch_filter, class: 'form-control', placeholder: 'Branch name or wildcard pattern to trigger on (leave blank for all)' %li = form.check_box :tag_push_events, class: 'form-check-input' = form.label :tag_push_events, class: 'list-label form-check-label ml-1' do %strong Tag push events - %p.light.ml-1 + %p.text-muted.ml-1 This URL will be triggered when a new tag is pushed to the repository %li = form.check_box :note_events, class: 'form-check-input' = form.label :note_events, class: 'list-label form-check-label ml-1' do %strong Comments - %p.light.ml-1 + %p.text-muted.ml-1 This URL will be triggered when someone adds a comment %li = form.check_box :confidential_note_events, class: 'form-check-input' = form.label :confidential_note_events, class: 'list-label form-check-label ml-1' do %strong Confidential Comments - %p.light.ml-1 + %p.text-muted.ml-1 This URL will be triggered when someone adds a comment on a confidential issue %li = form.check_box :issues_events, class: 'form-check-input' = form.label :issues_events, class: 'list-label form-check-label ml-1' do %strong Issues events - %p.light.ml-1 + %p.text-muted.ml-1 This URL will be triggered when an issue is created/updated/merged %li = form.check_box :confidential_issues_events, class: 'form-check-input' = form.label :confidential_issues_events, class: 'list-label form-check-label ml-1' do %strong Confidential Issues events - %p.light.ml-1 + %p.text-muted.ml-1 This URL will be triggered when a confidential issue is created/updated/merged %li = form.check_box :merge_requests_events, class: 'form-check-input' = form.label :merge_requests_events, class: 'list-label form-check-label ml-1' do %strong Merge request events - %p.light.ml-1 + %p.text-muted.ml-1 This URL will be triggered when a merge request is created/updated/merged %li = form.check_box :job_events, class: 'form-check-input' = form.label :job_events, class: 'list-label form-check-label ml-1' do %strong Job events - %p.light.ml-1 + %p.text-muted.ml-1 This URL will be triggered when the job status changes %li = form.check_box :pipeline_events, class: 'form-check-input' = form.label :pipeline_events, class: 'list-label form-check-label ml-1' do %strong Pipeline events - %p.light.ml-1 + %p.text-muted.ml-1 This URL will be triggered when the pipeline status changes %li = form.check_box :wiki_page_events, class: 'form-check-input' = form.label :wiki_page_events, class: 'list-label form-check-label ml-1' do %strong Wiki Page events - %p.light.ml-1 + %p.text-muted.ml-1 This URL will be triggered when a wiki page is created/updated .form-group = form.label :enable_ssl_verification, 'SSL verification', class: 'label-bold checkbox' diff --git a/changelogs/unreleased/119429-decouple-webhooks-from-integrations-within-project-settings.yml b/changelogs/unreleased/119429-decouple-webhooks-from-integrations-within-project-settings.yml new file mode 100644 index 00000000000..32a15defb58 --- /dev/null +++ b/changelogs/unreleased/119429-decouple-webhooks-from-integrations-within-project-settings.yml @@ -0,0 +1,5 @@ +--- +title: Decouple Webhooks from Integrations within Project > Settings +merge_request: 23136 +author: +type: changed diff --git a/changelogs/unreleased/bw-board-query-by-id.yml b/changelogs/unreleased/bw-board-query-by-id.yml new file mode 100644 index 00000000000..51246a4519f --- /dev/null +++ b/changelogs/unreleased/bw-board-query-by-id.yml @@ -0,0 +1,5 @@ +--- +title: Allow group/project board to be queried by ID via GraphQL +merge_request: 24825 +author: +type: added diff --git a/changelogs/unreleased/fix-pipeline-details-invalid-buttons.yml b/changelogs/unreleased/fix-pipeline-details-invalid-buttons.yml new file mode 100644 index 00000000000..afb162f7440 --- /dev/null +++ b/changelogs/unreleased/fix-pipeline-details-invalid-buttons.yml @@ -0,0 +1,5 @@ +--- +title: Fix pipeline details page initialisation on invalid pipeline +merge_request: 25302 +author: Fabio Huser +type: fixed diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index 4c12465f5b0..816512c83bb 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -2771,6 +2771,16 @@ type Group { avatarUrl: String """ + A single board of the group + """ + board( + """ + Find a board by its ID + """ + id: ID + ): Board + + """ Boards of the group """ boards( @@ -2790,6 +2800,11 @@ type Group { first: Int """ + Find a board by its ID + """ + id: ID + + """ Returns the last _n_ elements from the list. """ last: Int @@ -5255,6 +5270,16 @@ type Project { avatarUrl: String """ + A single board of the project + """ + board( + """ + Find a board by its ID + """ + id: ID + ): Board + + """ Boards of the project """ boards( @@ -5274,6 +5299,11 @@ type Project { first: Int """ + Find a board by its ID + """ + id: ID + + """ Returns the last _n_ elements from the list. """ last: Int diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 209b6da5ab2..2053bdb9404 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -369,10 +369,43 @@ "deprecationReason": null }, { + "name": "board", + "description": "A single board of the project", + "args": [ + { + "name": "id", + "description": "Find a board by its ID", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "Board", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "boards", "description": "Boards of the project", "args": [ { + "name": "id", + "description": "Find a board by its ID", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + }, + { "name": "after", "description": "Returns the elements in the list that come after the specified cursor.", "type": { @@ -3176,10 +3209,43 @@ "deprecationReason": null }, { + "name": "board", + "description": "A single board of the group", + "args": [ + { + "name": "id", + "description": "Find a board by its ID", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "Board", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "boards", "description": "Boards of the group", "args": [ { + "name": "id", + "description": "Find a board by its ID", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + }, + { "name": "after", "description": "Returns the elements in the list that come after the specified cursor.", "type": { diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 2b17b86b1d0..978fbc35125 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -426,6 +426,7 @@ Autogenerated return type of EpicTreeReorder | --- | ---- | ---------- | | `autoDevopsEnabled` | Boolean | Indicates whether Auto DevOps is enabled for all projects within this group | | `avatarUrl` | String | Avatar URL of the group | +| `board` | Board | A single board of the group | | `description` | String | Description of the namespace | | `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` | | `emailsDisabled` | Boolean | Indicates if a group has email notifications disabled | @@ -801,6 +802,7 @@ Information about pagination in a connection. | `archived` | Boolean | Indicates the archived status of the project | | `autocloseReferencedIssues` | Boolean | Indicates if issues referenced by merge requests and commits within the default branch are closed automatically | | `avatarUrl` | String | URL to avatar image file of the project | +| `board` | Board | A single board of the project | | `containerRegistryEnabled` | Boolean | Indicates if the project stores Docker container images in a container registry | | `createdAt` | Time | Timestamp of the project creation | | `description` | String | Short description of the project | diff --git a/doc/development/contributing/issue_workflow.md b/doc/development/contributing/issue_workflow.md index 7c2a7a6560e..ec2386a2bd4 100644 --- a/doc/development/contributing/issue_workflow.md +++ b/doc/development/contributing/issue_workflow.md @@ -2,7 +2,7 @@ ## Issue tracker guidelines -**[Search the issue tracker](https://gitlab.com/gitlab-org/gitlab-foss/issues)** for similar entries before +**[Search the issue tracker](https://gitlab.com/gitlab-org/gitlab/issues)** for similar entries before submitting your own, there's a good chance somebody else had the same issue or feature proposal. Show your support with an award emoji and/or join the discussion. @@ -35,7 +35,7 @@ project. ## Labels To allow for asynchronous issue handling, we use [milestones](https://gitlab.com/groups/gitlab-org/-/milestones) -and [labels](https://gitlab.com/gitlab-org/gitlab-foss/-/labels). Leads and product managers handle most of the +and [labels](https://gitlab.com/gitlab-org/gitlab/-/labels). Leads and product managers handle most of the scheduling into milestones. Labelling is a task for everyone. Most issues will have labels for at least one of the following: @@ -53,7 +53,7 @@ Most issues will have labels for at least one of the following: - Severity: ~`S1`, `~S2`, `~S3`, `~S4` All labels, their meaning and priority are defined on the -[labels page](https://gitlab.com/gitlab-org/gitlab-foss/-/labels). +[labels page](https://gitlab.com/gitlab-org/gitlab/-/labels). If you come across an issue that has none of these, and you're allowed to set labels, you can _always_ add the team and type, and often also the subject. @@ -372,14 +372,11 @@ A recent example of this was the issue for ## Feature proposals -To create a feature proposal for CE, open an issue on the -[issue tracker of CE](https://gitlab.com/gitlab-org/gitlab-foss/issues). - -For feature proposals for EE, open an issue on the -[issue tracker of EE](https://gitlab.com/gitlab-org/gitlab/issues). +To create a feature proposal, open an issue on the +[issue tracker](https://gitlab.com/gitlab-org/gitlab/issues). In order to help track the feature proposals, we have created a -[`feature`](https://gitlab.com/gitlab-org/gitlab-foss/issues?label_name=feature) label. For the time being, users that are not members +[`feature`](https://gitlab.com/gitlab-org/gitlab/issues?label_name=feature) label. For the time being, users that are not members of the project cannot add labels. You can instead ask one of the [core team](https://about.gitlab.com/community/core-team/) members to add the label ~feature to the issue or add the following code snippet right after your description in a new line: `~feature`. @@ -441,7 +438,7 @@ addressed. ## Technical and UX debt In order to track things that can be improved in GitLab's codebase, -we use the ~"technical debt" label in [GitLab's issue tracker](https://gitlab.com/gitlab-org/gitlab-foss/issues). +we use the ~"technical debt" label in [GitLab's issue tracker](https://gitlab.com/gitlab-org/gitlab/issues). For missed user experience requirements, we use the ~"UX debt" label. These labels should be added to issues that describe things that can be improved, diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md index d31729d2b0f..7698492b29b 100644 --- a/doc/development/migration_style_guide.md +++ b/doc/development/migration_style_guide.md @@ -472,7 +472,7 @@ end ``` If a computed update is needed, the value can be wrapped in `Arel.sql`, so Arel -treats it as an SQL literal. It's also a required deprecation for [Rails 6](https://gitlab.com/gitlab-org/gitlab-foss/issues/61451). +treats it as an SQL literal. It's also a required deprecation for [Rails 6](https://gitlab.com/gitlab-org/gitlab/issues/28497). The below example is the same as the one above, but the value is set to the product of the `bar` and `baz` columns: diff --git a/doc/development/module_with_instance_variables.md b/doc/development/module_with_instance_variables.md index 1687a9f5ed4..b0eab95190b 100644 --- a/doc/development/module_with_instance_variables.md +++ b/doc/development/module_with_instance_variables.md @@ -30,11 +30,11 @@ People are saying multiple inheritance is bad. Mixing multiple modules with multiple instance variables scattering everywhere suffer from the same issue. The same applies to `ActiveSupport::Concern`. See: [Consider replacing concerns with dedicated classes & composition]( -https://gitlab.com/gitlab-org/gitlab-foss/issues/23786) +https://gitlab.com/gitlab-org/gitlab/issues/16270) There's also a similar idea: [Use decorators and interface segregation to solve overgrowing models problem]( -https://gitlab.com/gitlab-org/gitlab-foss/issues/13484) +https://gitlab.com/gitlab-org/gitlab/issues/14235) Note that `included` doesn't solve the whole issue. They define the dependencies, but they still allow each modules to talk implicitly via the diff --git a/doc/development/namespaces_storage_statistics.md b/doc/development/namespaces_storage_statistics.md index 71c9a0b96fb..f175739e55e 100644 --- a/doc/development/namespaces_storage_statistics.md +++ b/doc/development/namespaces_storage_statistics.md @@ -25,7 +25,7 @@ by [`Namespaces#with_statistics`](https://gitlab.com/gitlab-org/gitlab/blob/4ab5 Additionally, the pattern that is currently used to update the project statistics (the callback) doesn't scale adequately. It is currently one of the largest -[database queries transactions on production](https://gitlab.com/gitlab-org/gitlab-foss/issues/62488) +[database queries transactions on production](https://gitlab.com/gitlab-org/gitlab/issues/29070) that takes the most time overall. We can't add one more query to it as it will increase the transaction's length. @@ -142,7 +142,7 @@ but we refresh them through Sidekiq jobs and in different transactions: 1. Create a second table (`namespace_aggregation_schedules`) with two columns `id` and `namespace_id`. 1. Whenever the statistics of a project changes, insert a row into `namespace_aggregation_schedules` - We don't insert a new row if there's already one related to the root namespace. - - Keeping in mind the length of the transaction that involves updating `project_statistics`(<https://gitlab.com/gitlab-org/gitlab-foss/issues/62488>), the insertion should be done in a different transaction and through a Sidekiq Job. + - Keeping in mind the length of the transaction that involves updating `project_statistics`(<https://gitlab.com/gitlab-org/gitlab/issues/29070>), the insertion should be done in a different transaction and through a Sidekiq Job. 1. After inserting the row, we schedule another worker to be executed async at two different moments: - One enqueued for immediate execution and another one scheduled in `1.5h` hours. - We only schedule the jobs, if we can obtain a `1.5h` lease on Redis on a key based on the root namespace ID. @@ -162,7 +162,7 @@ This implementation has the following benefits: The only downside of this approach is that namespaces' statistics are updated up to `1.5` hours after the change is done, which means there's a time window in which the statistics are inaccurate. Because we're still not -[enforcing storage limits](https://gitlab.com/gitlab-org/gitlab-foss/issues/30421), this is not a major problem. +[enforcing storage limits](https://gitlab.com/gitlab-org/gitlab/issues/17664), this is not a major problem. ## Conclusion diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md index d93c4b3cdae..202490f2638 100644 --- a/doc/user/project/integrations/webhooks.md +++ b/doc/user/project/integrations/webhooks.md @@ -45,7 +45,7 @@ They are available **per project** for GitLab Community Edition, and **per project and per group** for **GitLab Enterprise Edition**. Navigate to the webhooks page by going to your project's -**Settings ➔ Integrations**. +**Settings ➔ Webhooks**. ## Maximum number of webhooks (per tier) diff --git a/doc/user/project/push_options.md b/doc/user/project/push_options.md index c52320ef656..7af7960404d 100644 --- a/doc/user/project/push_options.md +++ b/doc/user/project/push_options.md @@ -6,8 +6,8 @@ type: reference > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/15643) in GitLab 11.7. -GitLab supports using [Git push options](https://git-scm.com/docs/git-push#Documentation/git-push.txt--oltoptiongt) -to perform various actions at the same time as pushing changes. +GitLab supports using client-side [Git push options](https://git-scm.com/docs/git-push#Documentation/git-push.txt--oltoptiongt) +to perform various actions at the same time as pushing changes. Additionally, [Push Rules](https://docs.gitlab.com/ee/push_rules/push_rules.html) offer server-side control and enforcement options. Currently, there are push options available for: diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 430c4b932b0..f1e788c0be1 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -6997,9 +6997,6 @@ msgstr "" msgid "Edit Pipeline Schedule %{id}" msgstr "" -msgid "Edit Project Hook" -msgstr "" - msgid "Edit Release" msgstr "" @@ -9451,6 +9448,9 @@ msgstr "" msgid "Go to %{link_to_google_takeout}." msgstr "" +msgid "Go to Webhooks" +msgstr "" + msgid "Go to commits" msgstr "" @@ -10607,10 +10607,13 @@ msgstr "" msgid "Instance license" msgstr "" +msgid "Integration Settings" +msgstr "" + msgid "Integrations" msgstr "" -msgid "Integrations Settings" +msgid "Integrations allow you to integrate GitLab with other applications" msgstr "" msgid "Interested parties can even contribute by pushing commits if they want to." @@ -14780,9 +14783,6 @@ msgstr "" msgid "Project Files" msgstr "" -msgid "Project Hooks" -msgstr "" - msgid "Project ID" msgstr "" @@ -14945,30 +14945,18 @@ msgstr "" msgid "ProjectService|Comment will be posted on each event" msgstr "" -msgid "ProjectService|Integrations" -msgstr "" - msgid "ProjectService|Last edit" msgstr "" msgid "ProjectService|Perform common operations on GitLab project: %{project_name}" msgstr "" -msgid "ProjectService|Project services" -msgstr "" - -msgid "ProjectService|Project services allow you to integrate GitLab with other applications" -msgstr "" - msgid "ProjectService|Service" msgstr "" msgid "ProjectService|Services" msgstr "" -msgid "ProjectService|Settings" -msgstr "" - msgid "ProjectService|To set up this service:" msgstr "" @@ -21811,6 +21799,15 @@ msgstr "" msgid "WebIDE|Merge request" msgstr "" +msgid "Webhook" +msgstr "" + +msgid "Webhook Logs" +msgstr "" + +msgid "Webhook Settings" +msgstr "" + msgid "Webhooks" msgstr "" @@ -21820,6 +21817,9 @@ msgstr "" msgid "Webhooks allow you to trigger a URL if, for example, new code is pushed or a new issue is created. You can configure webhooks to listen for specific events like pushes, issues or merge requests. Group webhooks will apply to all projects in a group, allowing you to standardize webhook functionality across your entire group." msgstr "" +msgid "Webhooks have moved. They can now be found under the Settings menu." +msgstr "" + msgid "Wednesday" msgstr "" diff --git a/spec/controllers/projects/hooks_controller_spec.rb b/spec/controllers/projects/hooks_controller_spec.rb index f50ef2d804c..e97f602d9ab 100644 --- a/spec/controllers/projects/hooks_controller_spec.rb +++ b/spec/controllers/projects/hooks_controller_spec.rb @@ -12,12 +12,11 @@ describe Projects::HooksController do end describe '#index' do - it 'redirects to settings/integrations page' do - get(:index, params: { namespace_id: project.namespace, project_id: project }) + it 'renders index with 200 status code' do + get :index, params: { namespace_id: project.namespace, project_id: project } - expect(response).to redirect_to( - project_settings_integrations_path(project) - ) + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template(:index) end end diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb index afc203562ba..40b2aa3042e 100644 --- a/spec/factories/ci/pipelines.rb +++ b/spec/factories/ci/pipelines.rb @@ -22,6 +22,7 @@ FactoryBot.define do factory :ci_pipeline do trait :invalid do + status { :failed } yaml_errors { 'invalid YAML' } failure_reason { :config_error } end diff --git a/spec/features/projects/navbar_spec.rb b/spec/features/projects/navbar_spec.rb index 4ab6b0ce506..95a8f974261 100644 --- a/spec/features/projects/navbar_spec.rb +++ b/spec/features/projects/navbar_spec.rb @@ -88,6 +88,7 @@ describe 'Project navbar' do _('General'), _('Members'), _('Integrations'), + _('Webhooks'), _('Repository'), _('CI / CD'), _('Operations'), diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index 63c0695fe95..1c72c54f0a1 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -1077,8 +1077,6 @@ describe 'Pipeline', :js do end context 'when pipeline has configuration errors' do - include_context 'pipeline builds' - let(:pipeline) do create(:ci_pipeline, :invalid, @@ -1119,6 +1117,10 @@ describe 'Pipeline', :js do %Q{span[title="#{pipeline.present.failure_reason}"]}) end end + + it 'contains a pipeline header with title' do + expect(page).to have_content "Pipeline ##{pipeline.id}" + end end context 'when pipeline is stuck' do diff --git a/spec/features/projects/services/user_views_services_spec.rb b/spec/features/projects/services/user_views_services_spec.rb index d9358a40602..cf403a131b0 100644 --- a/spec/features/projects/services/user_views_services_spec.rb +++ b/spec/features/projects/services/user_views_services_spec.rb @@ -14,7 +14,7 @@ describe 'User views services' do end it 'shows the list of available services' do - expect(page).to have_content('Project services') + expect(page).to have_content('Integrations') expect(page).to have_content('Campfire') expect(page).to have_content('HipChat') expect(page).to have_content('Assembla') diff --git a/spec/features/projects/settings/operations_settings_spec.rb b/spec/features/projects/settings/operations_settings_spec.rb index d57401471ff..3c9102431e8 100644 --- a/spec/features/projects/settings/operations_settings_spec.rb +++ b/spec/features/projects/settings/operations_settings_spec.rb @@ -35,7 +35,7 @@ describe 'Projects > Settings > For a forked project', :js do end it 'renders form for incident management' do - expect(page).to have_selector('h4', text: 'Incidents') + expect(page).to have_selector('h3', text: 'Incidents') end it 'sets correct default values' do diff --git a/spec/features/projects/settings/integration_settings_spec.rb b/spec/features/projects/settings/webhooks_settings_spec.rb index de987b879eb..7e22117c63c 100644 --- a/spec/features/projects/settings/integration_settings_spec.rb +++ b/spec/features/projects/settings/webhooks_settings_spec.rb @@ -2,11 +2,10 @@ require 'spec_helper' -describe 'Projects > Settings > Integration settings' do +describe 'Projects > Settings > Webhook Settings' do let(:project) { create(:project) } let(:user) { create(:user) } - let(:role) { :developer } - let(:integrations_path) { project_settings_integrations_path(project) } + let(:webhooks_path) { project_hooks_path(project) } before do sign_in(user) @@ -17,7 +16,7 @@ describe 'Projects > Settings > Integration settings' do let(:role) { :developer } it 'to be disallowed to view' do - visit integrations_path + visit webhooks_path expect(page.status_code).to eq(404) end @@ -33,7 +32,7 @@ describe 'Projects > Settings > Integration settings' do it 'show list of webhooks' do hook - visit integrations_path + visit webhooks_path expect(page.status_code).to eq(200) expect(page).to have_content(hook.url) @@ -49,7 +48,7 @@ describe 'Projects > Settings > Integration settings' do end it 'create webhook' do - visit integrations_path + visit webhooks_path fill_in 'hook_url', with: url check 'Tag push events' @@ -68,7 +67,7 @@ describe 'Projects > Settings > Integration settings' do it 'edit existing webhook' do hook - visit integrations_path + visit webhooks_path click_link 'Edit' fill_in 'hook_url', with: url @@ -81,25 +80,25 @@ describe 'Projects > Settings > Integration settings' do it 'test existing webhook', :js do WebMock.stub_request(:post, hook.url) - visit integrations_path + visit webhooks_path find('.hook-test-button.dropdown').click click_link 'Push events' - expect(current_path).to eq(integrations_path) + expect(current_path).to eq(webhooks_path) end context 'delete existing webhook' do it 'from webhooks list page' do hook - visit integrations_path + visit webhooks_path expect { click_link 'Delete' }.to change(ProjectHook, :count).by(-1) end it 'from webhook edit page' do hook - visit integrations_path + visit webhooks_path click_link 'Edit' expect { click_link 'Delete' }.to change(ProjectHook, :count).by(-1) diff --git a/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap b/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap index 3d56bef4b33..09977ecc7a3 100644 --- a/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap +++ b/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap @@ -8,13 +8,13 @@ exports[`grafana integration component default state to match the default snapsh <div class="settings-header" > - <h4 - class="js-section-header" + <h3 + class="js-section-header h4" > Grafana Authentication - </h4> + </h3> <gl-button-stub class="js-settings-toggle" diff --git a/spec/frontend/monitoring/components/charts/time_series_spec.js b/spec/frontend/monitoring/components/charts/time_series_spec.js index 49f2a70a8b2..4dd376faac0 100644 --- a/spec/frontend/monitoring/components/charts/time_series_spec.js +++ b/spec/frontend/monitoring/components/charts/time_series_spec.js @@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils'; import { setTestTimeout } from 'helpers/timeout'; import { GlLink } from '@gitlab/ui'; import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts'; +import { cloneDeep } from 'lodash'; import { shallowWrapperContainsSlotText } from 'helpers/vue_test_utils_helper'; import { chartColorValues } from '~/monitoring/constants'; import { createStore } from '~/monitoring/stores'; @@ -32,501 +33,563 @@ jest.mock('~/lib/utils/icon_utils', () => ({ describe('Time series component', () => { let mockGraphData; - let makeTimeSeriesChart; let store; - beforeEach(() => { - setTestTimeout(1000); - - store = createStore(); - - store.commit( - `monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`, - metricsDashboardPayload, - ); - - store.commit(`monitoringDashboard/${types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS}`, deploymentData); + const makeTimeSeriesChart = (graphData, type) => + shallowMount(TimeSeries, { + propsData: { + graphData: { ...graphData, type }, + deploymentData: store.state.monitoringDashboard.deploymentData, + projectPath: `${mockHost}${mockProjectDir}`, + }, + store, + }); - // Mock data contains 2 panel groups, with 1 and 2 panels respectively - store.commit( - `monitoringDashboard/${types.RECEIVE_METRIC_RESULT_SUCCESS}`, - mockedQueryResultPayload, - ); + describe('With a single time series', () => { + beforeEach(() => { + setTestTimeout(1000); - // Pick the second panel group and the first panel in it - [mockGraphData] = store.state.monitoringDashboard.dashboard.panel_groups[1].panels; + store = createStore(); - makeTimeSeriesChart = (graphData, type) => - shallowMount(TimeSeries, { - propsData: { - graphData: { ...graphData, type }, - deploymentData: store.state.monitoringDashboard.deploymentData, - projectPath: `${mockHost}${mockProjectDir}`, - }, - store, - }); - }); + store.commit( + `monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`, + metricsDashboardPayload, + ); - describe('general functions', () => { - let timeSeriesChart; + store.commit(`monitoringDashboard/${types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS}`, deploymentData); - const findChart = () => timeSeriesChart.find({ ref: 'chart' }); + // Mock data contains 2 panel groups, with 1 and 2 panels respectively + store.commit( + `monitoringDashboard/${types.RECEIVE_METRIC_RESULT_SUCCESS}`, + mockedQueryResultPayload, + ); - beforeEach(done => { - timeSeriesChart = makeTimeSeriesChart(mockGraphData, 'area-chart'); - timeSeriesChart.vm.$nextTick(done); + // Pick the second panel group and the first panel in it + [mockGraphData] = store.state.monitoringDashboard.dashboard.panel_groups[1].panels; }); - it('allows user to override max value label text using prop', () => { - timeSeriesChart.setProps({ legendMaxText: 'legendMaxText' }); - - return timeSeriesChart.vm.$nextTick().then(() => { - expect(timeSeriesChart.props().legendMaxText).toBe('legendMaxText'); - }); - }); + describe('general functions', () => { + let timeSeriesChart; - it('allows user to override average value label text using prop', () => { - timeSeriesChart.setProps({ legendAverageText: 'averageText' }); + const findChart = () => timeSeriesChart.find({ ref: 'chart' }); - return timeSeriesChart.vm.$nextTick().then(() => { - expect(timeSeriesChart.props().legendAverageText).toBe('averageText'); + beforeEach(done => { + timeSeriesChart = makeTimeSeriesChart(mockGraphData, 'area-chart'); + timeSeriesChart.vm.$nextTick(done); }); - }); - describe('events', () => { - describe('datazoom', () => { - let eChartMock; - let startValue; - let endValue; - - beforeEach(done => { - eChartMock = { - handlers: {}, - getOption: () => ({ - dataZoom: [ - { - startValue, - endValue, - }, - ], - }), - off: jest.fn(eChartEvent => { - delete eChartMock.handlers[eChartEvent]; - }), - on: jest.fn((eChartEvent, fn) => { - eChartMock.handlers[eChartEvent] = fn; - }), - }; - - timeSeriesChart = makeTimeSeriesChart(mockGraphData); - timeSeriesChart.vm.$nextTick(() => { - findChart().vm.$emit('created', eChartMock); - done(); - }); - }); + it('allows user to override max value label text using prop', () => { + timeSeriesChart.setProps({ legendMaxText: 'legendMaxText' }); - it('handles datazoom event from chart', () => { - startValue = 1577836800000; // 2020-01-01T00:00:00.000Z - endValue = 1577840400000; // 2020-01-01T01:00:00.000Z - eChartMock.handlers.datazoom(); - - expect(timeSeriesChart.emitted('datazoom')).toHaveLength(1); - expect(timeSeriesChart.emitted('datazoom')[0]).toEqual([ - { - start: new Date(startValue).toISOString(), - end: new Date(endValue).toISOString(), - }, - ]); + return timeSeriesChart.vm.$nextTick().then(() => { + expect(timeSeriesChart.props().legendMaxText).toBe('legendMaxText'); }); }); - }); - describe('methods', () => { - describe('formatTooltipText', () => { - let mockDate; - let mockCommitUrl; - let generateSeriesData; + it('allows user to override average value label text using prop', () => { + timeSeriesChart.setProps({ legendAverageText: 'averageText' }); - beforeEach(() => { - mockDate = deploymentData[0].created_at; - mockCommitUrl = deploymentData[0].commitUrl; - generateSeriesData = type => ({ - seriesData: [ - { - seriesName: timeSeriesChart.vm.chartData[0].name, - componentSubType: type, - value: [mockDate, 5.55555], - dataIndex: 0, - }, - ], - value: mockDate, - }); + return timeSeriesChart.vm.$nextTick().then(() => { + expect(timeSeriesChart.props().legendAverageText).toBe('averageText'); }); + }); - it('does not throw error if data point is outside the zoom range', () => { - const seriesDataWithoutValue = generateSeriesData('line'); - expect( - timeSeriesChart.vm.formatTooltipText({ - ...seriesDataWithoutValue, - seriesData: seriesDataWithoutValue.seriesData.map(data => ({ - ...data, - value: undefined, - })), - }), - ).toBeUndefined(); - }); + describe('events', () => { + describe('datazoom', () => { + let eChartMock; + let startValue; + let endValue; - describe('when series is of line type', () => { beforeEach(done => { - timeSeriesChart.vm.formatTooltipText(generateSeriesData('line')); - timeSeriesChart.vm.$nextTick(done); - }); + eChartMock = { + handlers: {}, + getOption: () => ({ + dataZoom: [ + { + startValue, + endValue, + }, + ], + }), + off: jest.fn(eChartEvent => { + delete eChartMock.handlers[eChartEvent]; + }), + on: jest.fn((eChartEvent, fn) => { + eChartMock.handlers[eChartEvent] = fn; + }), + }; - it('formats tooltip title', () => { - expect(timeSeriesChart.vm.tooltip.title).toBe('16 Jul 2019, 10:14AM'); + timeSeriesChart = makeTimeSeriesChart(mockGraphData); + timeSeriesChart.vm.$nextTick(() => { + findChart().vm.$emit('created', eChartMock); + done(); + }); }); - it('formats tooltip content', () => { - const name = 'Pod average'; - const value = '5.556'; - const dataIndex = 0; - const seriesLabel = timeSeriesChart.find(GlChartSeriesLabel); + it('handles datazoom event from chart', () => { + startValue = 1577836800000; // 2020-01-01T00:00:00.000Z + endValue = 1577840400000; // 2020-01-01T01:00:00.000Z + eChartMock.handlers.datazoom(); - expect(seriesLabel.vm.color).toBe(''); - expect(shallowWrapperContainsSlotText(seriesLabel, 'default', name)).toBe(true); - expect(timeSeriesChart.vm.tooltip.content).toEqual([ - { name, value, dataIndex, color: undefined }, + expect(timeSeriesChart.emitted('datazoom')).toHaveLength(1); + expect(timeSeriesChart.emitted('datazoom')[0]).toEqual([ + { + start: new Date(startValue).toISOString(), + end: new Date(endValue).toISOString(), + }, ]); - - expect( - shallowWrapperContainsSlotText( - timeSeriesChart.find(GlAreaChart), - 'tooltipContent', - value, - ), - ).toBe(true); }); }); + }); + + describe('methods', () => { + describe('formatTooltipText', () => { + let mockDate; + let mockCommitUrl; + let generateSeriesData; - describe('when series is of scatter type, for deployments', () => { beforeEach(() => { - timeSeriesChart.vm.formatTooltipText(generateSeriesData('scatter')); + mockDate = deploymentData[0].created_at; + mockCommitUrl = deploymentData[0].commitUrl; + generateSeriesData = type => ({ + seriesData: [ + { + seriesName: timeSeriesChart.vm.chartData[0].name, + componentSubType: type, + value: [mockDate, 5.55555], + dataIndex: 0, + }, + ], + value: mockDate, + }); }); - it('formats tooltip title', () => { - expect(timeSeriesChart.vm.tooltip.title).toBe('16 Jul 2019, 10:14AM'); + it('does not throw error if data point is outside the zoom range', () => { + const seriesDataWithoutValue = generateSeriesData('line'); + expect( + timeSeriesChart.vm.formatTooltipText({ + ...seriesDataWithoutValue, + seriesData: seriesDataWithoutValue.seriesData.map(data => ({ + ...data, + value: undefined, + })), + }), + ).toBeUndefined(); }); - it('formats tooltip sha', () => { - expect(timeSeriesChart.vm.tooltip.sha).toBe('f5bcd1d9'); + describe('when series is of line type', () => { + beforeEach(done => { + timeSeriesChart.vm.formatTooltipText(generateSeriesData('line')); + timeSeriesChart.vm.$nextTick(done); + }); + + it('formats tooltip title', () => { + expect(timeSeriesChart.vm.tooltip.title).toBe('16 Jul 2019, 10:14AM'); + }); + + it('formats tooltip content', () => { + const name = 'Pod average'; + const value = '5.556'; + const dataIndex = 0; + const seriesLabel = timeSeriesChart.find(GlChartSeriesLabel); + + expect(seriesLabel.vm.color).toBe(''); + expect(shallowWrapperContainsSlotText(seriesLabel, 'default', name)).toBe(true); + expect(timeSeriesChart.vm.tooltip.content).toEqual([ + { name, value, dataIndex, color: undefined }, + ]); + + expect( + shallowWrapperContainsSlotText( + timeSeriesChart.find(GlAreaChart), + 'tooltipContent', + value, + ), + ).toBe(true); + }); }); - it('formats tooltip commit url', () => { - expect(timeSeriesChart.vm.tooltip.commitUrl).toBe(mockCommitUrl); + describe('when series is of scatter type, for deployments', () => { + beforeEach(() => { + timeSeriesChart.vm.formatTooltipText(generateSeriesData('scatter')); + }); + + it('formats tooltip title', () => { + expect(timeSeriesChart.vm.tooltip.title).toBe('16 Jul 2019, 10:14AM'); + }); + + it('formats tooltip sha', () => { + expect(timeSeriesChart.vm.tooltip.sha).toBe('f5bcd1d9'); + }); + + it('formats tooltip commit url', () => { + expect(timeSeriesChart.vm.tooltip.commitUrl).toBe(mockCommitUrl); + }); }); }); - }); - describe('setSvg', () => { - const mockSvgName = 'mockSvgName'; + describe('setSvg', () => { + const mockSvgName = 'mockSvgName'; - beforeEach(done => { - timeSeriesChart.vm.setSvg(mockSvgName); - timeSeriesChart.vm.$nextTick(done); - }); + beforeEach(done => { + timeSeriesChart.vm.setSvg(mockSvgName); + timeSeriesChart.vm.$nextTick(done); + }); - it('gets svg path content', () => { - expect(iconUtils.getSvgIconPathContent).toHaveBeenCalledWith(mockSvgName); - }); + it('gets svg path content', () => { + expect(iconUtils.getSvgIconPathContent).toHaveBeenCalledWith(mockSvgName); + }); - it('sets svg path content', () => { - timeSeriesChart.vm.$nextTick(() => { - expect(timeSeriesChart.vm.svgs[mockSvgName]).toBe(`path://${mockSvgPathContent}`); + it('sets svg path content', () => { + timeSeriesChart.vm.$nextTick(() => { + expect(timeSeriesChart.vm.svgs[mockSvgName]).toBe(`path://${mockSvgPathContent}`); + }); }); - }); - it('contains an svg object within an array to properly render icon', () => { - timeSeriesChart.vm.$nextTick(() => { - expect(timeSeriesChart.vm.chartOptions.dataZoom).toEqual([ - { - handleIcon: `path://${mockSvgPathContent}`, - }, - ]); + it('contains an svg object within an array to properly render icon', () => { + timeSeriesChart.vm.$nextTick(() => { + expect(timeSeriesChart.vm.chartOptions.dataZoom).toEqual([ + { + handleIcon: `path://${mockSvgPathContent}`, + }, + ]); + }); }); }); - }); - describe('onResize', () => { - const mockWidth = 233; + describe('onResize', () => { + const mockWidth = 233; - beforeEach(() => { - jest.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(() => ({ - width: mockWidth, - })); - timeSeriesChart.vm.onResize(); - }); + beforeEach(() => { + jest.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(() => ({ + width: mockWidth, + })); + timeSeriesChart.vm.onResize(); + }); - it('sets area chart width', () => { - expect(timeSeriesChart.vm.width).toBe(mockWidth); + it('sets area chart width', () => { + expect(timeSeriesChart.vm.width).toBe(mockWidth); + }); }); }); - }); - describe('computed', () => { - const getChartOptions = () => findChart().props('option'); + describe('computed', () => { + const getChartOptions = () => findChart().props('option'); - describe('chartData', () => { - let chartData; - const seriesData = () => chartData[0]; + describe('chartData', () => { + let chartData; + const seriesData = () => chartData[0]; - beforeEach(() => { - ({ chartData } = timeSeriesChart.vm); - }); + beforeEach(() => { + ({ chartData } = timeSeriesChart.vm); + }); - it('utilizes all data points', () => { - const { values } = mockGraphData.metrics[0].result[0]; + it('utilizes all data points', () => { + const { values } = mockGraphData.metrics[0].result[0]; - expect(chartData.length).toBe(1); - expect(seriesData().data.length).toBe(values.length); - }); + expect(chartData.length).toBe(1); + expect(seriesData().data.length).toBe(values.length); + }); - it('creates valid data', () => { - const { data } = seriesData(); + it('creates valid data', () => { + const { data } = seriesData(); - expect( - data.filter( - ([time, value]) => new Date(time).getTime() > 0 && typeof value === 'number', - ).length, - ).toBe(data.length); - }); + expect( + data.filter( + ([time, value]) => new Date(time).getTime() > 0 && typeof value === 'number', + ).length, + ).toBe(data.length); + }); - it('formats line width correctly', () => { - expect(chartData[0].lineStyle.width).toBe(2); - }); + it('formats line width correctly', () => { + expect(chartData[0].lineStyle.width).toBe(2); + }); - it('formats line color correctly', () => { - expect(chartData[0].lineStyle.color).toBe(chartColorValues[0]); + it('formats line color correctly', () => { + expect(chartData[0].lineStyle.color).toBe(chartColorValues[0]); + }); }); - }); - describe('chartOptions', () => { - describe('are extended by `option`', () => { - const mockSeriesName = 'Extra series 1'; - const mockOption = { - option1: 'option1', - option2: 'option2', - }; - - it('arbitrary options', () => { - timeSeriesChart.setProps({ - option: mockOption, - }); + describe('chartOptions', () => { + describe('are extended by `option`', () => { + const mockSeriesName = 'Extra series 1'; + const mockOption = { + option1: 'option1', + option2: 'option2', + }; - return timeSeriesChart.vm.$nextTick().then(() => { - expect(getChartOptions()).toEqual(expect.objectContaining(mockOption)); - }); - }); + it('arbitrary options', () => { + timeSeriesChart.setProps({ + option: mockOption, + }); - it('additional series', () => { - timeSeriesChart.setProps({ - option: { - series: [ - { - name: mockSeriesName, - }, - ], - }, + return timeSeriesChart.vm.$nextTick().then(() => { + expect(getChartOptions()).toEqual(expect.objectContaining(mockOption)); + }); }); - return timeSeriesChart.vm.$nextTick().then(() => { - const optionSeries = getChartOptions().series; + it('additional series', () => { + timeSeriesChart.setProps({ + option: { + series: [ + { + name: mockSeriesName, + }, + ], + }, + }); + + return timeSeriesChart.vm.$nextTick().then(() => { + const optionSeries = getChartOptions().series; - expect(optionSeries.length).toEqual(2); - expect(optionSeries[0].name).toEqual(mockSeriesName); + expect(optionSeries.length).toEqual(2); + expect(optionSeries[0].name).toEqual(mockSeriesName); + }); }); - }); - it('additional y axis data', () => { - const mockCustomYAxisOption = { - name: 'Custom y axis label', - axisLabel: { - formatter: jest.fn(), - }, - }; + it('additional y axis data', () => { + const mockCustomYAxisOption = { + name: 'Custom y axis label', + axisLabel: { + formatter: jest.fn(), + }, + }; - timeSeriesChart.setProps({ - option: { - yAxis: mockCustomYAxisOption, - }, + timeSeriesChart.setProps({ + option: { + yAxis: mockCustomYAxisOption, + }, + }); + + return timeSeriesChart.vm.$nextTick().then(() => { + const { yAxis } = getChartOptions(); + + expect(yAxis[0]).toMatchObject(mockCustomYAxisOption); + }); }); - return timeSeriesChart.vm.$nextTick().then(() => { - const { yAxis } = getChartOptions(); + it('additional x axis data', () => { + const mockCustomXAxisOption = { + name: 'Custom x axis label', + }; + + timeSeriesChart.setProps({ + option: { + xAxis: mockCustomXAxisOption, + }, + }); + + return timeSeriesChart.vm.$nextTick().then(() => { + const { xAxis } = getChartOptions(); - expect(yAxis[0]).toMatchObject(mockCustomYAxisOption); + expect(xAxis).toMatchObject(mockCustomXAxisOption); + }); }); }); - it('additional x axis data', () => { - const mockCustomXAxisOption = { - name: 'Custom x axis label', - }; + describe('yAxis formatter', () => { + let dataFormatter; + let deploymentFormatter; - timeSeriesChart.setProps({ - option: { - xAxis: mockCustomXAxisOption, - }, + beforeEach(() => { + dataFormatter = getChartOptions().yAxis[0].axisLabel.formatter; + deploymentFormatter = getChartOptions().yAxis[1].axisLabel.formatter; }); - return timeSeriesChart.vm.$nextTick().then(() => { - const { xAxis } = getChartOptions(); + it('rounds to 3 decimal places', () => { + expect(dataFormatter(0.88888)).toBe('0.889'); + }); - expect(xAxis).toMatchObject(mockCustomXAxisOption); + it('deployment formatter is set as is required to display a tooltip', () => { + expect(deploymentFormatter).toEqual(expect.any(Function)); }); }); }); - describe('yAxis formatter', () => { - let dataFormatter; - let deploymentFormatter; + describe('deploymentSeries', () => { + it('utilizes deployment data', () => { + expect(timeSeriesChart.vm.deploymentSeries.yAxisIndex).toBe(1); // same as deployment y axis + expect(timeSeriesChart.vm.deploymentSeries.data).toEqual([ + ['2019-07-16T10:14:25.589Z', expect.any(Number)], + ['2019-07-16T11:14:25.589Z', expect.any(Number)], + ['2019-07-16T12:14:25.589Z', expect.any(Number)], + ]); - beforeEach(() => { - dataFormatter = getChartOptions().yAxis[0].axisLabel.formatter; - deploymentFormatter = getChartOptions().yAxis[1].axisLabel.formatter; + expect(timeSeriesChart.vm.deploymentSeries.symbolSize).toBe(14); }); + }); - it('rounds to 3 decimal places', () => { - expect(dataFormatter(0.88888)).toBe('0.889'); + describe('yAxisLabel', () => { + it('y axis is configured correctly', () => { + const { yAxis } = getChartOptions(); + + expect(yAxis).toHaveLength(2); + + const [dataAxis, deploymentAxis] = yAxis; + + expect(dataAxis.boundaryGap).toHaveLength(2); + expect(dataAxis.scale).toBe(true); + + expect(deploymentAxis.show).toBe(false); + expect(deploymentAxis.min).toEqual(expect.any(Number)); + expect(deploymentAxis.max).toEqual(expect.any(Number)); + expect(deploymentAxis.min).toBeLessThan(deploymentAxis.max); }); - it('deployment formatter is set as is required to display a tooltip', () => { - expect(deploymentFormatter).toEqual(expect.any(Function)); + it('constructs a label for the chart y-axis', () => { + const { yAxis } = getChartOptions(); + + expect(yAxis[0].name).toBe('Memory Used per Pod'); }); }); }); - describe('deploymentSeries', () => { - it('utilizes deployment data', () => { - expect(timeSeriesChart.vm.deploymentSeries.yAxisIndex).toBe(1); // same as deployment y axis - expect(timeSeriesChart.vm.deploymentSeries.data).toEqual([ - ['2019-07-16T10:14:25.589Z', expect.any(Number)], - ['2019-07-16T11:14:25.589Z', expect.any(Number)], - ['2019-07-16T12:14:25.589Z', expect.any(Number)], - ]); - - expect(timeSeriesChart.vm.deploymentSeries.symbolSize).toBe(14); - }); + afterEach(() => { + timeSeriesChart.destroy(); }); + }); - describe('yAxisLabel', () => { - it('y axis is configured correctly', () => { - const { yAxis } = getChartOptions(); + describe('wrapped components', () => { + const glChartComponents = [ + { + chartType: 'area-chart', + component: GlAreaChart, + }, + { + chartType: 'line-chart', + component: GlLineChart, + }, + ]; - expect(yAxis).toHaveLength(2); + glChartComponents.forEach(dynamicComponent => { + describe(`GitLab UI: ${dynamicComponent.chartType}`, () => { + let timeSeriesAreaChart; + const findChartComponent = () => timeSeriesAreaChart.find(dynamicComponent.component); - const [dataAxis, deploymentAxis] = yAxis; + beforeEach(done => { + timeSeriesAreaChart = makeTimeSeriesChart(mockGraphData, dynamicComponent.chartType); + timeSeriesAreaChart.vm.$nextTick(done); + }); - expect(dataAxis.boundaryGap).toHaveLength(2); - expect(dataAxis.scale).toBe(true); + afterEach(() => { + timeSeriesAreaChart.destroy(); + }); - expect(deploymentAxis.show).toBe(false); - expect(deploymentAxis.min).toEqual(expect.any(Number)); - expect(deploymentAxis.max).toEqual(expect.any(Number)); - expect(deploymentAxis.min).toBeLessThan(deploymentAxis.max); - }); + it('is a Vue instance', () => { + expect(findChartComponent().exists()).toBe(true); + expect(findChartComponent().isVueInstance()).toBe(true); + }); - it('constructs a label for the chart y-axis', () => { - const { yAxis } = getChartOptions(); + it('receives data properties needed for proper chart render', () => { + const props = findChartComponent().props(); - expect(yAxis[0].name).toBe('Memory Used per Pod'); - }); - }); - }); + expect(props.data).toBe(timeSeriesAreaChart.vm.chartData); + expect(props.option).toBe(timeSeriesAreaChart.vm.chartOptions); + expect(props.formatTooltipText).toBe(timeSeriesAreaChart.vm.formatTooltipText); + expect(props.thresholds).toBe(timeSeriesAreaChart.vm.thresholds); + }); - afterEach(() => { - timeSeriesChart.destroy(); - }); - }); + it('recieves a tooltip title', done => { + const mockTitle = 'mockTitle'; + timeSeriesAreaChart.vm.tooltip.title = mockTitle; - describe('wrapped components', () => { - const glChartComponents = [ - { - chartType: 'area-chart', - component: GlAreaChart, - }, - { - chartType: 'line-chart', - component: GlLineChart, - }, - ]; + timeSeriesAreaChart.vm.$nextTick(() => { + expect( + shallowWrapperContainsSlotText(findChartComponent(), 'tooltipTitle', mockTitle), + ).toBe(true); + done(); + }); + }); - glChartComponents.forEach(dynamicComponent => { - describe(`GitLab UI: ${dynamicComponent.chartType}`, () => { - let timeSeriesAreaChart; - const findChartComponent = () => timeSeriesAreaChart.find(dynamicComponent.component); + describe('when tooltip is showing deployment data', () => { + const mockSha = 'mockSha'; + const commitUrl = `${mockProjectDir}/-/commit/${mockSha}`; - beforeEach(done => { - timeSeriesAreaChart = makeTimeSeriesChart(mockGraphData, dynamicComponent.chartType); - timeSeriesAreaChart.vm.$nextTick(done); - }); + beforeEach(done => { + timeSeriesAreaChart.vm.tooltip.isDeployment = true; + timeSeriesAreaChart.vm.$nextTick(done); + }); - afterEach(() => { - timeSeriesAreaChart.destroy(); - }); + it('uses deployment title', () => { + expect( + shallowWrapperContainsSlotText(findChartComponent(), 'tooltipTitle', 'Deployed'), + ).toBe(true); + }); - it('is a Vue instance', () => { - expect(findChartComponent().exists()).toBe(true); - expect(findChartComponent().isVueInstance()).toBe(true); - }); + it('renders clickable commit sha in tooltip content', done => { + timeSeriesAreaChart.vm.tooltip.sha = mockSha; + timeSeriesAreaChart.vm.tooltip.commitUrl = commitUrl; - it('receives data properties needed for proper chart render', () => { - const props = findChartComponent().props(); + timeSeriesAreaChart.vm.$nextTick(() => { + const commitLink = timeSeriesAreaChart.find(GlLink); - expect(props.data).toBe(timeSeriesAreaChart.vm.chartData); - expect(props.option).toBe(timeSeriesAreaChart.vm.chartOptions); - expect(props.formatTooltipText).toBe(timeSeriesAreaChart.vm.formatTooltipText); - expect(props.thresholds).toBe(timeSeriesAreaChart.vm.thresholds); + expect(shallowWrapperContainsSlotText(commitLink, 'default', mockSha)).toBe(true); + expect(commitLink.attributes('href')).toEqual(commitUrl); + done(); + }); + }); + }); }); + }); + }); + }); - it('recieves a tooltip title', done => { - const mockTitle = 'mockTitle'; - timeSeriesAreaChart.vm.tooltip.title = mockTitle; + describe('with multiple time series', () => { + const mockedResultMultipleSeries = []; + const [, , panelData] = metricsDashboardPayload.panel_groups[1].panels; - timeSeriesAreaChart.vm.$nextTick(() => { - expect( - shallowWrapperContainsSlotText(findChartComponent(), 'tooltipTitle', mockTitle), - ).toBe(true); - done(); - }); - }); + for (let i = 0; i < panelData.metrics.length; i += 1) { + mockedResultMultipleSeries.push(cloneDeep(mockedQueryResultPayload)); + mockedResultMultipleSeries[ + i + ].metricId = `${panelData.metrics[i].metric_id}_${panelData.metrics[i].id}`; + } - describe('when tooltip is showing deployment data', () => { - const mockSha = 'mockSha'; - const commitUrl = `${mockProjectDir}/-/commit/${mockSha}`; + beforeEach(() => { + setTestTimeout(1000); - beforeEach(done => { - timeSeriesAreaChart.vm.tooltip.isDeployment = true; - timeSeriesAreaChart.vm.$nextTick(done); - }); + store = createStore(); - it('uses deployment title', () => { - expect( - shallowWrapperContainsSlotText(findChartComponent(), 'tooltipTitle', 'Deployed'), - ).toBe(true); - }); + store.commit( + `monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`, + metricsDashboardPayload, + ); - it('renders clickable commit sha in tooltip content', done => { - timeSeriesAreaChart.vm.tooltip.sha = mockSha; - timeSeriesAreaChart.vm.tooltip.commitUrl = commitUrl; + store.commit(`monitoringDashboard/${types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS}`, deploymentData); - timeSeriesAreaChart.vm.$nextTick(() => { - const commitLink = timeSeriesAreaChart.find(GlLink); + // Mock data contains the metric_id for a multiple time series panel + for (let i = 0; i < panelData.metrics.length; i += 1) { + store.commit( + `monitoringDashboard/${types.RECEIVE_METRIC_RESULT_SUCCESS}`, + mockedResultMultipleSeries[i], + ); + } - expect(shallowWrapperContainsSlotText(commitLink, 'default', mockSha)).toBe(true); - expect(commitLink.attributes('href')).toEqual(commitUrl); - done(); - }); - }); + // Pick the second panel group and the second panel in it + [, , mockGraphData] = store.state.monitoringDashboard.dashboard.panel_groups[1].panels; + }); + + describe('General functions', () => { + let timeSeriesChart; + + beforeEach(done => { + timeSeriesChart = makeTimeSeriesChart(mockGraphData, 'area-chart'); + timeSeriesChart.vm.$nextTick(done); + }); + + describe('computed', () => { + let chartData; + + beforeEach(() => { + ({ chartData } = timeSeriesChart.vm); + }); + + it('should contain different colors for each time series', () => { + expect(chartData[0].lineStyle.color).toBe('#1f78d1'); + expect(chartData[1].lineStyle.color).toBe('#1aaa55'); + expect(chartData[2].lineStyle.color).toBe('#fc9403'); + expect(chartData[3].lineStyle.color).toBe('#6d49cb'); + expect(chartData[4].lineStyle.color).toBe('#1f78d1'); }); }); }); diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js index 15c82242262..fcf70a1af63 100644 --- a/spec/frontend/monitoring/components/dashboard_spec.js +++ b/spec/frontend/monitoring/components/dashboard_spec.js @@ -22,7 +22,7 @@ import { } from '../mock_data'; const localVue = createLocalVue(); -const expectedPanelCount = 2; +const expectedPanelCount = 3; describe('Dashboard', () => { let store; diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js index 4d83933f2b8..bad3962dd8f 100644 --- a/spec/frontend/monitoring/mock_data.js +++ b/spec/frontend/monitoring/mock_data.js @@ -513,6 +513,48 @@ export const metricsDashboardPayload = { }, ], }, + { + title: 'memories', + type: 'area-chart', + y_label: 'memories', + metrics: [ + { + id: 'metric_of_ages_1000', + label: 'memory_1000', + unit: 'count', + prometheus_endpoint_path: '/root', + metric_id: 20, + }, + { + id: 'metric_of_ages_1001', + label: 'memory_1000', + unit: 'count', + prometheus_endpoint_path: '/root', + metric_id: 21, + }, + { + id: 'metric_of_ages_1002', + label: 'memory_1000', + unit: 'count', + prometheus_endpoint_path: '/root', + metric_id: 22, + }, + { + id: 'metric_of_ages_1003', + label: 'memory_1000', + unit: 'count', + prometheus_endpoint_path: '/root', + metric_id: 23, + }, + { + id: 'metric_of_ages_1004', + label: 'memory_1004', + unit: 'count', + prometheus_endpoint_path: '/root', + metric_id: 24, + }, + ], + }, ], }, ], diff --git a/spec/frontend/monitoring/store/mutations_spec.js b/spec/frontend/monitoring/store/mutations_spec.js index d9aebafb9ec..3fb7b84fae5 100644 --- a/spec/frontend/monitoring/store/mutations_spec.js +++ b/spec/frontend/monitoring/store/mutations_spec.js @@ -50,9 +50,10 @@ describe('Monitoring mutations', () => { expect(groups[0].panels).toHaveLength(1); expect(groups[0].panels[0].metrics).toHaveLength(1); - expect(groups[1].panels).toHaveLength(2); + expect(groups[1].panels).toHaveLength(3); expect(groups[1].panels[0].metrics).toHaveLength(1); expect(groups[1].panels[1].metrics).toHaveLength(1); + expect(groups[1].panels[2].metrics).toHaveLength(5); }); it('assigns metrics a metric id', () => { mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, payload); diff --git a/spec/frontend/releases/components/app_edit_spec.js b/spec/frontend/releases/components/app_edit_spec.js index cb940facbd6..b2dbb8cc435 100644 --- a/spec/frontend/releases/components/app_edit_spec.js +++ b/spec/frontend/releases/components/app_edit_spec.js @@ -13,7 +13,7 @@ describe('Release edit component', () => { beforeEach(() => { gon.api_version = 'v4'; - releaseClone = JSON.parse(JSON.stringify(convertObjectPropsToCamelCase(release))); + releaseClone = convertObjectPropsToCamelCase(release, { deep: true }); state = { release: releaseClone, diff --git a/spec/frontend/releases/components/evidence_block_spec.js b/spec/frontend/releases/components/evidence_block_spec.js index 7b896575965..fb62f4a3bfe 100644 --- a/spec/frontend/releases/components/evidence_block_spec.js +++ b/spec/frontend/releases/components/evidence_block_spec.js @@ -2,12 +2,14 @@ import { mount } from '@vue/test-utils'; import { GlLink } from '@gitlab/ui'; import { truncateSha } from '~/lib/utils/text_utility'; import Icon from '~/vue_shared/components/icon.vue'; -import { release } from '../mock_data'; +import { release as originalRelease } from '../mock_data'; import EvidenceBlock from '~/releases/components/evidence_block.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; describe('Evidence Block', () => { let wrapper; + let release; const factory = (options = {}) => { wrapper = mount(EvidenceBlock, { @@ -16,6 +18,8 @@ describe('Evidence Block', () => { }; beforeEach(() => { + release = convertObjectPropsToCamelCase(originalRelease, { deep: true }); + factory({ propsData: { release, @@ -32,7 +36,7 @@ describe('Evidence Block', () => { }); it('renders the title for the dowload link', () => { - expect(wrapper.find(GlLink).text()).toBe(`${release.tag_name}-evidence.json`); + expect(wrapper.find(GlLink).text()).toBe(`${release.tagName}-evidence.json`); }); it('renders the correct hover text for the download', () => { @@ -40,19 +44,19 @@ describe('Evidence Block', () => { }); it('renders the correct file link for download', () => { - expect(wrapper.find(GlLink).attributes().download).toBe(`${release.tag_name}-evidence.json`); + expect(wrapper.find(GlLink).attributes().download).toBe(`${release.tagName}-evidence.json`); }); describe('sha text', () => { it('renders the short sha initially', () => { - expect(wrapper.find('.js-short').text()).toBe(truncateSha(release.evidence_sha)); + expect(wrapper.find('.js-short').text()).toBe(truncateSha(release.evidenceSha)); }); it('renders the long sha after expansion', () => { wrapper.find('.js-text-expander-prepend').trigger('click'); return wrapper.vm.$nextTick().then(() => { - expect(wrapper.find('.js-expanded').text()).toBe(release.evidence_sha); + expect(wrapper.find('.js-expanded').text()).toBe(release.evidenceSha); }); }); }); @@ -68,7 +72,7 @@ describe('Evidence Block', () => { it('copies the sha', () => { expect(wrapper.find(ClipboardButton).attributes('data-clipboard-text')).toBe( - release.evidence_sha, + release.evidenceSha, ); }); }); diff --git a/spec/frontend/releases/components/release_block_footer_spec.js b/spec/frontend/releases/components/release_block_footer_spec.js index 4125d5c7e74..c63637c4cae 100644 --- a/spec/frontend/releases/components/release_block_footer_spec.js +++ b/spec/frontend/releases/components/release_block_footer_spec.js @@ -24,7 +24,7 @@ describe('Release block footer', () => { const factory = (props = {}) => { wrapper = mount(ReleaseBlockFooter, { propsData: { - ...convertObjectPropsToCamelCase(releaseClone), + ...convertObjectPropsToCamelCase(releaseClone, { deep: true }), ...props, }, }); diff --git a/spec/frontend/releases/components/release_block_header_spec.js b/spec/frontend/releases/components/release_block_header_spec.js index 157df15ff3c..78adad13f69 100644 --- a/spec/frontend/releases/components/release_block_header_spec.js +++ b/spec/frontend/releases/components/release_block_header_spec.js @@ -1,5 +1,5 @@ import { shallowMount } from '@vue/test-utils'; -import { cloneDeep, merge } from 'lodash'; +import { merge } from 'lodash'; import { GlLink } from '@gitlab/ui'; import ReleaseBlockHeader from '~/releases/components/release_block_header.vue'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; @@ -18,9 +18,7 @@ describe('Release block header', () => { }; beforeEach(() => { - release = convertObjectPropsToCamelCase(cloneDeep(originalRelease), { - ignoreKeyNames: ['_links'], - }); + release = convertObjectPropsToCamelCase(originalRelease, { deep: true }); }); afterEach(() => { @@ -39,13 +37,13 @@ describe('Release block header', () => { const link = findHeaderLink(); expect(link.text()).toBe(release.name); - expect(link.attributes('href')).toBe(release._links.self); + expect(link.attributes('href')).toBe(release.Links.self); }); }); describe('when _links.self is missing', () => { beforeEach(() => { - factory({ _links: { self: null } }); + factory({ Links: { self: null } }); }); it('renders the title as text', () => { diff --git a/spec/frontend/releases/components/release_block_milestone_info_spec.js b/spec/frontend/releases/components/release_block_milestone_info_spec.js index 5a3204a4ce2..10f5db96b31 100644 --- a/spec/frontend/releases/components/release_block_milestone_info_spec.js +++ b/spec/frontend/releases/components/release_block_milestone_info_spec.js @@ -2,12 +2,13 @@ import { mount } from '@vue/test-utils'; import { GlProgressBar, GlLink, GlBadge, GlButton } from '@gitlab/ui'; import { trimText } from 'helpers/text_helper'; import ReleaseBlockMilestoneInfo from '~/releases/components/release_block_milestone_info.vue'; -import { milestones } from '../mock_data'; +import { milestones as originalMilestones } from '../mock_data'; import { MAX_MILESTONES_TO_DISPLAY } from '~/releases/constants'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; describe('Release block milestone info', () => { let wrapper; - let milestonesClone; + let milestones; const factory = milestonesProp => { wrapper = mount(ReleaseBlockMilestoneInfo, { @@ -20,7 +21,7 @@ describe('Release block milestone info', () => { }; beforeEach(() => { - milestonesClone = JSON.parse(JSON.stringify(milestones)); + milestones = convertObjectPropsToCamelCase(originalMilestones, { deep: true }); }); afterEach(() => { @@ -32,7 +33,7 @@ describe('Release block milestone info', () => { const issuesContainer = () => wrapper.find('.js-issues-container'); describe('with default props', () => { - beforeEach(() => factory(milestonesClone)); + beforeEach(() => factory(milestones)); it('renders the correct percentage', () => { expect(milestoneProgressBarContainer().text()).toContain('41% complete'); @@ -53,13 +54,13 @@ describe('Release block milestone info', () => { it('renders a list of links to all associated milestones', () => { expect(trimText(milestoneListContainer().text())).toContain('Milestones 13.6 • 13.5'); - milestonesClone.forEach((m, i) => { + milestones.forEach((m, i) => { const milestoneLink = milestoneListContainer() .findAll(GlLink) .at(i); expect(milestoneLink.text()).toBe(m.title); - expect(milestoneLink.attributes('href')).toBe(m.web_url); + expect(milestoneLink.attributes('href')).toBe(m.webUrl); expect(milestoneLink.attributes('title')).toBe(m.description); }); }); @@ -84,7 +85,7 @@ describe('Release block milestone info', () => { beforeEach(() => { lotsOfMilestones = []; - const template = milestonesClone[0]; + const template = milestones[0]; for (let i = 0; i < MAX_MILESTONES_TO_DISPLAY + 10; i += 1) { lotsOfMilestones.push({ @@ -148,16 +149,16 @@ describe('Release block milestone info', () => { /** Ensures we don't have any issues with dividing by zero when computing percentages */ describe('when all issue counts are zero', () => { beforeEach(() => { - milestonesClone = milestonesClone.map(m => ({ + milestones = milestones.map(m => ({ ...m, - issue_stats: { - ...m.issue_stats, + issueStats: { + ...m.issueStats, opened: 0, closed: 0, }, })); - return factory(milestonesClone); + return factory(milestones); }); expectAllZeros(); @@ -165,12 +166,12 @@ describe('Release block milestone info', () => { describe('if the API response is missing the "issue_stats" property', () => { beforeEach(() => { - milestonesClone = milestonesClone.map(m => ({ + milestones = milestones.map(m => ({ ...m, - issue_stats: undefined, + issueStats: undefined, })); - return factory(milestonesClone); + return factory(milestones); }); expectAllZeros(); diff --git a/spec/frontend/releases/components/release_block_spec.js b/spec/frontend/releases/components/release_block_spec.js index aba1b8aff41..5d365b77560 100644 --- a/spec/frontend/releases/components/release_block_spec.js +++ b/spec/frontend/releases/components/release_block_spec.js @@ -5,10 +5,12 @@ import EvidenceBlock from '~/releases/components/evidence_block.vue'; import ReleaseBlock from '~/releases/components/release_block.vue'; import ReleaseBlockFooter from '~/releases/components/release_block_footer.vue'; import timeagoMixin from '~/vue_shared/mixins/timeago'; -import { release } from '../mock_data'; +import { release as originalRelease } from '../mock_data'; import Icon from '~/vue_shared/components/icon.vue'; import { scrollToElement } from '~/lib/utils/common_utils'; +const { convertObjectPropsToCamelCase } = jest.requireActual('~/lib/utils/common_utils'); + let mockLocationHash; jest.mock('~/lib/utils/url_utility', () => ({ __esModule: true, @@ -22,7 +24,7 @@ jest.mock('~/lib/utils/common_utils', () => ({ describe('Release block', () => { let wrapper; - let releaseClone; + let release; const factory = (releaseProp, featureFlags = {}) => { wrapper = mount(ReleaseBlock, { @@ -45,7 +47,7 @@ describe('Release block', () => { beforeEach(() => { jest.spyOn($.fn, 'renderGFM'); - releaseClone = JSON.parse(JSON.stringify(release)); + release = convertObjectPropsToCamelCase(originalRelease, { deep: true }); }); afterEach(() => { @@ -61,7 +63,7 @@ describe('Release block', () => { it('renders an edit button that links to the "Edit release" page', () => { expect(editButton().exists()).toBe(true); - expect(editButton().attributes('href')).toBe(release._links.edit_url); + expect(editButton().attributes('href')).toBe(release.Links.editUrl); }); it('renders release name', () => { @@ -74,7 +76,7 @@ describe('Release block', () => { }); it('renders release date', () => { - expect(wrapper.text()).toContain(timeagoMixin.methods.timeFormatted(release.released_at)); + expect(wrapper.text()).toContain(timeagoMixin.methods.timeFormatted(release.releasedAt)); }); it('renders number of assets provided', () => { @@ -129,72 +131,72 @@ describe('Release block', () => { }); it('renders commit sha', () => { - releaseClone.commit_path = '/commit/example'; + release.commitPath = '/commit/example'; - return factory(releaseClone).then(() => { - expect(wrapper.text()).toContain(release.commit.short_id); + return factory(release).then(() => { + expect(wrapper.text()).toContain(release.commit.shortId); expect(wrapper.find('a[href="/commit/example"]').exists()).toBe(true); }); }); it('renders tag name', () => { - releaseClone.tag_path = '/tag/example'; + release.tagPath = '/tag/example'; - return factory(releaseClone).then(() => { - expect(wrapper.text()).toContain(release.tag_name); + return factory(release).then(() => { + expect(wrapper.text()).toContain(release.tagName); expect(wrapper.find('a[href="/tag/example"]').exists()).toBe(true); }); }); - it("does not render an edit button if release._links.edit_url isn't a string", () => { - delete releaseClone._links; + it("does not render an edit button if release.Links.editUrl isn't a string", () => { + delete release.Links; - return factory(releaseClone).then(() => { + return factory(release).then(() => { expect(editButton().exists()).toBe(false); }); }); it('does not render the milestone list if no milestones are associated to the release', () => { - delete releaseClone.milestones; + delete release.milestones; - return factory(releaseClone).then(() => { + return factory(release).then(() => { expect(milestoneListLabel().exists()).toBe(false); }); }); it('renders upcoming release badge', () => { - releaseClone.upcoming_release = true; + release.upcomingRelease = true; - return factory(releaseClone).then(() => { + return factory(release).then(() => { expect(wrapper.text()).toContain('Upcoming Release'); }); }); - it('slugifies the tag_name before setting it as the elements ID', () => { - releaseClone.tag_name = 'a dangerous tag name <script>alert("hello")</script>'; + it('slugifies the tagName before setting it as the elements ID', () => { + release.tagName = 'a dangerous tag name <script>alert("hello")</script>'; - return factory(releaseClone).then(() => { + return factory(release).then(() => { expect(wrapper.attributes().id).toBe('a-dangerous-tag-name-script-alert-hello-script'); }); }); describe('evidence block', () => { it('renders the evidence block when the evidence is available and the feature flag is true', () => - factory(releaseClone, { releaseEvidenceCollection: true }).then(() => + factory(release, { releaseEvidenceCollection: true }).then(() => expect(wrapper.find(EvidenceBlock).exists()).toBe(true), )); it('does not render the evidence block when the evidence is available but the feature flag is false', () => - factory(releaseClone, { releaseEvidenceCollection: true }).then(() => + factory(release, { releaseEvidenceCollection: true }).then(() => expect(wrapper.find(EvidenceBlock).exists()).toBe(true), )); it('does not render the evidence block when there is no evidence', () => { - releaseClone.evidence_sha = null; + release.evidenceSha = null; - return factory(releaseClone).then(() => { + return factory(release).then(() => { expect(wrapper.find(EvidenceBlock).exists()).toBe(false); }); }); @@ -222,7 +224,7 @@ describe('Release block', () => { }); it("attempts to scroll itself into view if the anchor tag matches the release's tag name", () => { - mockLocationHash = release.tag_name; + mockLocationHash = release.tagName; return factory(release).then(() => { expect(scrollToElement).toHaveBeenCalledTimes(1); @@ -231,7 +233,7 @@ describe('Release block', () => { }); it('renders with a light blue background if it is the target of the anchor', () => { - mockLocationHash = release.tag_name; + mockLocationHash = release.tagName; return factory(release).then(() => { expect(hasTargetBlueBackground()).toBe(true); @@ -275,16 +277,16 @@ describe('Release block', () => { expect(milestoneLink.text()).toBe(milestone.title); - expect(milestoneLink.attributes('href')).toBe(milestone.web_url); + expect(milestoneLink.attributes('href')).toBe(milestone.webUrl); expect(milestoneLink.attributes('title')).toBe(milestone.description); }); }); it('renders the label as "Milestone" if only a single milestone is passed in', () => { - releaseClone.milestones = releaseClone.milestones.slice(0, 1); + release.milestones = release.milestones.slice(0, 1); - return factory(releaseClone, { releaseIssueSummary: false }).then(() => { + return factory(release, { releaseIssueSummary: false }).then(() => { expect( milestoneListLabel() .find('.js-label-text') diff --git a/spec/graphql/resolvers/boards_resolver_spec.rb b/spec/graphql/resolvers/boards_resolver_spec.rb index ab77dfa8fc3..02d6f808118 100644 --- a/spec/graphql/resolvers/boards_resolver_spec.rb +++ b/spec/graphql/resolvers/boards_resolver_spec.rb @@ -45,6 +45,21 @@ describe Resolvers::BoardsResolver do expect(resolve_boards).to eq [board1] end end + + context 'when querying for a single board' do + let(:board1) { create(:board, name: 'One', resource_parent: board_parent) } + + it 'returns specified board' do + expect(resolve_boards(args: { id: global_id_of(board1) })).to eq [board1] + end + + it 'returns nil if board not found' do + outside_parent = create(board_parent.class.underscore.to_sym) + outside_board = create(:board, name: 'outside board', resource_parent: outside_parent) + + expect(resolve_boards(args: { id: global_id_of(outside_board) })).to eq Board.none + end + end end describe '#resolve' do diff --git a/spec/javascripts/filtered_search/visual_token_value_spec.js b/spec/javascripts/filtered_search/visual_token_value_spec.js index a039e280028..4469ade1874 100644 --- a/spec/javascripts/filtered_search/visual_token_value_spec.js +++ b/spec/javascripts/filtered_search/visual_token_value_spec.js @@ -1,4 +1,4 @@ -import _ from 'underscore'; +import { escape as esc } from 'lodash'; import VisualTokenValue from '~/filtered_search/visual_token_value'; import AjaxCache from '~/lib/utils/ajax_cache'; import UsersCache from '~/lib/utils/users_cache'; @@ -121,7 +121,7 @@ describe('Filtered Search Visual Tokens', () => { expect(tokenValueElement.innerText.trim()).toBe(dummyUser.name); tokenValueElement.querySelector('.avatar').remove(); - expect(tokenValueElement.innerHTML.trim()).toBe(_.escape(dummyUser.name)); + expect(tokenValueElement.innerHTML.trim()).toBe(esc(dummyUser.name)); }) .then(done) .catch(done.fail); diff --git a/spec/javascripts/releases/components/app_index_spec.js b/spec/javascripts/releases/components/app_index_spec.js index bcf062f357a..962fe9c448d 100644 --- a/spec/javascripts/releases/components/app_index_spec.js +++ b/spec/javascripts/releases/components/app_index_spec.js @@ -12,6 +12,7 @@ import { release, releases, } from '../mock_data'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; describe('Releases App ', () => { const Component = Vue.extend(app); @@ -27,7 +28,10 @@ describe('Releases App ', () => { beforeEach(() => { store = createStore({ list: listModule }); - releasesPagination = _.range(21).map(index => ({ ...release, tag_name: `${index}.00` })); + releasesPagination = _.range(21).map(index => ({ + ...convertObjectPropsToCamelCase(release, { deep: true }), + tagName: `${index}.00`, + })); }); afterEach(() => { diff --git a/spec/javascripts/releases/stores/modules/list/actions_spec.js b/spec/javascripts/releases/stores/modules/list/actions_spec.js index 037c9d8d54a..bf85e18997b 100644 --- a/spec/javascripts/releases/stores/modules/list/actions_spec.js +++ b/spec/javascripts/releases/stores/modules/list/actions_spec.js @@ -8,16 +8,18 @@ import { import state from '~/releases/stores/modules/list/state'; import * as types from '~/releases/stores/modules/list/mutation_types'; import api from '~/api'; -import { parseIntPagination } from '~/lib/utils/common_utils'; -import { pageInfoHeadersWithoutPagination, releases } from '../../../mock_data'; +import { parseIntPagination, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import { pageInfoHeadersWithoutPagination, releases as originalReleases } from '../../../mock_data'; describe('Releases State actions', () => { let mockedState; let pageInfo; + let releases; beforeEach(() => { mockedState = state(); pageInfo = parseIntPagination(pageInfoHeadersWithoutPagination); + releases = convertObjectPropsToCamelCase(originalReleases, { deep: true }); }); describe('requestReleases', () => { diff --git a/spec/requests/api/graphql/boards/boards_query_spec.rb b/spec/requests/api/graphql/boards/boards_query_spec.rb index d0a2d0fffaf..a17554aba21 100644 --- a/spec/requests/api/graphql/boards/boards_query_spec.rb +++ b/spec/requests/api/graphql/boards/boards_query_spec.rb @@ -9,14 +9,12 @@ describe 'get list of boards' do describe 'for a project' do let(:board_parent) { create(:project, :repository, :private) } - let(:boards_data) { graphql_data['project']['boards']['edges'] } it_behaves_like 'group and project boards query' end describe 'for a group' do let(:board_parent) { create(:group, :private) } - let(:boards_data) { graphql_data['group']['boards']['edges'] } before do allow(board_parent).to receive(:multiple_issue_boards_available?).and_return(false) diff --git a/spec/support/shared_contexts/requests/api/graphql/group_and_project_boards_query_shared_context.rb b/spec/support/shared_contexts/requests/api/graphql/group_and_project_boards_query_shared_context.rb index e744c3d0abb..ca77c68c130 100644 --- a/spec/support/shared_contexts/requests/api/graphql/group_and_project_boards_query_shared_context.rb +++ b/spec/support/shared_contexts/requests/api/graphql/group_and_project_boards_query_shared_context.rb @@ -5,6 +5,8 @@ RSpec.shared_context 'group and project boards query context' do let(:current_user) { user } let(:params) { '' } let(:board_parent_type) { board_parent.class.to_s.downcase } + let(:boards_data) { graphql_data[board_parent_type]['boards']['edges'] } + let(:board_data) { graphql_data[board_parent_type]['board'] } let(:start_cursor) { graphql_data[board_parent_type]['boards']['pageInfo']['startCursor'] } let(:end_cursor) { graphql_data[board_parent_type]['boards']['pageInfo']['endCursor'] } @@ -28,6 +30,18 @@ RSpec.shared_context 'group and project boards query context' do ) end + def query_single_board(board_params = params) + graphql_query_for( + board_parent_type, + { 'fullPath' => board_parent.full_path }, + <<~BOARD + board(#{board_params}) { + #{all_graphql_fields_for('board'.classify)} + } + BOARD + ) + end + def grab_names(data = boards_data) data.map do |board| board.dig('node', 'name') diff --git a/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb index 6044fefd2f7..90ac60a6fe7 100644 --- a/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb @@ -89,4 +89,24 @@ RSpec.shared_examples 'group and project boards query' do end end end + + context 'when querying for a single board' do + before do + board_parent.add_reporter(current_user) + end + + it_behaves_like 'a working graphql query' do + before do + post_graphql(query_single_board, current_user: current_user) + end + end + + it 'finds the correct board' do + board = create(:board, resource_parent: board_parent, name: 'A') + + post_graphql(query_single_board("id: \"#{global_id_of(board)}\""), current_user: current_user) + + expect(board_data['name']).to eq board.name + end + end end diff --git a/spec/views/profiles/preferences/show.html.haml_spec.rb b/spec/views/profiles/preferences/show.html.haml_spec.rb index e3eb822b045..16e4bd9c6d1 100644 --- a/spec/views/profiles/preferences/show.html.haml_spec.rb +++ b/spec/views/profiles/preferences/show.html.haml_spec.rb @@ -56,7 +56,7 @@ describe 'profiles/preferences/show' do expect(rendered).not_to have_sourcegraph_field end - it 'does not display integrations settings' do + it 'does not display Integration Settings' do expect(rendered).not_to have_integrations_section end end |