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:
- 'spec/**/*.rb'
- 'ee/spec/**/*.rb'
Migration/BackgroundMigrationBaseClass:
Enabled: false

View File

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

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

View File

@ -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 },
];

View File

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

View File

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

View File

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

View File

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

View File

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

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

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 {
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 }),
},
});
}

View File

@ -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});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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="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"]`. |

View File

@ -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
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(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
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`
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 integrations,
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

View File

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

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
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**.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@ -17,10 +17,6 @@ module QA
end
end
def fabricate_via_api!
super
end
def api_get_path
"/projects/#{project.id}/deploy_tokens"
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 { 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);
});
});
});
});
});

View File

@ -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('adds the card tooltip text to the DOM', () => {
expect(findCardTooltip().exists()).toBe(true);
it('should render the tooltip text as the title attribute', () => {
const titleAttr = findLinkedPipeline().attributes('title');
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 () => {

View File

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

View File

@ -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.',
}),
);
});
});
});
});

View File

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

View File

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

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