diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-22 15:10:55 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-06-22 15:10:55 +0300 |
commit | a27c21b5af4f60ff11706f85289065272c616a0c (patch) | |
tree | 73ac1a10bfe1bd6a4c486f58fb663b262580627b | |
parent | 0b54de365bc766260093c9584c7470aff96d14fd (diff) |
Add latest changes from gitlab-org/gitlab@master
73 files changed, 544 insertions, 846 deletions
diff --git a/.gitlab/issue_templates/Pipeline Security issue implementation b/.gitlab/issue_templates/Pipeline Security issue implementation.md index 72807e98012..72807e98012 100644 --- a/.gitlab/issue_templates/Pipeline Security issue implementation +++ b/.gitlab/issue_templates/Pipeline Security issue implementation.md diff --git a/.rubocop_todo/layout/argument_alignment.yml b/.rubocop_todo/layout/argument_alignment.yml index 66d5737ab9c..826e20fb40f 100644 --- a/.rubocop_todo/layout/argument_alignment.yml +++ b/.rubocop_todo/layout/argument_alignment.yml @@ -1034,21 +1034,6 @@ Layout/ArgumentAlignment: - 'ee/spec/elastic/migrate/20220119120500_populate_commit_permissions_in_main_index_spec.rb' - 'ee/spec/elastic/migrate/20221124090600_add_namespace_ancestry_ids_to_original_index_mapping_spec.rb' - 'ee/spec/elastic/migrate/20221221110300_backfill_traversal_ids_to_blobs_and_wiki_blobs_spec.rb' - - 'ee/spec/features/account_recovery_regular_check_spec.rb' - - 'ee/spec/features/admin/admin_emails_spec.rb' - - 'ee/spec/features/admin/admin_settings_spec.rb' - - 'ee/spec/features/billings/billing_plans_spec.rb' - - 'ee/spec/features/boards/boards_spec.rb' - - 'ee/spec/features/boards/swimlanes/epics_swimlanes_drag_drop_spec.rb' - - 'ee/spec/features/burndown_charts_spec.rb' - - 'ee/spec/features/dashboards/todos_spec.rb' - - 'ee/spec/features/epic_boards/epic_boards_spec.rb' - - 'ee/spec/features/groups/saml_enforcement_spec.rb' - - 'ee/spec/features/groups/settings/domain_verification_spec.rb' - - 'ee/spec/features/groups/settings/protected_environments_spec.rb' - - 'ee/spec/features/groups/usage_quotas/pipelines_tab_spec.rb' - - 'ee/spec/features/markdown/metrics_spec.rb' - - 'ee/spec/features/merge_request/user_sees_approval_widget_spec.rb' - 'ee/spec/features/namespace_user_cap_reached_alert_spec.rb' - 'ee/spec/features/projects/environments/environment_spec.rb' - 'ee/spec/features/projects/environments/environments_spec.rb' @@ -1120,7 +1105,6 @@ Layout/ArgumentAlignment: - 'ee/spec/lib/ee/gitlab/scim/provisioning_service_spec.rb' - 'ee/spec/lib/ee/gitlab/usage/service_ping_report_spec.rb' - 'ee/spec/lib/ee/gitlab/usage_data_spec.rb' - - 'ee/spec/lib/elastic/latest/project_wiki_class_proxy_spec.rb' - 'ee/spec/lib/gitlab/analytics/cycle_analytics/data_collector_spec.rb' - 'ee/spec/lib/gitlab/analytics/cycle_analytics/distinct_stage_loader_spec.rb' - 'ee/spec/lib/gitlab/analytics/cycle_analytics/summary/stage_time_summary_spec.rb' @@ -1610,23 +1594,6 @@ Layout/ArgumentAlignment: - 'spec/features/projects/pipelines/pipeline_spec.rb' - 'spec/features/projects/pipelines/pipelines_spec.rb' - 'spec/features/projects/releases/user_views_release_spec.rb' - - 'spec/features/projects/settings/external_authorization_service_settings_spec.rb' - - 'spec/features/projects/settings/monitor_settings_spec.rb' - - 'spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb' - - 'spec/features/projects/settings/registry_settings_spec.rb' - - 'spec/features/projects/show/download_buttons_spec.rb' - - 'spec/features/projects/show/user_interacts_with_auto_devops_banner_spec.rb' - - 'spec/features/projects/tags/download_buttons_spec.rb' - - 'spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb' - - 'spec/features/search/user_searches_for_milestones_spec.rb' - - 'spec/features/search/user_searches_for_wiki_pages_spec.rb' - - 'spec/features/snippets/spam_snippets_spec.rb' - - 'spec/features/snippets/user_creates_snippet_spec.rb' - - 'spec/features/task_lists_spec.rb' - - 'spec/features/user_sees_revert_modal_spec.rb' - - 'spec/features/users/email_verification_on_login_spec.rb' - - 'spec/features/users/login_spec.rb' - - 'spec/features/users/overview_spec.rb' - 'spec/frontend/fixtures/autocomplete.rb' - 'spec/frontend/fixtures/autocomplete_sources.rb' - 'spec/frontend/fixtures/environments.rb' diff --git a/.rubocop_todo/rspec/missing_feature_category.yml b/.rubocop_todo/rspec/missing_feature_category.yml index 4d635a3071c..e3829a97b02 100644 --- a/.rubocop_todo/rspec/missing_feature_category.yml +++ b/.rubocop_todo/rspec/missing_feature_category.yml @@ -723,8 +723,6 @@ RSpec/MissingFeatureCategory: - 'ee/spec/lib/elastic/latest/issue_config_spec.rb' - 'ee/spec/lib/elastic/latest/merge_request_config_spec.rb' - 'ee/spec/lib/elastic/latest/note_config_spec.rb' - - 'ee/spec/lib/elastic/latest/project_wiki_class_proxy_spec.rb' - - 'ee/spec/lib/elastic/latest/project_wiki_instance_proxy_spec.rb' - 'ee/spec/lib/elastic/latest/routing_spec.rb' - 'ee/spec/lib/elastic/latest/snippet_instance_proxy_spec.rb' - 'ee/spec/lib/elastic/latest/user_config_spec.rb' diff --git a/.rubocop_todo/search/namespaced_class.yml b/.rubocop_todo/search/namespaced_class.yml index ba2dbafb3c7..71067dd4e1c 100644 --- a/.rubocop_todo/search/namespaced_class.yml +++ b/.rubocop_todo/search/namespaced_class.yml @@ -108,8 +108,6 @@ Search/NamespacedClass: - 'ee/lib/elastic/latest/note_instance_proxy.rb' - 'ee/lib/elastic/latest/project_class_proxy.rb' - 'ee/lib/elastic/latest/project_instance_proxy.rb' - - 'ee/lib/elastic/latest/project_wiki_class_proxy.rb' - - 'ee/lib/elastic/latest/project_wiki_instance_proxy.rb' - 'ee/lib/elastic/latest/query_context.rb' - 'ee/lib/elastic/latest/repository_class_proxy.rb' - 'ee/lib/elastic/latest/repository_instance_proxy.rb' @@ -142,8 +140,6 @@ Search/NamespacedClass: - 'ee/lib/elastic/v12p1/note_instance_proxy.rb' - 'ee/lib/elastic/v12p1/project_class_proxy.rb' - 'ee/lib/elastic/v12p1/project_instance_proxy.rb' - - 'ee/lib/elastic/v12p1/project_wiki_class_proxy.rb' - - 'ee/lib/elastic/v12p1/project_wiki_instance_proxy.rb' - 'ee/lib/elastic/v12p1/repository_class_proxy.rb' - 'ee/lib/elastic/v12p1/repository_instance_proxy.rb' - 'ee/lib/elastic/v12p1/routing.rb' diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue index 6655af92a55..c38c253564a 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue @@ -23,7 +23,7 @@ export default { default: () => [], }, }, - data: () => { + data() { return { timeout: null, updatingTooltip: false, diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue index 334fc01c9f7..258fa4edcda 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue @@ -5,16 +5,24 @@ export default { import( '~/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue' ), + + MrTerraformWidget: () => import('~/vue_merge_request_widget/extensions/terraform/index.vue'), }, + props: { mr: { type: Object, required: true, }, }, + computed: { + terraformPlansWidget() { + return this.mr.terraformReportsPath && 'MrTerraformWidget'; + }, + widgets() { - return ['MrSecurityWidget']; + return [this.terraformPlansWidget, 'MrSecurityWidget'].filter((w) => w); }, }, }; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue index cdce7c6625a..ec979861283 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue @@ -30,6 +30,11 @@ export default { required: false, default: 2, }, + rowIndex: { + type: Number, + required: false, + default: -1, + }, }, computed: { statusIcon() { @@ -44,6 +49,9 @@ export default { generatedSupportingText() { return generateText(this.data.supportingText); }, + shouldShowThirdLevel() { + return this.data.children?.length > 0 && this.level === 2; + }, }, methods: { onClickedAction(action) { @@ -60,16 +68,19 @@ export default { :widget-name="widgetName" :header="data.header" :help-popover="data.helpPopover" + :class="{ 'gl-border-top-0': rowIndex === 0 }" > <template #body> - <div class="gl-display-flex gl-flex-direction-column"> - <div> - <p v-safe-html="generatedText" class="gl-mb-0"></p> - <gl-link v-if="data.link" :href="data.link.href">{{ data.link.text }}</gl-link> - <p v-if="data.supportingText" v-safe-html="generatedSupportingText" class="gl-mb-0"></p> - <gl-badge v-if="data.badge" :variant="data.badge.variant || 'info'"> - {{ data.badge.text }} - </gl-badge> + <div class="gl-w-full gl-display-flex" :class="{ 'gl-flex-direction-column': level === 1 }"> + <div class="gl-display-flex gl-flex-grow-1"> + <div class="gl-display-flex gl-flex-grow-1 gl-flex-direction-column"> + <p v-safe-html="generatedText" class="gl-mb-0 gl-mr-1"></p> + <gl-link v-if="data.link" :href="data.link.href">{{ data.link.text }}</gl-link> + <p v-if="data.supportingText" v-safe-html="generatedSupportingText" class="gl-mb-0"></p> + <gl-badge v-if="data.badge" :variant="data.badge.variant || 'info'"> + {{ data.badge.text }} + </gl-badge> + </div> <actions :widget="widgetName" :tertiary-buttons="data.actions" @@ -78,10 +89,7 @@ export default { /> <p v-if="data.subtext" v-safe-html="generatedSubtext" class="gl-m-0 gl-font-sm"></p> </div> - <ul - v-if="data.children && data.children.length > 0 && level === 2" - class="gl-m-0 gl-p-0 gl-list-style-none" - > + <ul v-if="shouldShowThirdLevel" class="gl-m-0 gl-p-0 gl-list-style-none"> <li v-for="(childData, index) in data.children" :key="childData.id || index"> <dynamic-content :data="childData" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue index 54eb15c8ac8..e327d848d8f 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue @@ -10,6 +10,7 @@ import HelpPopover from '~/vue_shared/components/help_popover.vue'; import { DynamicScroller, DynamicScrollerItem } from 'vendor/vue-virtual-scroller'; import { EXTENSION_ICONS } from '../../constants'; import { createTelemetryHub } from '../extensions/telemetry'; +import { generateText } from '../extensions/utils'; import ContentRow from './widget_content_row.vue'; import DynamicContent from './dynamic_content.vue'; import StatusIcon from './status_icon.vue'; @@ -72,9 +73,12 @@ export default { }, // If the summary slot is not used, this value will be used as a fallback. summary: { - type: String, + type: Object, required: false, default: undefined, + validator: (s) => { + return Boolean(s.title); + }, }, // If the content slot is not used, this value will be used as a fallback. content: { @@ -154,7 +158,7 @@ export default { return { isExpandedForTheFirstTime: true, isCollapsed: true, - isLoading: false, + isLoading: true, isLoadingExpandedContent: false, summaryError: null, contentError: null, @@ -162,6 +166,12 @@ export default { }; }, computed: { + generatedSummary() { + return generateText(this.summary?.title || ''); + }, + generatedSubSummary() { + return generateText(this.summary?.subtitle || ''); + }, collapseButtonLabel() { return sprintf(this.isCollapsed ? __('Show details') : __('Hide details')); }, @@ -171,6 +181,9 @@ export default { hasActionButtons() { return this.actionButtons.length > 0 || Boolean(this.$scopedSlots['action-buttons']); }, + contentWithKeyField() { + return this.content?.map((item, index) => ({ ...item, id: item.id || index })); + }, }, watch: { hasError: { @@ -289,7 +302,7 @@ export default { <template> <section class="media-section" data-testid="widget-extension"> - <div class="gl-px-5 gl-pr-4 gl-py-4 gl-align-items-center gl-display-flex"> + <div class="gl-px-5 gl-pr-4 gl-py-4 gl-display-flex"> <status-icon :level="1" :name="widgetName" @@ -302,7 +315,14 @@ export default { > <div class="gl-flex-grow-1" data-testid="widget-extension-top-level-summary"> <span v-if="summaryError">{{ summaryError }}</span> - <slot v-else name="summary">{{ isLoading ? loadingText : summary }}</slot> + <slot v-else name="summary" + ><div v-safe-html="isLoading ? loadingText : generatedSummary"></div> + <div + v-if="!isLoading && generatedSubSummary" + v-safe-html="generatedSubSummary" + class="gl-font-sm gl-text-gray-700" + ></div + ></slot> </div> <div class="gl-display-flex"> <help-popover @@ -336,7 +356,7 @@ export default { </slot> </div> <div - v-if="isCollapsible" + v-if="isCollapsible && !isLoading" class="gl-border-l-1 gl-border-l-solid gl-border-gray-100 gl-ml-3 gl-pl-3 gl-h-6" > <gl-button @@ -376,8 +396,8 @@ export default { <div v-else class="gl-w-full"> <slot name="content"> <dynamic-scroller - v-if="content" - :items="content" + v-if="contentWithKeyField" + :items="contentWithKeyField" :min-item-size="32" :style="{ maxHeight: '170px' }" data-testid="dynamic-content-scroller" @@ -390,6 +410,9 @@ export default { :data="item" :widget-name="widgetName" :level="2" + :row-index="index" + data-testid="extension-list-item" + @clickedAction="onActionClick" /> </dynamic-scroller-item> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue b/app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue index 6155a912683..e7d8de97f20 100644 --- a/app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue +++ b/app/assets/javascripts/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue @@ -73,6 +73,9 @@ export default { hasSecurityReports() { return this.artifacts.length > 0; }, + summary() { + return { title: this.$options.i18n.scansHaveRun }; + }, }, methods: { handleIsLoading(value) { @@ -109,7 +112,7 @@ export default { :widget-name="$options.name" :is-collapsible="false" :help-popover="$options.widgetHelpPopover" - :summary="$options.i18n.scansHaveRun" + :summary="summary" @is-loading="handleIsLoading" > <template #action-buttons> diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/terraform/index.js b/app/assets/javascripts/vue_merge_request_widget/extensions/terraform/index.vue index c5cbed4a280..a6d12ed7aec 100644 --- a/app/assets/javascripts/vue_merge_request_widget/extensions/terraform/index.js +++ b/app/assets/javascripts/vue_merge_request_widget/extensions/terraform/index.vue @@ -1,12 +1,29 @@ +<script> import { __, n__, s__, sprintf } from '~/locale'; import axios from '~/lib/utils/axios_utils'; +import MrWidget from '~/vue_merge_request_widget/components/widget/widget.vue'; import { EXTENSION_ICONS } from '../../constants'; export default { name: 'WidgetTerraform', - enablePolling: true, + components: { + MrWidget, + }, + props: { + mr: { + type: Object, + required: true, + }, + }, + data() { + return { + terraformData: { + collapsed: null, + expanded: null, + }, + }; + }, i18n: { - label: s__('Terraform|Terraform reports'), loading: s__('Terraform|Loading Terraform reports...'), error: s__('Terraform|Failed to load Terraform reports'), reportGenerated: s__('Terraform|A Terraform report was generated in your pipelines.'), @@ -23,18 +40,13 @@ export default { reportErrored: s__('Terraform|Generating the report caused an error.'), fullLog: __('Full log'), }, - props: ['terraformReportsPath'], computed: { - // Extension computed props - statusIcon() { - return EXTENSION_ICONS.warning; + terraformReportsPath() { + return this.mr.terraformReportsPath; }, - }, - methods: { - // Extension methods - summary({ valid = [], invalid = [] }) { - let title; - let subtitle = ''; + + summary() { + const { valid = [], invalid = [] } = this.terraformData.collapsed || {}; const validText = sprintf( n__( @@ -60,20 +72,13 @@ export default { false, ); - if (valid.length) { - title = validText; - if (invalid.length) { - subtitle = invalidText; - } - } else { - title = invalidText; - } - return { - subject: title, - meta: subtitle, + title: valid.length ? validText : invalidText, + subtitle: valid.length && invalid.length ? invalidText : undefined, }; }, + }, + methods: { fetchCollapsedData() { return axios .get(this.terraformReportsPath) @@ -84,6 +89,10 @@ export default { const formattedData = this.prepareReports(reports); + const { valid, invalid } = formattedData; + this.terraformData.collapsed = formattedData; + this.terraformData.expanded = [...valid, ...invalid]; + return { ...res, data: formattedData, @@ -91,14 +100,10 @@ export default { }) .catch(() => { const formattedData = this.prepareReports([{ tf_report_error: 'api_error' }]); - + this.terraformData.collapsed = formattedData; return { data: formattedData }; }); }, - fetchFullData() { - const { valid, invalid } = this.collapsedData; - return Promise.resolve([...valid, ...invalid]); - }, createReportRow(report, iconName) { const addNum = Number(report.create); const changeNum = Number(report.update); @@ -176,4 +181,20 @@ export default { return { valid, invalid }; }, }, + + WARNING_ICON: EXTENSION_ICONS.warning, }; +</script> + +<template> + <mr-widget + :error-text="$options.i18n.error" + :status-icon-name="$options.WARNING_ICON" + :loading-text="$options.i18n.loading" + :widget-name="$options.name" + :is-collapsible="Boolean(terraformData.collapsed)" + :summary="summary" + :content="terraformData.expanded" + :fetch-collapsed-data="fetchCollapsedData" + /> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index af9e303594a..9baa2ed15d6 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -55,7 +55,6 @@ import eventHub from './event_hub'; import mergeRequestQueryVariablesMixin from './mixins/merge_request_query_variables'; import getStateQuery from './queries/get_state.query.graphql'; import getStateSubscription from './queries/get_state.subscription.graphql'; -import terraformExtension from './extensions/terraform'; import accessibilityExtension from './extensions/accessibility'; import codeQualityExtension from './extensions/code_quality'; import testReportExtension from './extensions/test_report'; @@ -238,9 +237,6 @@ export default { this.mr.mergePipelinesEnabled && this.mr.sourceProjectId !== this.mr.targetProjectId, ); }, - shouldRenderTerraformPlans() { - return Boolean(this.mr?.terraformReportsPath); - }, shouldRenderTestReport() { return Boolean(this.mr?.testResultsPath); }, @@ -292,11 +288,6 @@ export default { this.initPostMergeDeploymentsPolling(); } }, - shouldRenderTerraformPlans(newVal) { - if (newVal) { - this.registerTerraformPlans(); - } - }, shouldRenderCodeQuality(newVal) { if (newVal) { this.registerCodeQualityExtension(); @@ -546,11 +537,6 @@ export default { dismissSuggestPipelines() { this.mr.isDismissedSuggestPipeline = true; }, - registerTerraformPlans() { - if (this.shouldRenderTerraformPlans) { - registerExtension(terraformExtension); - } - }, registerAccessibilityExtension() { if (this.shouldShowAccessibilityReport) { registerExtension(accessibilityExtension); diff --git a/app/assets/javascripts/work_items/components/work_item_description.vue b/app/assets/javascripts/work_items/components/work_item_description.vue index 61dec21cae4..7e062776f98 100644 --- a/app/assets/javascripts/work_items/components/work_item_description.vue +++ b/app/assets/javascripts/work_items/components/work_item_description.vue @@ -9,7 +9,6 @@ import EditedAt from '~/issues/show/components/edited.vue'; import Tracking from '~/tracking'; import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue'; import { autocompleteDataSources, markdownPreviewPath } from '../utils'; -import workItemDescriptionSubscription from '../graphql/work_item_description.subscription.graphql'; import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql'; import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql'; import { i18n, TRACKING_CATEGORY_SHOW, WIDGET_TYPE_DESCRIPTION } from '../constants'; @@ -75,14 +74,6 @@ export default { error() { this.$emit('error', i18n.fetchError); }, - subscribeToMore: { - document: workItemDescriptionSubscription, - variables() { - return { - issuableId: this.workItemId, - }; - }, - }, }, }, computed: { diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue index 1ac40fe7dcb..5539226e84e 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail.vue @@ -39,10 +39,7 @@ import { WIDGET_TYPE_NOTES, } from '../constants'; -import workItemDatesSubscription from '../../graphql_shared/subscriptions/work_item_dates.subscription.graphql'; -import workItemTitleSubscription from '../graphql/work_item_title.subscription.graphql'; -import workItemAssigneesSubscription from '../graphql/work_item_assignees.subscription.graphql'; -import workItemMilestoneSubscription from '../graphql/work_item_milestone.subscription.graphql'; +import workItemUpdatedSubscription from '../graphql/work_item_updated.subscription.graphql'; import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql'; import updateWorkItemTaskMutation from '../graphql/update_work_item_task.mutation.graphql'; import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql'; @@ -165,52 +162,17 @@ export default { document.title = `${this.workItem.title} · ${this.workItem?.workItemType?.name}${path}`; } }, - subscribeToMore: [ - { - document: workItemTitleSubscription, - variables() { - return { - issuableId: this.workItem.id, - }; - }, - skip() { - return !this.workItem?.id; - }, - }, - { - document: workItemDatesSubscription, - variables() { - return { - issuableId: this.workItem.id, - }; - }, - skip() { - return !this.isWidgetPresent(WIDGET_TYPE_START_AND_DUE_DATE) || !this.workItem?.id; - }, - }, - { - document: workItemAssigneesSubscription, - variables() { - return { - issuableId: this.workItem.id, - }; - }, - skip() { - return !this.isWidgetPresent(WIDGET_TYPE_ASSIGNEES) || !this.workItem?.id; - }, + subscribeToMore: { + document: workItemUpdatedSubscription, + variables() { + return { + id: this.workItem.id, + }; }, - { - document: workItemMilestoneSubscription, - variables() { - return { - issuableId: this.workItem.id, - }; - }, - skip() { - return !this.isWidgetPresent(WIDGET_TYPE_MILESTONE) || !this.workItem?.id; - }, + skip() { + return !this.workItem?.id; }, - ], + }, }, }, computed: { diff --git a/app/assets/javascripts/work_items/components/work_item_labels.vue b/app/assets/javascripts/work_items/components/work_item_labels.vue index 015c86ba043..662ba880753 100644 --- a/app/assets/javascripts/work_items/components/work_item_labels.vue +++ b/app/assets/javascripts/work_items/components/work_item_labels.vue @@ -7,7 +7,6 @@ import labelSearchQuery from '~/sidebar/components/labels/labels_select_widget/g import LabelItem from '~/sidebar/components/labels/labels_select_widget/label_item.vue'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import { isScopedLabel } from '~/lib/utils/common_utils'; -import workItemLabelsSubscription from 'ee_else_ce/work_items/graphql/work_item_labels.subscription.graphql'; import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql'; import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql'; @@ -87,14 +86,6 @@ export default { error() { this.$emit('error', i18n.fetchError); }, - subscribeToMore: { - document: workItemLabelsSubscription, - variables() { - return { - issuableId: this.workItemId, - }; - }, - }, }, searchLabels: { query: labelSearchQuery, diff --git a/app/assets/javascripts/work_items/graphql/work_item_assignees.subscription.graphql b/app/assets/javascripts/work_items/graphql/work_item_assignees.subscription.graphql deleted file mode 100644 index d5b2de8c4c6..00000000000 --- a/app/assets/javascripts/work_items/graphql/work_item_assignees.subscription.graphql +++ /dev/null @@ -1,21 +0,0 @@ -#import "~/graphql_shared/fragments/user.fragment.graphql" - -subscription issuableAssignees($issuableId: IssuableID!) { - issuableAssigneesUpdated(issuableId: $issuableId) { - ... on WorkItem { - id - widgets { - ... on WorkItemWidgetAssignees { - type - allowsMultipleAssignees - canInviteMembers - assignees { - nodes { - ...User - } - } - } - } - } - } -} diff --git a/app/assets/javascripts/work_items/graphql/work_item_description.subscription.graphql b/app/assets/javascripts/work_items/graphql/work_item_description.subscription.graphql deleted file mode 100644 index 4eb3d8067d9..00000000000 --- a/app/assets/javascripts/work_items/graphql/work_item_description.subscription.graphql +++ /dev/null @@ -1,20 +0,0 @@ -subscription issuableDescription($issuableId: IssuableID!) { - issuableDescriptionUpdated(issuableId: $issuableId) { - ... on WorkItem { - id - widgets { - ... on WorkItemWidgetDescription { - type - description - descriptionHtml - lastEditedAt - lastEditedBy { - id - name - webPath - } - } - } - } - } -} diff --git a/app/assets/javascripts/work_items/graphql/work_item_labels.subscription.graphql b/app/assets/javascripts/work_items/graphql/work_item_labels.subscription.graphql deleted file mode 100644 index 86d936bf4dd..00000000000 --- a/app/assets/javascripts/work_items/graphql/work_item_labels.subscription.graphql +++ /dev/null @@ -1,19 +0,0 @@ -#import "~/graphql_shared/fragments/label.fragment.graphql" - -subscription workItemLabels($issuableId: IssuableID!) { - issuableLabelsUpdated(issuableId: $issuableId) { - ... on WorkItem { - id - widgets { - ... on WorkItemWidgetLabels { - type - labels { - nodes { - ...Label - } - } - } - } - } - } -} diff --git a/app/assets/javascripts/work_items/graphql/work_item_milestone.subscription.graphql b/app/assets/javascripts/work_items/graphql/work_item_milestone.subscription.graphql deleted file mode 100644 index f5163003fe5..00000000000 --- a/app/assets/javascripts/work_items/graphql/work_item_milestone.subscription.graphql +++ /dev/null @@ -1,17 +0,0 @@ -#import "~/work_items/graphql/milestone.fragment.graphql" - -subscription issuableMilestone($issuableId: IssuableID!) { - issuableMilestoneUpdated(issuableId: $issuableId) { - ... on WorkItem { - id - widgets { - ... on WorkItemWidgetMilestone { - type - milestone { - ...MilestoneFragment - } - } - } - } - } -} diff --git a/app/assets/javascripts/work_items/graphql/work_item_title.subscription.graphql b/app/assets/javascripts/work_items/graphql/work_item_title.subscription.graphql deleted file mode 100644 index 2ac01b79d6f..00000000000 --- a/app/assets/javascripts/work_items/graphql/work_item_title.subscription.graphql +++ /dev/null @@ -1,8 +0,0 @@ -subscription issuableTitleUpdated($issuableId: IssuableID!) { - issuableTitleUpdated(issuableId: $issuableId) { - ... on WorkItem { - id - title - } - } -} diff --git a/app/assets/javascripts/work_items/graphql/work_item_updated.subscription.graphql b/app/assets/javascripts/work_items/graphql/work_item_updated.subscription.graphql new file mode 100644 index 00000000000..8b63f46ab28 --- /dev/null +++ b/app/assets/javascripts/work_items/graphql/work_item_updated.subscription.graphql @@ -0,0 +1,7 @@ +#import "./work_item.fragment.graphql" + +subscription workItemUpdated($id: WorkItemID!) { + workItemUpdated(workItemId: $id) { + ...WorkItem + } +} diff --git a/app/models/design_management/repository.rb b/app/models/design_management/repository.rb index 39077fdbcb1..7410944e174 100644 --- a/app/models/design_management/repository.rb +++ b/app/models/design_management/repository.rb @@ -8,7 +8,7 @@ module DesignManagement belongs_to :project, inverse_of: :design_management_repository validates :project, presence: true, uniqueness: true - delegate :lfs_enabled?, :storage, :repository_storage, to: :project + delegate :lfs_enabled?, :storage, :repository_storage, :run_after_commit, to: :project def repository ::DesignManagement::GitRepository.new( diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb index 2749404b7b5..af804865b8b 100644 --- a/app/models/personal_access_token.rb +++ b/app/models/personal_access_token.rb @@ -99,9 +99,13 @@ class PersonalAccessToken < ApplicationRecord def expires_at_before_instance_max_expiry_date return unless expires_at - if expires_at > MAX_PERSONAL_ACCESS_TOKEN_LIFETIME_IN_DAYS.days.from_now - errors.add(:expires_at, _('must expire in 365 days')) - end + max_expiry_date = Date.current.advance(days: MAX_PERSONAL_ACCESS_TOKEN_LIFETIME_IN_DAYS) + return unless expires_at > max_expiry_date + + errors.add( + :expires_at, + format(_("must be before %{expiry_date}"), expiry_date: max_expiry_date) + ) end end diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 2279ab301dc..a5c12384b59 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -58,6 +58,10 @@ module Projects unless remove_repository(project.wiki.repository) raise_error(s_('DeleteProject|Failed to remove wiki repository. Please try again or contact administrator.')) end + + unless remove_repository(project.design_repository) + raise_error(s_('DeleteProject|Failed to remove design repository. Please try again or contact administrator.')) + end end def trash_relation_repositories! diff --git a/config/locales/en.yml b/config/locales/en.yml index 1de1c20259d..0d7110be68a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -25,6 +25,8 @@ en: member: user: "The member's email address" invite_email: "The member's email address" + personal_access_token: + expires_at: "Expiration date" project/error_tracking_setting: token: "Auth Token" project: "Project" diff --git a/doc/administration/audit_event_streaming/graphql_api.md b/doc/administration/audit_event_streaming/graphql_api.md index c1e24fae901..89f6cb22b13 100644 --- a/doc/administration/audit_event_streaming/graphql_api.md +++ b/doc/administration/audit_event_streaming/graphql_api.md @@ -481,6 +481,20 @@ Streaming destination is deleted if: - The returned `errors` object is empty. - The API responds with `200 OK`. +To remove an HTTP header, use the GraphQL `auditEventsStreamingInstanceHeadersDestroy` mutation. +To retrieve the header ID, +[list all the custom HTTP headers](#list-streaming-destinations) for the instance. + +```graphql +mutation { + auditEventsStreamingInstanceHeadersDestroy(input: { headerId: "gid://gitlab/AuditEvents::Streaming::InstanceHeader/<id>" }) { + errors + } +} +``` + +The header is deleted if the returned `errors` object is empty. + ### Google Cloud Logging configurations > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/409422) in GitLab 16.1. diff --git a/doc/administration/geo/setup/index.md b/doc/administration/geo/setup/index.md index 359f706a8aa..8ac64a963bb 100644 --- a/doc/administration/geo/setup/index.md +++ b/doc/administration/geo/setup/index.md @@ -34,8 +34,8 @@ If both Geo sites are based on the [1K reference architecture](../../reference_a - [Using Linux package PostgreSQL instances](database.md) . - [Using external PostgreSQL instances](external_database.md) 1. [Configure GitLab](../replication/configuration.md) to set the **primary** and **secondary** sites. -1. Recommended: [Configure unified URLs](../secondary_proxy/index.md) to use a single, unified URL for all Geo sites. -1. Optional: [Configure Object storage replication](../../object_storage.md) +1. Recommended: [Configure unified URLs](../secondary_proxy/index.md#set-up-a-unified-url-for-geo-sites) to use a single, unified URL for all Geo sites. +1. Optional: [Configure Object storage replication](../replication/object_storage.md) 1. Optional: [Configure a secondary LDAP server](../../auth/ldap/index.md) for the **secondary** sites. See [notes on LDAP](../index.md#ldap). 1. Optional: [Configure Container Registry for the secondary site](../replication/container_registry.md). 1. Follow the [Using a Geo Site](../replication/usage.md) guide. diff --git a/doc/ci/chatops/index.md b/doc/ci/chatops/index.md index 7be12d0c0fd..09f2758113f 100644 --- a/doc/ci/chatops/index.md +++ b/doc/ci/chatops/index.md @@ -49,18 +49,20 @@ When the job runs: ## Run a CI/CD job -You can run a CI/CD job from chat with the `/project-name run` -[slash command](../../integration/slash_commands.md). - -Prerequisites: +Prerequisite: - You must have at least the Developer role for the project. -To run a CI/CD job: +You can run a CI/CD job on the default branch from chat. To run a CI/CD job: + +- In the chat client, enter `/<project-name> run <job name> <arguments>` where: -- In the chat client, enter `/project-name run <job name> <arguments>`. + - `<project-name>` is the name of the project. + - `<job name>` is the name of the CI/CD job to run. + - `<arguments>` is the arguments to pass to the CI/CD job. ChatOps schedules a pipeline that contains only the specified job. +Other [slash commands](../../user/project/integrations/gitlab_slack_application.md#slash-commands) are also available. ### Exclude a job from ChatOps diff --git a/doc/ci/runners/saas/linux_saas_runner.md b/doc/ci/runners/saas/linux_saas_runner.md index d95fc701056..c340a2b0f89 100644 --- a/doc/ci/runners/saas/linux_saas_runner.md +++ b/doc/ci/runners/saas/linux_saas_runner.md @@ -85,12 +85,8 @@ Unlike the normal SaaS runners on Linux, each virtual machine is re-used up to 4 As we want to encourage people to contribute, these runners are free of charge. -<!--- start_remove The following content will be removed on remove_date: '2023-08-22' --> - -## Pre-clone script (removed) +## Pre-clone script (deprecated) This feature was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/391896) in GitLab 15.9 -and [removed](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/29405) in 16.0. +and [will be removed](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/29405) in 17.0. Use [`pre_get_sources_script`](../../../ci/yaml/index.md#hookspre_get_sources_script) instead. - -<!--- end_remove --> diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md index 3d6fa87d809..0cfdf7fb4ea 100644 --- a/doc/development/migration_style_guide.md +++ b/doc/development/migration_style_guide.md @@ -203,6 +203,11 @@ and explains how to perform them without requiring downtime. Your migration **must be** reversible. This is very important, as it should be possible to downgrade in case of a vulnerability or bugs. +**Note**: On GitLab production environments, if a problem occurs, a roll-forward strategy is used instead of rolling back migrations using `db:rollback`. +On self-managed instances we advise users to restore the backup which was created before the upgrade process started. +The `down` method is used primarily in the development environment, for example, when a developer wants to ensure +their local copy of `structure.sql` file and database are in a consistent state when switching between commits or branches. + In your migration, add a comment describing how the reversibility of the migration was tested. diff --git a/doc/integration/slash_commands.md b/doc/integration/slash_commands.md index 02c7debc6dc..e36ee164002 100644 --- a/doc/integration/slash_commands.md +++ b/doc/integration/slash_commands.md @@ -1,51 +1,11 @@ --- -stage: Manage -group: Import and Integrate -info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments +redirect_to: '../user/project/integrations/gitlab_slack_application.md#slash-commands' +remove_date: '2023-09-19' --- -# Slash commands in Mattermost and Slack **(FREE)** +This document was moved to [another location](../user/project/integrations/gitlab_slack_application.md#slash-commands). -> [Moved](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/24780) from GitLab Ultimate to GitLab Free in 11.9. - -If you want to control and view GitLab content while you're -working in Slack and Mattermost, you can use slash commands. -Type the command as a message in your chat client to activate it. -For Slack, this requires an [integration configuration](../user/project/integrations/slack_slash_commands.md). - -Slash commands are scoped to a project and require -a specified trigger command during configuration. -You should use the project name as the trigger command. - -If you're using the [GitLab for Slack app](../user/project/integrations/gitlab_slack_application.md) for -your GitLab.com projects, [add the `gitlab` keyword at the beginning of the command](../user/project/integrations/gitlab_slack_application.md#slash-commands) -(for example, `/gitlab <project-name> issue show <id>`). - -Assuming `project-name` is the trigger command, the slash commands are: - -| Command | Effect | -| ------- | ------ | -| `/project-name help` | Shows all available slash commands. | -| `/project-name issue new <title> <shift+return> <description>` | Creates a new issue with title `<title>` and description `<description>`. | -| `/project-name issue show <id>` | Shows the issue with ID `<id>`. | -| `/project-name issue close <id>` | Closes the issue with ID `<id>`. | -| `/project-name issue search <query>` | Shows up to 5 issues matching `<query>`. | -| `/project-name issue move <id> to <project>` | Moves the issue with ID `<id>` to `<project>`. | -| `/project-name issue comment <id> <shift+return> <comment>` | Adds a new comment with comment body `<comment>` to the issue with ID `<id>`. | -| `/project-name deploy <from> to <to>` | [Deploys](#deploy-command) from the `<from>` environment to the `<to>` environment. | -| `/project-name run <job name> <arguments>` | Executes the [ChatOps](../ci/chatops/index.md) job `<job name>` on the default branch. | - -## Issue commands - -You can create a new issue, display issue details, and search up to 5 issues. - -## Deploy command - -To deploy to an environment, GitLab tries to find a deployment -manual action in the pipeline. - -If there's only one action for a given environment, it is triggered. -If more than one action is defined, GitLab finds an action -name that equals the environment name to deploy to. - -The command returns an error if no matching action is found. +<!-- This redirect file can be deleted after 2023-09-19. --> +<!-- Redirects that point to other docs in the same project expire in three months. --> +<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. --> +<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html --> diff --git a/doc/operations/incident_management/slack.md b/doc/operations/incident_management/slack.md index b007f253fd8..aa0d057bfd8 100644 --- a/doc/operations/incident_management/slack.md +++ b/doc/operations/incident_management/slack.md @@ -37,7 +37,7 @@ Prerequisites: 1. Authorize GitLab to take actions on behalf of your Slack user. Each user must do this before they can use any of the incident slash commands. - To start the authorization flow, try executing a non-incident [Slack slash command](../../integration/slash_commands.md), + To start the authorization flow, try executing a non-incident [Slack slash command](../../user/project/integrations/gitlab_slack_application.md#slash-commands), like `/gitlab <project-alias> issue show <id>`. The `<project-alias>` you select must be a project that has the GitLab for Slack app set up. For more information, see [issue 377548](https://gitlab.com/gitlab-org/gitlab/-/issues/377548). diff --git a/doc/subscriptions/gitlab_dedicated/index.md b/doc/subscriptions/gitlab_dedicated/index.md index f14f38f4717..9bdf089e4ee 100644 --- a/doc/subscriptions/gitlab_dedicated/index.md +++ b/doc/subscriptions/gitlab_dedicated/index.md @@ -43,7 +43,7 @@ GitLab Dedicated supports instance-level [SAML OmniAuth](../../integration/saml. GitLab Dedicated offers public connectivity by default with support for IP allowlists. You can [optionally specify a list of IP addresses](../../administration/dedicated/index.md#ip-allowlist) that can access your GitLab Dedicated instance. Subsequently, when an IP not on the allowlist tries to access your instance the connection is refused. -Private connectivity via [AWS PrivateLink](https://aws.amazon.com/privatelink/) is also offered as an option. Both [inbound](../../administration/dedicated/index.md#inbound-private-link) and [outbound](../../administration/dedicated/index.md#outbound-private-link) PrivateLinks are supported. When connecting to an internal service running in your VPC over HTTPS via PrivateLink, GitLab Dedicated supports the ability to use a private SSL certificate, which can be provided when [updating your instance configuration](../../administration/dedicated/index.md#custom-certificates). +Private connectivity via [AWS PrivateLink](https://aws.amazon.com/privatelink/) is also offered as an option. Both [inbound](../../administration/dedicated/index.md#inbound-private-link) and [outbound](../../administration/dedicated/index.md#outbound-private-link) PrivateLinks are supported. When connecting to internal resources over an outbound PrivateLink with non public certificates, you can specify a list of certificates that are trusted by GitLab. These certificates can be provided when [updating your instance configuration](../../administration/dedicated/index.md#custom-certificates). #### Encryption diff --git a/doc/user/admin_area/moderate_users.md b/doc/user/admin_area/moderate_users.md index c357e16c967..fd08c58cbcd 100644 --- a/doc/user/admin_area/moderate_users.md +++ b/doc/user/admin_area/moderate_users.md @@ -91,7 +91,7 @@ A blocked user: - Cannot sign in. - Cannot access Git repositories or the API. - Does not receive any notifications from GitLab. -- Cannot use [slash commands](../../integration/slash_commands.md). +- Cannot use [slash commands](../../user/project/integrations/gitlab_slack_application.md#slash-commands). - Does not consume a [seat](../../subscriptions/self_managed/index.md#billable-users). Personal projects, and group and user history of the blocked user are left intact. @@ -149,7 +149,7 @@ A deactivated user: - Cannot access Git repositories or the API. - Does not receive any notifications from GitLab. -- Cannot use [slash commands](../../integration/slash_commands.md). +- Cannot use [slash commands](../../user/project/integrations/gitlab_slack_application.md#slash-commands). - Does not consume a [seat](../../subscriptions/self_managed/index.md#billable-users). Personal projects, and group and user history of the deactivated user are left intact. diff --git a/doc/user/profile/achievements.md b/doc/user/profile/achievements.md index 4e3248ceb10..31ce72e8423 100644 --- a/doc/user/profile/achievements.md +++ b/doc/user/profile/achievements.md @@ -109,6 +109,23 @@ mutation achievementsCreate($file: Upload!) { } ``` +To supply the avatar file, call the mutation using `curl`: + +```shell +curl 'https://gitlab.com/api/graphql' \ + -H "Authorization: Bearer <your-pat-token>" \ + -H "Content-Type: multipart/form-data" \ + -F operations='{ "query": "mutation ($file: Upload!) { achievementsCreate(input: { namespaceId: \"gid://gitlab/Namespace/<namespace-id>\", name: \"<name>\", description: \"<description>\", avatar: $file }) { achievement { id name description avatarUrl } } }", "variables": { "file": null } }' \ + -F map='{ "0": ["variables.file"] }' \ + -F 0='@/path/to/your/file.jpg' +``` + +When successful, the response returns the achievement ID: + +```shell +{"data":{"achievementsCreate":{"achievement":{"id":"gid://gitlab/Achievements::Achievement/1","name":"<name>","description":"<description>","avatarUrl":"https://gitlab.com/uploads/-/system/achievements/achievement/avatar/1/file.jpg"}}}} +``` + ## Update an achievement You can change the name, description, and avatar of an achievement at any time. diff --git a/doc/user/project/integrations/gitlab_slack_application.md b/doc/user/project/integrations/gitlab_slack_application.md index 8790c7213e5..77a08585e08 100644 --- a/doc/user/project/integrations/gitlab_slack_application.md +++ b/doc/user/project/integrations/gitlab_slack_application.md @@ -52,43 +52,43 @@ you can also select **Reinstall GitLab for Slack app**. ## Slash commands -After installing the GitLab for Slack app, everyone in your Slack workspace can use slash commands. +You can use slash commands to run common GitLab operations. Replace `<project>` with a project full path. -Replace `<project>` with the project full path, or create a shorter [project alias](#create-a-project-alias-for-slash-commands) for the slash commands. +**For the GitLab for Slack app**: -| Command | Effect | -| ------- | ------ | +- You must authorize your Slack user on GitLab.com when you run your first slash command. +- You can [create a shorter project alias](#create-a-project-alias-for-slash-commands) for slash commands. + +**For [Slack slash commands](slack_slash_commands.md) on self-managed GitLab, [Mattermost slash commands](mattermost_slash_commands.md), and [ChatOps](../../../ci/chatops/index.md)**, do not add `gitlab` at the beginning of the command. + +The following slash commands are available: + +| Command | Description | +| ------- | ----------- | | `/gitlab help` | Shows all available slash commands. | -| `/gitlab <project> issue new <title> <shift+return> <description>` | Creates a new issue with the title `<title>` and description `<description>`. | +| `/gitlab <project> issue new <title>` <kbd>Shift</kbd>+<kbd>Enter</kbd> `<description>` | Creates a new issue with the title `<title>` and description `<description>`. | | `/gitlab <project> issue show <id>` | Shows the issue with the ID `<id>`. | | `/gitlab <project> issue close <id>` | Closes the issue with the ID `<id>`. | -| `/gitlab <project> issue search <query>` | Shows up to 5 issues matching `<query>`. | +| `/gitlab <project> issue search <query>` | Shows up to five issues matching `<query>`. | | `/gitlab <project> issue move <id> to <project>` | Moves the issue with the ID `<id>` to `<project>`. | -| `/gitlab <project> issue comment <id> <shift+return> <comment>` | Adds a new comment with the comment body `<comment>` to the issue with the ID `<id>`. | +| `/gitlab <project> issue comment <id>` <kbd>Shift</kbd>+<kbd>Enter</kbd> `<comment>` | Adds a new comment with the comment body `<comment>` to the issue with the ID `<id>`. | | `/gitlab <project> deploy <from> to <to>` | [Deploys](#the-deploy-slash-command) from the `<from>` environment to the `<to>` environment. | | `/gitlab <project> run <job name> <arguments>` | Executes the [ChatOps](../../../ci/chatops/index.md) job `<job name>` on the default branch. | -| `/gitlab incident declare` | **Beta** Opens modal to [create a new incident](../../../operations/incident_management/slack.md). | +| `/gitlab incident declare` | Opens a modal to [create a new incident from Slack](../../../operations/incident_management/slack.md) (Beta). | -### The deploy slash command +### The `deploy` slash command -To deploy to an environment, GitLab tries to find a deployment -manual action in the pipeline. +To deploy to an environment, GitLab tries to find a deployment manual action in the pipeline. -If there's only one action for a given environment, it is triggered. -If more than one action is defined, GitLab finds an action -name that matches the environment name to deploy to. +If only one action is defined for a given environment, it is triggered. +If more than one action is defined, GitLab tries to find an action name +that matches the environment name to deploy to. The command returns an error if no matching action is found. -### User authorization - -When you perform your first slash command, you must -authorize your Slack user on GitLab.com. - ### Create a project alias for slash commands -By default, slash commands expect a project full path. To use a shorter alias -instead: +By default, slash commands expect a project full path. To create a shorter project alias in the GitLab for Slack app: 1. Go to your project's home page. 1. Go to **Settings > Integrations** (only visible on GitLab.com). diff --git a/doc/user/project/integrations/mattermost_slash_commands.md b/doc/user/project/integrations/mattermost_slash_commands.md index a8d8323a651..6a09a1cfbcd 100644 --- a/doc/user/project/integrations/mattermost_slash_commands.md +++ b/doc/user/project/integrations/mattermost_slash_commands.md @@ -6,12 +6,14 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Mattermost slash commands **(FREE)** -You can use slash commands to run common GitLab operations, like creating an issue, -from a [Mattermost](https://mattermost.com/) chat environment. +You can use [slash commands](gitlab_slack_application.md#slash-commands) to run common GitLab operations, +like creating an issue, from a [Mattermost](https://mattermost.com/) chat environment. GitLab can also send events (such as `issue created`) to Mattermost as part of the separately configured [Mattermost notifications](mattermost.md). +For a list of available slash commands, see [Slash commands](gitlab_slack_application.md#slash-commands). + ## Configuration options GitLab provides different ways to configure Mattermost slash commands. For any of these options, @@ -109,7 +111,7 @@ Your slash command can now communicate with your GitLab project. Prerequisite: -- To run [slash commands](#available-slash-commands), you must have +- To run [slash commands](gitlab_slack_application.md#slash-commands), you must have [permission](../../permissions.md#project-members-permissions) to perform the action in the GitLab project. @@ -120,21 +122,10 @@ To interact with GitLab using Mattermost slash commands: You can see all authorized chat accounts in your Mattermost profile page under **Chat**. -## Available slash commands - -The available slash commands for Mattermost are: - -| Command | Description | Example | -| ------- | ----------- | ------- | -| `/<trigger> issue new <title>` <kbd>Shift</kbd>+<kbd>Enter</kbd> `<description>` | Create a new issue in the project that `<trigger>` is tied to. `<description>` is optional. | `/gitlab issue new We need to change the homepage` | -| `/<trigger> issue show <issue-number>` | Show the issue with ID `<issue-number>` from the project that `<trigger>` is tied to. | `/gitlab issue show 42` | -| `/<trigger> deploy <environment> to <environment>` | Start the CI/CD job that deploys from one environment to another (for example, `staging` to `production`). CI/CD must be [properly configured](../../../ci/yaml/index.md). | `/gitlab deploy staging to production` | -| `/<trigger> help` | View a list of available slash commands. | `/gitlab help` | - ## Related topics -- [Mattermost slash commands](https://developers.mattermost.com/integrate/slash-commands/) -- [Linux package Mattermost](../../../integration/mattermost/index.md) +- [Mattermost Linux package](../../../integration/mattermost/index.md) +- [Slash commands at Mattermost](https://developers.mattermost.com/integrate/slash-commands/) ## Troubleshooting diff --git a/doc/user/project/integrations/slack_slash_commands.md b/doc/user/project/integrations/slack_slash_commands.md index 74bd12561fb..c1e04f2aa63 100644 --- a/doc/user/project/integrations/slack_slash_commands.md +++ b/doc/user/project/integrations/slack_slash_commands.md @@ -10,16 +10,18 @@ NOTE: This feature is only configurable on self-managed GitLab instances. For GitLab.com, use the [GitLab for Slack app](gitlab_slack_application.md) instead. -If you want to control and view GitLab content while you're -working in Slack, you can use Slack slash commands. -To use Slack slash commands, you must configure both Slack and GitLab. +You can use [slash commands](gitlab_slack_application.md#slash-commands) to run common GitLab operations, +like creating an issue, from a [Slack](https://slack.com/) chat environment. +To use slash commands in Slack, you must configure both Slack and GitLab. -GitLab can also send events (for example, `issue created`) to Slack as notifications. -The [Slack notifications integration](slack.md) is configured separately. +GitLab can also send events (such as `issue created`) to Slack as part of the +separately configured [Slack notifications](slack.md). -## Configure GitLab and Slack +For a list of available slash commands, see [Slash commands](gitlab_slack_application.md#slash-commands). -Slack slash command integrations are scoped to a project. +## Configure the integration + +Slack slash commands are scoped to a project. To configure Slack slash commands: 1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project. 1. Select **Settings > Integrations**. @@ -35,7 +37,3 @@ Slack slash command integrations are scoped to a project. 1. On the Slack configuration page, select **Save Integration** and copy the **Token**. 1. Go back to the GitLab configuration page and paste in the **Token**. 1. Ensure the **Active** checkbox is selected and select **Save changes**. - -## Slash commands - -You can now use the available [Slack slash commands](../../../integration/slash_commands.md). diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 2bf093bdfda..7d92f9e38b3 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -6288,6 +6288,9 @@ msgstr "" msgid "Artifacts|We want you to be able to use this page to easily manage your CI/CD job artifacts. We are working to improve this experience and would appreciate any feedback you have about the improvements we are making." msgstr "" +msgid "As this is a newly created account, to get started, click the link below to confirm your account." +msgstr "" + msgid "As we continue to build more features for SAST, we'd love your feedback on the SAST configuration feature in %{linkStart}this issue%{linkEnd}." msgstr "" @@ -8567,6 +8570,9 @@ msgstr "" msgid "By quarter" msgstr "" +msgid "By using a primary email tied to an Enterprise e-mail address, it is understood that this account is an Enterprise User." +msgstr "" + msgid "By week" msgstr "" @@ -14934,6 +14940,9 @@ msgstr "" msgid "Delete video" msgstr "" +msgid "DeleteProject|Failed to remove design repository. Please try again or contact administrator." +msgstr "" + msgid "DeleteProject|Failed to remove events. Please try again or contact administrator." msgstr "" @@ -17288,6 +17297,12 @@ msgstr "" msgid "Enterprise" msgstr "" +msgid "Enterprise User" +msgstr "" + +msgid "Enterprise User Account on GitLab" +msgstr "" + msgid "EnterpriseUsers|The user detail cannot be updated" msgstr "" @@ -17297,9 +17312,6 @@ msgstr "" msgid "EnterpriseUsers|The user is already an enterprise user of the group" msgstr "" -msgid "EnterpriseUsers|The user is not a member of the group" -msgstr "" - msgid "Environment" msgstr "" @@ -19477,6 +19489,9 @@ msgstr "" msgid "For individual use, create a separate account under your personal email address, not tied to the Enterprise email domain or group." msgstr "" +msgid "For individual use, create a separate account under your personal email address, not tied to the Enterprise email domain." +msgstr "" + msgid "For investigating IT service disruptions or outages" msgstr "" @@ -45536,9 +45551,6 @@ msgstr "" msgid "Terraform|Terraform init command" msgstr "" -msgid "Terraform|Terraform reports" -msgstr "" - msgid "Terraform|Terraform states" msgstr "" @@ -53072,6 +53084,12 @@ msgstr "" msgid "Your GitLab account has been locked due to an excessive number of unsuccessful sign in attempts. You can wait for your account to automatically unlock in %{duration} or you can click the link below to unlock now." msgstr "" +msgid "Your GitLab account is now an %{source_link}:" +msgstr "" + +msgid "Your GitLab account is now an Enterprise User (%{source_link}):" +msgstr "" + msgid "Your GitLab account request has been approved!" msgstr "" @@ -55035,6 +55053,9 @@ msgstr "" msgid "must be at least 1 day" msgstr "" +msgid "must be before %{expiry_date}" +msgstr "" + msgid "must be greater than start date" msgstr "" @@ -55068,9 +55089,6 @@ msgstr "" msgid "must contain only a discord user ID." msgstr "" -msgid "must expire in 365 days" -msgstr "" - msgid "must have a repository" msgstr "" diff --git a/qa/Gemfile b/qa/Gemfile index a06914ca7de..c907240333f 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -3,7 +3,7 @@ source 'https://rubygems.org' gem 'gitlab-qa', '~> 11', '>= 11.2.0', require: 'gitlab/qa' -gem 'gitlab_quality-test_tooling', '~> 0.8.1', require: false +gem 'gitlab_quality-test_tooling', '~> 0.8.2', require: false gem 'activesupport', '~> 6.1.7.2' # This should stay in sync with the root's Gemfile gem 'allure-rspec', '~> 2.20.0' gem 'capybara', '~> 3.39.2' diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index e4e89e7e2e1..94eae5f80bb 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -111,7 +111,7 @@ GEM rainbow (>= 3, < 4) table_print (= 1.5.7) zeitwerk (>= 2, < 3) - gitlab_quality-test_tooling (0.8.1) + gitlab_quality-test_tooling (0.8.2) activesupport (>= 6.1, < 7.1) gitlab (~> 4.19) http (~> 5.0) @@ -324,7 +324,7 @@ DEPENDENCIES fog-core (= 2.1.0) fog-google (~> 1.19) gitlab-qa (~> 11, >= 11.2.0) - gitlab_quality-test_tooling (~> 0.8.1) + gitlab_quality-test_tooling (~> 0.8.2) influxdb-client (~> 2.9) knapsack (~> 4.0) nokogiri (~> 1.15, >= 1.15.2) diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_new_branch_rule_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_new_branch_rule_spec.rb index 82074919ad4..1d9dccbddf6 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/add_new_branch_rule_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_new_branch_rule_spec.rb @@ -2,11 +2,7 @@ module QA RSpec.describe 'Create' do - describe 'Branch Rules Overview', product_group: :source_code, - quarantine: { - issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/403583', - type: :flaky - } do + describe 'Branch Rules Overview', product_group: :source_code do let(:branch_name) { 'new-branch' } let(:allowed_to_push_role) { Resource::ProtectedBranch::Roles::NO_ONE } let(:allowed_to_merge_role) { Resource::ProtectedBranch::Roles::MAINTAINERS } diff --git a/spec/features/projects/settings/external_authorization_service_settings_spec.rb b/spec/features/projects/settings/external_authorization_service_settings_spec.rb index 4a56e6c8bbf..4214e6fc767 100644 --- a/spec/features/projects/settings/external_authorization_service_settings_spec.rb +++ b/spec/features/projects/settings/external_authorization_service_settings_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe 'Projects > Settings > External Authorization Classification Label setting', -feature_category: :groups_and_projects do + feature_category: :groups_and_projects do let(:user) { create(:user) } let(:project) { create(:project_empty_repo) } diff --git a/spec/features/projects/settings/monitor_settings_spec.rb b/spec/features/projects/settings/monitor_settings_spec.rb index c5a5826a778..b46451f4255 100644 --- a/spec/features/projects/settings/monitor_settings_spec.rb +++ b/spec/features/projects/settings/monitor_settings_spec.rb @@ -18,8 +18,11 @@ RSpec.describe 'Projects > Settings > For a forked project', :js, feature_catego visit project_path(project) wait_for_requests - expect(page).to have_selector('.sidebar-sub-level-items a[aria-label="Error Tracking"]', - text: 'Error Tracking', visible: :hidden) + expect(page).to have_selector( + '.sidebar-sub-level-items a[aria-label="Error Tracking"]', + text: 'Error Tracking', + visible: :hidden + ) end end diff --git a/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb b/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb index 50693dda685..1ab88ec0fff 100644 --- a/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb +++ b/spec/features/projects/settings/registry_settings_cleanup_tags_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe 'Project > Settings > Packages and registries > Container registry tag expiration policy', -feature_category: :groups_and_projects do + feature_category: :groups_and_projects do let_it_be(:user) { create(:user) } let_it_be(:project, reload: true) { create(:project, namespace: user.namespace) } diff --git a/spec/features/projects/settings/registry_settings_spec.rb b/spec/features/projects/settings/registry_settings_spec.rb index b8016a5d2df..9df82e447aa 100644 --- a/spec/features/projects/settings/registry_settings_spec.rb +++ b/spec/features/projects/settings/registry_settings_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe 'Project > Settings > Packages and registries > Container registry tag expiration policy', -feature_category: :groups_and_projects do + feature_category: :groups_and_projects do let_it_be(:user) { create(:user) } let_it_be(:project, reload: true) { create(:project, namespace: user.namespace) } diff --git a/spec/features/projects/show/download_buttons_spec.rb b/spec/features/projects/show/download_buttons_spec.rb index a4df6a56e02..8e27b4b2ede 100644 --- a/spec/features/projects/show/download_buttons_spec.rb +++ b/spec/features/projects/show/download_buttons_spec.rb @@ -9,18 +9,24 @@ RSpec.describe 'Projects > Show > Download buttons', feature_category: :groups_a let(:project) { create(:project, :repository) } let(:pipeline) do - create(:ci_pipeline, - project: project, - sha: project.commit.sha, - ref: project.default_branch, - status: status) + create( + :ci_pipeline, + project: project, + sha: project.commit.sha, + ref: project.default_branch, + status: status + ) end let!(:build) do - create(:ci_build, :success, :artifacts, - pipeline: pipeline, - status: pipeline.status, - name: 'build') + create( + :ci_build, + :success, + :artifacts, + pipeline: pipeline, + status: pipeline.status, + name: 'build' + ) end before do diff --git a/spec/features/projects/show/user_interacts_with_auto_devops_banner_spec.rb b/spec/features/projects/show/user_interacts_with_auto_devops_banner_spec.rb index 997a804e6ac..98714da34f2 100644 --- a/spec/features/projects/show/user_interacts_with_auto_devops_banner_spec.rb +++ b/spec/features/projects/show/user_interacts_with_auto_devops_banner_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe 'Project > Show > User interacts with auto devops implicitly enabled banner', -feature_category: :groups_and_projects do + feature_category: :groups_and_projects do let(:project) { create(:project, :repository) } let(:user) { create(:user) } diff --git a/spec/features/projects/tags/download_buttons_spec.rb b/spec/features/projects/tags/download_buttons_spec.rb index 570721fc951..275d364f267 100644 --- a/spec/features/projects/tags/download_buttons_spec.rb +++ b/spec/features/projects/tags/download_buttons_spec.rb @@ -10,18 +10,22 @@ RSpec.describe 'Download buttons in tags page', feature_category: :source_code_m let(:project) { create(:project, :repository) } let(:pipeline) do - create(:ci_pipeline, - project: project, - sha: project.commit(tag).sha, - ref: tag, - status: status) + create( + :ci_pipeline, + project: project, + sha: project.commit(tag).sha, + ref: tag, + status: status + ) end let!(:build) do - create(:ci_build, :success, :artifacts, - pipeline: pipeline, - status: pipeline.status, - name: 'build') + create( + :ci_build, :success, :artifacts, + pipeline: pipeline, + status: pipeline.status, + name: 'build' + ) end before do diff --git a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb index 79744633d0c..9a772ff8e44 100644 --- a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb +++ b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb @@ -9,11 +9,13 @@ RSpec.describe 'Projects > Wiki > User views wiki in project page', feature_cate context 'when repository is disabled for project' do let(:project) do - create(:project, - :wiki_repo, - :repository_disabled, - :merge_requests_disabled, - :builds_disabled) + create( + :project, + :wiki_repo, + :repository_disabled, + :merge_requests_disabled, + :builds_disabled + ) end context 'when wiki homepage contains a link' do @@ -37,8 +39,13 @@ RSpec.describe 'Projects > Wiki > User views wiki in project page', feature_cate context 'when using asciidoc' do before do - create(:wiki_page, wiki: project.wiki, title: 'home', content: 'link:other-page[some link]', - format: :asciidoc) + create( + :wiki_page, + wiki: project.wiki, + title: 'home', + content: 'link:other-page[some link]', + format: :asciidoc + ) end it_behaves_like 'wiki homepage contains a link' diff --git a/spec/features/search/user_searches_for_milestones_spec.rb b/spec/features/search/user_searches_for_milestones_spec.rb index 238e59be940..7ca7958f61b 100644 --- a/spec/features/search/user_searches_for_milestones_spec.rb +++ b/spec/features/search/user_searches_for_milestones_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe 'User searches for milestones', :js, :clean_gitlab_redis_rate_limiting, -feature_category: :global_search do + feature_category: :global_search do let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project, namespace: user.namespace) } let_it_be(:milestone1) { create(:milestone, title: 'Foo', project: project) } diff --git a/spec/features/search/user_searches_for_wiki_pages_spec.rb b/spec/features/search/user_searches_for_wiki_pages_spec.rb index 1d8bdc58ce6..65f262075f9 100644 --- a/spec/features/search/user_searches_for_wiki_pages_spec.rb +++ b/spec/features/search/user_searches_for_wiki_pages_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe 'User searches for wiki pages', :js, :clean_gitlab_redis_rate_limiting, -feature_category: :global_search do + feature_category: :global_search do let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project, :repository, :wiki_repo, namespace: user.namespace) } let_it_be(:wiki_page) do diff --git a/spec/features/snippets/spam_snippets_spec.rb b/spec/features/snippets/spam_snippets_spec.rb index 0e3f96906de..c2ac33a3b8f 100644 --- a/spec/features/snippets/spam_snippets_spec.rb +++ b/spec/features/snippets/spam_snippets_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe 'snippet editor with spam', skip: "Will be handled in https://gitlab.com/gitlab-org/gitlab/-/issues/217722", - feature_category: :source_code_management do + feature_category: :source_code_management do include_context 'includes Spam constants' let_it_be(:user) { create(:user) } diff --git a/spec/features/snippets/user_creates_snippet_spec.rb b/spec/features/snippets/user_creates_snippet_spec.rb index f4b6b552d46..090d854081a 100644 --- a/spec/features/snippets/user_creates_snippet_spec.rb +++ b/spec/features/snippets/user_creates_snippet_spec.rb @@ -81,8 +81,10 @@ RSpec.describe 'User creates snippet', :js, feature_category: :source_code_manag context 'when snippets default visibility level is restricted' do before do - stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PRIVATE], - default_snippet_visibility: Gitlab::VisibilityLevel::PRIVATE) + stub_application_setting( + restricted_visibility_levels: [Gitlab::VisibilityLevel::PRIVATE], + default_snippet_visibility: Gitlab::VisibilityLevel::PRIVATE + ) end it 'creates a snippet using the lowest available visibility level as default' do diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb index 8a9d2ff42d9..beadeab1736 100644 --- a/spec/features/task_lists_spec.rb +++ b/spec/features/task_lists_spec.rb @@ -137,8 +137,7 @@ RSpec.describe 'Task Lists', :js, feature_category: :team_planning do describe 'multiple tasks' do let!(:note) do - create(:note, note: markdown, noteable: issue, - project: project, author: user) + create(:note, note: markdown, noteable: issue, project: project, author: user) end it 'renders for note body' do @@ -171,8 +170,7 @@ RSpec.describe 'Task Lists', :js, feature_category: :team_planning do describe 'single incomplete task' do let!(:note) do - create(:note, note: single_incomplete_markdown, noteable: issue, - project: project, author: user) + create(:note, note: single_incomplete_markdown, noteable: issue, project: project, author: user) end it 'renders for note body' do @@ -186,8 +184,7 @@ RSpec.describe 'Task Lists', :js, feature_category: :team_planning do describe 'single complete task' do let!(:note) do - create(:note, note: single_complete_markdown, noteable: issue, - project: project, author: user) + create(:note, note: single_complete_markdown, noteable: issue, project: project, author: user) end it 'renders for note body' do diff --git a/spec/features/user_sees_revert_modal_spec.rb b/spec/features/user_sees_revert_modal_spec.rb index aca32d26bdb..2b0ff2cdf01 100644 --- a/spec/features/user_sees_revert_modal_spec.rb +++ b/spec/features/user_sees_revert_modal_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe 'Merge request > User sees revert modal', :js, :sidekiq_might_not_need_inline, -feature_category: :code_review_workflow do + feature_category: :code_review_workflow do let(:project) { create(:project, :public, :repository) } let(:user) { project.creator } let(:merge_request) { create(:merge_request, source_project: project) } diff --git a/spec/features/users/email_verification_on_login_spec.rb b/spec/features/users/email_verification_on_login_spec.rb index 481ff52b800..1854e812b73 100644 --- a/spec/features/users/email_verification_on_login_spec.rb +++ b/spec/features/users/email_verification_on_login_spec.rb @@ -358,10 +358,12 @@ RSpec.describe 'Email Verification On Login', :clean_gitlab_redis_rate_limiting, def expect_log_message(event = nil, times = 1, reason: '', message: nil) expect(Gitlab::AppLogger).to have_received(:info) .exactly(times).times - .with(message || hash_including(message: 'Email Verification', - event: event, - username: user.username, - ip: '127.0.0.1', - reason: reason)) + .with(message || hash_including( + message: 'Email Verification', + event: event, + username: user.username, + ip: '127.0.0.1', + reason: reason + )) end end diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb index 5529f0fa49e..047590fb3aa 100644 --- a/spec/features/users/login_spec.rb +++ b/spec/features/users/login_spec.rb @@ -390,8 +390,12 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_ end before do - stub_omniauth_saml_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'], - providers: [mock_saml_config_with_upstream_two_factor_authn_contexts]) + stub_omniauth_saml_config( + enabled: true, + auto_link_saml_user: true, + allow_single_sign_on: ['saml'], + providers: [mock_saml_config_with_upstream_two_factor_authn_contexts] + ) end it 'displays the remember me checkbox' do @@ -415,8 +419,10 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_ context 'when authn_context is worth two factors' do let(:mock_saml_response) do File.read('spec/fixtures/authentication/saml_response.xml') - .gsub('urn:oasis:names:tc:SAML:2.0:ac:classes:Password', - 'urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS') + .gsub( + 'urn:oasis:names:tc:SAML:2.0:ac:classes:Password', + 'urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS' + ) end it 'signs user in without prompting for second factor' do @@ -991,8 +997,7 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_ context 'when the user already enabled 2FA' do before do - user.update!(otp_required_for_login: true, - otp_secret: User.generate_otp_secret(32)) + user.update!(otp_required_for_login: true, otp_secret: User.generate_otp_secret(32)) end it 'asks the user to accept the terms' do diff --git a/spec/features/users/overview_spec.rb b/spec/features/users/overview_spec.rb index ff903358931..fdd0c38a718 100644 --- a/spec/features/users/overview_spec.rb +++ b/spec/features/users/overview_spec.rb @@ -9,12 +9,14 @@ RSpec.describe 'Overview tab on a user profile', :js, feature_category: :user_pr def push_code_contribution event = create(:push_event, project: contributed_project, author: user) - create(:push_event_payload, - event: event, - commit_from: '11f9ac0a48b62cef25eedede4c1819964f08d5ce', - commit_to: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2', - commit_count: 3, - ref: 'master') + create( + :push_event_payload, + event: event, + commit_from: '11f9ac0a48b62cef25eedede4c1819964f08d5ce', + commit_to: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2', + commit_count: 3, + ref: 'master' + ) end before do diff --git a/spec/frontend/diffs/components/diff_discussions_spec.js b/spec/frontend/diffs/components/diff_discussions_spec.js index 73d9f2d6d45..40c617da0aa 100644 --- a/spec/frontend/diffs/components/diff_discussions_spec.js +++ b/spec/frontend/diffs/components/diff_discussions_spec.js @@ -25,11 +25,13 @@ describe('DiffDiscussions', () => { }); }; + const findNoteableDiscussion = () => wrapper.findComponent(NoteableDiscussion); + describe('template', () => { it('should have notes list', () => { createComponent(); - expect(wrapper.findComponent(NoteableDiscussion).exists()).toBe(true); + expect(findNoteableDiscussion().exists()).toBe(true); expect(wrapper.findComponent(DiscussionNotes).exists()).toBe(true); expect( wrapper.findComponent(DiscussionNotes).findAllComponents(TimelineEntryItem).length, @@ -51,11 +53,11 @@ describe('DiffDiscussions', () => { it('dispatches toggleDiscussion when clicking collapse button', () => { createComponent({ shouldCollapseDiscussions: true }); - jest.spyOn(wrapper.vm.$store, 'dispatch').mockImplementation(); - const diffNotesToggle = findDiffNotesToggle(); - diffNotesToggle.trigger('click'); + jest.spyOn(store, 'dispatch').mockImplementation(); + + findDiffNotesToggle().trigger('click'); - expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith('toggleDiscussion', { + expect(store.dispatch).toHaveBeenCalledWith('toggleDiscussion', { discussionId: discussionsMockData.id, }); }); @@ -77,12 +79,12 @@ describe('DiffDiscussions', () => { discussions[0].expanded = false; createComponent({ discussions, shouldCollapseDiscussions: true }); - expect(wrapper.findComponent(NoteableDiscussion).isVisible()).toBe(false); + expect(findNoteableDiscussion().isVisible()).toBe(false); }); it('renders badge on avatar', () => { createComponent({ renderAvatarBadge: true }); - const noteableDiscussion = wrapper.findComponent(NoteableDiscussion); + const noteableDiscussion = findNoteableDiscussion(); expect(noteableDiscussion.find('.design-note-pin').exists()).toBe(true); expect(noteableDiscussion.find('.design-note-pin').text().trim()).toBe('1'); diff --git a/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap b/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap index 296d7924243..02d17b8dfd2 100644 --- a/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap +++ b/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap @@ -16,14 +16,16 @@ exports[`~/vue_merge_request_widget/components/widget/dynamic_content.vue render </div> <div class=\\"gl-display-flex gl-align-items-baseline\\"> <status-icon-stub level=\\"2\\" name=\\"MyWidget\\" iconname=\\"success\\"></status-icon-stub> - <div class=\\"gl-display-flex gl-flex-direction-column\\"> - <div> - <p class=\\"gl-mb-0\\">Main text for the row</p> - <gl-link-stub href=\\"https://gitlab.com\\">Optional link to display after text</gl-link-stub> - <!----> - <gl-badge-stub size=\\"md\\" variant=\\"info\\" iconsize=\\"md\\"> - Badge is optional. Text to be displayed inside badge - </gl-badge-stub> + <div class=\\"gl-w-full gl-display-flex\\"> + <div class=\\"gl-display-flex gl-flex-grow-1\\"> + <div class=\\"gl-display-flex gl-flex-grow-1 gl-flex-direction-column\\"> + <p class=\\"gl-mb-0 gl-mr-1\\">Main text for the row</p> + <gl-link-stub href=\\"https://gitlab.com\\">Optional link to display after text</gl-link-stub> + <!----> + <gl-badge-stub size=\\"md\\" variant=\\"info\\" iconsize=\\"md\\"> + Badge is optional. Text to be displayed inside badge + </gl-badge-stub> + </div> <actions-stub widget=\\"MyWidget\\" tertiarybuttons=\\"\\" class=\\"gl-ml-auto gl-pl-3\\"></actions-stub> <p class=\\"gl-m-0 gl-font-sm\\">Optional: Smaller sub-text to be displayed below the main text</p> </div> @@ -40,12 +42,14 @@ exports[`~/vue_merge_request_widget/components/widget/dynamic_content.vue render </div> <div class=\\"gl-display-flex gl-align-items-baseline\\"> <!----> - <div class=\\"gl-display-flex gl-flex-direction-column\\"> - <div> - <p class=\\"gl-mb-0\\">This is recursive. It will be listed in level 3.</p> - <!----> - <!----> - <!----> + <div class=\\"gl-w-full gl-display-flex\\"> + <div class=\\"gl-display-flex gl-flex-grow-1\\"> + <div class=\\"gl-display-flex gl-flex-grow-1 gl-flex-direction-column\\"> + <p class=\\"gl-mb-0 gl-mr-1\\">This is recursive. It will be listed in level 3.</p> + <!----> + <!----> + <!----> + </div> <actions-stub widget=\\"MyWidget\\" tertiarybuttons=\\"\\" class=\\"gl-ml-auto gl-pl-3\\"></actions-stub> <!----> </div> diff --git a/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js index 4972c522733..9343a3a5e90 100644 --- a/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js @@ -9,6 +9,7 @@ import ActionButtons from '~/vue_merge_request_widget/components/widget/action_b import Widget from '~/vue_merge_request_widget/components/widget/widget.vue'; import WidgetContentRow from '~/vue_merge_request_widget/components/widget/widget_content_row.vue'; import * as logger from '~/lib/logger'; +import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; jest.mock('~/vue_merge_request_widget/components/extensions/telemetry', () => ({ @@ -29,7 +30,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => { const findHelpPopover = () => wrapper.findComponent(HelpPopover); const findDynamicScroller = () => wrapper.findByTestId('dynamic-content-scroller'); - const createComponent = ({ propsData, slots, mountFn = shallowMountExtended } = {}) => { + const createComponent = async ({ propsData, slots, mountFn = shallowMountExtended } = {}) => { wrapper = mountFn(Widget, { propsData: { isCollapsible: false, @@ -49,6 +50,8 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => { ContentRow: WidgetContentRow, }, }); + + await axios.waitForAll(); }; describe('on mount', () => { @@ -105,9 +108,9 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => { }, }); - expect(wrapper.text()).not.toContain('Loading'); - await nextTick(); expect(wrapper.text()).toContain('Loading'); + await axios.waitForAll(); + expect(wrapper.text()).not.toContain('Loading'); }); it('validates widget name', () => { @@ -185,10 +188,10 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => { }); describe('content', () => { - it('displays summary property when summary slot is not provided', () => { - createComponent({ + it('displays summary property when summary slot is not provided', async () => { + await createComponent({ propsData: { - summary: 'Hello world', + summary: { title: 'Hello world' }, }, }); @@ -256,8 +259,8 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => { }); describe('handle collapse toggle', () => { - it('displays the toggle button correctly', () => { - createComponent({ + it('displays the toggle button correctly', async () => { + await createComponent({ propsData: { isCollapsible: true, }, @@ -271,7 +274,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => { }); it('does not display the content slot until toggle is clicked', async () => { - createComponent({ + await createComponent({ propsData: { isCollapsible: true, }, @@ -286,8 +289,8 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => { expect(findExpandedSection().text()).toBe('More complex content'); }); - it('emits a toggle even when button is toggled', () => { - createComponent({ + it('emits a toggle even when button is toggled', async () => { + await createComponent({ propsData: { isCollapsible: true, }, @@ -301,8 +304,8 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => { expect(wrapper.emitted('toggle')).toEqual([[{ expanded: true }]]); }); - it('does not display the toggle button if isCollapsible is false', () => { - createComponent({ + it('does not display the toggle button if isCollapsible is false', async () => { + await createComponent({ propsData: { isCollapsible: false, }, @@ -326,7 +329,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => { const fetchExpandedData = jest.fn().mockResolvedValue(mockDataExpanded); - createComponent({ + await createComponent({ propsData: { isCollapsible: true, fetchCollapsedData: () => Promise.resolve(mockDataCollapsed), @@ -358,7 +361,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => { it('allows refetching when fetch expanded data returns an error', async () => { const fetchExpandedData = jest.fn().mockRejectedValue({ error: true }); - createComponent({ + await createComponent({ propsData: { isCollapsible: true, fetchExpandedData, @@ -385,7 +388,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => { it('resets the error message when another request is fetched', async () => { const fetchExpandedData = jest.fn().mockRejectedValue({ error: true }); - createComponent({ + await createComponent({ propsData: { isCollapsible: true, fetchExpandedData, @@ -465,8 +468,8 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => { }, ]; - beforeEach(() => { - createComponent({ + beforeEach(async () => { + await createComponent({ mountFn: mountExtended, propsData: { isCollapsible: true, diff --git a/spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js b/spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js index 5baed8ff211..6aa12c37374 100644 --- a/spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js +++ b/spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js @@ -5,9 +5,7 @@ import api from '~/api'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status'; import Poll from '~/lib/utils/poll'; -import extensionsContainer from '~/vue_merge_request_widget/components/extensions/container'; -import { registerExtension } from '~/vue_merge_request_widget/components/extensions'; -import terraformExtension from '~/vue_merge_request_widget/extensions/terraform'; +import terraformExtension from '~/vue_merge_request_widget/extensions/terraform/index.vue'; import { plans, validPlanWithName, @@ -25,22 +23,20 @@ describe('Terraform extension', () => { const endpoint = '/path/to/terraform/report.json'; const findListItem = (at) => wrapper.findAllByTestId('extension-list-item').at(at); - - registerExtension(terraformExtension); + const findActionButton = (at) => wrapper.findAllByTestId('extension-actions-button').at(at); const mockPollingApi = (response, body, header) => { mock.onGet(endpoint).reply(response, body, header); }; const createComponent = () => { - wrapper = mountExtended(extensionsContainer, { + wrapper = mountExtended(terraformExtension, { propsData: { mr: { terraformReportsPath: endpoint, }, }, }); - return axios.waitForAll(); }; beforeEach(() => { @@ -54,24 +50,27 @@ describe('Terraform extension', () => { describe('summary', () => { describe('while loading', () => { const loadingText = 'Loading Terraform reports...'; + it('should render loading text', async () => { mockPollingApi(HTTP_STATUS_OK, plans, {}); createComponent(); expect(wrapper.text()).toContain(loadingText); + await waitForPromises(); expect(wrapper.text()).not.toContain(loadingText); }); }); describe('when the fetching fails', () => { - beforeEach(() => { + beforeEach(async () => { mockPollingApi(HTTP_STATUS_INTERNAL_SERVER_ERROR, null, {}); - return createComponent(); + createComponent(); + await axios.waitForAll(); }); - it('should generate one invalid plan and render correct summary text', () => { - expect(wrapper.text()).toContain('1 Terraform report failed to generate'); + it('should show the error text', () => { + expect(wrapper.text()).toContain('Failed to load Terraform reports'); }); }); @@ -82,9 +81,10 @@ describe('Terraform extension', () => { ${'2 valid reports'} | ${{ 0: validPlanWithName, 1: validPlanWithName }} | ${'2 Terraform reports were generated in your pipelines'} | ${''} ${'1 valid and 2 invalid reports'} | ${{ 0: validPlanWithName, 1: invalidPlanWithName, 2: invalidPlanWithName }} | ${'Terraform report was generated in your pipelines'} | ${'2 Terraform reports failed to generate'} `('and received $responseType', ({ response, summaryTitle, summarySubtitle }) => { - beforeEach(() => { + beforeEach(async () => { mockPollingApi(HTTP_STATUS_OK, response, {}); - return createComponent(); + createComponent(); + await axios.waitForAll(); }); it(`should render correct summary text`, () => { @@ -101,7 +101,8 @@ describe('Terraform extension', () => { describe('expanded data', () => { beforeEach(async () => { mockPollingApi(HTTP_STATUS_OK, plans, {}); - await createComponent(); + createComponent(); + await axios.waitForAll(); wrapper.findByTestId('toggle-button').trigger('click'); }); @@ -136,7 +137,7 @@ describe('Terraform extension', () => { api.trackRedisHllUserEvent.mockClear(); api.trackRedisCounterEvent.mockClear(); - findListItem(0).find('[data-testid="extension-actions-button"]').trigger('click'); + findActionButton(0).trigger('click'); expect(api.trackRedisHllUserEvent).toHaveBeenCalledTimes(1); expect(api.trackRedisHllUserEvent).toHaveBeenCalledWith( @@ -161,10 +162,10 @@ describe('Terraform extension', () => { }); describe('successful poll', () => { - beforeEach(() => { + beforeEach(async () => { mockPollingApi(HTTP_STATUS_OK, plans, {}); - - return createComponent(); + createComponent(); + await axios.waitForAll(); }); it('does not make additional requests after poll is successful', () => { @@ -173,13 +174,14 @@ describe('Terraform extension', () => { }); describe('polling fails', () => { - beforeEach(() => { + beforeEach(async () => { mockPollingApi(HTTP_STATUS_INTERNAL_SERVER_ERROR, null, {}); - return createComponent(); + createComponent(); + await axios.waitForAll(); }); - it('generates one broken plan', () => { - expect(wrapper.text()).toContain('1 Terraform report failed to generate'); + it('renders the error text', () => { + expect(wrapper.text()).toContain('Failed to load Terraform reports'); }); it('does not make additional requests after poll is unsuccessful', () => { diff --git a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js index 2e901783e07..18d7b2397c9 100644 --- a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js +++ b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js @@ -1,9 +1,10 @@ import { GlDisclosureDropdown } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { createMockDirective } from 'helpers/vue_mock_directive'; +import { stubComponent } from 'helpers/stub_component'; import EmojiPicker from '~/emoji/components/picker.vue'; import waitForPromises from 'helpers/wait_for_promises'; import ReplyButton from '~/notes/components/note_actions/reply_button.vue'; @@ -15,18 +16,19 @@ Vue.use(VueApollo); describe('Work Item Note Actions', () => { let wrapper; const noteId = '1'; + const showSpy = jest.fn(); const findReplyButton = () => wrapper.findComponent(ReplyButton); - const findEditButton = () => wrapper.find('[data-testid="edit-work-item-note"]'); - const findEmojiButton = () => wrapper.find('[data-testid="note-emoji-button"]'); + const findEditButton = () => wrapper.findByTestId('edit-work-item-note'); + const findEmojiButton = () => wrapper.findByTestId('note-emoji-button'); const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown); - const findDeleteNoteButton = () => wrapper.find('[data-testid="delete-note-action"]'); - const findCopyLinkButton = () => wrapper.find('[data-testid="copy-link-action"]'); - const findAssignUnassignButton = () => wrapper.find('[data-testid="assign-note-action"]'); - const findReportAbuseToAdminButton = () => wrapper.find('[data-testid="abuse-note-action"]'); - const findAuthorBadge = () => wrapper.find('[data-testid="author-badge"]'); - const findMaxAccessLevelBadge = () => wrapper.find('[data-testid="max-access-level-badge"]'); - const findContributorBadge = () => wrapper.find('[data-testid="contributor-badge"]'); + const findDeleteNoteButton = () => wrapper.findByTestId('delete-note-action'); + const findCopyLinkButton = () => wrapper.findByTestId('copy-link-action'); + const findAssignUnassignButton = () => wrapper.findByTestId('assign-note-action'); + const findReportAbuseToAdminButton = () => wrapper.findByTestId('abuse-note-action'); + const findAuthorBadge = () => wrapper.findByTestId('author-badge'); + const findMaxAccessLevelBadge = () => wrapper.findByTestId('max-access-level-badge'); + const findContributorBadge = () => wrapper.findByTestId('contributor-badge'); const addEmojiMutationResolver = jest.fn().mockResolvedValue({ data: { @@ -51,7 +53,7 @@ describe('Work Item Note Actions', () => { maxAccessLevelOfAuthor = '', projectName = 'Project name', } = {}) => { - wrapper = shallowMount(WorkItemNoteActions, { + wrapper = shallowMountExtended(WorkItemNoteActions, { propsData: { showReply, showEdit, @@ -72,15 +74,21 @@ describe('Work Item Note Actions', () => { }, stubs: { EmojiPicker: EmojiPickerStub, + GlDisclosureDropdown: stubComponent(GlDisclosureDropdown, { + methods: { close: showSpy }, + }), }, apolloProvider: createMockApollo([[addAwardEmojiMutation, addEmojiMutationResolver]]), directives: { GlTooltip: createMockDirective('gl-tooltip'), }, }); - wrapper.vm.$refs.dropdown.close = jest.fn(); }; + afterEach(() => { + showSpy.mockClear(); + }); + describe('reply button', () => { it('is visible by default', () => { createComponent(); @@ -173,6 +181,7 @@ describe('Work Item Note Actions', () => { findDeleteNoteButton().vm.$emit('action'); expect(wrapper.emitted('deleteNote')).toEqual([[]]); + expect(showSpy).toHaveBeenCalled(); }); }); @@ -188,6 +197,7 @@ describe('Work Item Note Actions', () => { findCopyLinkButton().vm.$emit('action'); expect(wrapper.emitted('notifyCopyDone')).toEqual([[]]); + expect(showSpy).toHaveBeenCalled(); }); }); @@ -214,6 +224,7 @@ describe('Work Item Note Actions', () => { findAssignUnassignButton().vm.$emit('action'); expect(wrapper.emitted('assignUser')).toEqual([[]]); + expect(showSpy).toHaveBeenCalled(); }); }); @@ -240,6 +251,7 @@ describe('Work Item Note Actions', () => { findReportAbuseToAdminButton().vm.$emit('action'); expect(wrapper.emitted('reportAbuse')).toEqual([[]]); + expect(showSpy).toHaveBeenCalled(); }); }); diff --git a/spec/frontend/work_items/components/work_item_description_spec.js b/spec/frontend/work_items/components/work_item_description_spec.js index b910e9854f8..2b3fdd1b31c 100644 --- a/spec/frontend/work_items/components/work_item_description_spec.js +++ b/spec/frontend/work_items/components/work_item_description_spec.js @@ -12,14 +12,12 @@ import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue import WorkItemDescription from '~/work_items/components/work_item_description.vue'; import WorkItemDescriptionRendered from '~/work_items/components/work_item_description_rendered.vue'; import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants'; -import workItemDescriptionSubscription from '~/work_items/graphql/work_item_description.subscription.graphql'; import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql'; import { autocompleteDataSources, markdownPreviewPath } from '~/work_items/utils'; import { updateWorkItemMutationResponse, workItemByIidResponseFactory, - workItemDescriptionSubscriptionResponse, workItemQueryResponse, } from '../mock_data'; @@ -34,7 +32,6 @@ describe('WorkItemDescription', () => { Vue.use(VueApollo); const mutationSuccessHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse); - const subscriptionHandler = jest.fn().mockResolvedValue(workItemDescriptionSubscriptionResponse); let workItemResponseHandler; const findForm = () => wrapper.findComponent(GlForm); @@ -63,7 +60,6 @@ describe('WorkItemDescription', () => { apolloProvider: createMockApollo([ [workItemByIidQuery, workItemResponseHandler], [updateWorkItemMutation, mutationHandler], - [workItemDescriptionSubscription, subscriptionHandler], ]), propsData: { workItemId: id, @@ -83,14 +79,6 @@ describe('WorkItemDescription', () => { } }; - it('has a subscription', async () => { - await createComponent(); - - expect(subscriptionHandler).toHaveBeenCalledWith({ - issuableId: workItemQueryResponse.data.workItem.id, - }); - }); - describe('editing description', () => { it('passes correct autocompletion data and preview markdown sources and enables quick actions', async () => { const { diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js index d8ba8ea74f2..0266533a11c 100644 --- a/spec/frontend/work_items/components/work_item_detail_spec.js +++ b/spec/frontend/work_items/components/work_item_detail_spec.js @@ -31,20 +31,13 @@ import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_sel import WorkItemTodos from '~/work_items/components/work_item_todos.vue'; import { i18n } from '~/work_items/constants'; import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql'; -import workItemDatesSubscription from '~/graphql_shared/subscriptions/work_item_dates.subscription.graphql'; -import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql'; -import workItemAssigneesSubscription from '~/work_items/graphql/work_item_assignees.subscription.graphql'; -import workItemMilestoneSubscription from '~/work_items/graphql/work_item_milestone.subscription.graphql'; import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; import updateWorkItemTaskMutation from '~/work_items/graphql/update_work_item_task.mutation.graphql'; +import workItemUpdatedSubscription from '~/work_items/graphql/work_item_updated.subscription.graphql'; import { mockParent, - workItemDatesSubscriptionResponse, workItemByIidResponseFactory, - workItemTitleSubscriptionResponse, - workItemAssigneesSubscriptionResponse, - workItemMilestoneSubscriptionResponse, objectiveType, mockWorkItemCommentNote, } from '../mock_data'; @@ -63,16 +56,11 @@ describe('WorkItemDetail component', () => { canDelete: true, }); const successHandler = jest.fn().mockResolvedValue(workItemQueryResponse); - const datesSubscriptionHandler = jest.fn().mockResolvedValue(workItemDatesSubscriptionResponse); - const titleSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse); - const milestoneSubscriptionHandler = jest - .fn() - .mockResolvedValue(workItemMilestoneSubscriptionResponse); - const assigneesSubscriptionHandler = jest - .fn() - .mockResolvedValue(workItemAssigneesSubscriptionResponse); const showModalHandler = jest.fn(); const { id } = workItemQueryResponse.data.workspace.workItems.nodes[0]; + const workItemUpdatedSubscriptionHandler = jest + .fn() + .mockResolvedValue({ data: { workItemUpdated: null } }); const findAlert = () => wrapper.findComponent(GlAlert); const findEmptyState = () => wrapper.findComponent(GlEmptyState); @@ -102,17 +90,13 @@ describe('WorkItemDetail component', () => { updateInProgress = false, workItemIid = '1', handler = successHandler, - subscriptionHandler = titleSubscriptionHandler, confidentialityMock = [updateWorkItemMutation, jest.fn()], error = undefined, workItemsMvc2Enabled = false, } = {}) => { const handlers = [ [workItemByIidQuery, handler], - [workItemTitleSubscription, subscriptionHandler], - [workItemDatesSubscription, datesSubscriptionHandler], - [workItemAssigneesSubscription, assigneesSubscriptionHandler], - [workItemMilestoneSubscription, milestoneSubscriptionHandler], + [workItemUpdatedSubscription, workItemUpdatedSubscriptionHandler], confidentialityMock, ]; @@ -163,13 +147,18 @@ describe('WorkItemDetail component', () => { }); describe('when there is no `workItemIid` prop', () => { - beforeEach(() => { + beforeEach(async () => { createComponent({ workItemIid: null }); + await waitForPromises(); }); it('skips the work item query', () => { expect(successHandler).not.toHaveBeenCalled(); }); + + it('skips the work item updated subscription', () => { + expect(workItemUpdatedSubscriptionHandler).not.toHaveBeenCalled(); + }); }); describe('when loading', () => { @@ -203,6 +192,10 @@ describe('WorkItemDetail component', () => { it('renders todos widget if logged in', () => { expect(findWorkItemTodos().exists()).toBe(true); }); + + it('calls the work item updated subscription', () => { + expect(workItemUpdatedSubscriptionHandler).toHaveBeenCalledWith({ id }); + }); }); describe('close button', () => { @@ -487,60 +480,6 @@ describe('WorkItemDetail component', () => { expect(findAlert().text()).toBe(updateError); }); - - describe('subscriptions', () => { - it('calls the title subscription', async () => { - createComponent(); - await waitForPromises(); - - expect(titleSubscriptionHandler).toHaveBeenCalledWith({ issuableId: id }); - }); - - describe('assignees subscription', () => { - describe('when the assignees widget exists', () => { - it('calls the assignees subscription', async () => { - createComponent(); - await waitForPromises(); - - expect(assigneesSubscriptionHandler).toHaveBeenCalledWith({ issuableId: id }); - }); - }); - - describe('when the assignees widget does not exist', () => { - it('does not call the assignees subscription', async () => { - const response = workItemByIidResponseFactory({ assigneesWidgetPresent: false }); - const handler = jest.fn().mockResolvedValue(response); - createComponent({ handler }); - await waitForPromises(); - - expect(assigneesSubscriptionHandler).not.toHaveBeenCalled(); - }); - }); - }); - - describe('dates subscription', () => { - describe('when the due date widget exists', () => { - it('calls the dates subscription', async () => { - createComponent(); - await waitForPromises(); - - expect(datesSubscriptionHandler).toHaveBeenCalledWith({ issuableId: id }); - }); - }); - - describe('when the due date widget does not exist', () => { - it('does not call the dates subscription', async () => { - const response = workItemByIidResponseFactory({ datesWidgetPresent: false }); - const handler = jest.fn().mockResolvedValue(response); - createComponent({ handler }); - await waitForPromises(); - - expect(datesSubscriptionHandler).not.toHaveBeenCalled(); - }); - }); - }); - }); - describe('assignees widget', () => { it('renders assignees component when widget is returned from the API', async () => { createComponent(); @@ -617,28 +556,6 @@ describe('WorkItemDetail component', () => { expect(findWorkItemMilestone().exists()).toBe(exists); }); - - describe('milestone subscription', () => { - describe('when the milestone widget exists', () => { - it('calls the milestone subscription', async () => { - createComponent(); - await waitForPromises(); - - expect(milestoneSubscriptionHandler).toHaveBeenCalledWith({ issuableId: id }); - }); - }); - - describe('when the assignees widget does not exist', () => { - it('does not call the milestone subscription', async () => { - const response = workItemByIidResponseFactory({ milestoneWidgetPresent: false }); - const handler = jest.fn().mockResolvedValue(response); - createComponent({ handler }); - await waitForPromises(); - - expect(milestoneSubscriptionHandler).not.toHaveBeenCalled(); - }); - }); - }); }); it('calls the work item query', async () => { diff --git a/spec/frontend/work_items/components/work_item_labels_spec.js b/spec/frontend/work_items/components/work_item_labels_spec.js index 6894aa236e3..4a20e654060 100644 --- a/spec/frontend/work_items/components/work_item_labels_spec.js +++ b/spec/frontend/work_items/components/work_item_labels_spec.js @@ -6,7 +6,6 @@ import waitForPromises from 'helpers/wait_for_promises'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import labelSearchQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql'; -import workItemLabelsSubscription from 'ee_else_ce/work_items/graphql/work_item_labels.subscription.graphql'; import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql'; import WorkItemLabels from '~/work_items/components/work_item_labels.vue'; @@ -16,7 +15,6 @@ import { mockLabels, workItemByIidResponseFactory, updateWorkItemMutationResponse, - workItemLabelsSubscriptionResponse, } from '../mock_data'; Vue.use(VueApollo); @@ -38,7 +36,6 @@ describe('WorkItemLabels component', () => { const successUpdateWorkItemMutationHandler = jest .fn() .mockResolvedValue(updateWorkItemMutationResponse); - const subscriptionHandler = jest.fn().mockResolvedValue(workItemLabelsSubscriptionResponse); const errorHandler = jest.fn().mockRejectedValue('Houston, we have a problem'); const createComponent = ({ @@ -53,7 +50,6 @@ describe('WorkItemLabels component', () => { [workItemByIidQuery, workItemQueryHandler], [labelSearchQuery, searchQueryHandler], [updateWorkItemMutation, updateWorkItemMutationHandler], - [workItemLabelsSubscription, subscriptionHandler], ]), provide: { fullPath: 'test-project-path', @@ -246,16 +242,6 @@ describe('WorkItemLabels component', () => { expect(updateWorkItemMutationHandler).not.toHaveBeenCalled(); }); - - it('has a subscription', async () => { - createComponent(); - - await waitForPromises(); - - expect(subscriptionHandler).toHaveBeenCalledWith({ - issuableId: workItemId, - }); - }); }); it('calls the work item query', async () => { diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index a873462ea63..c9c73d29903 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -803,154 +803,6 @@ export const deleteWorkItemMutationErrorResponse = { }, }; -export const workItemDatesSubscriptionResponse = { - data: { - issuableDatesUpdated: { - id: 'gid://gitlab/WorkItem/1', - widgets: [ - { - __typename: 'WorkItemWidgetStartAndDueDate', - dueDate: '2022-12-31', - startDate: '2022-01-01', - }, - ], - }, - }, -}; - -export const workItemTitleSubscriptionResponse = { - data: { - issuableTitleUpdated: { - id: 'gid://gitlab/WorkItem/1', - title: 'new title', - }, - }, -}; - -export const workItemDescriptionSubscriptionResponse = { - data: { - issuableDescriptionUpdated: { - id: 'gid://gitlab/WorkItem/1', - widgets: [ - { - __typename: 'WorkItemWidgetDescription', - type: 'DESCRIPTION', - description: 'New description', - descriptionHtml: '<p>New description</p>', - lastEditedAt: '2022-09-21T06:18:42Z', - lastEditedBy: { - id: 'gid://gitlab/User/2', - name: 'Someone else', - webPath: '/not-you', - }, - }, - ], - }, - }, -}; - -export const workItemWeightSubscriptionResponse = { - data: { - issuableWeightUpdated: { - id: 'gid://gitlab/WorkItem/1', - widgets: [ - { - __typename: 'WorkItemWidgetWeight', - weight: 1, - }, - ], - }, - }, -}; - -export const workItemAssigneesSubscriptionResponse = { - data: { - issuableAssigneesUpdated: { - id: 'gid://gitlab/WorkItem/1', - widgets: [ - { - __typename: 'WorkItemAssigneesWeight', - assignees: { - nodes: [mockAssignees[0]], - }, - }, - ], - }, - }, -}; - -export const workItemLabelsSubscriptionResponse = { - data: { - issuableLabelsUpdated: { - id: 'gid://gitlab/WorkItem/1', - widgets: [ - { - __typename: 'WorkItemWidgetLabels', - type: 'LABELS', - allowsScopedLabels: false, - labels: { - nodes: mockLabels, - }, - }, - ], - }, - }, -}; - -export const workItemIterationSubscriptionResponse = { - data: { - issuableIterationUpdated: { - id: 'gid://gitlab/WorkItem/1', - widgets: [ - { - __typename: 'WorkItemWidgetIteration', - iteration: { - description: 'Iteration description', - dueDate: '2022-07-29', - id: 'gid://gitlab/Iteration/1125', - iid: '95', - startDate: '2022-06-22', - title: 'Iteration subcription title', - }, - }, - ], - }, - }, -}; - -export const workItemHealthStatusSubscriptionResponse = { - data: { - issuableHealthStatusUpdated: { - id: 'gid://gitlab/WorkItem/1', - widgets: [ - { - __typename: 'WorkItemWidgetHealthStatus', - healthStatus: 'needsAttention', - }, - ], - }, - }, -}; - -export const workItemMilestoneSubscriptionResponse = { - data: { - issuableMilestoneUpdated: { - id: 'gid://gitlab/WorkItem/1', - widgets: [ - { - __typename: 'WorkItemWidgetMilestone', - type: 'MILESTONE', - milestone: { - id: 'gid://gitlab/Milestone/1125', - expired: false, - title: 'Milestone title', - }, - }, - ], - }, - }, -}; - export const workItemHierarchyEmptyResponse = { data: { workspace: { diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js index b5d54a7c319..4e31ee30552 100644 --- a/spec/frontend/work_items/router_spec.js +++ b/spec/frontend/work_items/router_spec.js @@ -2,28 +2,14 @@ import { mount } from '@vue/test-utils'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; -import { - currentUserResponse, - workItemAssigneesSubscriptionResponse, - workItemDatesSubscriptionResponse, - workItemByIidResponseFactory, - workItemTitleSubscriptionResponse, - workItemLabelsSubscriptionResponse, - workItemMilestoneSubscriptionResponse, - workItemDescriptionSubscriptionResponse, -} from 'jest/work_items/mock_data'; +import { currentUserResponse, workItemByIidResponseFactory } from 'jest/work_items/mock_data'; import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql'; import App from '~/work_items/components/app.vue'; import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql'; -import workItemDatesSubscription from '~/graphql_shared/subscriptions/work_item_dates.subscription.graphql'; -import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql'; -import workItemAssigneesSubscription from '~/work_items/graphql/work_item_assignees.subscription.graphql'; -import workItemLabelsSubscription from 'ee_else_ce/work_items/graphql/work_item_labels.subscription.graphql'; -import workItemMilestoneSubscription from '~/work_items/graphql/work_item_milestone.subscription.graphql'; -import workItemDescriptionSubscription from '~/work_items/graphql/work_item_description.subscription.graphql'; import CreateWorkItem from '~/work_items/pages/create_work_item.vue'; import WorkItemsRoot from '~/work_items/pages/work_item_root.vue'; import { createRouter } from '~/work_items/router'; +import workItemUpdatedSubscription from '~/work_items/graphql/work_item_updated.subscription.graphql'; jest.mock('~/behaviors/markdown/render_gfm'); @@ -34,18 +20,9 @@ describe('Work items router', () => { const workItemQueryHandler = jest.fn().mockResolvedValue(workItemByIidResponseFactory()); const currentUserQueryHandler = jest.fn().mockResolvedValue(currentUserResponse); - const datesSubscriptionHandler = jest.fn().mockResolvedValue(workItemDatesSubscriptionResponse); - const titleSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse); - const assigneesSubscriptionHandler = jest + const workItemUpdatedSubscriptionHandler = jest .fn() - .mockResolvedValue(workItemAssigneesSubscriptionResponse); - const labelsSubscriptionHandler = jest.fn().mockResolvedValue(workItemLabelsSubscriptionResponse); - const milestoneSubscriptionHandler = jest - .fn() - .mockResolvedValue(workItemMilestoneSubscriptionResponse); - const descriptionSubscriptionHandler = jest - .fn() - .mockResolvedValue(workItemDescriptionSubscriptionResponse); + .mockResolvedValue({ data: { workItemUpdated: null } }); const createComponent = async (routeArg) => { const router = createRouter('/work_item'); @@ -56,12 +33,7 @@ describe('Work items router', () => { const handlers = [ [workItemByIidQuery, workItemQueryHandler], [currentUserQuery, currentUserQueryHandler], - [workItemDatesSubscription, datesSubscriptionHandler], - [workItemTitleSubscription, titleSubscriptionHandler], - [workItemAssigneesSubscription, assigneesSubscriptionHandler], - [workItemLabelsSubscription, labelsSubscriptionHandler], - [workItemMilestoneSubscription, milestoneSubscriptionHandler], - [workItemDescriptionSubscription, descriptionSubscriptionHandler], + [workItemUpdatedSubscription, workItemUpdatedSubscriptionHandler], ]; wrapper = mount(App, { diff --git a/spec/migrations/cleanup_bigint_conversion_for_merge_request_metrics_for_self_hosts_spec.rb b/spec/migrations/cleanup_bigint_conversion_for_merge_request_metrics_for_self_hosts_spec.rb index b056ade9b7b..dc4adae9138 100644 --- a/spec/migrations/cleanup_bigint_conversion_for_merge_request_metrics_for_self_hosts_spec.rb +++ b/spec/migrations/cleanup_bigint_conversion_for_merge_request_metrics_for_self_hosts_spec.rb @@ -48,6 +48,7 @@ RSpec.describe CleanupBigintConversionForMergeRequestMetricsForSelfHosts, featur # As we call `schema_migrate_down!` before each example, and for this migration # `#down` is same as `#up`, we need to ensure we start from the expected state. connection = described_class.new.connection + connection.execute('ALTER TABLE merge_request_metrics ALTER COLUMN id TYPE bigint') connection.execute('ALTER TABLE merge_request_metrics DROP COLUMN IF EXISTS id_convert_to_bigint') end @@ -70,6 +71,7 @@ RSpec.describe CleanupBigintConversionForMergeRequestMetricsForSelfHosts, featur # As we call `schema_migrate_down!` before each example, and for this migration # `#down` is same as `#up`, we need to ensure we start from the expected state. connection = described_class.new.connection + connection.execute('ALTER TABLE merge_request_metrics ALTER COLUMN id TYPE bigint') connection.execute('ALTER TABLE merge_request_metrics ADD COLUMN IF NOT EXISTS id_convert_to_bigint integer') end diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb index 8e86518912c..fd60d279110 100644 --- a/spec/models/personal_access_token_spec.rb +++ b/spec/models/personal_access_token_spec.rb @@ -257,7 +257,7 @@ RSpec.describe PersonalAccessToken, feature_category: :system_access do end context 'validates expires_at' do - let(:max_expiration_date) { described_class::MAX_PERSONAL_ACCESS_TOKEN_LIFETIME_IN_DAYS.days.from_now } + let(:max_expiration_date) { Date.current + described_class::MAX_PERSONAL_ACCESS_TOKEN_LIFETIME_IN_DAYS } it "can't be blank" do personal_access_token.expires_at = nil @@ -274,12 +274,14 @@ RSpec.describe PersonalAccessToken, feature_category: :system_access do end end - context 'when expires_in is more than MAX_PERSONAL_ACCESS_TOKEN_LIFETIME_IN_DAYS days' do + context 'when expires_in is more than MAX_PERSONAL_ACCESS_TOKEN_LIFETIME_IN_DAYS days', :freeze_time do it 'is invalid' do personal_access_token.expires_at = max_expiration_date + 1.day expect(personal_access_token).not_to be_valid - expect(personal_access_token.errors[:expires_at].first).to eq('must expire in 365 days') + expect(personal_access_token.errors.full_messages.to_sentence).to eq( + "Expiration date must be before #{max_expiration_date}" + ) end end end diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index 7aa6980fb24..ccf58964c71 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -456,14 +456,63 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi end context 'repository removal' do - # 1. Project repository - # 2. Wiki repository - it 'removal of existing repos' do - expect_next_instances_of(Repositories::DestroyService, 2) do |instance| - expect(instance).to receive(:execute).and_return(status: :success) + describe '.trash_project_repositories!' do + let(:trash_project_repositories!) { described_class.new(project, user, {}).send(:trash_project_repositories!) } + + # Destroys 3 repositories: + # 1. Project repository + # 2. Wiki repository + # 3. Design repository + + it 'Repositories::DestroyService is called for existing repos' do + expect_next_instances_of(Repositories::DestroyService, 3) do |instance| + expect(instance).to receive(:execute).and_return(status: :success) + end + + trash_project_repositories! end - described_class.new(project, user, {}).execute + context 'when the removal has errors' do + using RSpec::Parameterized::TableSyntax + + let(:mock_error) { instance_double(Repositories::DestroyService, execute: { message: 'foo', status: :error }) } + let(:project_repository) { project.repository } + let(:wiki_repository) { project.wiki.repository } + let(:design_repository) { project.design_repository } + + where(:repo, :message) do + ref(:project_repository) | 'Failed to remove project repository. Please try again or contact administrator.' + ref(:wiki_repository) | 'Failed to remove wiki repository. Please try again or contact administrator.' + ref(:design_repository) | 'Failed to remove design repository. Please try again or contact administrator.' + end + + with_them do + before do + allow(Repositories::DestroyService).to receive(:new).with(anything).and_call_original + allow(Repositories::DestroyService).to receive(:new).with(repo).and_return(mock_error) + end + + it 'raises correct error' do + expect { trash_project_repositories! }.to raise_error(Projects::DestroyService::DestroyError, message) + end + end + end + end + + it 'removes project repository' do + expect { destroy_project(project, user, {}) }.to change { project.repository.exists? }.from(true).to(false) + end + + it 'removes wiki repository' do + project.create_wiki unless project.wiki.repository.exists? + + expect { destroy_project(project, user, {}) }.to change { project.wiki.repository.exists? }.from(true).to(false) + end + + it 'removes design repository' do + project.design_repository.create_if_not_exists + + expect { destroy_project(project, user, {}) }.to change { project.design_repository.exists? }.from(true).to(false) end end diff --git a/spec/services/resource_access_tokens/create_service_spec.rb b/spec/services/resource_access_tokens/create_service_spec.rb index 31e4e008d4f..940d9858cdd 100644 --- a/spec/services/resource_access_tokens/create_service_spec.rb +++ b/spec/services/resource_access_tokens/create_service_spec.rb @@ -199,16 +199,14 @@ RSpec.describe ResourceAccessTokens::CreateService, feature_category: :system_ac end end - context 'expiry of the project bot member' do - it 'project bot membership does not expire' do - response = subject - access_token = response.payload[:access_token] - project_bot = access_token.user + it 'project bot membership expires when PAT expires' do + response = subject + access_token = response.payload[:access_token] + project_bot = access_token.user - expect(resource.members.find_by(user_id: project_bot.id).expires_at).to eq( - max_pat_access_token_lifetime.to_date - ) - end + expect(resource.members.find_by(user_id: project_bot.id).expires_at).to eq( + max_pat_access_token_lifetime.to_date + ) end end diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml index adee31ca644..b9ce1f80c02 100644 --- a/spec/support/rspec_order_todo.yml +++ b/spec/support/rspec_order_todo.yml @@ -1213,8 +1213,6 @@ - './ee/spec/lib/elastic/latest/merge_request_config_spec.rb' - './ee/spec/lib/elastic/latest/note_config_spec.rb' - './ee/spec/lib/elastic/latest/project_instance_proxy_spec.rb' -- './ee/spec/lib/elastic/latest/project_wiki_class_proxy_spec.rb' -- './ee/spec/lib/elastic/latest/project_wiki_instance_proxy_spec.rb' - './ee/spec/lib/elastic/latest/routing_spec.rb' - './ee/spec/lib/elastic/latest/snippet_instance_proxy_spec.rb' - './ee/spec/lib/elastic/migration_spec.rb' |