Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-05-04 12:07:48 +00:00
parent 48640cf76a
commit 8ed0a009f0
55 changed files with 695 additions and 239 deletions

View File

@ -747,3 +747,6 @@ Performance/ActiveRecordSubtransactionMethods:
Exclude: Exclude:
- 'spec/**/*.rb' - 'spec/**/*.rb'
- 'ee/spec/**/*.rb' - 'ee/spec/**/*.rb'
Migration/BackgroundMigrationBaseClass:
Enabled: false

View File

@ -11,12 +11,6 @@ Gitlab/PolicyRuleBoolean:
Exclude: Exclude:
- 'ee/app/policies/ee/identity_provider_policy.rb' - '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 # Offense count: 218
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: PreferredName. # Configuration parameters: PreferredName.

View File

@ -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'

View File

@ -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 { export default {
name: 'ValueStreamMetrics', name: 'ValueStreamMetrics',
components: { components: {
@ -52,13 +68,24 @@ export default {
required: false, required: false,
default: null, default: null,
}, },
groupBy: {
type: Array,
required: false,
default: () => [],
},
}, },
data() { data() {
return { return {
metrics: [], metrics: [],
groupedMetrics: [],
isLoading: false, isLoading: false,
}; };
}, },
computed: {
hasGroupedMetrics() {
return Boolean(this.groupBy.length);
},
},
watch: { watch: {
requestParams(newVal, oldVal) { requestParams(newVal, oldVal) {
if (!isEqual(newVal, oldVal)) { if (!isEqual(newVal, oldVal)) {
@ -76,6 +103,11 @@ export default {
return fetchMetricsData(this.requests, this.requestPath, this.requestParams) return fetchMetricsData(this.requests, this.requestPath, this.requestParams)
.then((data) => { .then((data) => {
this.metrics = this.filterFn ? this.filterFn(data) : data; this.metrics = this.filterFn ? this.filterFn(data) : data;
if (this.hasGroupedMetrics) {
this.groupedMetrics = groupRawMetrics(this.groupBy, this.metrics);
}
this.isLoading = false; this.isLoading = false;
}) })
.catch(() => { .catch(() => {
@ -86,14 +118,35 @@ export default {
}; };
</script> </script>
<template> <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" /> <gl-skeleton-loading v-if="isLoading" class="gl-h-auto gl-py-3 gl-pr-9 gl-my-6" />
<metric-tile <template v-else>
v-for="metric in metrics" <div v-if="hasGroupedMetrics" class="gl-flex-direction-column">
v-show="!isLoading" <div
:key="metric.identifier" v-for="group in groupedMetrics"
:metric="metric" :key="group.key"
class="gl-my-6 gl-pr-9" 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> </div>
</template> </template>

View File

@ -56,3 +56,14 @@ export const METRICS_POPOVER_CONTENT = {
description: s__('ValueStreamAnalytics|Number of commits pushed to the default branch'), 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 },
];

View File

@ -80,17 +80,14 @@ export default {
<template> <template>
<div <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-testid="board-add-new-column"
data-qa-selector="board_add_new_list" data-qa-selector="board_add_new_list"
> >
<div <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 <h3 class="gl-font-size-h2 gl-px-5 gl-py-5 gl-m-0" data-testid="board-add-column-form-title">
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"
>
{{ $options.i18n.newList }} {{ $options.i18n.newList }}
</h3> </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" 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"> <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 }} {{ $options.i18n.scope }}
</h3> </h3>
<p class="gl-mb-3">{{ $options.i18n.scopeDescription }}</p> <p class="gl-mb-3">{{ $options.i18n.scopeDescription }}</p>
@ -147,23 +144,18 @@ export default {
</gl-dropdown> </gl-dropdown>
</gl-form-group> </gl-form-group>
</div> </div>
<div <div class="gl-display-flex gl-mb-4">
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
>
<gl-button <gl-button
data-testid="addNewColumnButton" data-testid="addNewColumnButton"
:disabled="!selectedId" :disabled="!selectedId"
variant="confirm" variant="confirm"
class="gl-mr-4" class="gl-mr-3 gl-ml-4"
@click="$emit('add-list')" @click="$emit('add-list')"
>{{ $options.i18n.add }}</gl-button >{{ $options.i18n.add }}</gl-button
> >
<gl-button data-testid="cancelAddNewColumn" @click="setAddColumnFormVisibility(false)">{{
$options.i18n.cancel
}}</gl-button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -75,7 +75,7 @@ export default {
v-if="!isSwimlanesOn" v-if="!isSwimlanesOn"
ref="list" ref="list"
v-bind="draggableOptions" 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" @end="moveList"
> >
<board-column <board-column

View File

@ -3,6 +3,7 @@ import { GlLoadingIcon } from '@gitlab/ui';
import { mapActions, mapState, mapGetters } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import { getCookie, setCookie } from '~/lib/utils/common_utils'; import { getCookie, setCookie } from '~/lib/utils/common_utils';
import ValueStreamMetrics from '~/analytics/shared/components/value_stream_metrics.vue'; 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 { toYmd } from '~/analytics/shared/utils';
import PathNavigation from '~/cycle_analytics/components/path_navigation.vue'; import PathNavigation from '~/cycle_analytics/components/path_navigation.vue';
import StageTable from '~/cycle_analytics/components/stage_table.vue'; import StageTable from '~/cycle_analytics/components/stage_table.vue';
@ -150,6 +151,7 @@ export default {
pageTitle: __('Value Stream Analytics'), pageTitle: __('Value Stream Analytics'),
recentActivity: __('Recent Project Activity'), recentActivity: __('Recent Project Activity'),
}, },
VSA_METRICS_GROUPS,
}; };
</script> </script>
<template> <template>
@ -178,6 +180,7 @@ export default {
:request-path="endpoints.fullPath" :request-path="endpoints.fullPath"
:request-params="filterParams" :request-params="filterParams"
:requests="metricsRequests" :requests="metricsRequests"
:group-by="$options.VSA_METRICS_GROUPS"
/> />
<gl-loading-icon v-if="isLoading" size="lg" /> <gl-loading-icon v-if="isLoading" size="lg" />
<stage-table <stage-table

View File

@ -1,5 +1,12 @@
<script> <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 { convertToGraphQLId } from '~/graphql_shared/utils';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
@ -21,6 +28,7 @@ export default {
GlButton, GlButton,
GlLink, GlLink,
GlLoadingIcon, GlLoadingIcon,
GlTooltip,
}, },
actionSizeClasses: ['gl-h-7 gl-w-7'], actionSizeClasses: ['gl-h-7 gl-w-7'],
mixins: [glFeatureFlagMixin()], mixins: [glFeatureFlagMixin()],
@ -48,6 +56,7 @@ export default {
}, },
data() { data() {
return { return {
hasActionTooltip: false,
isActionLoading: false, isActionLoading: false,
}; };
}, },
@ -139,13 +148,16 @@ export default {
showAction() { showAction() {
return Boolean(this.action?.method && this.action?.icon && this.action?.ariaLabel); return Boolean(this.action?.method && this.action?.icon && this.action?.ariaLabel);
}, },
showCardTooltip() {
return !this.hasActionTooltip;
},
sourceJobName() { sourceJobName() {
return this.pipeline.sourceJob?.name ?? ''; return this.pipeline.sourceJob?.name ?? '';
}, },
sourceJobInfo() { sourceJobInfo() {
return this.isDownstream ? sprintf(__('Created by %{job}'), { job: this.sourceJobName }) : ''; return this.isDownstream ? sprintf(__('Created by %{job}'), { job: this.sourceJobName }) : '';
}, },
tooltipText() { cardTooltipText() {
return `${this.downstreamTitle} #${this.pipeline.id} - ${this.pipelineStatus.label} - return `${this.downstreamTitle} #${this.pipeline.id} - ${this.pipelineStatus.label} -
${this.sourceJobInfo}`; ${this.sourceJobInfo}`;
}, },
@ -191,6 +203,9 @@ export default {
retryPipeline() { retryPipeline() {
this.executePipelineAction(RetryPipelineMutation); this.executePipelineAction(RetryPipelineMutation);
}, },
setActionTooltip(flag) {
this.hasActionTooltip = flag;
},
}, },
}; };
</script> </script>
@ -198,14 +213,15 @@ export default {
<template> <template>
<div <div
ref="linkedPipeline" ref="linkedPipeline"
v-gl-tooltip
class="gl-h-full gl-display-flex! gl-border-solid gl-border-gray-100 gl-border-1" class="gl-h-full gl-display-flex! gl-border-solid gl-border-gray-100 gl-border-1"
:class="flexDirection" :class="flexDirection"
:title="tooltipText"
data-qa-selector="linked_pipeline_container" data-qa-selector="linked_pipeline_container"
@mouseover="onDownstreamHovered" @mouseover="onDownstreamHovered"
@mouseleave="onDownstreamHoverLeave" @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-w-full gl-bg-white gl-p-3" :class="cardSpacingClass">
<div class="gl-display-flex gl-pr-3"> <div class="gl-display-flex gl-pr-3">
<ci-status <ci-status
@ -227,12 +243,16 @@ export default {
</div> </div>
<gl-button <gl-button
v-if="showAction" v-if="showAction"
v-gl-tooltip
:title="action.ariaLabel"
:loading="isActionLoading" :loading="isActionLoading"
:icon="action.icon" :icon="action.icon"
class="gl-rounded-full!" class="gl-rounded-full!"
:class="$options.actionSizeClasses" :class="$options.actionSizeClasses"
:aria-label="action.ariaLabel" :aria-label="action.ariaLabel"
@click="action.method" @click="action.method"
@mouseover="setActionTooltip(true)"
@mouseout="setActionTooltip(false)"
/> />
<div v-else :class="$options.actionSizeClasses"></div> <div v-else :class="$options.actionSizeClasses"></div>
</div> </div>

View File

@ -47,6 +47,23 @@ export default {
return this.$options.i18n.noAttentionRequestedNoPermission; 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: { methods: {
toggleAttentionRequired() { toggleAttentionRequired() {
@ -57,6 +74,7 @@ export default {
this.$emit('toggle-attention-requested', { this.$emit('toggle-attention-requested', {
user: this.user, user: this.user,
callback: this.toggleAttentionRequiredComplete, callback: this.toggleAttentionRequiredComplete,
direction: this.request.direction,
}); });
}, },
toggleAttentionRequiredComplete() { toggleAttentionRequiredComplete() {
@ -74,8 +92,8 @@ export default {
> >
<gl-button <gl-button
:loading="loading" :loading="loading"
:variant="user.attention_requested ? 'warning' : 'default'" :variant="request.variant"
:icon="user.attention_requested ? 'attention-solid' : 'attention'" :icon="request.icon"
:aria-label="tooltipTitle" :aria-label="tooltipTitle"
:class="{ 'gl-pointer-events-none': !user.can_update_merge_request }" :class="{ 'gl-pointer-events-none': !user.can_update_merge_request }"
size="small" size="small"

View File

@ -0,0 +1,7 @@
mutation mergeRequestRemoveAttentionRequest($projectPath: ID!, $iid: String!, $userId: ID!) {
mergeRequestRemoveAttentionRequest(
input: { projectPath: $projectPath, iid: $iid, userId: $userId }
) {
errors
}
}

View File

@ -0,0 +1,5 @@
mutation mergeRequestRequestAttention($projectPath: ID!, $iid: String!, $userId: ID!) {
mergeRequestRequestAttention(input: { projectPath: $projectPath, iid: $iid, userId: $userId }) {
errors
}
}

View File

@ -1,7 +0,0 @@
mutation mergeRequestToggleAttentionRequested($projectPath: ID!, $iid: String!, $userId: UserID!) {
mergeRequestToggleAttentionRequested(
input: { projectPath: $projectPath, iid: $iid, userId: $userId }
) {
errors
}
}

View File

@ -5,7 +5,8 @@ import createGqClient, { fetchPolicies } from '~/lib/graphql';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import reviewerRereviewMutation from '../queries/reviewer_rereview.mutation.graphql'; import reviewerRereviewMutation from '../queries/reviewer_rereview.mutation.graphql';
import sidebarDetailsMRQuery from '../queries/sidebar_details_mr.query.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 = { const queries = {
merge_request: sidebarDetailsMRQuery, merge_request: sidebarDetailsMRQuery,
@ -92,9 +93,19 @@ export default class SidebarService {
}); });
} }
toggleAttentionRequested(userId) { requestAttention(userId) {
return gqClient.mutate({ 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: { variables: {
userId: convertToGraphQLId(TYPE_USER, `${userId}`), userId: convertToGraphQLId(TYPE_USER, `${userId}`),
projectPath: this.fullPath, projectPath: this.fullPath,

View File

@ -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 { try {
const isReviewer = type === 'reviewer'; const isReviewer = type === 'reviewer';
const reviewerOrAssignee = isReviewer const reviewerOrAssignee = isReviewer
? this.store.findReviewer(user) ? this.store.findReviewer(user)
: this.store.findAssignee(user); : this.store.findAssignee(user);
await this.service.toggleAttentionRequested(user.id); await mutations[direction]?.(user.id);
if (reviewerOrAssignee.attention_requested) { if (reviewerOrAssignee.attention_requested) {
toast( toast(
@ -138,7 +143,7 @@ export default class SidebarMediator {
captureError: true, captureError: true,
actionConfig: { actionConfig: {
title: __('Try again'), title: __('Try again'),
clickHandler: () => this.toggleAttentionRequired(type, { user, callback }), clickHandler: () => this.toggleAttentionRequired(type, { user, callback, direction }),
}, },
}); });
} }

View File

@ -49,6 +49,7 @@
height: calc(100vh - #{$issue-board-list-difference-xs}); height: calc(100vh - #{$issue-board-list-difference-xs});
overflow-x: scroll; overflow-x: scroll;
min-height: 200px; min-height: 200px;
border-left: 8px solid var(--gray-10, $white);
@include media-breakpoint-only(sm) { @include media-breakpoint-only(sm) {
height: calc(100vh - #{$issue-board-list-difference-sm}); height: calc(100vh - #{$issue-board-list-difference-sm});

View File

@ -29,13 +29,12 @@ class Import::BitbucketController < Import::BaseController
end end
end end
# We need to re-expose controller's internal method 'status' as action.
# rubocop:disable Lint/UselessMethodDefinition
def status def status
super super
end end
# rubocop:enable Lint/UselessMethodDefinition
def realtime_changes
super
end
def create def create
bitbucket_client = Bitbucket::Client.new(credentials) bitbucket_client = Bitbucket::Client.new(credentials)

View File

@ -52,13 +52,12 @@ class Import::BitbucketServerController < Import::BaseController
redirect_to status_import_bitbucket_server_path redirect_to status_import_bitbucket_server_path
end end
# We need to re-expose controller's internal method 'status' as action.
# rubocop:disable Lint/UselessMethodDefinition
def status def status
super super
end end
# rubocop:enable Lint/UselessMethodDefinition
def realtime_changes
super
end
protected protected

View File

@ -54,10 +54,6 @@ class Import::FogbugzController < Import::BaseController
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def realtime_changes
super
end
def create def create
repo = client.repo(params[:repo_id]) repo = client.repo(params[:repo_id])
fb_session = { uri: session[:fogbugz_uri], token: session[:fogbugz_token] } fb_session = { uri: session[:fogbugz_uri], token: session[:fogbugz_token] }

View File

@ -16,10 +16,12 @@ class Import::GiteaController < Import::GithubController
super super
end 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 def status
super super
end end
# rubocop:enable Lint/UselessMethodDefinition
protected protected

View File

@ -16,9 +16,12 @@ class Import::GitlabController < Import::BaseController
redirect_to status_import_gitlab_url redirect_to status_import_gitlab_url
end end
# We need to re-expose controller's internal method 'status' as action.
# rubocop:disable Lint/UselessMethodDefinition
def status def status
super super
end end
# rubocop:enable Lint/UselessMethodDefinition
def create def create
repo = client.project(params[:repo_id].to_i) repo = client.project(params[:repo_id].to_i)

View File

@ -10,9 +10,12 @@ class Import::ManifestController < Import::BaseController
def new def new
end end
# We need to re-expose controller's internal method 'status' as action.
# rubocop:disable Lint/UselessMethodDefinition
def status def status
super super
end end
# rubocop:enable Lint/UselessMethodDefinition
def upload def upload
group = Group.find(params[:group_id]) group = Group.find(params[:group_id])
@ -36,10 +39,6 @@ class Import::ManifestController < Import::BaseController
end end
end end
def realtime_changes
super
end
def create def create
repository = importable_repos.find do |project| repository = importable_repos.find do |project|
project[:id] == params[:repo_id].to_i project[:id] == params[:repo_id].to_i

View File

@ -13,7 +13,9 @@ module Projects
prepend_before_action :repository, :project_without_auth prepend_before_action :repository, :project_without_auth
feature_category :incident_management 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 def create
token = extract_alert_manager_token(request) token = extract_alert_manager_token(request)

View File

@ -1,10 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class TagsFinder < GitRefsFinder class TagsFinder < GitRefsFinder
def initialize(repository, params)
super(repository, params)
end
def execute(gitaly_pagination: false) def execute(gitaly_pagination: false)
tags = if gitaly_pagination tags = if gitaly_pagination
repository.tags_sorted_by(sort, pagination_params) repository.tags_sorted_by(sort, pagination_params)

View File

@ -68,6 +68,12 @@ module IssueResolverArguments
description: 'Negated arguments.', description: 'Negated arguments.',
prepare: ->(negated_args, ctx) { negated_args.to_h }, prepare: ->(negated_args, ctx) { negated_args.to_h },
required: false 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 end
def resolve_with_lookahead(**args) def resolve_with_lookahead(**args)

View File

@ -93,6 +93,7 @@ module NamespacesHelper
namespace_actual_plan_name: namespace.actual_plan_name, namespace_actual_plan_name: namespace.actual_plan_name,
namespace_path: namespace.full_path, namespace_path: namespace.full_path,
namespace_id: namespace.id, namespace_id: namespace.id,
user_namespace: namespace.user_namespace?.to_s,
page_size: page_size page_size: page_size
} }
end end

View File

@ -25,9 +25,7 @@ module MergeRequests
# expose issuable create method so it can be called from email # expose issuable create method so it can be called from email
# handler CreateMergeRequestHandler # handler CreateMergeRequestHandler
def create(merge_request) public :create
super
end
private private

View File

@ -3,22 +3,13 @@
%fieldset %fieldset
.form-group .form-group
.form-check = f.gitlab_ui_checkbox_component :password_authentication_enabled_for_web,
= f.check_box :password_authentication_enabled_for_web, class: 'form-check-input' _('Allow password authentication for the web interface'),
= f.label :password_authentication_enabled_for_web, class: 'form-check-label' do help_text: _('Clear this checkbox to use an external authentication provider instead.')
= _('Allow password authentication for the web interface')
.form-text.text-muted
= _('Clear this checkbox to use an external authentication provider instead.')
.form-group .form-group
.form-check = f.gitlab_ui_checkbox_component :password_authentication_enabled_for_git,
= f.check_box :password_authentication_enabled_for_git, class: 'form-check-input' _('Allow password authentication for Git over HTTP(S)'),
= f.label :password_authentication_enabled_for_git, class: 'form-check-label' do 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.')
= _('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.')
- if omniauth_enabled? && button_based_providers.any? - if omniauth_enabled? && button_based_providers.any?
%fieldset.form-group %fieldset.form-group
%legend.gl-font-base.gl-mb-3.gl-border-none.gl-font-weight-bold= _('Enabled OAuth authentication sources') %legend.gl-font-base.gl-mb-3.gl-border-none.gl-font-weight-bold= _('Enabled OAuth authentication sources')
@ -27,13 +18,11 @@
= source = source
.form-group .form-group
= f.label :two_factor_authentication, _('Two-factor authentication'), class: 'label-bold' = f.label :two_factor_authentication, _('Two-factor authentication'), class: 'label-bold'
.form-check - help_text = _('Enforce two-factor authentication for all user sign-ins.')
= f.check_box :require_two_factor_authentication, class: 'form-check-input' - help_link = link_to _('Learn more.'), help_page_path('security/two_factor_authentication.md'), target: '_blank', rel: 'noopener noreferrer'
= f.label :require_two_factor_authentication, class: 'form-check-label' do = f.gitlab_ui_checkbox_component :require_two_factor_authentication,
= _('Enforce two-factor authentication') _('Enforce two-factor authentication'),
%p.form-text.text-muted help_text: '%{help_text} %{help_link}'.html_safe % { help_text: help_text, help_link: help_link }
= _('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'
.form-group .form-group
= f.label :two_factor_authentication, _('Two-factor grace period'), class: 'label-bold' = 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' = f.number_field :two_factor_grace_period, min: 0, class: 'form-control gl-form-input', placeholder: '0'
@ -42,22 +31,18 @@
.form-group .form-group
= f.label :admin_mode, _('Admin Mode'), class: 'label-bold' = f.label :admin_mode, _('Admin Mode'), class: 'label-bold'
= sprite_icon('lock', css_class: 'gl-icon') = sprite_icon('lock', css_class: 'gl-icon')
.form-check - help_text = _('Require additional authentication for administrative tasks.')
= f.check_box :admin_mode, class: 'form-check-input' - 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.label :admin_mode, class: 'form-check-label' do = f.gitlab_ui_checkbox_component :admin_mode,
= _('Enable admin mode') _('Enable admin mode'),
%p.form-text.text-muted help_text: '%{help_text} %{help_link}'.html_safe % { help_text: help_text, help_link: help_link }
= _('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'
.form-group .form-group
= f.label :unknown_sign_in, _('Email notification for unknown sign-ins'), class: 'label-bold' = f.label :unknown_sign_in, _('Email notification for unknown sign-ins'), class: 'label-bold'
.form-check - help_text = _('Notify users by email when sign-in location is not recognized.')
= f.check_box :notify_on_unknown_sign_in, class: 'form-check-input' - help_link = link_to _('Learn more.'), help_page_path('user/profile/unknown_sign_in_notification.md'), target: '_blank', rel: 'noopener noreferrer'
= f.label :notify_on_unknown_sign_in, class: 'form-check-label' do = f.gitlab_ui_checkbox_component :notify_on_unknown_sign_in,
= _('Enable email notification') _('Enable email notification'),
%p.form-text.text-muted help_text: '%{help_text} %{help_link}'.html_safe % { help_text: help_text, help_link: help_link }
= _('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'
.form-group .form-group
= f.label :home_page_url, _('Home page URL'), class: 'label-bold' = 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' = f.text_field :home_page_url, class: 'form-control gl-form-input', placeholder: 'http://company.example.com', :'aria-describedby' => 'home_help_block'

View File

@ -39,10 +39,9 @@
.label-container .label-container
- if generic_commit_status.tags.any? - if generic_commit_status.tags.any?
- generic_commit_status.tags.each do |tag| - generic_commit_status.tags.each do |tag|
%span.badge.badge-primary = gl_badge_tag tag, variant: :info, size: :sm
= tag
- if retried - if retried
%span.badge.badge-warning retried = gl_badge_tag retried, variant: :warning, size: :sm
- if pipeline_link - if pipeline_link
%td %td

View File

@ -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

View File

@ -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

View File

@ -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="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="groupissuescreatedafter"></a>`createdAfter` | [`Time`](#time) | Issues created after this date. |
| <a id="groupissuescreatedbefore"></a>`createdBefore` | [`Time`](#time) | Issues created before 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="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="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"]`. | | <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="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="projectissuecreatedafter"></a>`createdAfter` | [`Time`](#time) | Issues created after this date. |
| <a id="projectissuecreatedbefore"></a>`createdBefore` | [`Time`](#time) | Issues created before 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="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="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"]`. | | <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="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="projectissuestatuscountscreatedafter"></a>`createdAfter` | [`Time`](#time) | Issues created after this date. |
| <a id="projectissuestatuscountscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Issues created before 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="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="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. | | <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="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="projectissuescreatedafter"></a>`createdAfter` | [`Time`](#time) | Issues created after this date. |
| <a id="projectissuescreatedbefore"></a>`createdBefore` | [`Time`](#time) | Issues created before 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="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="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"]`. | | <a id="projectissuesiids"></a>`iids` | [`[String!]`](#string) | List of IIDs of issues. For example, `["1", "2"]`. |

View File

@ -190,40 +190,43 @@ data to be in the new format.
## Example ## Example
The table `integrations` has a field called `properties`, stored in JSON. For all rows, The `routes` table has a `source_type` field that's used for a polymorphic relationship.
extract the `url` key from this JSON object and store it in the `integrations.url` As part of a database redesign, we're removing the polymorphic relationship. One step of
column. Millions of integrations exist, and parsing JSON is slow, so you can't the work will be migrating data from the `source_id` column into a new singular foreign key.
do this work in a regular migration. 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 ```ruby
class Gitlab::BackgroundMigration::ExtractIntegrationsUrl class Gitlab::BackgroundMigration::BackfillRouteNamespaceId < BatchedMigrationJob
class Integration < ::ApplicationRecord # For illustration purposes, if we were to use a local model we could
self.table_name = 'integrations' # define it like below, using an `ApplicationRecord` as the base class
end # class Route < ::ApplicationRecord
# self.table_name = 'routes'
# end
def perform(start_id, end_id) def perform
Integration.where(id: start_id..end_id).each do |integration| each_sub_batch(
json = JSON.load(integration.properties) operation_name: :update_all,
batching_scope: -> (relation) { relation.where("source_type <> 'UnusedType'") }
integration.update(url: json['url']) if json['url'] ) do |sub_batch|
rescue JSON::ParserError sub_batch.update_all('namespace_id = source_id')
# 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
end end
end end
end end
``` ```
NOTE: NOTE:
To get a `connection` in the batched background migration,use an inheritance Job classes must be subclasses of `BatchedMigrationJob` to be
relation using the following base class `Gitlab::BackgroundMigration::BaseJob`. correctly handled by the batched migration framework. Any subclass of
For example: `class Gitlab::BackgroundMigration::ExtractIntegrationsUrl < Gitlab::BackgroundMigration::BaseJob` `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 integrations, 1. Add a new trigger to the database to update newly created and updated routes,
similar to this example: similar to this example:
```ruby ```ruby
@ -232,7 +235,7 @@ do this work in a regular migration.
LANGUAGE plpgsql LANGUAGE plpgsql
AS $$ AS $$
BEGIN BEGIN
NEW."url" := NEW.properties -> "url" NEW."namespace_id" = NEW."source_id"
RETURN NEW; RETURN NEW;
END; 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: 1. Create a post-deployment migration that queues the migration for existing data:
```ruby ```ruby
class QueueExtractIntegrationsUrl < Gitlab::Database::Migration[1.0] class QueueBackfillRoutesNamespaceId < Gitlab::Database::Migration[1.0]
disable_ddl_transaction! disable_ddl_transaction!
MIGRATION = 'ExtractIntegrationsUrl' MIGRATION = 'BackfillRouteNamespaceId'
DELAY_INTERVAL = 2.minutes DELAY_INTERVAL = 2.minutes
def up def up
queue_batched_background_migration( queue_batched_background_migration(
MIGRATION, MIGRATION,
:integrations, :routes,
:id, :id,
job_interval: DELAY_INTERVAL job_interval: DELAY_INTERVAL
) )
@ -259,7 +262,7 @@ do this work in a regular migration.
def down def down
Gitlab::Database::BackgroundMigration::BatchedMigration Gitlab::Database::BackgroundMigration::BatchedMigration
.for_configuration(MIGRATION, :integrations, :id, []).delete_all .for_configuration(MIGRATION, :routes, :id, []).delete_all
end end
end end
``` ```
@ -272,14 +275,14 @@ do this work in a regular migration.
that checks that the batched background migration is completed. For example: that checks that the batched background migration is completed. For example:
```ruby ```ruby
class FinalizeExtractIntegrationsUrlJobs < Gitlab::Database::Migration[1.0] class FinalizeBackfillRouteNamespaceId < Gitlab::Database::Migration[1.0]
MIGRATION = 'ExtractIntegrationsUrl' MIGRATION = 'BackfillRouteNamespaceId'
disable_ddl_transaction! disable_ddl_transaction!
def up def up
ensure_batched_background_migration_is_finished( ensure_batched_background_migration_is_finished(
job_class_name: MIGRATION, job_class_name: MIGRATION,
table_name: :integrations, table_name: :routes,
column_name: :id, column_name: :id,
job_arguments: [] 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 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. 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 ## Testing

View File

@ -67,6 +67,11 @@ Fill in the following form to contact us and learn more about this offering.
<!-- markdownlint-disable --> <!-- 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> <script src="//page.gitlab.com/js/forms2/js/forms2.min.js"></script>
<form id="mktoForm_3226"></form> <form id="mktoForm_3226"></form>
<script>MktoForms2.loadForm("//page.gitlab.com", "194-VVC-221", 3226);</script> <script>MktoForms2.loadForm("//page.gitlab.com", "194-VVC-221", 3226);</script>

View File

@ -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 ## Report abuse from a merge request
1. On the merge request, in the top right corner, either: 1. On the merge request, in the top right corner, select the vertical ellipsis (**{ellipsis_v}**).
- Select **Report abuse**. This option is displayed if you do not have permission to close the merge request. 1. Select **Report abuse**.
- 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. Submit an abuse report. 1. Submit an abuse report.
1. Select **Send report**. 1. Select **Send report**.

View File

@ -3,10 +3,6 @@
module Bitbucket module Bitbucket
module Representation module Representation
class Repo < Representation::Base class Repo < Representation::Base
def initialize(raw)
super(raw)
end
def owner_and_slug def owner_and_slug
@owner_and_slug ||= full_name.split('/', 2) @owner_and_slug ||= full_name.split('/', 2)
end end

View File

@ -3,10 +3,6 @@
module BitbucketServer module BitbucketServer
module Representation module Representation
class Repo < Representation::Base class Repo < Representation::Base
def initialize(raw)
super(raw)
end
def project_key def project_key
raw.dig('project', 'key') raw.dig('project', 'key')
end end

View File

@ -50,3 +50,12 @@ Style/FrozenStringLiteralComment:
Enabled: true Enabled: true
Details: >- Details: >-
This removes the need for calling "freeze", reducing noise in the code. 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'

View File

@ -8,10 +8,6 @@ module Gitlab
class String < Lexeme::Value class String < Lexeme::Value
PATTERN = /("(?<string>.*?)")|('(?<string>.*?)')/.freeze PATTERN = /("(?<string>.*?)")|('(?<string>.*?)')/.freeze
def initialize(value)
super(value)
end
def evaluate(variables = {}) def evaluate(variables = {})
@value.to_s @value.to_s
end end

View File

@ -7,10 +7,6 @@ module Gitlab
include ActiveModel::Validations include ActiveModel::Validations
include Entry::Validators include Entry::Validators
def initialize(entry)
super(entry)
end
def messages def messages
errors.full_messages.map do |error| errors.full_messages.map do |error|
"#{location} #{error}".downcase "#{location} #{error}".downcase

View File

@ -13,10 +13,6 @@ module Gitlab
end end
end end
def initialize(repository, name, target, target_commit)
super(repository, name, target, target_commit)
end
def active? def active?
self.dereferenced_target.committed_date >= STALE_BRANCH_THRESHOLD.ago self.dereferenced_target.committed_date >= STALE_BRANCH_THRESHOLD.ago
end end

View File

@ -10,10 +10,6 @@ module Gitlab
@expires_in = expires_in @expires_in = expires_in
end end
def cache_key(key)
super(key)
end
def clear_cache!(key) def clear_cache!(key)
with do |redis| with do |redis|
keys = read(key).map { |value| "#{cache_namespace}:#{value}" } keys = read(key).map { |value| "#{cache_namespace}:#{value}" }

View File

@ -4,10 +4,6 @@ module Gitlab
class SnippetSearchResults < SearchResults class SnippetSearchResults < SearchResults
include SnippetsHelper 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) def objects(scope, page: nil, per_page: DEFAULT_PER_PAGE, preload_method: nil)
paginated_objects(snippet_titles, page, per_page) paginated_objects(snippet_titles, page, per_page)
end end

View File

@ -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" msgid "UsageQuota|%{help_link_start}Shared runners%{help_link_end} are disabled, so there are no limits set on pipeline usage"
msgstr "" msgstr ""
msgid "UsageQuota|%{linkStart}Shared runners%{linkEnd} are disabled, so there are no limits set on pipeline usage"
msgstr ""
msgid "UsageQuota|%{linkTitle} help link" msgid "UsageQuota|%{linkTitle} help link"
msgstr "" msgstr ""
@ -40934,6 +40937,9 @@ msgstr ""
msgid "UsageQuota|Snippets" msgid "UsageQuota|Snippets"
msgstr "" msgstr ""
msgid "UsageQuota|Something went wrong while fetching pipeline statistics"
msgstr ""
msgid "UsageQuota|Something went wrong while fetching project storage statistics" msgid "UsageQuota|Something went wrong while fetching project storage statistics"
msgstr "" msgstr ""
@ -41663,6 +41669,9 @@ msgstr ""
msgid "ValueStreamAnalytics|Average number of deployments to production per day." msgid "ValueStreamAnalytics|Average number of deployments to production per day."
msgstr "" msgstr ""
msgid "ValueStreamAnalytics|DORA metrics"
msgstr ""
msgid "ValueStreamAnalytics|Dashboard" msgid "ValueStreamAnalytics|Dashboard"
msgstr "" 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." 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 "" 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." msgid "ValueStreamAnalytics|Median time between merge request merge and deployment to a production environment for all MRs deployed in the given time period."
msgstr "" msgstr ""

View File

@ -17,10 +17,6 @@ module QA
end end
end end
def fabricate_via_api!
super
end
def api_get_path def api_get_path
"/groups/#{group.id}/deploy_tokens" "/groups/#{group.id}/deploy_tokens"
end end

View File

@ -15,10 +15,6 @@ module QA
Page::Project::Settings::AccessTokens.perform(&:created_access_token) Page::Project::Settings::AccessTokens.perform(&:created_access_token)
end end
def fabricate_via_api!
super
end
def api_get_path def api_get_path
"/projects/#{project.api_resource[:id]}/access_tokens" "/projects/#{project.api_resource[:id]}/access_tokens"
end end

View File

@ -17,10 +17,6 @@ module QA
end end
end end
def fabricate_via_api!
super
end
def api_get_path def api_get_path
"/projects/#{project.id}/deploy_tokens" "/projects/#{project.id}/deploy_tokens"
end end

View File

@ -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

View File

@ -1,11 +1,11 @@
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui'; import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import metricsData from 'test_fixtures/projects/analytics/value_stream_analytics/summary.json'; 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 waitForPromises from 'helpers/wait_for_promises';
import ValueStreamMetrics from '~/analytics/shared/components/value_stream_metrics.vue'; import ValueStreamMetrics from '~/analytics/shared/components/value_stream_metrics.vue';
import { METRIC_TYPE_SUMMARY } from '~/api/analytics_api'; 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 { prepareTimeMetricsData } from '~/analytics/shared/utils';
import MetricTile from '~/analytics/shared/components/metric_tile.vue'; import MetricTile from '~/analytics/shared/components/metric_tile.vue';
import createFlash from '~/flash'; import createFlash from '~/flash';
@ -27,7 +27,7 @@ describe('ValueStreamMetrics', () => {
}); });
const createComponent = (props = {}) => { const createComponent = (props = {}) => {
return shallowMount(ValueStreamMetrics, { return shallowMountExtended(ValueStreamMetrics, {
propsData: { propsData: {
requestPath, requestPath,
requestParams: {}, requestParams: {},
@ -38,6 +38,7 @@ describe('ValueStreamMetrics', () => {
}; };
const findMetrics = () => wrapper.findAllComponents(MetricTile); const findMetrics = () => wrapper.findAllComponents(MetricTile);
const findMetricsGroups = () => wrapper.findAllByTestId('vsa-metrics-group');
const expectToHaveRequest = (fields) => { const expectToHaveRequest = (fields) => {
expect(mockGetValueStreamSummaryMetrics).toHaveBeenCalledWith({ expect(mockGetValueStreamSummaryMetrics).toHaveBeenCalledWith({
@ -63,24 +64,6 @@ describe('ValueStreamMetrics', () => {
expect(wrapper.findComponent(GlSkeletonLoading).exists()).toBe(true); 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', () => { describe('with data loaded', () => {
beforeEach(async () => { beforeEach(async () => {
await waitForPromises(); 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);
});
});
});
}); });
}); });

View File

@ -1,6 +1,6 @@
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo'; 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 { mount } from '@vue/test-utils';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
@ -37,14 +37,15 @@ describe('Linked pipeline', () => {
}; };
const findButton = () => wrapper.find(GlButton); const findButton = () => wrapper.find(GlButton);
const findRetryButton = () => wrapper.findByLabelText('Retry downstream pipeline');
const findCancelButton = () => wrapper.findByLabelText('Cancel downstream pipeline'); const findCancelButton = () => wrapper.findByLabelText('Cancel downstream pipeline');
const findCardTooltip = () => wrapper.findComponent(GlTooltip);
const findDownstreamPipelineTitle = () => wrapper.findByTestId('downstream-title'); 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 findLinkedPipeline = () => wrapper.find({ ref: 'linkedPipeline' });
const findLoadingIcon = () => wrapper.find(GlLoadingIcon); const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findPipelineLabel = () => wrapper.findByTestId('downstream-pipeline-label');
const findPipelineLink = () => wrapper.findByTestId('pipelineLink'); 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 createWrapper = ({ propsData, downstreamRetryAction = false }) => {
const mockApollo = createMockApollo(); const mockApollo = createMockApollo();
@ -101,18 +102,13 @@ describe('Linked pipeline', () => {
expect(wrapper.text()).toContain(`#${props.pipeline.id}`); expect(wrapper.text()).toContain(`#${props.pipeline.id}`);
}); });
it('should correctly compute the tooltip text', () => { it('adds the card tooltip text to the DOM', () => {
expect(wrapper.vm.tooltipText).toContain(mockPipeline.project.name); expect(findCardTooltip().exists()).toBe(true);
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', () => { expect(findCardTooltip().text()).toContain(mockPipeline.project.name);
const titleAttr = findLinkedPipeline().attributes('title'); expect(findCardTooltip().text()).toContain(mockPipeline.status.label);
expect(findCardTooltip().text()).toContain(mockPipeline.sourceJob.name);
expect(titleAttr).toContain(mockPipeline.project.name); expect(findCardTooltip().text()).toContain(mockPipeline.id);
expect(titleAttr).toContain(mockPipeline.status.label);
}); });
it('should display multi-project label when pipeline project id is not the same as triggered pipeline project 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); 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('and the retry button is clicked', () => {
describe('on success', () => { describe('on success', () => {
beforeEach(async () => { beforeEach(async () => {
@ -258,6 +262,14 @@ describe('Linked pipeline', () => {
expect(findRetryButton().exists()).toBe(false); 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('and the cancel button is clicked', () => {
describe('on success', () => { describe('on success', () => {
beforeEach(async () => { beforeEach(async () => {

View File

@ -68,6 +68,7 @@ describe('Attention require toggle', () => {
{ {
user: { attention_requested: true, can_update_merge_request: true }, user: { attention_requested: true, can_update_merge_request: true },
callback: expect.anything(), callback: expect.anything(),
direction: 'remove',
}, },
]); ]);
}); });

View File

@ -1,4 +1,5 @@
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import * as urlUtility from '~/lib/utils/url_utility'; import * as urlUtility from '~/lib/utils/url_utility';
import SidebarService, { gqClient } from '~/sidebar/services/sidebar_service'; 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 { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import Mock from './mock_data'; import Mock from './mock_data';
jest.mock('~/flash');
jest.mock('~/vue_shared/plugins/global_toast'); jest.mock('~/vue_shared/plugins/global_toast');
jest.mock('~/commons/nav/user_merge_requests'); jest.mock('~/commons/nav/user_merge_requests');
@ -122,25 +124,39 @@ describe('Sidebar mediator', () => {
}); });
describe('toggleAttentionRequested', () => { describe('toggleAttentionRequested', () => {
let attentionRequiredService; let requestAttentionMock;
let removeAttentionRequestMock;
beforeEach(() => { beforeEach(() => {
attentionRequiredService = jest requestAttentionMock = jest.spyOn(mediator.service, 'requestAttention').mockResolvedValue();
.spyOn(mediator.service, 'toggleAttentionRequested') removeAttentionRequestMock = jest
.spyOn(mediator.service, 'removeAttentionRequest')
.mockResolvedValue(); .mockResolvedValue();
}); });
it('calls attentionRequired service method', async () => { it.each`
mediator.store.reviewers = [{ id: 1, attention_requested: false, username: 'root' }]; 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', { await mediator.toggleAttentionRequested('reviewer', {
user: { id: 1, username: 'root' }, user: { id: 1, username: 'root' },
callback: jest.fn(), callback: jest.fn(),
}); direction: serviceMethod,
});
expect(attentionRequiredService).toHaveBeenCalledWith(1); expect(methods[serviceMethod]).toHaveBeenCalledWith(1);
expect(refreshUserMergeRequestCounts).toHaveBeenCalled(); expect(refreshUserMergeRequestCounts).toHaveBeenCalled();
}); },
);
it.each` it.each`
type | method type | method
@ -172,5 +188,27 @@ describe('Sidebar mediator', () => {
expect(toast).toHaveBeenCalledWith(toastMessage); 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.',
}),
);
});
});
}); });
}); });

View File

@ -389,6 +389,34 @@ RSpec.describe Resolvers::IssuesResolver do
end end
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 describe 'sorting' do
context 'when sorting by created' do context 'when sorting by created' do
it 'sorts issues ascending' do it 'sorts issues ascending' do

View File

@ -275,6 +275,7 @@ RSpec.describe NamespacesHelper do
namespace_actual_plan_name: user_group.actual_plan_name, namespace_actual_plan_name: user_group.actual_plan_name,
namespace_path: user_group.full_path, namespace_path: user_group.full_path,
namespace_id: user_group.id, namespace_id: user_group.id,
user_namespace: user_group.user_namespace?.to_s,
page_size: Kaminari.config.default_per_page page_size: Kaminari.config.default_per_page
}) })
end end

View File

@ -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