diff options
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 |