summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.rubocop.yml3
-rw-r--r--.rubocop_todo.yml6
-rw-r--r--.rubocop_todo/migration/background_migration_base_class.yml92
-rw-r--r--app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue69
-rw-r--r--app/assets/javascripts/analytics/shared/constants.js11
-rw-r--r--app/assets/javascripts/boards/components/board_add_new_column_form.vue26
-rw-r--r--app/assets/javascripts/boards/components/board_content.vue2
-rw-r--r--app/assets/javascripts/cycle_analytics/components/base.vue3
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue28
-rw-r--r--app/assets/javascripts/sidebar/components/attention_requested_toggle.vue22
-rw-r--r--app/assets/javascripts/sidebar/queries/remove_attention_request.mutation.graphql7
-rw-r--r--app/assets/javascripts/sidebar/queries/request_attention.mutation.graphql5
-rw-r--r--app/assets/javascripts/sidebar/queries/toggle_attention_requested.mutation.graphql7
-rw-r--r--app/assets/javascripts/sidebar/services/sidebar_service.js17
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js11
-rw-r--r--app/assets/stylesheets/page_bundles/boards.scss1
-rw-r--r--app/controllers/import/bitbucket_controller.rb7
-rw-r--r--app/controllers/import/bitbucket_server_controller.rb7
-rw-r--r--app/controllers/import/fogbugz_controller.rb4
-rw-r--r--app/controllers/import/gitea_controller.rb4
-rw-r--r--app/controllers/import/gitlab_controller.rb3
-rw-r--r--app/controllers/import/manifest_controller.rb7
-rw-r--r--app/controllers/projects/alerting/notifications_controller.rb4
-rw-r--r--app/finders/tags_finder.rb4
-rw-r--r--app/graphql/resolvers/concerns/issue_resolver_arguments.rb6
-rw-r--r--app/helpers/namespaces_helper.rb1
-rw-r--r--app/services/merge_requests/create_service.rb4
-rw-r--r--app/views/admin/application_settings/_signin.html.haml57
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml5
-rw-r--r--config/events/1651052988_gitlab__usage_data_counters__ci_template_unique_counter_ci_templates_unique.yml23
-rw-r--r--config/events/1651053267_event_create_service_action_active_users_project_repo.yml23
-rw-r--r--doc/api/graphql/reference/index.md8
-rw-r--r--doc/development/database/batched_background_migrations.md72
-rw-r--r--doc/subscriptions/gitlab_dedicated/index.md5
-rw-r--r--doc/user/report_abuse.md6
-rw-r--r--lib/bitbucket/representation/repo.rb4
-rw-r--r--lib/bitbucket_server/representation/repo.rb4
-rw-r--r--lib/gitlab/background_migration/.rubocop.yml9
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/string.rb4
-rw-r--r--lib/gitlab/config/entry/validator.rb4
-rw-r--r--lib/gitlab/git/branch.rb4
-rw-r--r--lib/gitlab/reactive_cache_set_cache.rb4
-rw-r--r--lib/gitlab/snippet_search_results.rb4
-rw-r--r--locale/gitlab.pot12
-rw-r--r--qa/qa/resource/group_deploy_token.rb4
-rw-r--r--qa/qa/resource/project_access_token.rb4
-rw-r--r--qa/qa/resource/project_deploy_token.rb4
-rw-r--r--rubocop/cop/migration/background_migration_base_class.rb33
-rw-r--r--spec/frontend/cycle_analytics/value_stream_metrics_spec.js46
-rw-r--r--spec/frontend/pipelines/graph/linked_pipeline_spec.js42
-rw-r--r--spec/frontend/sidebar/components/attention_requested_toggle_spec.js1
-rw-r--r--spec/frontend/sidebar/sidebar_mediator_spec.js62
-rw-r--r--spec/graphql/resolvers/issues_resolver_spec.rb28
-rw-r--r--spec/helpers/namespaces_helper_spec.rb1
-rw-r--r--spec/rubocop/cop/migration/background_migration_base_class_spec.rb104
55 files changed, 697 insertions, 241 deletions
diff --git a/.rubocop.yml b/.rubocop.yml
index 7fe98a55324..aefb24e0b5d 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -747,3 +747,6 @@ Performance/ActiveRecordSubtransactionMethods:
Exclude:
- 'spec/**/*.rb'
- 'ee/spec/**/*.rb'
+
+Migration/BackgroundMigrationBaseClass:
+ Enabled: false
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 76605609d52..e9264ffc8d1 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -11,12 +11,6 @@ Gitlab/PolicyRuleBoolean:
Exclude:
- 'ee/app/policies/ee/identity_provider_policy.rb'
-# Offense count: 22
-# Cop supports --auto-correct.
-# Configuration parameters: AllowComments.
-Lint/UselessMethodDefinition:
- Enabled: false
-
# Offense count: 218
# Cop supports --auto-correct.
# Configuration parameters: PreferredName.
diff --git a/.rubocop_todo/migration/background_migration_base_class.yml b/.rubocop_todo/migration/background_migration_base_class.yml
new file mode 100644
index 00000000000..9307aba4685
--- /dev/null
+++ b/.rubocop_todo/migration/background_migration_base_class.yml
@@ -0,0 +1,92 @@
+---
+Migration/BackgroundMigrationBaseClass:
+ Exclude:
+ - 'lib/gitlab/background_migration/add_primary_email_to_emails_if_user_confirmed.rb'
+ - 'lib/gitlab/background_migration/backfill_artifact_expiry_date.rb'
+ - 'lib/gitlab/background_migration/backfill_ci_namespace_mirrors.rb'
+ - 'lib/gitlab/background_migration/backfill_ci_project_mirrors.rb'
+ - 'lib/gitlab/background_migration/backfill_ci_queuing_tables.rb'
+ - 'lib/gitlab/background_migration/backfill_draft_status_on_merge_requests.rb'
+ - 'lib/gitlab/background_migration/backfill_integrations_type_new.rb'
+ - 'lib/gitlab/background_migration/backfill_issue_search_data.rb'
+ - 'lib/gitlab/background_migration/backfill_iteration_cadence_id_for_boards.rb'
+ - 'lib/gitlab/background_migration/backfill_jira_tracker_deployment_type2.rb'
+ - 'lib/gitlab/background_migration/backfill_member_namespace_for_group_members.rb'
+ - 'lib/gitlab/background_migration/backfill_namespace_id_for_namespace_route.rb'
+ - 'lib/gitlab/background_migration/backfill_namespace_id_for_project_route.rb'
+ - 'lib/gitlab/background_migration/backfill_namespace_traversal_ids_children.rb'
+ - 'lib/gitlab/background_migration/backfill_namespace_traversal_ids_roots.rb'
+ - 'lib/gitlab/background_migration/backfill_projects_with_coverage.rb'
+ - 'lib/gitlab/background_migration/backfill_project_repositories.rb'
+ - 'lib/gitlab/background_migration/backfill_project_settings.rb'
+ - 'lib/gitlab/background_migration/backfill_snippet_repositories.rb'
+ - 'lib/gitlab/background_migration/backfill_topics_title.rb'
+ - 'lib/gitlab/background_migration/backfill_upvotes_count_on_issues.rb'
+ - 'lib/gitlab/background_migration/backfill_user_namespace.rb'
+ - 'lib/gitlab/background_migration/backfill_work_item_type_id_for_issues.rb'
+ - 'lib/gitlab/background_migration/cleanup_draft_data_from_faulty_regex.rb'
+ - 'lib/gitlab/background_migration/cleanup_orphaned_lfs_objects_projects.rb'
+ - 'lib/gitlab/background_migration/copy_ci_builds_columns_to_security_scans.rb'
+ - 'lib/gitlab/background_migration/create_security_setting.rb'
+ - 'lib/gitlab/background_migration/delete_orphaned_deployments.rb'
+ - 'lib/gitlab/background_migration/disable_expiration_policies_linked_to_no_container_images.rb'
+ - 'lib/gitlab/background_migration/drop_invalid_remediations.rb'
+ - 'lib/gitlab/background_migration/drop_invalid_security_findings.rb'
+ - 'lib/gitlab/background_migration/drop_invalid_vulnerabilities.rb'
+ - 'lib/gitlab/background_migration/encrypt_integration_properties.rb'
+ - 'lib/gitlab/background_migration/encrypt_static_object_token.rb'
+ - 'lib/gitlab/background_migration/extract_project_topics_into_separate_table.rb'
+ - 'lib/gitlab/background_migration/fix_duplicate_project_name_and_path.rb'
+ - 'lib/gitlab/background_migration/fix_first_mentioned_in_commit_at.rb'
+ - 'lib/gitlab/background_migration/fix_incorrect_max_seats_used.rb'
+ - 'lib/gitlab/background_migration/fix_merge_request_diff_commit_users.rb'
+ - 'lib/gitlab/background_migration/fix_projects_without_project_feature.rb'
+ - 'lib/gitlab/background_migration/fix_projects_without_prometheus_service.rb'
+ - 'lib/gitlab/background_migration/fix_vulnerability_occurrences_with_hashes_as_raw_metadata.rb'
+ - 'lib/gitlab/background_migration/legacy_uploads_migrator.rb'
+ - 'lib/gitlab/background_migration/legacy_upload_mover.rb'
+ - 'lib/gitlab/background_migration/merge_topics_with_same_name.rb'
+ - 'lib/gitlab/background_migration/migrate_approver_to_approval_rules.rb'
+ - 'lib/gitlab/background_migration/migrate_approver_to_approval_rules_check_progress.rb'
+ - 'lib/gitlab/background_migration/migrate_approver_to_approval_rules_in_batch.rb'
+ - 'lib/gitlab/background_migration/migrate_job_artifact_registry_to_ssf.rb'
+ - 'lib/gitlab/background_migration/migrate_merge_request_diff_commit_users.rb'
+ - 'lib/gitlab/background_migration/migrate_null_private_profile_to_false.rb'
+ - 'lib/gitlab/background_migration/migrate_pages_to_zip_storage.rb'
+ - 'lib/gitlab/background_migration/migrate_personal_namespace_project_maintainer_to_owner.rb'
+ - 'lib/gitlab/background_migration/migrate_project_taggings_context_from_tags_to_topics.rb'
+ - 'lib/gitlab/background_migration/migrate_requirements_to_work_items.rb'
+ - 'lib/gitlab/background_migration/migrate_shimo_confluence_integration_category.rb'
+ - 'lib/gitlab/background_migration/migrate_stage_status.rb'
+ - 'lib/gitlab/background_migration/migrate_u2f_webauthn.rb'
+ - 'lib/gitlab/background_migration/move_container_registry_enabled_to_project_feature.rb'
+ - 'lib/gitlab/background_migration/nullify_orphan_runner_id_on_ci_builds.rb'
+ - 'lib/gitlab/background_migration/populate_container_repository_migration_plan.rb'
+ - 'lib/gitlab/background_migration/populate_latest_pipeline_ids.rb'
+ - 'lib/gitlab/background_migration/populate_namespace_statistics.rb'
+ - 'lib/gitlab/background_migration/populate_resolved_on_default_branch_column.rb'
+ - 'lib/gitlab/background_migration/populate_status_column_of_security_scans.rb'
+ - 'lib/gitlab/background_migration/populate_test_reports_issue_id.rb'
+ - 'lib/gitlab/background_migration/populate_topics_non_private_projects_count.rb'
+ - 'lib/gitlab/background_migration/populate_topics_total_projects_count_cache.rb'
+ - 'lib/gitlab/background_migration/populate_uuids_for_security_findings.rb'
+ - 'lib/gitlab/background_migration/populate_vulnerability_reads.rb'
+ - 'lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid.rb'
+ - 'lib/gitlab/background_migration/recalculate_vulnerability_finding_signatures_for_findings.rb'
+ - 'lib/gitlab/background_migration/remove_all_trace_expiration_dates.rb'
+ - 'lib/gitlab/background_migration/remove_duplicate_vulnerabilities_findings.rb'
+ - 'lib/gitlab/background_migration/remove_occurrence_pipelines_and_duplicate_vulnerabilities_findings.rb'
+ - 'lib/gitlab/background_migration/remove_vulnerability_finding_links.rb'
+ - 'lib/gitlab/background_migration/reset_duplicate_ci_runners_token_encrypted_values_on_projects.rb'
+ - 'lib/gitlab/background_migration/reset_duplicate_ci_runners_token_values_on_projects.rb'
+ - 'lib/gitlab/background_migration/steal_migrate_merge_request_diff_commit_users.rb'
+ - 'lib/gitlab/background_migration/update_jira_tracker_data_deployment_type_based_on_url.rb'
+ - 'lib/gitlab/background_migration/update_timelogs_null_spent_at.rb'
+ - 'lib/gitlab/background_migration/update_timelogs_project_id.rb'
+ - 'lib/gitlab/background_migration/update_users_where_two_factor_auth_required_from_group.rb'
+ - 'lib/gitlab/background_migration/update_vulnerability_occurrences_location.rb'
+ - 'lib/gitlab/background_migration/mailers/unconfirm_mailer.rb'
+ - 'lib/gitlab/background_migration/project_namespaces/models/project.rb'
+ - 'lib/gitlab/background_migration/project_namespaces/models/namespace.rb'
+ - 'lib/gitlab/background_migration/project_namespaces/backfill_project_namespaces.rb'
+ - 'lib/gitlab/background_migration/backfill_integrations_enable_ssl_verification.rb'
diff --git a/app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue b/app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue
index 1a3544e7677..acc82761e88 100644
--- a/app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue
+++ b/app/assets/javascripts/analytics/shared/components/value_stream_metrics.vue
@@ -28,6 +28,22 @@ const fetchMetricsData = (reqs = [], path, params) => {
);
};
+const extractMetricsGroupData = (keyList = [], data = []) => {
+ if (!keyList.length || !data.length) return [];
+ return data.filter(({ identifier = '' }) => identifier.length && keyList.includes(identifier));
+};
+
+const groupRawMetrics = (groups = [], rawData = []) => {
+ return groups.map((curr) => {
+ const { keys, ...rest } = curr;
+ return {
+ data: extractMetricsGroupData(keys, rawData),
+ keys,
+ ...rest,
+ };
+ });
+};
+
export default {
name: 'ValueStreamMetrics',
components: {
@@ -52,13 +68,24 @@ export default {
required: false,
default: null,
},
+ groupBy: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
},
data() {
return {
metrics: [],
+ groupedMetrics: [],
isLoading: false,
};
},
+ computed: {
+ hasGroupedMetrics() {
+ return Boolean(this.groupBy.length);
+ },
+ },
watch: {
requestParams(newVal, oldVal) {
if (!isEqual(newVal, oldVal)) {
@@ -76,6 +103,11 @@ export default {
return fetchMetricsData(this.requests, this.requestPath, this.requestParams)
.then((data) => {
this.metrics = this.filterFn ? this.filterFn(data) : data;
+
+ if (this.hasGroupedMetrics) {
+ this.groupedMetrics = groupRawMetrics(this.groupBy, this.metrics);
+ }
+
this.isLoading = false;
})
.catch(() => {
@@ -86,14 +118,35 @@ export default {
};
</script>
<template>
- <div class="gl-display-flex gl-flex-wrap" data-testid="vsa-metrics">
+ <div class="gl-display-flex gl-mt-6" data-testid="vsa-metrics">
<gl-skeleton-loading v-if="isLoading" class="gl-h-auto gl-py-3 gl-pr-9 gl-my-6" />
- <metric-tile
- v-for="metric in metrics"
- v-show="!isLoading"
- :key="metric.identifier"
- :metric="metric"
- class="gl-my-6 gl-pr-9"
- />
+ <template v-else>
+ <div v-if="hasGroupedMetrics" class="gl-flex-direction-column">
+ <div
+ v-for="group in groupedMetrics"
+ :key="group.key"
+ class="gl-mb-7"
+ data-testid="vsa-metrics-group"
+ >
+ <h4 class="gl-my-0">{{ group.title }}</h4>
+ <div class="gl-display-flex gl-flex-wrap">
+ <metric-tile
+ v-for="metric in group.data"
+ :key="metric.identifier"
+ :metric="metric"
+ class="gl-mt-5 gl-pr-10"
+ />
+ </div>
+ </div>
+ </div>
+ <div v-else class="gl-display-flex gl-flex-wrap gl-mb-7">
+ <metric-tile
+ v-for="metric in metrics"
+ :key="metric.identifier"
+ :metric="metric"
+ class="gl-mt-5 gl-pr-10"
+ />
+ </div>
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/analytics/shared/constants.js b/app/assets/javascripts/analytics/shared/constants.js
index 2ac144ceb5e..05e68bf5d9a 100644
--- a/app/assets/javascripts/analytics/shared/constants.js
+++ b/app/assets/javascripts/analytics/shared/constants.js
@@ -56,3 +56,14 @@ export const METRICS_POPOVER_CONTENT = {
description: s__('ValueStreamAnalytics|Number of commits pushed to the default branch'),
},
};
+
+const KEY_METRICS_TITLE = s__('ValueStreamAnalytics|Key metrics');
+const KEY_METRICS_KEYS = ['lead_time', 'cycle_time', 'issues', 'commits', 'deploys'];
+
+const DORA_METRICS_TITLE = s__('ValueStreamAnalytics|DORA metrics');
+const DORA_METRICS_KEYS = ['deployment_frequency', 'lead_time_for_changes'];
+
+export const VSA_METRICS_GROUPS = [
+ { key: 'key_metrics', title: KEY_METRICS_TITLE, keys: KEY_METRICS_KEYS },
+ { key: 'dora_metrics', title: DORA_METRICS_TITLE, keys: DORA_METRICS_KEYS },
+];
diff --git a/app/assets/javascripts/boards/components/board_add_new_column_form.vue b/app/assets/javascripts/boards/components/board_add_new_column_form.vue
index 70ba90bb1d4..10c7a3db2d3 100644
--- a/app/assets/javascripts/boards/components/board_add_new_column_form.vue
+++ b/app/assets/javascripts/boards/components/board_add_new_column_form.vue
@@ -80,17 +80,14 @@ export default {
<template>
<div
- class="board-add-new-list board gl-display-inline-block gl-h-full gl-px-3 gl-vertical-align-top gl-white-space-normal gl-flex-shrink-0"
+ class="board-add-new-list board gl-display-inline-block gl-h-full gl-vertical-align-top gl-white-space-normal gl-flex-shrink-0 gl-rounded-base gl-px-3"
data-testid="board-add-new-column"
data-qa-selector="board_add_new_list"
>
<div
- class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base gl-bg-white"
+ class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base"
>
- <h3
- class="gl-font-size-h2 gl-px-5 gl-py-4 gl-m-0 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100"
- data-testid="board-add-column-form-title"
- >
+ <h3 class="gl-font-size-h2 gl-px-5 gl-py-5 gl-m-0" data-testid="board-add-column-form-title">
{{ $options.i18n.newList }}
</h3>
@@ -98,7 +95,7 @@ export default {
class="gl-display-flex gl-flex-direction-column gl-h-full gl-overflow-y-auto gl-align-items-flex-start"
>
<div class="gl-px-5">
- <h3 class="gl-font-lg gl-mt-5 gl-mb-2">
+ <h3 class="gl-font-lg gl-mt-3 gl-mb-2">
{{ $options.i18n.scope }}
</h3>
<p class="gl-mb-3">{{ $options.i18n.scopeDescription }}</p>
@@ -147,23 +144,18 @@ export default {
</gl-dropdown>
</gl-form-group>
</div>
- <div
- class="gl-display-flex gl-p-3 gl-border-t-1 gl-border-t-solid gl-border-gray-100 gl-bg-gray-10 gl-rounded-bottom-left-base gl-rounded-bottom-right-base"
- >
- <gl-button
- data-testid="cancelAddNewColumn"
- class="gl-ml-auto gl-mr-3"
- @click="setAddColumnFormVisibility(false)"
- >{{ $options.i18n.cancel }}</gl-button
- >
+ <div class="gl-display-flex gl-mb-4">
<gl-button
data-testid="addNewColumnButton"
:disabled="!selectedId"
variant="confirm"
- class="gl-mr-4"
+ class="gl-mr-3 gl-ml-4"
@click="$emit('add-list')"
>{{ $options.i18n.add }}</gl-button
>
+ <gl-button data-testid="cancelAddNewColumn" @click="setAddColumnFormVisibility(false)">{{
+ $options.i18n.cancel
+ }}</gl-button>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue
index 5c6d7748edd..8868b9b2f3e 100644
--- a/app/assets/javascripts/boards/components/board_content.vue
+++ b/app/assets/javascripts/boards/components/board_content.vue
@@ -75,7 +75,7 @@ export default {
v-if="!isSwimlanesOn"
ref="list"
v-bind="draggableOptions"
- class="boards-list gl-w-full gl-py-5 gl-px-3 gl-white-space-nowrap"
+ class="boards-list gl-w-full gl-py-5 gl-pr-3 gl-white-space-nowrap"
@end="moveList"
>
<board-column
diff --git a/app/assets/javascripts/cycle_analytics/components/base.vue b/app/assets/javascripts/cycle_analytics/components/base.vue
index 735d8f26794..1883030e51f 100644
--- a/app/assets/javascripts/cycle_analytics/components/base.vue
+++ b/app/assets/javascripts/cycle_analytics/components/base.vue
@@ -3,6 +3,7 @@ import { GlLoadingIcon } from '@gitlab/ui';
import { mapActions, mapState, mapGetters } from 'vuex';
import { getCookie, setCookie } from '~/lib/utils/common_utils';
import ValueStreamMetrics from '~/analytics/shared/components/value_stream_metrics.vue';
+import { VSA_METRICS_GROUPS } from '~/analytics/shared/constants';
import { toYmd } from '~/analytics/shared/utils';
import PathNavigation from '~/cycle_analytics/components/path_navigation.vue';
import StageTable from '~/cycle_analytics/components/stage_table.vue';
@@ -150,6 +151,7 @@ export default {
pageTitle: __('Value Stream Analytics'),
recentActivity: __('Recent Project Activity'),
},
+ VSA_METRICS_GROUPS,
};
</script>
<template>
@@ -178,6 +180,7 @@ export default {
:request-path="endpoints.fullPath"
:request-params="filterParams"
:requests="metricsRequests"
+ :group-by="$options.VSA_METRICS_GROUPS"
/>
<gl-loading-icon v-if="isLoading" size="lg" />
<stage-table
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
index a98acf3457c..f1f8022845e 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
@@ -1,5 +1,12 @@
<script>
-import { GlBadge, GlButton, GlLink, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
+import {
+ GlBadge,
+ GlButton,
+ GlLink,
+ GlLoadingIcon,
+ GlTooltip,
+ GlTooltipDirective,
+} from '@gitlab/ui';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { __, sprintf } from '~/locale';
@@ -21,6 +28,7 @@ export default {
GlButton,
GlLink,
GlLoadingIcon,
+ GlTooltip,
},
actionSizeClasses: ['gl-h-7 gl-w-7'],
mixins: [glFeatureFlagMixin()],
@@ -48,6 +56,7 @@ export default {
},
data() {
return {
+ hasActionTooltip: false,
isActionLoading: false,
};
},
@@ -139,13 +148,16 @@ export default {
showAction() {
return Boolean(this.action?.method && this.action?.icon && this.action?.ariaLabel);
},
+ showCardTooltip() {
+ return !this.hasActionTooltip;
+ },
sourceJobName() {
return this.pipeline.sourceJob?.name ?? '';
},
sourceJobInfo() {
return this.isDownstream ? sprintf(__('Created by %{job}'), { job: this.sourceJobName }) : '';
},
- tooltipText() {
+ cardTooltipText() {
return `${this.downstreamTitle} #${this.pipeline.id} - ${this.pipelineStatus.label} -
${this.sourceJobInfo}`;
},
@@ -191,6 +203,9 @@ export default {
retryPipeline() {
this.executePipelineAction(RetryPipelineMutation);
},
+ setActionTooltip(flag) {
+ this.hasActionTooltip = flag;
+ },
},
};
</script>
@@ -198,14 +213,15 @@ export default {
<template>
<div
ref="linkedPipeline"
- v-gl-tooltip
class="gl-h-full gl-display-flex! gl-border-solid gl-border-gray-100 gl-border-1"
:class="flexDirection"
- :title="tooltipText"
data-qa-selector="linked_pipeline_container"
@mouseover="onDownstreamHovered"
@mouseleave="onDownstreamHoverLeave"
>
+ <gl-tooltip v-if="showCardTooltip" :target="() => $refs.linkedPipeline">
+ {{ cardTooltipText }}
+ </gl-tooltip>
<div class="gl-w-full gl-bg-white gl-p-3" :class="cardSpacingClass">
<div class="gl-display-flex gl-pr-3">
<ci-status
@@ -227,12 +243,16 @@ export default {
</div>
<gl-button
v-if="showAction"
+ v-gl-tooltip
+ :title="action.ariaLabel"
:loading="isActionLoading"
:icon="action.icon"
class="gl-rounded-full!"
:class="$options.actionSizeClasses"
:aria-label="action.ariaLabel"
@click="action.method"
+ @mouseover="setActionTooltip(true)"
+ @mouseout="setActionTooltip(false)"
/>
<div v-else :class="$options.actionSizeClasses"></div>
</div>
diff --git a/app/assets/javascripts/sidebar/components/attention_requested_toggle.vue b/app/assets/javascripts/sidebar/components/attention_requested_toggle.vue
index cd1aae155d2..031de669489 100644
--- a/app/assets/javascripts/sidebar/components/attention_requested_toggle.vue
+++ b/app/assets/javascripts/sidebar/components/attention_requested_toggle.vue
@@ -47,6 +47,23 @@ export default {
return this.$options.i18n.noAttentionRequestedNoPermission;
},
+ request() {
+ const state = {
+ variant: 'default',
+ icon: 'attention',
+ direction: 'add',
+ };
+
+ if (this.user.attention_requested) {
+ Object.assign(state, {
+ variant: 'warning',
+ icon: 'attention-solid',
+ direction: 'remove',
+ });
+ }
+
+ return state;
+ },
},
methods: {
toggleAttentionRequired() {
@@ -57,6 +74,7 @@ export default {
this.$emit('toggle-attention-requested', {
user: this.user,
callback: this.toggleAttentionRequiredComplete,
+ direction: this.request.direction,
});
},
toggleAttentionRequiredComplete() {
@@ -74,8 +92,8 @@ export default {
>
<gl-button
:loading="loading"
- :variant="user.attention_requested ? 'warning' : 'default'"
- :icon="user.attention_requested ? 'attention-solid' : 'attention'"
+ :variant="request.variant"
+ :icon="request.icon"
:aria-label="tooltipTitle"
:class="{ 'gl-pointer-events-none': !user.can_update_merge_request }"
size="small"
diff --git a/app/assets/javascripts/sidebar/queries/remove_attention_request.mutation.graphql b/app/assets/javascripts/sidebar/queries/remove_attention_request.mutation.graphql
new file mode 100644
index 00000000000..1a7a9629b07
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/remove_attention_request.mutation.graphql
@@ -0,0 +1,7 @@
+mutation mergeRequestRemoveAttentionRequest($projectPath: ID!, $iid: String!, $userId: ID!) {
+ mergeRequestRemoveAttentionRequest(
+ input: { projectPath: $projectPath, iid: $iid, userId: $userId }
+ ) {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/sidebar/queries/request_attention.mutation.graphql b/app/assets/javascripts/sidebar/queries/request_attention.mutation.graphql
new file mode 100644
index 00000000000..99f9beea17b
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/request_attention.mutation.graphql
@@ -0,0 +1,5 @@
+mutation mergeRequestRequestAttention($projectPath: ID!, $iid: String!, $userId: ID!) {
+ mergeRequestRequestAttention(input: { projectPath: $projectPath, iid: $iid, userId: $userId }) {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/sidebar/queries/toggle_attention_requested.mutation.graphql b/app/assets/javascripts/sidebar/queries/toggle_attention_requested.mutation.graphql
deleted file mode 100644
index 495d272b698..00000000000
--- a/app/assets/javascripts/sidebar/queries/toggle_attention_requested.mutation.graphql
+++ /dev/null
@@ -1,7 +0,0 @@
-mutation mergeRequestToggleAttentionRequested($projectPath: ID!, $iid: String!, $userId: UserID!) {
- mergeRequestToggleAttentionRequested(
- input: { projectPath: $projectPath, iid: $iid, userId: $userId }
- ) {
- errors
- }
-}
diff --git a/app/assets/javascripts/sidebar/services/sidebar_service.js b/app/assets/javascripts/sidebar/services/sidebar_service.js
index 90d8f2098bb..ea170203576 100644
--- a/app/assets/javascripts/sidebar/services/sidebar_service.js
+++ b/app/assets/javascripts/sidebar/services/sidebar_service.js
@@ -5,7 +5,8 @@ import createGqClient, { fetchPolicies } from '~/lib/graphql';
import axios from '~/lib/utils/axios_utils';
import reviewerRereviewMutation from '../queries/reviewer_rereview.mutation.graphql';
import sidebarDetailsMRQuery from '../queries/sidebar_details_mr.query.graphql';
-import toggleAttentionRequestedMutation from '../queries/toggle_attention_requested.mutation.graphql';
+import requestAttentionMutation from '../queries/request_attention.mutation.graphql';
+import removeAttentionRequestMutation from '../queries/remove_attention_request.mutation.graphql';
const queries = {
merge_request: sidebarDetailsMRQuery,
@@ -92,9 +93,19 @@ export default class SidebarService {
});
}
- toggleAttentionRequested(userId) {
+ requestAttention(userId) {
return gqClient.mutate({
- mutation: toggleAttentionRequestedMutation,
+ mutation: requestAttentionMutation,
+ variables: {
+ userId: convertToGraphQLId(TYPE_USER, `${userId}`),
+ projectPath: this.fullPath,
+ iid: this.iid.toString(),
+ },
+ });
+ }
+ removeAttentionRequest(userId) {
+ return gqClient.mutate({
+ mutation: removeAttentionRequestMutation,
variables: {
userId: convertToGraphQLId(TYPE_USER, `${userId}`),
projectPath: this.fullPath,
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index 83fb8f31dfb..75a1d00fe19 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -98,14 +98,19 @@ export default class SidebarMediator {
}
}
- async toggleAttentionRequested(type, { user, callback }) {
+ async toggleAttentionRequested(type, { user, callback, direction }) {
+ const mutations = {
+ add: (id) => this.service.requestAttention(id),
+ remove: (id) => this.service.removeAttentionRequest(id),
+ };
+
try {
const isReviewer = type === 'reviewer';
const reviewerOrAssignee = isReviewer
? this.store.findReviewer(user)
: this.store.findAssignee(user);
- await this.service.toggleAttentionRequested(user.id);
+ await mutations[direction]?.(user.id);
if (reviewerOrAssignee.attention_requested) {
toast(
@@ -138,7 +143,7 @@ export default class SidebarMediator {
captureError: true,
actionConfig: {
title: __('Try again'),
- clickHandler: () => this.toggleAttentionRequired(type, { user, callback }),
+ clickHandler: () => this.toggleAttentionRequired(type, { user, callback, direction }),
},
});
}
diff --git a/app/assets/stylesheets/page_bundles/boards.scss b/app/assets/stylesheets/page_bundles/boards.scss
index d4eba805a41..81d35b8bc7b 100644
--- a/app/assets/stylesheets/page_bundles/boards.scss
+++ b/app/assets/stylesheets/page_bundles/boards.scss
@@ -49,6 +49,7 @@
height: calc(100vh - #{$issue-board-list-difference-xs});
overflow-x: scroll;
min-height: 200px;
+ border-left: 8px solid var(--gray-10, $white);
@include media-breakpoint-only(sm) {
height: calc(100vh - #{$issue-board-list-difference-sm});
diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb
index 7c9525d1744..55707000cf8 100644
--- a/app/controllers/import/bitbucket_controller.rb
+++ b/app/controllers/import/bitbucket_controller.rb
@@ -29,13 +29,12 @@ class Import::BitbucketController < Import::BaseController
end
end
+ # We need to re-expose controller's internal method 'status' as action.
+ # rubocop:disable Lint/UselessMethodDefinition
def status
super
end
-
- def realtime_changes
- super
- end
+ # rubocop:enable Lint/UselessMethodDefinition
def create
bitbucket_client = Bitbucket::Client.new(credentials)
diff --git a/app/controllers/import/bitbucket_server_controller.rb b/app/controllers/import/bitbucket_server_controller.rb
index 31e9694ca1d..00f3f0b08b2 100644
--- a/app/controllers/import/bitbucket_server_controller.rb
+++ b/app/controllers/import/bitbucket_server_controller.rb
@@ -52,13 +52,12 @@ class Import::BitbucketServerController < Import::BaseController
redirect_to status_import_bitbucket_server_path
end
+ # We need to re-expose controller's internal method 'status' as action.
+ # rubocop:disable Lint/UselessMethodDefinition
def status
super
end
-
- def realtime_changes
- super
- end
+ # rubocop:enable Lint/UselessMethodDefinition
protected
diff --git a/app/controllers/import/fogbugz_controller.rb b/app/controllers/import/fogbugz_controller.rb
index 377292d47d8..c223d9d211e 100644
--- a/app/controllers/import/fogbugz_controller.rb
+++ b/app/controllers/import/fogbugz_controller.rb
@@ -54,10 +54,6 @@ class Import::FogbugzController < Import::BaseController
end
# rubocop: enable CodeReuse/ActiveRecord
- def realtime_changes
- super
- end
-
def create
repo = client.repo(params[:repo_id])
fb_session = { uri: session[:fogbugz_uri], token: session[:fogbugz_token] }
diff --git a/app/controllers/import/gitea_controller.rb b/app/controllers/import/gitea_controller.rb
index a1a9f4112f2..4b4ac07b389 100644
--- a/app/controllers/import/gitea_controller.rb
+++ b/app/controllers/import/gitea_controller.rb
@@ -16,10 +16,12 @@ class Import::GiteaController < Import::GithubController
super
end
- # Must be defined or it will 404
+ # We need to re-expose controller's internal method 'status' as action.
+ # rubocop:disable Lint/UselessMethodDefinition
def status
super
end
+ # rubocop:enable Lint/UselessMethodDefinition
protected
diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb
index fa9517c3545..c846d9d225a 100644
--- a/app/controllers/import/gitlab_controller.rb
+++ b/app/controllers/import/gitlab_controller.rb
@@ -16,9 +16,12 @@ class Import::GitlabController < Import::BaseController
redirect_to status_import_gitlab_url
end
+ # We need to re-expose controller's internal method 'status' as action.
+ # rubocop:disable Lint/UselessMethodDefinition
def status
super
end
+ # rubocop:enable Lint/UselessMethodDefinition
def create
repo = client.project(params[:repo_id].to_i)
diff --git a/app/controllers/import/manifest_controller.rb b/app/controllers/import/manifest_controller.rb
index 956d0c9a2ae..461ba982969 100644
--- a/app/controllers/import/manifest_controller.rb
+++ b/app/controllers/import/manifest_controller.rb
@@ -10,9 +10,12 @@ class Import::ManifestController < Import::BaseController
def new
end
+ # We need to re-expose controller's internal method 'status' as action.
+ # rubocop:disable Lint/UselessMethodDefinition
def status
super
end
+ # rubocop:enable Lint/UselessMethodDefinition
def upload
group = Group.find(params[:group_id])
@@ -36,10 +39,6 @@ class Import::ManifestController < Import::BaseController
end
end
- def realtime_changes
- super
- end
-
def create
repository = importable_repos.find do |project|
project[:id] == params[:repo_id].to_i
diff --git a/app/controllers/projects/alerting/notifications_controller.rb b/app/controllers/projects/alerting/notifications_controller.rb
index ab5264b9716..82fff287c4a 100644
--- a/app/controllers/projects/alerting/notifications_controller.rb
+++ b/app/controllers/projects/alerting/notifications_controller.rb
@@ -13,7 +13,9 @@ module Projects
prepend_before_action :repository, :project_without_auth
feature_category :incident_management
- urgency :medium, [:create]
+ # Goal is to increase the urgency to medium.
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/361310.
+ urgency :low, [:create]
def create
token = extract_alert_manager_token(request)
diff --git a/app/finders/tags_finder.rb b/app/finders/tags_finder.rb
index 6bc5419e704..16bba62f766 100644
--- a/app/finders/tags_finder.rb
+++ b/app/finders/tags_finder.rb
@@ -1,10 +1,6 @@
# frozen_string_literal: true
class TagsFinder < GitRefsFinder
- def initialize(repository, params)
- super(repository, params)
- end
-
def execute(gitaly_pagination: false)
tags = if gitaly_pagination
repository.tags_sorted_by(sort, pagination_params)
diff --git a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb
index 432d6f48607..de44dbb26d7 100644
--- a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb
+++ b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb
@@ -68,6 +68,12 @@ module IssueResolverArguments
description: 'Negated arguments.',
prepare: ->(negated_args, ctx) { negated_args.to_h },
required: false
+ argument :crm_contact_id, GraphQL::Types::String,
+ required: false,
+ description: 'ID of a contact assigned to the issues.'
+ argument :crm_organization_id, GraphQL::Types::String,
+ required: false,
+ description: 'ID of an organization assigned to the issues.'
end
def resolve_with_lookahead(**args)
diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb
index cf386ee398a..8914c974f6c 100644
--- a/app/helpers/namespaces_helper.rb
+++ b/app/helpers/namespaces_helper.rb
@@ -93,6 +93,7 @@ module NamespacesHelper
namespace_actual_plan_name: namespace.actual_plan_name,
namespace_path: namespace.full_path,
namespace_id: namespace.id,
+ user_namespace: namespace.user_namespace?.to_s,
page_size: page_size
}
end
diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb
index 9c525ae8489..8e0f72eb380 100644
--- a/app/services/merge_requests/create_service.rb
+++ b/app/services/merge_requests/create_service.rb
@@ -25,9 +25,7 @@ module MergeRequests
# expose issuable create method so it can be called from email
# handler CreateMergeRequestHandler
- def create(merge_request)
- super
- end
+ public :create
private
diff --git a/app/views/admin/application_settings/_signin.html.haml b/app/views/admin/application_settings/_signin.html.haml
index 28e0ee25a5d..48f0b9b2c31 100644
--- a/app/views/admin/application_settings/_signin.html.haml
+++ b/app/views/admin/application_settings/_signin.html.haml
@@ -3,22 +3,13 @@
%fieldset
.form-group
- .form-check
- = f.check_box :password_authentication_enabled_for_web, class: 'form-check-input'
- = f.label :password_authentication_enabled_for_web, class: 'form-check-label' do
- = _('Allow password authentication for the web interface')
- .form-text.text-muted
- = _('Clear this checkbox to use an external authentication provider instead.')
+ = f.gitlab_ui_checkbox_component :password_authentication_enabled_for_web,
+ _('Allow password authentication for the web interface'),
+ help_text: _('Clear this checkbox to use an external authentication provider instead.')
.form-group
- .form-check
- = f.check_box :password_authentication_enabled_for_git, class: 'form-check-input'
- = f.label :password_authentication_enabled_for_git, class: 'form-check-label' do
- = _('Allow password authentication for Git over HTTP(S)')
- .form-text.text-muted
- - if Gitlab::Auth::Ldap::Config.enabled?
- = _('Clear this checkbox to use a personal access token or LDAP password instead.')
- - else
- = _('Clear this checkbox to use a personal access token instead.')
+ = f.gitlab_ui_checkbox_component :password_authentication_enabled_for_git,
+ _('Allow password authentication for Git over HTTP(S)'),
+ help_text: Gitlab::Auth::Ldap::Config.enabled? ? _('Clear this checkbox to use a personal access token or LDAP password instead.') : _('Clear this checkbox to use a personal access token instead.')
- if omniauth_enabled? && button_based_providers.any?
%fieldset.form-group
%legend.gl-font-base.gl-mb-3.gl-border-none.gl-font-weight-bold= _('Enabled OAuth authentication sources')
@@ -27,13 +18,11 @@
= source
.form-group
= f.label :two_factor_authentication, _('Two-factor authentication'), class: 'label-bold'
- .form-check
- = f.check_box :require_two_factor_authentication, class: 'form-check-input'
- = f.label :require_two_factor_authentication, class: 'form-check-label' do
- = _('Enforce two-factor authentication')
- %p.form-text.text-muted
- = _('Enforce two-factor authentication for all user sign-ins.')
- = link_to _('Learn more.'), help_page_path('security/two_factor_authentication.md'), target: '_blank', rel: 'noopener noreferrer'
+ - help_text = _('Enforce two-factor authentication for all user sign-ins.')
+ - help_link = link_to _('Learn more.'), help_page_path('security/two_factor_authentication.md'), target: '_blank', rel: 'noopener noreferrer'
+ = f.gitlab_ui_checkbox_component :require_two_factor_authentication,
+ _('Enforce two-factor authentication'),
+ help_text: '%{help_text} %{help_link}'.html_safe % { help_text: help_text, help_link: help_link }
.form-group
= f.label :two_factor_authentication, _('Two-factor grace period'), class: 'label-bold'
= f.number_field :two_factor_grace_period, min: 0, class: 'form-control gl-form-input', placeholder: '0'
@@ -42,22 +31,18 @@
.form-group
= f.label :admin_mode, _('Admin Mode'), class: 'label-bold'
= sprite_icon('lock', css_class: 'gl-icon')
- .form-check
- = f.check_box :admin_mode, class: 'form-check-input'
- = f.label :admin_mode, class: 'form-check-label' do
- = _('Enable admin mode')
- %p.form-text.text-muted
- = _('Require additional authentication for administrative tasks.')
- = link_to _('Learn more.'), help_page_path('user/admin_area/settings/sign_in_restrictions', anchor: 'admin-mode'), target: '_blank', rel: 'noopener noreferrer'
+ - help_text = _('Require additional authentication for administrative tasks.')
+ - help_link = link_to _('Learn more.'), help_page_path('user/admin_area/settings/sign_in_restrictions', anchor: 'admin-mode'), target: '_blank', rel: 'noopener noreferrer'
+ = f.gitlab_ui_checkbox_component :admin_mode,
+ _('Enable admin mode'),
+ help_text: '%{help_text} %{help_link}'.html_safe % { help_text: help_text, help_link: help_link }
.form-group
= f.label :unknown_sign_in, _('Email notification for unknown sign-ins'), class: 'label-bold'
- .form-check
- = f.check_box :notify_on_unknown_sign_in, class: 'form-check-input'
- = f.label :notify_on_unknown_sign_in, class: 'form-check-label' do
- = _('Enable email notification')
- %p.form-text.text-muted
- = _('Notify users by email when sign-in location is not recognized.')
- = link_to _('Learn more.'), help_page_path('user/profile/unknown_sign_in_notification.md'), target: '_blank', rel: 'noopener noreferrer'
+ - help_text = _('Notify users by email when sign-in location is not recognized.')
+ - help_link = link_to _('Learn more.'), help_page_path('user/profile/unknown_sign_in_notification.md'), target: '_blank', rel: 'noopener noreferrer'
+ = f.gitlab_ui_checkbox_component :notify_on_unknown_sign_in,
+ _('Enable email notification'),
+ help_text: '%{help_text} %{help_link}'.html_safe % { help_text: help_text, help_link: help_link }
.form-group
= f.label :home_page_url, _('Home page URL'), class: 'label-bold'
= f.text_field :home_page_url, class: 'form-control gl-form-input', placeholder: 'http://company.example.com', :'aria-describedby' => 'home_help_block'
diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
index 9e3d9b4258a..a3569d41714 100644
--- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
@@ -39,10 +39,9 @@
.label-container
- if generic_commit_status.tags.any?
- generic_commit_status.tags.each do |tag|
- %span.badge.badge-primary
- = tag
+ = gl_badge_tag tag, variant: :info, size: :sm
- if retried
- %span.badge.badge-warning retried
+ = gl_badge_tag retried, variant: :warning, size: :sm
- if pipeline_link
%td
diff --git a/config/events/1651052988_gitlab__usage_data_counters__ci_template_unique_counter_ci_templates_unique.yml b/config/events/1651052988_gitlab__usage_data_counters__ci_template_unique_counter_ci_templates_unique.yml
new file mode 100644
index 00000000000..47234c07f59
--- /dev/null
+++ b/config/events/1651052988_gitlab__usage_data_counters__ci_template_unique_counter_ci_templates_unique.yml
@@ -0,0 +1,23 @@
+---
+description: Run Pipeline
+category: Gitlab::UsageDataCounters::CiTemplateUniqueCounter
+action: ci_templates_unique
+label_description:
+property_description:
+value_description:
+extra_properties:
+identifiers:
+product_section: ops
+product_stage: configure
+product_group: group::configure
+product_category: infrastructure_as_code
+milestone: "15.0"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84337
+distributions:
+- ce
+- ee
+tiers:
+- free
+- premium
+- ultimate
+
diff --git a/config/events/1651053267_event_create_service_action_active_users_project_repo.yml b/config/events/1651053267_event_create_service_action_active_users_project_repo.yml
new file mode 100644
index 00000000000..bab5dd9e7b3
--- /dev/null
+++ b/config/events/1651053267_event_create_service_action_active_users_project_repo.yml
@@ -0,0 +1,23 @@
+---
+description: Perform Git operation (read/write/push)
+category: EventCreateService
+action: action_active_users_project_repo
+label_description:
+property_description:
+value_description:
+extra_properties:
+identifiers:
+product_section: dev
+product_stage: create
+product_group: group::source code
+product_category: source_code_management
+milestone: "15.0"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/83795
+distributions:
+- ce
+- ee
+tiers:
+- free
+- premium
+- ultimate
+
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 9b021a92061..7927be6d75b 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -11579,6 +11579,8 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="groupissuesconfidential"></a>`confidential` | [`Boolean`](#boolean) | Filter for confidential issues. If "false", excludes confidential issues. If "true", returns only confidential issues. |
| <a id="groupissuescreatedafter"></a>`createdAfter` | [`Time`](#time) | Issues created after this date. |
| <a id="groupissuescreatedbefore"></a>`createdBefore` | [`Time`](#time) | Issues created before this date. |
+| <a id="groupissuescrmcontactid"></a>`crmContactId` | [`String`](#string) | ID of a contact assigned to the issues. |
+| <a id="groupissuescrmorganizationid"></a>`crmOrganizationId` | [`String`](#string) | ID of an organization assigned to the issues. |
| <a id="groupissuesepicid"></a>`epicId` | [`String`](#string) | ID of an epic associated with the issues, "none" and "any" values are supported. |
| <a id="groupissuesiid"></a>`iid` | [`String`](#string) | IID of the issue. For example, "1". |
| <a id="groupissuesiids"></a>`iids` | [`[String!]`](#string) | List of IIDs of issues. For example, `["1", "2"]`. |
@@ -14859,6 +14861,8 @@ Returns [`Issue`](#issue).
| <a id="projectissueconfidential"></a>`confidential` | [`Boolean`](#boolean) | Filter for confidential issues. If "false", excludes confidential issues. If "true", returns only confidential issues. |
| <a id="projectissuecreatedafter"></a>`createdAfter` | [`Time`](#time) | Issues created after this date. |
| <a id="projectissuecreatedbefore"></a>`createdBefore` | [`Time`](#time) | Issues created before this date. |
+| <a id="projectissuecrmcontactid"></a>`crmContactId` | [`String`](#string) | ID of a contact assigned to the issues. |
+| <a id="projectissuecrmorganizationid"></a>`crmOrganizationId` | [`String`](#string) | ID of an organization assigned to the issues. |
| <a id="projectissueepicid"></a>`epicId` | [`String`](#string) | ID of an epic associated with the issues, "none" and "any" values are supported. |
| <a id="projectissueiid"></a>`iid` | [`String`](#string) | IID of the issue. For example, "1". |
| <a id="projectissueiids"></a>`iids` | [`[String!]`](#string) | List of IIDs of issues. For example, `["1", "2"]`. |
@@ -14899,6 +14903,8 @@ Returns [`IssueStatusCountsType`](#issuestatuscountstype).
| <a id="projectissuestatuscountsconfidential"></a>`confidential` | [`Boolean`](#boolean) | Filter for confidential issues. If "false", excludes confidential issues. If "true", returns only confidential issues. |
| <a id="projectissuestatuscountscreatedafter"></a>`createdAfter` | [`Time`](#time) | Issues created after this date. |
| <a id="projectissuestatuscountscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Issues created before this date. |
+| <a id="projectissuestatuscountscrmcontactid"></a>`crmContactId` | [`String`](#string) | ID of a contact assigned to the issues. |
+| <a id="projectissuestatuscountscrmorganizationid"></a>`crmOrganizationId` | [`String`](#string) | ID of an organization assigned to the issues. |
| <a id="projectissuestatuscountsiid"></a>`iid` | [`String`](#string) | IID of the issue. For example, "1". |
| <a id="projectissuestatuscountsiids"></a>`iids` | [`[String!]`](#string) | List of IIDs of issues. For example, `["1", "2"]`. |
| <a id="projectissuestatuscountslabelname"></a>`labelName` | [`[String]`](#string) | Labels applied to this issue. |
@@ -14936,6 +14942,8 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="projectissuesconfidential"></a>`confidential` | [`Boolean`](#boolean) | Filter for confidential issues. If "false", excludes confidential issues. If "true", returns only confidential issues. |
| <a id="projectissuescreatedafter"></a>`createdAfter` | [`Time`](#time) | Issues created after this date. |
| <a id="projectissuescreatedbefore"></a>`createdBefore` | [`Time`](#time) | Issues created before this date. |
+| <a id="projectissuescrmcontactid"></a>`crmContactId` | [`String`](#string) | ID of a contact assigned to the issues. |
+| <a id="projectissuescrmorganizationid"></a>`crmOrganizationId` | [`String`](#string) | ID of an organization assigned to the issues. |
| <a id="projectissuesepicid"></a>`epicId` | [`String`](#string) | ID of an epic associated with the issues, "none" and "any" values are supported. |
| <a id="projectissuesiid"></a>`iid` | [`String`](#string) | IID of the issue. For example, "1". |
| <a id="projectissuesiids"></a>`iids` | [`[String!]`](#string) | List of IIDs of issues. For example, `["1", "2"]`. |
diff --git a/doc/development/database/batched_background_migrations.md b/doc/development/database/batched_background_migrations.md
index 0064be1e4a5..3177044f3a6 100644
--- a/doc/development/database/batched_background_migrations.md
+++ b/doc/development/database/batched_background_migrations.md
@@ -190,40 +190,43 @@ data to be in the new format.
## Example
-The table `integrations` has a field called `properties`, stored in JSON. For all rows,
-extract the `url` key from this JSON object and store it in the `integrations.url`
-column. Millions of integrations exist, and parsing JSON is slow, so you can't
-do this work in a regular migration.
+The `routes` table has a `source_type` field that's used for a polymorphic relationship.
+As part of a database redesign, we're removing the polymorphic relationship. One step of
+the work will be migrating data from the `source_id` column into a new singular foreign key.
+Because we intend to delete old rows later, there's no need to update them as part of the
+background migration.
-1. Start by defining our migration class:
+1. Start by defining our migration class, which should inherit
+ from `Gitlab::BackgroundMigration::BatchedMigrationJob`:
```ruby
- class Gitlab::BackgroundMigration::ExtractIntegrationsUrl
- class Integration < ::ApplicationRecord
- self.table_name = 'integrations'
- end
-
- def perform(start_id, end_id)
- Integration.where(id: start_id..end_id).each do |integration|
- json = JSON.load(integration.properties)
-
- integration.update(url: json['url']) if json['url']
- rescue JSON::ParserError
- # If the JSON is invalid we don't want to keep the job around forever,
- # instead we'll just leave the "url" field to whatever the default value
- # is.
- next
+ class Gitlab::BackgroundMigration::BackfillRouteNamespaceId < BatchedMigrationJob
+ # For illustration purposes, if we were to use a local model we could
+ # define it like below, using an `ApplicationRecord` as the base class
+ # class Route < ::ApplicationRecord
+ # self.table_name = 'routes'
+ # end
+
+ def perform
+ each_sub_batch(
+ operation_name: :update_all,
+ batching_scope: -> (relation) { relation.where("source_type <> 'UnusedType'") }
+ ) do |sub_batch|
+ sub_batch.update_all('namespace_id = source_id')
end
end
end
```
NOTE:
- To get a `connection` in the batched background migration,use an inheritance
- relation using the following base class `Gitlab::BackgroundMigration::BaseJob`.
- For example: `class Gitlab::BackgroundMigration::ExtractIntegrationsUrl < Gitlab::BackgroundMigration::BaseJob`
-
-1. Add a new trigger to the database to update newly created and updated integrations,
+ Job classes must be subclasses of `BatchedMigrationJob` to be
+ correctly handled by the batched migration framework. Any subclass of
+ `BatchedMigrationJob` will be initialized with necessary arguments to
+ execute the batch, as well as a connection to the tracking database.
+ Additional `job_arguments` set on the migration will be passed to the
+ job's `perform` method.
+
+1. Add a new trigger to the database to update newly created and updated routes,
similar to this example:
```ruby
@@ -232,7 +235,7 @@ do this work in a regular migration.
LANGUAGE plpgsql
AS $$
BEGIN
- NEW."url" := NEW.properties -> "url"
+ NEW."namespace_id" = NEW."source_id"
RETURN NEW;
END;
$$;
@@ -242,16 +245,16 @@ do this work in a regular migration.
1. Create a post-deployment migration that queues the migration for existing data:
```ruby
- class QueueExtractIntegrationsUrl < Gitlab::Database::Migration[1.0]
+ class QueueBackfillRoutesNamespaceId < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
- MIGRATION = 'ExtractIntegrationsUrl'
+ MIGRATION = 'BackfillRouteNamespaceId'
DELAY_INTERVAL = 2.minutes
def up
queue_batched_background_migration(
MIGRATION,
- :integrations,
+ :routes,
:id,
job_interval: DELAY_INTERVAL
)
@@ -259,7 +262,7 @@ do this work in a regular migration.
def down
Gitlab::Database::BackgroundMigration::BatchedMigration
- .for_configuration(MIGRATION, :integrations, :id, []).delete_all
+ .for_configuration(MIGRATION, :routes, :id, []).delete_all
end
end
```
@@ -272,14 +275,14 @@ do this work in a regular migration.
that checks that the batched background migration is completed. For example:
```ruby
- class FinalizeExtractIntegrationsUrlJobs < Gitlab::Database::Migration[1.0]
- MIGRATION = 'ExtractIntegrationsUrl'
+ class FinalizeBackfillRouteNamespaceId < Gitlab::Database::Migration[1.0]
+ MIGRATION = 'BackfillRouteNamespaceId'
disable_ddl_transaction!
def up
ensure_batched_background_migration_is_finished(
job_class_name: MIGRATION,
- table_name: :integrations,
+ table_name: :routes,
column_name: :id,
job_arguments: []
)
@@ -295,7 +298,8 @@ do this work in a regular migration.
instance, the data is advisory, and not mission-critical), then you can skip this
final step. This step confirms that the migration is completed, and all of the rows were migrated.
-After the batched migration is completed, you can safely remove the `integrations.properties` column.
+After the batched migration is completed, you can safely depend on the
+data in `routes.namespace_id` being populated.
## Testing
diff --git a/doc/subscriptions/gitlab_dedicated/index.md b/doc/subscriptions/gitlab_dedicated/index.md
index b970dc04a01..909a053fb1d 100644
--- a/doc/subscriptions/gitlab_dedicated/index.md
+++ b/doc/subscriptions/gitlab_dedicated/index.md
@@ -67,6 +67,11 @@ Fill in the following form to contact us and learn more about this offering.
<!-- markdownlint-disable -->
+<!-- NOTE: The following form only shows when the site is served under HTTPS,
+ so it will not appear when developing locally or in a review app.
+ See https://gitlab.com/gitlab-com/marketing/marketing-operations/-/issues/6238#note_923358643
+-->
+
<script src="//page.gitlab.com/js/forms2/js/forms2.min.js"></script>
<form id="mktoForm_3226"></form>
<script>MktoForms2.loadForm("//page.gitlab.com", "194-VVC-221", 3226);</script>
diff --git a/doc/user/report_abuse.md b/doc/user/report_abuse.md
index c0fb29b435a..93d9a1e773e 100644
--- a/doc/user/report_abuse.md
+++ b/doc/user/report_abuse.md
@@ -53,10 +53,8 @@ A URL to the reported user's comment is pre-filled in the abuse report's
## Report abuse from a merge request
-1. On the merge request, in the top right corner, either:
- - Select **Report abuse**. This option is displayed if you do not have permission to close the merge request.
- - Next to **Mark as draft**, select the down arrow (**{chevron-down}**) and then select **Report abuse**.
- This option is displayed if you have permission to close the merge request.
+1. On the merge request, in the top right corner, select the vertical ellipsis (**{ellipsis_v}**).
+1. Select **Report abuse**.
1. Submit an abuse report.
1. Select **Send report**.
diff --git a/lib/bitbucket/representation/repo.rb b/lib/bitbucket/representation/repo.rb
index fa4780dd8de..8d5b15e299a 100644
--- a/lib/bitbucket/representation/repo.rb
+++ b/lib/bitbucket/representation/repo.rb
@@ -3,10 +3,6 @@
module Bitbucket
module Representation
class Repo < Representation::Base
- def initialize(raw)
- super(raw)
- end
-
def owner_and_slug
@owner_and_slug ||= full_name.split('/', 2)
end
diff --git a/lib/bitbucket_server/representation/repo.rb b/lib/bitbucket_server/representation/repo.rb
index 4cd5b75bbed..a3c5f387941 100644
--- a/lib/bitbucket_server/representation/repo.rb
+++ b/lib/bitbucket_server/representation/repo.rb
@@ -3,10 +3,6 @@
module BitbucketServer
module Representation
class Repo < Representation::Base
- def initialize(raw)
- super(raw)
- end
-
def project_key
raw.dig('project', 'key')
end
diff --git a/lib/gitlab/background_migration/.rubocop.yml b/lib/gitlab/background_migration/.rubocop.yml
index 50112a51675..116c84c3759 100644
--- a/lib/gitlab/background_migration/.rubocop.yml
+++ b/lib/gitlab/background_migration/.rubocop.yml
@@ -50,3 +50,12 @@ Style/FrozenStringLiteralComment:
Enabled: true
Details: >-
This removes the need for calling "freeze", reducing noise in the code.
+
+Migration/BackgroundMigrationBaseClass:
+ Enabled: true
+ Exclude:
+ - 'batching_strategies/**/*.rb'
+ - 'job_coordinator.rb'
+ - 'base_job.rb'
+ - 'batched_migration_job.rb'
+ - 'logger.rb'
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/string.rb b/lib/gitlab/ci/pipeline/expression/lexeme/string.rb
index e90e764bcd9..798cea34db6 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/string.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/string.rb
@@ -8,10 +8,6 @@ module Gitlab
class String < Lexeme::Value
PATTERN = /("(?<string>.*?)")|('(?<string>.*?)')/.freeze
- def initialize(value)
- super(value)
- end
-
def evaluate(variables = {})
@value.to_s
end
diff --git a/lib/gitlab/config/entry/validator.rb b/lib/gitlab/config/entry/validator.rb
index e5efd4a7b0a..297645a65c1 100644
--- a/lib/gitlab/config/entry/validator.rb
+++ b/lib/gitlab/config/entry/validator.rb
@@ -7,10 +7,6 @@ module Gitlab
include ActiveModel::Validations
include Entry::Validators
- def initialize(entry)
- super(entry)
- end
-
def messages
errors.full_messages.map do |error|
"#{location} #{error}".downcase
diff --git a/lib/gitlab/git/branch.rb b/lib/gitlab/git/branch.rb
index fbe52db9c0b..9637f8756b1 100644
--- a/lib/gitlab/git/branch.rb
+++ b/lib/gitlab/git/branch.rb
@@ -13,10 +13,6 @@ module Gitlab
end
end
- def initialize(repository, name, target, target_commit)
- super(repository, name, target, target_commit)
- end
-
def active?
self.dereferenced_target.committed_date >= STALE_BRANCH_THRESHOLD.ago
end
diff --git a/lib/gitlab/reactive_cache_set_cache.rb b/lib/gitlab/reactive_cache_set_cache.rb
index e4a92ed5122..7ccbeadfd8a 100644
--- a/lib/gitlab/reactive_cache_set_cache.rb
+++ b/lib/gitlab/reactive_cache_set_cache.rb
@@ -10,10 +10,6 @@ module Gitlab
@expires_in = expires_in
end
- def cache_key(key)
- super(key)
- end
-
def clear_cache!(key)
with do |redis|
keys = read(key).map { |value| "#{cache_namespace}:#{value}" }
diff --git a/lib/gitlab/snippet_search_results.rb b/lib/gitlab/snippet_search_results.rb
index 581d6b738f3..b77f48d1a2c 100644
--- a/lib/gitlab/snippet_search_results.rb
+++ b/lib/gitlab/snippet_search_results.rb
@@ -4,10 +4,6 @@ module Gitlab
class SnippetSearchResults < SearchResults
include SnippetsHelper
- def initialize(current_user, query)
- super(current_user, query)
- end
-
def objects(scope, page: nil, per_page: DEFAULT_PER_PAGE, preload_method: nil)
paginated_objects(snippet_titles, page, per_page)
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index ab11f549de3..14b9a67a672 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -40835,6 +40835,9 @@ msgstr ""
msgid "UsageQuota|%{help_link_start}Shared runners%{help_link_end} are disabled, so there are no limits set on pipeline usage"
msgstr ""
+msgid "UsageQuota|%{linkStart}Shared runners%{linkEnd} are disabled, so there are no limits set on pipeline usage"
+msgstr ""
+
msgid "UsageQuota|%{linkTitle} help link"
msgstr ""
@@ -40934,6 +40937,9 @@ msgstr ""
msgid "UsageQuota|Snippets"
msgstr ""
+msgid "UsageQuota|Something went wrong while fetching pipeline statistics"
+msgstr ""
+
msgid "UsageQuota|Something went wrong while fetching project storage statistics"
msgstr ""
@@ -41663,6 +41669,9 @@ msgstr ""
msgid "ValueStreamAnalytics|Average number of deployments to production per day."
msgstr ""
+msgid "ValueStreamAnalytics|DORA metrics"
+msgstr ""
+
msgid "ValueStreamAnalytics|Dashboard"
msgstr ""
@@ -41672,6 +41681,9 @@ msgstr ""
msgid "ValueStreamAnalytics|Items in Value Stream Analytics are currently filtered by their creation time. There is an %{epic_link_start}epic%{epic_link_end} that will change the Value Stream Analytics date filter to use the end event time for the selected stage."
msgstr ""
+msgid "ValueStreamAnalytics|Key metrics"
+msgstr ""
+
msgid "ValueStreamAnalytics|Median time between merge request merge and deployment to a production environment for all MRs deployed in the given time period."
msgstr ""
diff --git a/qa/qa/resource/group_deploy_token.rb b/qa/qa/resource/group_deploy_token.rb
index c1d6be6547a..4c9b296ece1 100644
--- a/qa/qa/resource/group_deploy_token.rb
+++ b/qa/qa/resource/group_deploy_token.rb
@@ -17,10 +17,6 @@ module QA
end
end
- def fabricate_via_api!
- super
- end
-
def api_get_path
"/groups/#{group.id}/deploy_tokens"
end
diff --git a/qa/qa/resource/project_access_token.rb b/qa/qa/resource/project_access_token.rb
index f5cd8798f19..58cb3e667c0 100644
--- a/qa/qa/resource/project_access_token.rb
+++ b/qa/qa/resource/project_access_token.rb
@@ -15,10 +15,6 @@ module QA
Page::Project::Settings::AccessTokens.perform(&:created_access_token)
end
- def fabricate_via_api!
- super
- end
-
def api_get_path
"/projects/#{project.api_resource[:id]}/access_tokens"
end
diff --git a/qa/qa/resource/project_deploy_token.rb b/qa/qa/resource/project_deploy_token.rb
index b31a7c25157..d6125103025 100644
--- a/qa/qa/resource/project_deploy_token.rb
+++ b/qa/qa/resource/project_deploy_token.rb
@@ -17,10 +17,6 @@ module QA
end
end
- def fabricate_via_api!
- super
- end
-
def api_get_path
"/projects/#{project.id}/deploy_tokens"
end
diff --git a/rubocop/cop/migration/background_migration_base_class.rb b/rubocop/cop/migration/background_migration_base_class.rb
new file mode 100644
index 00000000000..50cbe3a69c3
--- /dev/null
+++ b/rubocop/cop/migration/background_migration_base_class.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module RuboCop
+ module Cop
+ module Migration
+ class BackgroundMigrationBaseClass < RuboCop::Cop::Cop
+ MSG = 'Batched background migration jobs should inherit from Gitlab::BackgroundMigration::BatchedMigrationJob'
+
+ def_node_search :top_level_module?, <<~PATTERN
+ (module (const nil? :Gitlab) (module (const nil? :BackgroundMigration) ...))
+ PATTERN
+
+ def_node_matcher :matching_parent_namespace?, <<~PATTERN
+ {nil? (const (const {cbase nil?} :Gitlab) :BackgroundMigration)}
+ PATTERN
+
+ def_node_search :inherits_batched_migration_job?, <<~PATTERN
+ (class _ (const #matching_parent_namespace? :BatchedMigrationJob) ...)
+ PATTERN
+
+ def on_module(module_node)
+ return unless top_level_module?(module_node)
+
+ top_level_class_node = module_node.each_descendant(:class).first
+
+ return if top_level_class_node.nil? || inherits_batched_migration_job?(top_level_class_node)
+
+ add_offense(top_level_class_node, location: :expression)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/frontend/cycle_analytics/value_stream_metrics_spec.js b/spec/frontend/cycle_analytics/value_stream_metrics_spec.js
index 6199e61df0c..4a3e8146b13 100644
--- a/spec/frontend/cycle_analytics/value_stream_metrics_spec.js
+++ b/spec/frontend/cycle_analytics/value_stream_metrics_spec.js
@@ -1,11 +1,11 @@
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import metricsData from 'test_fixtures/projects/analytics/value_stream_analytics/summary.json';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import ValueStreamMetrics from '~/analytics/shared/components/value_stream_metrics.vue';
import { METRIC_TYPE_SUMMARY } from '~/api/analytics_api';
-import { METRICS_POPOVER_CONTENT } from '~/analytics/shared/constants';
+import { VSA_METRICS_GROUPS, METRICS_POPOVER_CONTENT } from '~/analytics/shared/constants';
import { prepareTimeMetricsData } from '~/analytics/shared/utils';
import MetricTile from '~/analytics/shared/components/metric_tile.vue';
import createFlash from '~/flash';
@@ -27,7 +27,7 @@ describe('ValueStreamMetrics', () => {
});
const createComponent = (props = {}) => {
- return shallowMount(ValueStreamMetrics, {
+ return shallowMountExtended(ValueStreamMetrics, {
propsData: {
requestPath,
requestParams: {},
@@ -38,6 +38,7 @@ describe('ValueStreamMetrics', () => {
};
const findMetrics = () => wrapper.findAllComponents(MetricTile);
+ const findMetricsGroups = () => wrapper.findAllByTestId('vsa-metrics-group');
const expectToHaveRequest = (fields) => {
expect(mockGetValueStreamSummaryMetrics).toHaveBeenCalledWith({
@@ -63,24 +64,6 @@ describe('ValueStreamMetrics', () => {
expect(wrapper.findComponent(GlSkeletonLoading).exists()).toBe(true);
});
- it('renders hidden MetricTile components for each metric', async () => {
- await waitForPromises();
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ isLoading: true });
-
- await nextTick();
-
- const components = findMetrics();
-
- expect(components).toHaveLength(metricsData.length);
-
- metricsData.forEach((metric, index) => {
- expect(components.at(index).isVisible()).toBe(false);
- });
- });
-
describe('with data loaded', () => {
beforeEach(async () => {
await waitForPromises();
@@ -160,6 +143,27 @@ describe('ValueStreamMetrics', () => {
});
});
});
+
+ describe('groupBy', () => {
+ beforeEach(async () => {
+ mockGetValueStreamSummaryMetrics = jest.fn().mockResolvedValue({ data: metricsData });
+ wrapper = createComponent({ groupBy: VSA_METRICS_GROUPS });
+ await waitForPromises();
+ });
+
+ it('renders the metrics as separate groups', () => {
+ const groups = findMetricsGroups();
+ expect(groups).toHaveLength(VSA_METRICS_GROUPS.length);
+ });
+
+ it('renders titles for each group', () => {
+ const groups = findMetricsGroups();
+ groups.wrappers.forEach((g, index) => {
+ const { title } = VSA_METRICS_GROUPS[index];
+ expect(g.html()).toContain(title);
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/pipelines/graph/linked_pipeline_spec.js b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
index 9d420816b47..570731764d3 100644
--- a/spec/frontend/pipelines/graph/linked_pipeline_spec.js
+++ b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import { GlButton, GlLoadingIcon } from '@gitlab/ui';
+import { GlButton, GlLoadingIcon, GlTooltip } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import createMockApollo from 'helpers/mock_apollo_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
@@ -37,14 +37,15 @@ describe('Linked pipeline', () => {
};
const findButton = () => wrapper.find(GlButton);
- const findRetryButton = () => wrapper.findByLabelText('Retry downstream pipeline');
const findCancelButton = () => wrapper.findByLabelText('Cancel downstream pipeline');
+ const findCardTooltip = () => wrapper.findComponent(GlTooltip);
const findDownstreamPipelineTitle = () => wrapper.findByTestId('downstream-title');
- const findPipelineLabel = () => wrapper.findByTestId('downstream-pipeline-label');
+ const findExpandButton = () => wrapper.findByTestId('expand-pipeline-button');
const findLinkedPipeline = () => wrapper.find({ ref: 'linkedPipeline' });
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const findPipelineLabel = () => wrapper.findByTestId('downstream-pipeline-label');
const findPipelineLink = () => wrapper.findByTestId('pipelineLink');
- const findExpandButton = () => wrapper.findByTestId('expand-pipeline-button');
+ const findRetryButton = () => wrapper.findByLabelText('Retry downstream pipeline');
const createWrapper = ({ propsData, downstreamRetryAction = false }) => {
const mockApollo = createMockApollo();
@@ -101,18 +102,13 @@ describe('Linked pipeline', () => {
expect(wrapper.text()).toContain(`#${props.pipeline.id}`);
});
- it('should correctly compute the tooltip text', () => {
- expect(wrapper.vm.tooltipText).toContain(mockPipeline.project.name);
- expect(wrapper.vm.tooltipText).toContain(mockPipeline.status.label);
- expect(wrapper.vm.tooltipText).toContain(mockPipeline.sourceJob.name);
- expect(wrapper.vm.tooltipText).toContain(mockPipeline.id);
- });
-
- it('should render the tooltip text as the title attribute', () => {
- const titleAttr = findLinkedPipeline().attributes('title');
+ it('adds the card tooltip text to the DOM', () => {
+ expect(findCardTooltip().exists()).toBe(true);
- expect(titleAttr).toContain(mockPipeline.project.name);
- expect(titleAttr).toContain(mockPipeline.status.label);
+ expect(findCardTooltip().text()).toContain(mockPipeline.project.name);
+ expect(findCardTooltip().text()).toContain(mockPipeline.status.label);
+ expect(findCardTooltip().text()).toContain(mockPipeline.sourceJob.name);
+ expect(findCardTooltip().text()).toContain(mockPipeline.id);
});
it('should display multi-project label when pipeline project id is not the same as triggered pipeline project id', () => {
@@ -204,6 +200,14 @@ describe('Linked pipeline', () => {
expect(findRetryButton().exists()).toBe(true);
});
+ it('hides the card tooltip when the action button tooltip is hovered', async () => {
+ expect(findCardTooltip().exists()).toBe(true);
+
+ await findRetryButton().trigger('mouseover');
+
+ expect(findCardTooltip().exists()).toBe(false);
+ });
+
describe('and the retry button is clicked', () => {
describe('on success', () => {
beforeEach(async () => {
@@ -258,6 +262,14 @@ describe('Linked pipeline', () => {
expect(findRetryButton().exists()).toBe(false);
});
+ it('hides the card tooltip when the action button tooltip is hovered', async () => {
+ expect(findCardTooltip().exists()).toBe(true);
+
+ await findCancelButton().trigger('mouseover');
+
+ expect(findCardTooltip().exists()).toBe(false);
+ });
+
describe('and the cancel button is clicked', () => {
describe('on success', () => {
beforeEach(async () => {
diff --git a/spec/frontend/sidebar/components/attention_requested_toggle_spec.js b/spec/frontend/sidebar/components/attention_requested_toggle_spec.js
index e3eabc6a0f6..959fa799eb7 100644
--- a/spec/frontend/sidebar/components/attention_requested_toggle_spec.js
+++ b/spec/frontend/sidebar/components/attention_requested_toggle_spec.js
@@ -68,6 +68,7 @@ describe('Attention require toggle', () => {
{
user: { attention_requested: true, can_update_merge_request: true },
callback: expect.anything(),
+ direction: 'remove',
},
]);
});
diff --git a/spec/frontend/sidebar/sidebar_mediator_spec.js b/spec/frontend/sidebar/sidebar_mediator_spec.js
index c472a98bf0b..82fb10ab1d2 100644
--- a/spec/frontend/sidebar/sidebar_mediator_spec.js
+++ b/spec/frontend/sidebar/sidebar_mediator_spec.js
@@ -1,4 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as urlUtility from '~/lib/utils/url_utility';
import SidebarService, { gqClient } from '~/sidebar/services/sidebar_service';
@@ -8,6 +9,7 @@ import toast from '~/vue_shared/plugins/global_toast';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import Mock from './mock_data';
+jest.mock('~/flash');
jest.mock('~/vue_shared/plugins/global_toast');
jest.mock('~/commons/nav/user_merge_requests');
@@ -122,25 +124,39 @@ describe('Sidebar mediator', () => {
});
describe('toggleAttentionRequested', () => {
- let attentionRequiredService;
+ let requestAttentionMock;
+ let removeAttentionRequestMock;
beforeEach(() => {
- attentionRequiredService = jest
- .spyOn(mediator.service, 'toggleAttentionRequested')
+ requestAttentionMock = jest.spyOn(mediator.service, 'requestAttention').mockResolvedValue();
+ removeAttentionRequestMock = jest
+ .spyOn(mediator.service, 'removeAttentionRequest')
.mockResolvedValue();
});
- it('calls attentionRequired service method', async () => {
- mediator.store.reviewers = [{ id: 1, attention_requested: false, username: 'root' }];
+ it.each`
+ attentionIsCurrentlyRequested | serviceMethod
+ ${true} | ${'remove'}
+ ${false} | ${'add'}
+ `(
+ "calls the $serviceMethod service method when the user's attention request is set to $attentionIsCurrentlyRequested",
+ async ({ serviceMethod }) => {
+ const methods = {
+ add: requestAttentionMock,
+ remove: removeAttentionRequestMock,
+ };
+ mediator.store.reviewers = [{ id: 1, attention_requested: false, username: 'root' }];
- await mediator.toggleAttentionRequested('reviewer', {
- user: { id: 1, username: 'root' },
- callback: jest.fn(),
- });
+ await mediator.toggleAttentionRequested('reviewer', {
+ user: { id: 1, username: 'root' },
+ callback: jest.fn(),
+ direction: serviceMethod,
+ });
- expect(attentionRequiredService).toHaveBeenCalledWith(1);
- expect(refreshUserMergeRequestCounts).toHaveBeenCalled();
- });
+ expect(methods[serviceMethod]).toHaveBeenCalledWith(1);
+ expect(refreshUserMergeRequestCounts).toHaveBeenCalled();
+ },
+ );
it.each`
type | method
@@ -172,5 +188,27 @@ describe('Sidebar mediator', () => {
expect(toast).toHaveBeenCalledWith(toastMessage);
},
);
+
+ describe('errors', () => {
+ beforeEach(() => {
+ jest
+ .spyOn(mediator.service, 'removeAttentionRequest')
+ .mockRejectedValueOnce(new Error('Something went wrong'));
+ });
+
+ it('shows an error message', async () => {
+ await mediator.toggleAttentionRequested('reviewer', {
+ user: { id: 1, username: 'root' },
+ callback: jest.fn(),
+ direction: 'remove',
+ });
+
+ expect(createFlash).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: 'Updating the attention request for root failed.',
+ }),
+ );
+ });
+ });
});
});
diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb
index 81aeee0a3d2..3569244e514 100644
--- a/spec/graphql/resolvers/issues_resolver_spec.rb
+++ b/spec/graphql/resolvers/issues_resolver_spec.rb
@@ -389,6 +389,34 @@ RSpec.describe Resolvers::IssuesResolver do
end
end
+ describe 'filtering by crm' do
+ let_it_be(:organization) { create(:organization, group: group) }
+ let_it_be(:contact1) { create(:contact, group: group, organization: organization) }
+ let_it_be(:contact2) { create(:contact, group: group, organization: organization) }
+ let_it_be(:contact3) { create(:contact, group: group) }
+ let_it_be(:crm_issue1) { create(:issue, project: project) }
+ let_it_be(:crm_issue2) { create(:issue, project: project) }
+ let_it_be(:crm_issue3) { create(:issue, project: project) }
+
+ before_all do
+ create(:issue_customer_relations_contact, issue: crm_issue1, contact: contact1)
+ create(:issue_customer_relations_contact, issue: crm_issue2, contact: contact2)
+ create(:issue_customer_relations_contact, issue: crm_issue3, contact: contact3)
+ end
+
+ context 'contact' do
+ it 'returns only the issues for the contact' do
+ expect(resolve_issues({ crm_contact_id: contact1.id })).to contain_exactly(crm_issue1)
+ end
+ end
+
+ context 'organization' do
+ it 'returns only the issues for the contact' do
+ expect(resolve_issues({ crm_organization_id: organization.id })).to contain_exactly(crm_issue1, crm_issue2)
+ end
+ end
+ end
+
describe 'sorting' do
context 'when sorting by created' do
it 'sorts issues ascending' do
diff --git a/spec/helpers/namespaces_helper_spec.rb b/spec/helpers/namespaces_helper_spec.rb
index 52c1130e818..cb09c2f89e1 100644
--- a/spec/helpers/namespaces_helper_spec.rb
+++ b/spec/helpers/namespaces_helper_spec.rb
@@ -275,6 +275,7 @@ RSpec.describe NamespacesHelper do
namespace_actual_plan_name: user_group.actual_plan_name,
namespace_path: user_group.full_path,
namespace_id: user_group.id,
+ user_namespace: user_group.user_namespace?.to_s,
page_size: Kaminari.config.default_per_page
})
end
diff --git a/spec/rubocop/cop/migration/background_migration_base_class_spec.rb b/spec/rubocop/cop/migration/background_migration_base_class_spec.rb
new file mode 100644
index 00000000000..0a110418139
--- /dev/null
+++ b/spec/rubocop/cop/migration/background_migration_base_class_spec.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require_relative '../../../../rubocop/cop/migration/background_migration_base_class'
+
+RSpec.describe RuboCop::Cop::Migration::BackgroundMigrationBaseClass do
+ subject(:cop) { described_class.new }
+
+ context 'when the migration class inherits from BatchedMigrationJob' do
+ it 'does not register any offenses' do
+ expect_no_offenses(<<~RUBY)
+ module Gitlab
+ module BackgroundMigration
+ class MyJob < BatchedMigrationJob
+ def perform
+ connection.execute("select 1")
+ end
+ end
+ end
+ end
+ RUBY
+ end
+ end
+
+ context 'when the migration class inherits from the namespaced BatchedMigrationJob' do
+ it 'does not register any offenses' do
+ expect_no_offenses(<<~RUBY)
+ module Gitlab
+ module BackgroundMigration
+ class MyJob < Gitlab::BackgroundMigration::BatchedMigrationJob
+ def perform
+ connection.execute("select 1")
+ end
+ end
+ end
+ end
+ RUBY
+ end
+ end
+
+ context 'when the migration class inherits from the top-level namespaced BatchedMigrationJob' do
+ it 'does not register any offenses' do
+ expect_no_offenses(<<~RUBY)
+ module Gitlab
+ module BackgroundMigration
+ class MyJob < ::Gitlab::BackgroundMigration::BatchedMigrationJob
+ def perform
+ connection.execute("select 1")
+ end
+ end
+ end
+ end
+ RUBY
+ end
+ end
+
+ context 'when a nested class is used inside the job class' do
+ it 'does not register any offenses' do
+ expect_no_offenses(<<~RUBY)
+ module Gitlab
+ module BackgroundMigration
+ class MyJob < BatchedMigrationJob
+ class Project < ApplicationRecord
+ self.table_name = 'projects'
+ end
+
+ def perform
+ Project.update!(name: 'hi')
+ end
+ end
+ end
+ end
+ RUBY
+ end
+ end
+
+ context 'when the migration class inherits from another class' do
+ it 'registers an offense' do
+ expect_offense(<<~RUBY)
+ module Gitlab
+ module BackgroundMigration
+ class MyJob < SomeOtherClass
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{described_class::MSG}
+ end
+ end
+ end
+ RUBY
+ end
+ end
+
+ context 'when the migration class does not inherit from anything' do
+ it 'registers an offense' do
+ expect_offense(<<~RUBY)
+ module Gitlab
+ module BackgroundMigration
+ class MyJob
+ ^^^^^^^^^^^ #{described_class::MSG}
+ end
+ end
+ end
+ RUBY
+ end
+ end
+end