Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
05be289de1
commit
9a66377d7f
|
|
@ -306,6 +306,59 @@ rspec system pg14 praefect:
|
|||
- .rspec-system-parallel
|
||||
- .rails:rules:praefect-with-db
|
||||
|
||||
# Test jobs that run without Gitaly's transactions enabled. These will be removed once
|
||||
# transactions are always in use in Gitaly.
|
||||
rspec migration pg14 no_gitaly_transactions:
|
||||
extends:
|
||||
- rspec migration pg14
|
||||
- .gitaly-without-transactions
|
||||
|
||||
rspec background_migration pg14 no_gitaly_transactions:
|
||||
extends:
|
||||
- rspec background_migration pg14
|
||||
- .gitaly-without-transactions
|
||||
|
||||
rspec unit pg14 no_gitaly_transactions:
|
||||
extends:
|
||||
- rspec unit pg14
|
||||
- .gitaly-without-transactions
|
||||
|
||||
rspec integration pg14 no_gitaly_transactions:
|
||||
extends:
|
||||
- rspec integration pg14
|
||||
- .gitaly-without-transactions
|
||||
|
||||
rspec system pg14 no_gitaly_transactions:
|
||||
extends:
|
||||
- rspec system pg14
|
||||
- .gitaly-without-transactions
|
||||
|
||||
rspec-ee migration pg14 no_gitaly_transactions:
|
||||
extends:
|
||||
- rspec-ee migration pg14
|
||||
- .gitaly-without-transactions
|
||||
|
||||
rspec-ee background_migration pg14 no_gitaly_transactions:
|
||||
extends:
|
||||
- rspec-ee background_migration pg14
|
||||
- .gitaly-without-transactions
|
||||
|
||||
rspec-ee unit pg14 no_gitaly_transactions:
|
||||
extends:
|
||||
- rspec-ee unit pg14
|
||||
- .gitaly-without-transactions
|
||||
|
||||
rspec-ee integration pg14 no_gitaly_transactions:
|
||||
extends:
|
||||
- rspec-ee integration pg14
|
||||
- .gitaly-without-transactions
|
||||
|
||||
rspec-ee system pg14 no_gitaly_transactions:
|
||||
extends:
|
||||
- rspec-ee system pg14
|
||||
- .gitaly-without-transactions
|
||||
|
||||
|
||||
# Dedicated job to test DB library code against PG13.
|
||||
# Note that these are already tested against PG13 in the `rspec unit pg13` / `rspec-ee unit pg13` jobs.
|
||||
rspec db-library-code pg13:
|
||||
|
|
|
|||
|
|
@ -56,6 +56,16 @@ include:
|
|||
variables:
|
||||
GITALY_PRAEFECT_WITH_DB: '1'
|
||||
|
||||
.gitaly-with-transactions:
|
||||
variables:
|
||||
GITALY_TRANSACTIONS_ENABLED: "true"
|
||||
|
||||
.gitaly-without-transactions:
|
||||
extends:
|
||||
- .rails:rules:gitaly-without-transactions
|
||||
variables:
|
||||
GITALY_TRANSACTIONS_ENABLED: "false"
|
||||
|
||||
.rspec-base-needs:
|
||||
needs:
|
||||
- job: "clone-gitlab-repo"
|
||||
|
|
@ -68,6 +78,7 @@ include:
|
|||
- .rails-job-base
|
||||
- .base-artifacts
|
||||
- .repo-from-artifacts
|
||||
- .gitaly-with-transactions
|
||||
stage: test
|
||||
variables:
|
||||
RUBY_GC_MALLOC_LIMIT: 67108864
|
||||
|
|
|
|||
|
|
@ -2039,6 +2039,11 @@
|
|||
- if: '$CI_MERGE_REQUEST_LABELS =~ /pipeline:run-praefect-with-db/'
|
||||
allow_failure: true
|
||||
|
||||
.rails:rules:gitaly-without-transactions:
|
||||
rules:
|
||||
- <<: *if-schedule-maintenance
|
||||
- if: '$CI_MERGE_REQUEST_LABELS =~ /pipeline:run-without-gitaly-transactions/'
|
||||
|
||||
.rails:rules:ee-and-foss-migration:
|
||||
rules:
|
||||
- <<: *if-fork-merge-request
|
||||
|
|
|
|||
|
|
@ -73,19 +73,6 @@ Layout/ArgumentAlignment:
|
|||
- 'ee/spec/requests/api/deployments_spec.rb'
|
||||
- 'ee/spec/requests/api/dora/metrics_spec.rb'
|
||||
- 'ee/spec/requests/api/epics_spec.rb'
|
||||
- 'ee/spec/requests/api/graphql/ci/runner_spec.rb'
|
||||
- 'ee/spec/requests/api/graphql/group/dast_profile_schedule_spec.rb'
|
||||
- 'ee/spec/requests/api/graphql/group/epic/epic_children_spec.rb'
|
||||
- 'ee/spec/requests/api/graphql/group/epic/epic_issues_spec.rb'
|
||||
- 'ee/spec/requests/api/graphql/group/epics_spec.rb'
|
||||
- 'ee/spec/requests/api/graphql/merge_request_reviewer_spec.rb'
|
||||
- 'ee/spec/requests/api/graphql/merge_requests/approval_state_spec.rb'
|
||||
- 'ee/spec/requests/api/graphql/mutations/boards/issues/issue_move_list_spec.rb'
|
||||
- 'ee/spec/requests/api/graphql/mutations/boards/lists/create_spec.rb'
|
||||
- 'ee/spec/requests/api/graphql/mutations/boards/lists/update_limit_metrics_spec.rb'
|
||||
- 'ee/spec/requests/api/graphql/mutations/boards/update_epic_user_preferences_spec.rb'
|
||||
- 'ee/spec/requests/api/graphql/mutations/compliance_management/frameworks/destroy_spec.rb'
|
||||
- 'ee/spec/requests/api/graphql/mutations/compliance_management/frameworks/update_spec.rb'
|
||||
- 'ee/spec/requests/api/graphql/mutations/epic_tree/reorder_spec.rb'
|
||||
- 'ee/spec/requests/api/graphql/mutations/issues/set_weight_spec.rb'
|
||||
- 'ee/spec/requests/api/graphql/mutations/iterations/cadences/create_spec.rb'
|
||||
|
|
@ -307,19 +294,6 @@ Layout/ArgumentAlignment:
|
|||
- 'lib/gitlab/sidekiq_config/worker.rb'
|
||||
- 'lib/gitlab/spamcheck/client.rb'
|
||||
- 'lib/gitlab/usage/metrics/instrumentations/database_metric.rb'
|
||||
- 'lib/gitlab/usage_data.rb'
|
||||
- 'lib/gitlab/usage_data_counters/ci_template_unique_counter.rb'
|
||||
- 'lib/gitlab/usage_data_counters/merge_request_activity_unique_counter.rb'
|
||||
- 'lib/peek/views/detailed_view.rb'
|
||||
- 'lib/peek/views/redis_detailed.rb'
|
||||
- 'lib/tasks/gems.rake'
|
||||
- 'lib/tasks/gitlab/feature_categories.rake'
|
||||
- 'lib/tasks/gitlab/seed.rake'
|
||||
- 'lib/tasks/gitlab/seed/runner_fleet.rake'
|
||||
- 'lib/tasks/gitlab/shell.rake'
|
||||
- 'lib/tasks/gitlab/uploads/sanitize.rake'
|
||||
- 'rubocop/cop/gitlab/finder_with_find_by.rb'
|
||||
- 'rubocop/cop/qa/ambiguous_page_object_name.rb'
|
||||
- 'rubocop/cop/rspec/modify_sidekiq_middleware.rb'
|
||||
- 'scripts/packages/automated_cleanup.rb'
|
||||
- 'scripts/rubocop-parse'
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ Lint/SymbolConversion:
|
|||
- 'app/graphql/resolvers/environments/last_deployment_resolver.rb'
|
||||
- 'app/graphql/resolvers/user_discussions_count_resolver.rb'
|
||||
- 'app/graphql/resolvers/user_notes_count_resolver.rb'
|
||||
- 'app/helpers/breadcrumbs_helper.rb'
|
||||
- 'app/helpers/notifications_helper.rb'
|
||||
- 'app/helpers/projects_helper.rb'
|
||||
- 'app/models/application_record.rb'
|
||||
|
|
@ -28,7 +27,6 @@ Lint/SymbolConversion:
|
|||
- 'app/models/work_items/widgets/assignees.rb'
|
||||
- 'app/models/work_items/widgets/base.rb'
|
||||
- 'app/presenters/snippet_presenter.rb'
|
||||
- 'app/services/cloud_seed/google_cloud/create_cloudsql_instance_service.rb'
|
||||
- 'app/services/concerns/deploy_token_methods.rb'
|
||||
- 'app/services/concerns/rate_limited_service.rb'
|
||||
- 'app/services/container_expiration_policies/cleanup_service.rb'
|
||||
|
|
@ -37,30 +35,22 @@ Lint/SymbolConversion:
|
|||
- 'app/services/notification_recipients/builder/default.rb'
|
||||
- 'app/services/notification_service.rb'
|
||||
- 'app/workers/project_export_worker.rb'
|
||||
- 'ee/app/components/billing/plan_component.rb'
|
||||
- 'ee/app/controllers/projects/security/scanned_resources_controller.rb'
|
||||
- 'ee/app/graphql/ee/resolvers/projects/branch_rules_resolver.rb'
|
||||
- 'ee/app/graphql/types/ci/jobs_duration_statistics_type.rb'
|
||||
- 'ee/app/models/concerns/ee/protected_branch.rb'
|
||||
- 'ee/app/models/geo_node_status.rb'
|
||||
- 'ee/app/policies/ee/group_policy.rb'
|
||||
- 'ee/app/policies/ee/project_policy.rb'
|
||||
- 'ee/app/serializers/integrations/zentao_serializers/issue_entity.rb'
|
||||
- 'ee/app/serializers/report_list_entity.rb'
|
||||
- 'ee/app/services/security/security_orchestration_policies/ci_action/base.rb'
|
||||
- 'ee/app/services/vulnerabilities/manually_create_service.rb'
|
||||
- 'ee/app/workers/security/store_scans_worker.rb'
|
||||
- 'ee/db/fixtures/development/35_merge_request_predictions.rb'
|
||||
- 'ee/lib/api/concerns/dependency_proxy/packages_helpers.rb'
|
||||
- 'ee/lib/ee/api/helpers.rb'
|
||||
- 'ee/lib/elastic/latest/note_class_proxy.rb'
|
||||
- 'ee/lib/gitlab/applied_ml/suggested_reviewers/client.rb'
|
||||
- 'ee/lib/gitlab/elastic/search_results.rb'
|
||||
- 'ee/lib/gitlab/geo/replicator.rb'
|
||||
- 'ee/lib/gitlab/graphql/aggregations/epics/epic_node.rb'
|
||||
- 'ee/lib/search/zoekt/search_results.rb'
|
||||
- 'ee/spec/controllers/admin/audit_logs_controller_spec.rb'
|
||||
- 'ee/spec/controllers/groups/audit_events_controller_spec.rb'
|
||||
- 'ee/spec/controllers/projects/audit_events_controller_spec.rb'
|
||||
- 'ee/spec/factories/ci/builds.rb'
|
||||
- 'ee/spec/factories/ci/pipelines.rb'
|
||||
|
|
@ -141,21 +131,6 @@ Lint/SymbolConversion:
|
|||
- 'spec/lib/api/entities/nuget/search_result_spec.rb'
|
||||
- 'spec/lib/api/helpers/rate_limiter_spec.rb'
|
||||
- 'spec/lib/bulk_imports/projects/pipelines/project_feature_pipeline_spec.rb'
|
||||
- 'spec/lib/bulk_imports/projects/pipelines/snippets_repository_pipeline_spec.rb'
|
||||
- 'spec/lib/constraints/group_url_constrainer_spec.rb'
|
||||
- 'spec/lib/constraints/project_url_constrainer_spec.rb'
|
||||
- 'spec/lib/constraints/user_url_constrainer_spec.rb'
|
||||
- 'spec/lib/container_registry/gitlab_api_client_spec.rb'
|
||||
- 'spec/lib/generators/gitlab/usage_metric_definition/redis_hll_generator_spec.rb'
|
||||
- 'spec/lib/generators/gitlab/usage_metric_definition_generator_spec.rb'
|
||||
- 'spec/lib/gitlab/ci/config/entry/job_spec.rb'
|
||||
- 'spec/lib/gitlab/ci/config/entry/jobs_spec.rb'
|
||||
- 'spec/lib/gitlab/ci/config/entry/processable_spec.rb'
|
||||
- 'spec/lib/gitlab/ci/parsers/accessibility/pa11y_spec.rb'
|
||||
- 'spec/lib/gitlab/ci/parsers/codequality/code_climate_spec.rb'
|
||||
- 'spec/lib/gitlab/ci/reports/accessibility_reports_spec.rb'
|
||||
- 'spec/lib/gitlab/ci/reports/codequality_reports_spec.rb'
|
||||
- 'spec/lib/gitlab/ci/yaml_processor_spec.rb'
|
||||
- 'spec/lib/google_api/cloud_platform/client_spec.rb'
|
||||
- 'spec/lib/service_ping/devops_report_spec.rb'
|
||||
- 'spec/models/appearance_spec.rb'
|
||||
|
|
|
|||
|
|
@ -176,6 +176,17 @@ const Api = {
|
|||
});
|
||||
},
|
||||
|
||||
groupSubgroups(id, options) {
|
||||
const url = Api.buildUrl(this.subgroupsPath).replace(':id', encodeURIComponent(id));
|
||||
|
||||
return axios.get(url, {
|
||||
params: {
|
||||
per_page: DEFAULT_PER_PAGE,
|
||||
...options,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
inviteGroupMembers(id, data) {
|
||||
const url = Api.buildUrl(this.groupInvitationsPath).replace(':id', encodeURIComponent(id));
|
||||
|
||||
|
|
|
|||
|
|
@ -922,7 +922,7 @@ export default {
|
|||
:key="activeIssuable.iid"
|
||||
:work-item-iid="activeIssuable.iid"
|
||||
is-drawer
|
||||
class="gl-pt-0!"
|
||||
class="gl-pt-0! work-item-drawer"
|
||||
@work-item-updated="updateIssuablesCache"
|
||||
@work-item-emoji-updated="updateIssuableEmojis"
|
||||
@addChild="refetchIssuables"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlExperimentBadge, GlButton } from '@gitlab/ui';
|
||||
import { GlExperimentBadge } from '@gitlab/ui';
|
||||
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
|
||||
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
|
|
@ -8,6 +8,7 @@ import EmptyState from '../components/empty_state.vue';
|
|||
import * as i18n from '../translations';
|
||||
import { BASE_SORT_FIELDS, MODEL_ENTITIES } from '../constants';
|
||||
import ModelRow from '../components/model_row.vue';
|
||||
import ModelCreate from '../components/model_create.vue';
|
||||
import ActionsDropdown from '../components/actions_dropdown.vue';
|
||||
import getModelsQuery from '../graphql/queries/get_models.query.graphql';
|
||||
import { makeLoadModelErrorMessage } from '../translations';
|
||||
|
|
@ -17,10 +18,10 @@ export default {
|
|||
name: 'IndexMlModels',
|
||||
components: {
|
||||
ModelRow,
|
||||
ModelCreate,
|
||||
MetadataItem,
|
||||
TitleArea,
|
||||
GlExperimentBadge,
|
||||
GlButton,
|
||||
EmptyState,
|
||||
ActionsDropdown,
|
||||
SearchableList,
|
||||
|
|
@ -28,6 +29,7 @@ export default {
|
|||
provide() {
|
||||
return {
|
||||
mlflowTrackingUrl: this.mlflowTrackingUrl,
|
||||
projectPath: this.projectPath,
|
||||
};
|
||||
},
|
||||
props: {
|
||||
|
|
@ -35,10 +37,6 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
createModelPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
canWriteModelRegistry: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
|
@ -129,12 +127,7 @@ export default {
|
|||
<metadata-item icon="machine-learning" :text="$options.i18n.modelsCountLabel(count)" />
|
||||
</template>
|
||||
<template #right-actions>
|
||||
<gl-button
|
||||
v-if="canWriteModelRegistry"
|
||||
:href="createModelPath"
|
||||
data-testid="create-model-button"
|
||||
>{{ $options.i18n.CREATE_MODEL_LABEL }}</gl-button
|
||||
>
|
||||
<model-create v-if="canWriteModelRegistry" />
|
||||
|
||||
<actions-dropdown />
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,234 @@
|
|||
<script>
|
||||
import {
|
||||
GlAlert,
|
||||
GlButton,
|
||||
GlForm,
|
||||
GlFormGroup,
|
||||
GlFormInput,
|
||||
GlFormTextarea,
|
||||
GlModal,
|
||||
} from '@gitlab/ui';
|
||||
import { __, s__ } from '~/locale';
|
||||
import { visitUrl } from '~/lib/utils/url_utility';
|
||||
import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
||||
import { uploadModel } from '../services/upload_model';
|
||||
import createModelVersionMutation from '../graphql/mutations/create_model_version.mutation.graphql';
|
||||
import createModelMutation from '../graphql/mutations/create_model.mutation.graphql';
|
||||
|
||||
export default {
|
||||
name: 'ModelCreate',
|
||||
components: {
|
||||
GlAlert,
|
||||
GlButton,
|
||||
GlModal,
|
||||
GlForm,
|
||||
GlFormGroup,
|
||||
GlFormInput,
|
||||
GlFormTextarea,
|
||||
ImportArtifactZone: () => import('./import_artifact_zone.vue'),
|
||||
},
|
||||
inject: ['projectPath'],
|
||||
data() {
|
||||
return {
|
||||
name: null,
|
||||
version: null,
|
||||
description: null,
|
||||
versionDescription: null,
|
||||
modalVisible: false,
|
||||
errorMessage: null,
|
||||
selectedFile: null,
|
||||
modelData: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
showImportArtifactZone() {
|
||||
return this.version && this.name;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async createModel() {
|
||||
const { data } = await this.$apollo.mutate({
|
||||
mutation: createModelMutation,
|
||||
variables: {
|
||||
projectPath: this.projectPath,
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
},
|
||||
});
|
||||
return data;
|
||||
},
|
||||
async createModelVersion(modelGid) {
|
||||
const { data } = await this.$apollo.mutate({
|
||||
mutation: createModelVersionMutation,
|
||||
variables: {
|
||||
projectPath: this.projectPath,
|
||||
modelId: modelGid,
|
||||
version: this.version,
|
||||
description: this.versionDescription,
|
||||
},
|
||||
});
|
||||
return data;
|
||||
},
|
||||
async create($event) {
|
||||
$event.preventDefault();
|
||||
|
||||
this.errorMessage = '';
|
||||
try {
|
||||
// attempt creating a model
|
||||
if (!this.modelData) {
|
||||
this.modelData = await this.createModel();
|
||||
}
|
||||
const modelErrors = this.modelData?.mlModelCreate?.errors || [];
|
||||
if (modelErrors.length) {
|
||||
this.errorMessage = modelErrors.join(', ');
|
||||
} else if (this.version) {
|
||||
// model creation is successful, attempt creating a version
|
||||
const versionData = await this.createModelVersion(this.modelData.mlModelCreate.model.id);
|
||||
const versionErrors = versionData?.mlModelVersionCreate?.errors || [];
|
||||
|
||||
if (versionErrors.length) {
|
||||
this.errorMessage = versionErrors.join(', ');
|
||||
} else {
|
||||
// attempt importing model artifacts
|
||||
const { importPath } = versionData.mlModelVersionCreate.modelVersion._links;
|
||||
await uploadModel({ importPath, file: this.selectedFile });
|
||||
const { showPath } = versionData.mlModelVersionCreate.modelVersion._links;
|
||||
visitUrl(showPath);
|
||||
}
|
||||
} else {
|
||||
const { showPath } = this.modelData.mlModelCreate.model._links;
|
||||
visitUrl(showPath);
|
||||
}
|
||||
} catch (error) {
|
||||
Sentry.captureException(error);
|
||||
this.errorMessage = s__(
|
||||
'MlModelRegistry|Error creating model, version and uploading artifacts. Please try again.',
|
||||
);
|
||||
}
|
||||
},
|
||||
showCreateModal() {
|
||||
this.modalVisible = true;
|
||||
},
|
||||
cancelModal() {
|
||||
this.modalVisible = false;
|
||||
},
|
||||
hideAlert() {
|
||||
this.errorMessage = null;
|
||||
},
|
||||
},
|
||||
i18n: {},
|
||||
modal: {
|
||||
id: 'ml-experiments-delete-modal',
|
||||
actionPrimary: {
|
||||
text: __('Create'),
|
||||
attributes: { variant: 'confirm' },
|
||||
},
|
||||
actionCancel: {
|
||||
text: __('Cancel'),
|
||||
},
|
||||
nameDescription: s__(
|
||||
'MlModelRegistry|Model name must not contain spaces or upper case letter.',
|
||||
),
|
||||
namePlaceholder: s__('MlModelRegistry|For example my-model'),
|
||||
versionDescription: s__('MlModelRegistry|Leave empty to skip version creation.'),
|
||||
versionPlaceholder: s__('MlModelRegistry|For example 1.0.0'),
|
||||
descriptionPlaceholder: s__('MlModelRegistry|Enter some model description'),
|
||||
versionDescriptionTitle: s__('MlModelRegistry|Version Description'),
|
||||
versionDescriptionPlaceholder: s__(
|
||||
'MlModelRegistry|Initial version name. Must be a semantic version.',
|
||||
),
|
||||
buttonTitle: s__('MlModelRegistry|Create model'),
|
||||
title: s__('MlModelRegistry|Create model, version & import artifacts'),
|
||||
modelName: s__('MlModelRegistry|Model name'),
|
||||
modelDescription: __('Description'),
|
||||
version: __('Version'),
|
||||
import: __('Import'),
|
||||
modelSuccessButVersionArtifactFailAlert: {
|
||||
id: 'ml-model-success-version-artifact-failed',
|
||||
message: s__(
|
||||
'MlModelRegistry|Model has been created but version or artifacts could not be uploaded. Try creating model version.',
|
||||
),
|
||||
variant: 'warning',
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<gl-button @click="showCreateModal">{{ $options.modal.buttonTitle }}</gl-button>
|
||||
<gl-modal
|
||||
v-model="modalVisible"
|
||||
modal-id="create-model-modal"
|
||||
:title="$options.modal.title"
|
||||
:action-primary="$options.modal.actionPrimary"
|
||||
:action-cancel="$options.modal.actionCancel"
|
||||
size="sm"
|
||||
@primary="create"
|
||||
@cancel="cancelModal"
|
||||
>
|
||||
<gl-form>
|
||||
<gl-form-group
|
||||
:label="$options.modal.modelName"
|
||||
label-for="nameId"
|
||||
:description="$options.modal.nameDescription"
|
||||
>
|
||||
<gl-form-input
|
||||
id="nameId"
|
||||
v-model="name"
|
||||
data-testid="nameId"
|
||||
type="text"
|
||||
:placeholder="$options.modal.namePlaceholder"
|
||||
/>
|
||||
</gl-form-group>
|
||||
<gl-form-group :label="$options.modal.modelDescription" label-for="descriptionId">
|
||||
<gl-form-textarea
|
||||
id="descriptionId"
|
||||
v-model="description"
|
||||
data-testid="descriptionId"
|
||||
:placeholder="$options.modal.descriptionPlaceholder"
|
||||
/>
|
||||
</gl-form-group>
|
||||
<gl-form-group
|
||||
:label="$options.modal.version"
|
||||
label-for="versionId"
|
||||
:description="$options.modal.versionDescription"
|
||||
>
|
||||
<gl-form-input
|
||||
id="versionId"
|
||||
v-model="version"
|
||||
data-testid="versionId"
|
||||
type="text"
|
||||
:placeholder="$options.modal.versionPlaceholder"
|
||||
/>
|
||||
</gl-form-group>
|
||||
<gl-form-group
|
||||
:label="$options.modal.versionDescriptionTitle"
|
||||
label-for="versionDescriptionId"
|
||||
>
|
||||
<gl-form-textarea
|
||||
id="versionDescriptionId"
|
||||
v-model="versionDescription"
|
||||
data-testid="versionDescriptionId"
|
||||
:placeholder="$options.modal.versionDescriptionPlaceholder"
|
||||
/>
|
||||
</gl-form-group>
|
||||
<gl-form-group
|
||||
v-if="showImportArtifactZone"
|
||||
:label="$options.modal.Import"
|
||||
label-for="versionImportArtifactZone"
|
||||
>
|
||||
<import-artifact-zone
|
||||
id="versionImportArtifactZone"
|
||||
v-model="selectedFile"
|
||||
:submit-on-select="false"
|
||||
/>
|
||||
</gl-form-group>
|
||||
</gl-form>
|
||||
|
||||
<gl-alert v-if="errorMessage" variant="danger" @dismiss="hideAlert">{{
|
||||
errorMessage
|
||||
}}</gl-alert>
|
||||
</gl-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -10,6 +10,7 @@ import {
|
|||
GlModal,
|
||||
GlModalDirective,
|
||||
} from '@gitlab/ui';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { sprintf, n__, s__ } from '~/locale';
|
||||
import {
|
||||
getParameterByName,
|
||||
|
|
@ -92,13 +93,13 @@ export default {
|
|||
projectPath: this.projectPath,
|
||||
};
|
||||
},
|
||||
update({ project: { branchRules } }) {
|
||||
update({ project: { branchRules, group } }) {
|
||||
const branchRule = branchRules.nodes.find((rule) => rule.name === this.branch);
|
||||
this.branchRule = branchRule;
|
||||
this.branchProtection = branchRule?.branchProtection;
|
||||
this.statusChecks = branchRule?.externalStatusChecks?.nodes || [];
|
||||
this.matchingBranchesCount = branchRule?.matchingBranchesCount;
|
||||
|
||||
this.groupId = getIdFromGraphQLId(group?.id) || '';
|
||||
if (!this.showApprovers) return;
|
||||
// The approval rules app uses a separate endpoint to fetch the list of approval rules.
|
||||
// In future, we will update the GraphQL request to include the approval rules data.
|
||||
|
|
@ -118,6 +119,7 @@ export default {
|
|||
branchProtection: {},
|
||||
statusChecks: [],
|
||||
branchRule: {},
|
||||
groupId: null,
|
||||
matchingBranchesCount: null,
|
||||
isAllowedToMergeDrawerOpen: false,
|
||||
isRuleUpdating: false,
|
||||
|
|
@ -355,6 +357,7 @@ export default {
|
|||
:users="mergeAccessLevels.users"
|
||||
:groups="mergeAccessLevels.groups"
|
||||
:is-loading="isRuleUpdating"
|
||||
:group-id="groupId"
|
||||
:title="s__('BranchRules|Edit allowed to merge')"
|
||||
@editRule="
|
||||
editBranchRule({
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import ProtectionRow from './protection_row.vue';
|
|||
|
||||
export const i18n = {
|
||||
rolesTitle: s__('BranchRules|Roles'),
|
||||
usersTitle: s__('BranchRules|Users'),
|
||||
usersAndGroupsTitle: s__('BranchRules|Users & groups'),
|
||||
groupsTitle: s__('BranchRules|Groups'),
|
||||
};
|
||||
|
||||
|
|
@ -117,20 +117,13 @@ export default {
|
|||
<!-- Roles -->
|
||||
<protection-row v-if="roles.length" :title="$options.i18n.rolesTitle" :access-levels="roles" />
|
||||
|
||||
<!-- Users -->
|
||||
<!-- Users and Groups -->
|
||||
<protection-row
|
||||
v-if="users.length"
|
||||
v-if="hasUsers || hasGroups"
|
||||
:show-divider="hasRoles"
|
||||
:title="$options.i18n.usersTitle"
|
||||
:users="users"
|
||||
/>
|
||||
|
||||
<!-- Groups -->
|
||||
<protection-row
|
||||
v-if="groups.length"
|
||||
:show-divider="showDivider"
|
||||
:title="$options.i18n.groupsTitle"
|
||||
:access-levels="groups"
|
||||
:groups="groups"
|
||||
:title="$options.i18n.usersAndGroupsTitle"
|
||||
/>
|
||||
|
||||
<!-- Status checks -->
|
||||
|
|
|
|||
|
|
@ -36,6 +36,11 @@ export default {
|
|||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
groups: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
statusCheckUrl: {
|
||||
type: String,
|
||||
required: false,
|
||||
|
|
@ -50,6 +55,9 @@ export default {
|
|||
this.users.length - this.$options.MAX_VISIBLE_AVATARS,
|
||||
);
|
||||
},
|
||||
usersAndGroups() {
|
||||
return [...this.users, ...this.groups];
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -65,9 +73,9 @@ export default {
|
|||
<div v-if="statusCheckUrl" class="gl-flex-grow-1">{{ statusCheckUrl }}</div>
|
||||
|
||||
<gl-avatars-inline
|
||||
v-if="users.length"
|
||||
v-if="usersAndGroups.length"
|
||||
class="!gl-w-1/4"
|
||||
:avatars="users"
|
||||
:avatars="usersAndGroups"
|
||||
:collapsed="true"
|
||||
:max-visible="$options.MAX_VISIBLE_AVATARS"
|
||||
:avatar-size="$options.AVATAR_SIZE"
|
||||
|
|
@ -77,7 +85,7 @@ export default {
|
|||
>
|
||||
<template #avatar="{ avatar }">
|
||||
<gl-avatar-link
|
||||
:key="avatar.username"
|
||||
:key="avatar.name"
|
||||
v-gl-tooltip
|
||||
target="_blank"
|
||||
:href="avatar.webUrl"
|
||||
|
|
@ -86,6 +94,7 @@ export default {
|
|||
<gl-avatar
|
||||
:src="avatar.avatarUrl"
|
||||
:label="avatar.name"
|
||||
:entity-name="avatar.name"
|
||||
:alt="avatar.name"
|
||||
:size="$options.AVATAR_SIZE"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -22,11 +22,13 @@ export default {
|
|||
},
|
||||
users: {
|
||||
type: Array,
|
||||
required: true,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
groups: {
|
||||
type: Array,
|
||||
required: true,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
roles: {
|
||||
type: Array,
|
||||
|
|
@ -36,6 +38,11 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
groupId: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
|
|
@ -43,8 +50,8 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
updatedGroups: [],
|
||||
updatedUsers: [],
|
||||
updatedGroups: this.groups,
|
||||
updatedUsers: this.users,
|
||||
isRuleUpdated: false,
|
||||
};
|
||||
},
|
||||
|
|
@ -64,6 +71,7 @@ export default {
|
|||
getRuleEditData() {
|
||||
return [
|
||||
...this.formatItemsData(this.updatedUsers, 'userId', 'User'), // eslint-disable-line @gitlab/require-i18n-strings
|
||||
...this.formatItemsData(this.updatedGroups, 'groupId', 'Group'), // eslint-disable-line @gitlab/require-i18n-strings
|
||||
];
|
||||
},
|
||||
formatItemsIds(items) {
|
||||
|
|
@ -109,8 +117,17 @@ export default {
|
|||
:items="formatItemsIds(users)"
|
||||
is-project-only-namespace
|
||||
:users-options="$options.projectUsersOptions"
|
||||
data-testid="users-selector"
|
||||
@change="handleRuleDataUpdate('updatedUsers', $event)"
|
||||
/>
|
||||
<items-selector
|
||||
type="groups"
|
||||
:items="formatItemsIds(groups)"
|
||||
:group-id="groupId"
|
||||
data-testid="groups-selector"
|
||||
is-project-only-namespace
|
||||
@change="handleRuleDataUpdate('updatedGroups', $event)"
|
||||
/>
|
||||
</gl-form-group>
|
||||
</template>
|
||||
</gl-drawer>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
query getBranchRulesDetails($projectPath: ID!) {
|
||||
project(fullPath: $projectPath) {
|
||||
id
|
||||
group {
|
||||
id
|
||||
}
|
||||
branchRules {
|
||||
nodes {
|
||||
id
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export const getAccessLevels = (accessLevels = {}) => {
|
|||
const src = node.user.avatarUrl;
|
||||
accessLevelTypes.users.push({ src, ...node.user });
|
||||
} else if (node.group) {
|
||||
accessLevelTypes.groups.push(node);
|
||||
accessLevelTypes.groups.push(node.group);
|
||||
} else {
|
||||
accessLevelTypes.roles.push({ accessLevelDescription: node.accessLevelDescription });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export default {
|
|||
return sprintf(__('Delete %{name}'), { name: this.name });
|
||||
},
|
||||
fullName() {
|
||||
return this.data.fullName;
|
||||
return this.data.fullName || this.data.name;
|
||||
},
|
||||
name() {
|
||||
return this.data.name;
|
||||
|
|
|
|||
|
|
@ -40,6 +40,16 @@ export default {
|
|||
required: false,
|
||||
default: null,
|
||||
},
|
||||
groupPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
groupId: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
autofocus: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
|
@ -128,6 +138,10 @@ export default {
|
|||
},
|
||||
async fetchGroupsBySearchTerm(search) {
|
||||
let groups = [];
|
||||
if (parseBoolean(this.groupId)) {
|
||||
groups = await this.fetchSubgroupsBySearchTerm(search);
|
||||
return groups;
|
||||
}
|
||||
if (parseBoolean(this.isProjectNamespace)) {
|
||||
groups = await this.fetchProjectGroups(search);
|
||||
} else {
|
||||
|
|
@ -164,6 +178,17 @@ export default {
|
|||
})),
|
||||
);
|
||||
},
|
||||
async fetchSubgroupsBySearchTerm(search) {
|
||||
let groups = [];
|
||||
const subgroups = await Api.groupSubgroups(this.groupId, search);
|
||||
groups = subgroups?.data || [];
|
||||
return groups?.map((group) => ({
|
||||
text: group.fullName,
|
||||
value: group.name,
|
||||
type: 'group',
|
||||
...group,
|
||||
}));
|
||||
},
|
||||
fetchDeployKeysBySearchTerm() {
|
||||
// TODO - implement API request (follow-up)
|
||||
// https://gitlab.com/gitlab-org/gitlab/-/issues/432494
|
||||
|
|
|
|||
|
|
@ -650,24 +650,24 @@ export default {
|
|||
/>
|
||||
</div>
|
||||
</template>
|
||||
<work-item-detail-modal
|
||||
v-if="!isModal"
|
||||
ref="modal"
|
||||
:work-item-id="modalWorkItemId"
|
||||
:work-item-iid="modalWorkItemIid"
|
||||
:work-item-full-path="modalWorkItemNamespaceFullPath"
|
||||
:show="true"
|
||||
@close="updateUrl"
|
||||
@openReportAbuse="toggleReportAbuseDrawer(true, $event)"
|
||||
/>
|
||||
<abuse-category-selector
|
||||
v-if="isReportDrawerOpen"
|
||||
:reported-user-id="reportedUserId"
|
||||
:reported-from-url="reportedUrl"
|
||||
:show-drawer="true"
|
||||
@close-drawer="toggleReportAbuseDrawer(false)"
|
||||
/>
|
||||
</section>
|
||||
</section>
|
||||
<work-item-detail-modal
|
||||
v-if="!isModal"
|
||||
ref="modal"
|
||||
:work-item-id="modalWorkItemId"
|
||||
:work-item-iid="modalWorkItemIid"
|
||||
:work-item-full-path="modalWorkItemNamespaceFullPath"
|
||||
:show="true"
|
||||
@close="updateUrl"
|
||||
@openReportAbuse="toggleReportAbuseDrawer(true, $event)"
|
||||
/>
|
||||
<abuse-category-selector
|
||||
v-if="isReportDrawerOpen"
|
||||
:reported-user-id="reportedUserId"
|
||||
:reported-from-url="reportedUrl"
|
||||
:show-drawer="true"
|
||||
@close-drawer="toggleReportAbuseDrawer(false)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -143,12 +143,23 @@ $work-item-overview-gap-width: 2rem;
|
|||
}
|
||||
}
|
||||
|
||||
.work-item-view {
|
||||
.gl-modal .work-item-view,
|
||||
.work-item-drawer .work-item-view {
|
||||
container-name: work-item-view;
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
@container work-item-view (max-width: #{calc($breakpoint-md - 1px)}) {
|
||||
@mixin container-and-media($query) {
|
||||
@container work-item-view (#{$query}) {
|
||||
@content;
|
||||
}
|
||||
|
||||
@media ($query) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@include container-and-media("max-width: #{calc($breakpoint-md - 0.02px)}") {
|
||||
.work-item-overview {
|
||||
display: block !important;
|
||||
}
|
||||
|
|
@ -176,7 +187,7 @@ $work-item-overview-gap-width: 2rem;
|
|||
margin-top: $gl-spacing-scale-5;
|
||||
}
|
||||
|
||||
@container work-item-view (min-width: #{$breakpoint-md}) {
|
||||
@include container-and-media("min-width: #{calc($breakpoint-md)}") {
|
||||
.work-item-attributes-wrapper {
|
||||
top: calc(#{$calc-application-header-height} + #{$work-item-sticky-header-height});
|
||||
height: calc(#{$calc-application-viewport-height} - #{$work-item-sticky-header-height});
|
||||
|
|
@ -190,9 +201,7 @@ $work-item-overview-gap-width: 2rem;
|
|||
.work-item-date-picker {
|
||||
max-width: 175px;
|
||||
}
|
||||
}
|
||||
|
||||
@container work-item-view (min-width: #{$breakpoint-md}) {
|
||||
.work-item-overview-right-sidebar {
|
||||
margin-top: 0;
|
||||
grid-row-start: 1;
|
||||
|
|
|
|||
|
|
@ -65,9 +65,9 @@ class Admin::GroupsController < Admin::ApplicationController
|
|||
def destroy
|
||||
Groups::DestroyService.new(@group, current_user).async_execute
|
||||
|
||||
redirect_to admin_groups_path,
|
||||
status: :found,
|
||||
alert: format(_('Group %{group_name} was scheduled for deletion.'), group_name: @group.name)
|
||||
flash[:toast] = format(_("Group '%{group_name}' is being deleted."), group_name: @group.full_name)
|
||||
|
||||
redirect_to admin_groups_path, status: :found
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ class Admin::ProjectsController < Admin::ApplicationController
|
|||
|
||||
def destroy
|
||||
::Projects::DestroyService.new(@project, current_user, {}).async_execute
|
||||
flash[:notice] = format(_("Project '%{project_name}' is in the process of being deleted."), project_name: @project.full_name)
|
||||
flash[:toast] = format(_("Project '%{project_name}' is being deleted."), project_name: @project.full_name)
|
||||
|
||||
redirect_to admin_projects_path, status: :found
|
||||
rescue Projects::DestroyService::DestroyError => e
|
||||
|
|
|
|||
|
|
@ -173,7 +173,9 @@ class GroupsController < Groups::ApplicationController
|
|||
def destroy
|
||||
Groups::DestroyService.new(@group, current_user).async_execute
|
||||
|
||||
redirect_to root_path, status: :found, alert: "Group '#{@group.name}' was scheduled for deletion."
|
||||
flash[:toast] = format(_("Group '%{group_name}' is being deleted."), group_name: @group.full_name)
|
||||
|
||||
redirect_to root_path, status: :found
|
||||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
|
|
|
|||
|
|
@ -194,7 +194,7 @@ class ProjectsController < Projects::ApplicationController
|
|||
return access_denied! unless can?(current_user, :remove_project, @project)
|
||||
|
||||
::Projects::DestroyService.new(@project, current_user, {}).async_execute
|
||||
flash[:notice] = _("Project '%{project_name}' is in the process of being deleted.") % { project_name: @project.full_name }
|
||||
flash[:toast] = format(_("Project '%{project_name}' is being deleted."), project_name: @project.full_name)
|
||||
|
||||
redirect_to dashboard_projects_path, status: :found
|
||||
rescue Projects::DestroyService::DestroyError => e
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ module BreadcrumbsHelper
|
|||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
'itemListElement': build_item_list_elements&.map&.with_index do |item, index|
|
||||
itemListElement: build_item_list_elements&.map&.with_index do |item, index|
|
||||
{
|
||||
'@type' => 'ListItem',
|
||||
'position' => index + 1,
|
||||
|
|
|
|||
|
|
@ -187,11 +187,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
|
|||
AnchorData.new(
|
||||
true,
|
||||
statistic_icon('rocket-launch') +
|
||||
n_('%{strong_start}%{release_count}%{strong_end} Release', '%{strong_start}%{release_count}%{strong_end} Releases', releases_count).html_safe % {
|
||||
(n_('%{strong_start}%{release_count}%{strong_end} Release', '%{strong_start}%{release_count}%{strong_end} Releases', releases_count).html_safe % {
|
||||
release_count: number_with_delimiter(releases_count),
|
||||
strong_start: '<strong class="project-stat-value">'.html_safe,
|
||||
strong_end: '</strong>'.html_safe
|
||||
},
|
||||
}),
|
||||
project_releases_path(project)
|
||||
)
|
||||
end
|
||||
|
|
@ -205,11 +205,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
|
|||
AnchorData.new(
|
||||
true,
|
||||
statistic_icon('environment') +
|
||||
n_('%{strong_start}%{count}%{strong_end} Environment', '%{strong_start}%{count}%{strong_end} Environments', environments_count).html_safe % {
|
||||
(n_('%{strong_start}%{count}%{strong_end} Environment', '%{strong_start}%{count}%{strong_end} Environments', environments_count).html_safe % {
|
||||
count: number_with_delimiter(environments_count),
|
||||
strong_start: '<strong class="project-stat-value">'.html_safe,
|
||||
strong_end: '</strong>'.html_safe
|
||||
},
|
||||
}),
|
||||
project_environments_path(project)
|
||||
)
|
||||
end
|
||||
|
|
@ -218,11 +218,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
|
|||
AnchorData.new(
|
||||
true,
|
||||
statistic_icon('commit') +
|
||||
n_('%{strong_start}%{commit_count}%{strong_end} Commit', '%{strong_start}%{commit_count}%{strong_end} Commits', statistics.commit_count).html_safe % {
|
||||
(n_('%{strong_start}%{commit_count}%{strong_end} Commit', '%{strong_start}%{commit_count}%{strong_end} Commits', statistics.commit_count).html_safe % {
|
||||
commit_count: number_with_delimiter(statistics.commit_count),
|
||||
strong_start: '<strong class="project-stat-value">'.html_safe,
|
||||
strong_end: '</strong>'.html_safe
|
||||
},
|
||||
}),
|
||||
empty_repo? ? nil : project_commits_path(project, default_branch_or_main)
|
||||
)
|
||||
end
|
||||
|
|
@ -231,11 +231,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
|
|||
AnchorData.new(
|
||||
true,
|
||||
statistic_icon('branch') +
|
||||
n_('%{strong_start}%{branch_count}%{strong_end} Branch', '%{strong_start}%{branch_count}%{strong_end} Branches', repository.branch_count).html_safe % {
|
||||
(n_('%{strong_start}%{branch_count}%{strong_end} Branch', '%{strong_start}%{branch_count}%{strong_end} Branches', repository.branch_count).html_safe % {
|
||||
branch_count: number_with_delimiter(repository.branch_count),
|
||||
strong_start: '<strong class="project-stat-value">'.html_safe,
|
||||
strong_end: '</strong>'.html_safe
|
||||
},
|
||||
}),
|
||||
empty_repo? ? nil : project_branches_path(project)
|
||||
)
|
||||
end
|
||||
|
|
@ -252,11 +252,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
|
|||
AnchorData.new(
|
||||
true,
|
||||
statistic_icon('terraform') +
|
||||
n_('%{strong_start}%{terraform_states_count}%{strong_end} Terraform State', '%{strong_start}%{terraform_states_count}%{strong_end} Terraform States', project.terraform_states.count).html_safe % {
|
||||
(n_('%{strong_start}%{terraform_states_count}%{strong_end} Terraform State', '%{strong_start}%{terraform_states_count}%{strong_end} Terraform States', project.terraform_states.count).html_safe % {
|
||||
terraform_states_count: number_with_delimiter(project.terraform_states.count),
|
||||
strong_start: '<strong class="project-stat-value">'.html_safe,
|
||||
strong_end: '</strong>'.html_safe
|
||||
} + terraform_warn_icon,
|
||||
}) + terraform_warn_icon,
|
||||
project_terraform_index_path(project)
|
||||
)
|
||||
end
|
||||
|
|
@ -266,11 +266,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
|
|||
AnchorData.new(
|
||||
true,
|
||||
statistic_icon('label') +
|
||||
n_('%{strong_start}%{tag_count}%{strong_end} Tag', '%{strong_start}%{tag_count}%{strong_end} Tags', repository.tag_count).html_safe % {
|
||||
(n_('%{strong_start}%{tag_count}%{strong_end} Tag', '%{strong_start}%{tag_count}%{strong_end} Tags', repository.tag_count).html_safe % {
|
||||
tag_count: number_with_delimiter(repository.tag_count),
|
||||
strong_start: '<strong class="project-stat-value">'.html_safe,
|
||||
strong_end: '</strong>'.html_safe
|
||||
},
|
||||
}),
|
||||
empty_repo? ? nil : project_tags_path(project)
|
||||
)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -34,12 +34,12 @@ module CloudSeed
|
|||
current_user.id,
|
||||
project.id,
|
||||
{
|
||||
'google_oauth2_token': google_oauth2_token,
|
||||
'gcp_project_id': gcp_project_id,
|
||||
'instance_name': instance_name,
|
||||
'database_version': database_version,
|
||||
'environment_name': environment_name,
|
||||
'is_protected': protected?
|
||||
google_oauth2_token: google_oauth2_token,
|
||||
gcp_project_id: gcp_project_id,
|
||||
instance_name: instance_name,
|
||||
database_version: database_version,
|
||||
environment_name: environment_name,
|
||||
is_protected: protected?
|
||||
}
|
||||
)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ class CohortsService
|
|||
month_totals =
|
||||
all_months
|
||||
.map { |activity_month| counts_by_month[[registration_month, activity_month]] }
|
||||
.reduce([]) { |result, total| result << result.last.to_i + total.to_i }
|
||||
.reduce([]) { |result, total| result << (result.last.to_i + total.to_i) }
|
||||
.reverse
|
||||
|
||||
overall_total = month_totals.first
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ module ValidatesClassificationLabel
|
|||
|
||||
def rejection_reason_for_label(label)
|
||||
reason_from_service = ::Gitlab::ExternalAuthorization.rejection_reason(current_user, label).presence
|
||||
reason_from_service || _("Access to '%{classification_label}' not allowed") % { classification_label: label }
|
||||
reason_from_service || (_("Access to '%{classification_label}' not allowed") % { classification_label: label })
|
||||
end
|
||||
|
||||
def classification_label_change?(record, attribute_name)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ module Groups
|
|||
module GroupLinks
|
||||
class DestroyService < ::Groups::BaseService
|
||||
def execute(one_or_more_links, skip_authorization: false)
|
||||
unless skip_authorization || group && can?(current_user, :admin_group_member, group)
|
||||
unless skip_authorization || (group && can?(current_user, :admin_group_member, group))
|
||||
return error('Not Found', 404)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -79,10 +79,10 @@ module IssuableLinks
|
|||
link = relate_issuables(referenced_object)
|
||||
|
||||
if link.errors.any?
|
||||
@errors << _("%{ref} cannot be added: %{error}") % {
|
||||
@errors << (_("%{ref} cannot be added: %{error}") % {
|
||||
ref: referenced_object.to_reference,
|
||||
error: link.errors.messages.values.flatten.to_sentence
|
||||
}
|
||||
})
|
||||
else
|
||||
after_create_for(link)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@ module Issues
|
|||
end
|
||||
|
||||
def start_position
|
||||
@start_position ||= (RelativePositioning::START_POSITION - (gaps / 2) * gap_size).to_i
|
||||
@start_position ||= (RelativePositioning::START_POSITION - ((gaps / 2) * gap_size)).to_i
|
||||
end
|
||||
|
||||
def with_retry(initial_batch_size, exit_batch_size)
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ module Jira
|
|||
|
||||
def error_message(error)
|
||||
reportable_error_message(error) ||
|
||||
s_('JiraRequest|An error occurred while requesting data from Jira. Check your %{docs_link_start}Jira integration configuration%{docs_link_end} and try again.').html_safe % { docs_link_start: config_docs_link_start, docs_link_end: '</a>'.html_safe }
|
||||
(s_('JiraRequest|An error occurred while requesting data from Jira. Check your %{docs_link_start}Jira integration configuration%{docs_link_end} and try again.').html_safe % { docs_link_start: config_docs_link_start, docs_link_end: '</a>'.html_safe })
|
||||
end
|
||||
|
||||
# Returns a user-facing error message if possible, otherwise `nil`.
|
||||
|
|
@ -113,7 +113,7 @@ module Jira
|
|||
when 'Forbidden'
|
||||
s_('JiraRequest|The credentials for accessing Jira are not allowed to access the data. Check your %{docs_link_start}Jira integration credentials%{docs_link_end} and try again.').html_safe % { docs_link_start: auth_docs_link_start, docs_link_end: '</a>'.html_safe }
|
||||
when 'Bad Request'
|
||||
jira_ruby_json_error_message(error.response.body) || s_('JiraRequest|An error occurred while requesting data from Jira. Check your %{docs_link_start}Jira integration configuration%{docs_link_end} and try again.').html_safe % { docs_link_start: config_docs_link_start, docs_link_end: '</a>'.html_safe }
|
||||
jira_ruby_json_error_message(error.response.body) || (s_('JiraRequest|An error occurred while requesting data from Jira. Check your %{docs_link_start}Jira integration configuration%{docs_link_end} and try again.').html_safe % { docs_link_start: config_docs_link_start, docs_link_end: '</a>'.html_safe })
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -938,7 +938,7 @@ Settings['sidekiq']['routing_rules'] = Settings.build_sidekiq_routing_rules(Sett
|
|||
# GitLab Shell
|
||||
#
|
||||
Settings['gitlab_shell'] ||= {}
|
||||
Settings.gitlab_shell['path'] = Settings.absolute(Settings.gitlab_shell['path'] || Settings.gitlab['user_home'] + '/gitlab-shell/')
|
||||
Settings.gitlab_shell['path'] = Settings.absolute(Settings.gitlab_shell['path'] || (Settings.gitlab['user_home'] + '/gitlab-shell/'))
|
||||
Settings.gitlab_shell['hooks_path'] = :deprecated_use_gitlab_shell_path_instead
|
||||
Settings.gitlab_shell['authorized_keys_file'] ||= File.join(Dir.home, '.ssh', 'authorized_keys')
|
||||
Settings.gitlab_shell['secret_file'] ||= Rails.root.join('.gitlab_shell_secret')
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ module CarrierWave
|
|||
# avoid a get by using local references
|
||||
local_directory = connection.directories.new(key: @uploader.fog_directory)
|
||||
local_file = local_directory.files.new(key: path)
|
||||
expire_at = options[:expire_at] || ::Fog::Time.now + @uploader.fog_authenticated_url_expiration
|
||||
expire_at = options[:expire_at] || (::Fog::Time.now + @uploader.fog_authenticated_url_expiration)
|
||||
case @uploader.fog_credentials[:provider]
|
||||
when 'AWS', 'Google', 'AzureRM'
|
||||
local_file.url(expire_at, options)
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ module Kaminari
|
|||
# Total count has to be 0 if loaded records are 0
|
||||
return @total_count = 0 if (current_page == 1) && @records.empty?
|
||||
# Total count is calculable at the last page
|
||||
return @total_count = (current_page - 1) * limit_value + @records.length if @records.any? && (@records.length < limit_value)
|
||||
return @total_count = ((current_page - 1) * limit_value) + @records.length if @records.any? && (@records.length < limit_value)
|
||||
end
|
||||
|
||||
limit = options.fetch(:limit, MAX_COUNT_LIMIT).to_i
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@
|
|||
release: 15.0 # XX.Y
|
||||
- name: Limited Availability GitLab SaaS runners on macOS (x86-64) # Match the release post entry
|
||||
description: | # Do not modify this line, instead modify the lines below.
|
||||
GitLab SaaS runners on macOS are now in [Limited Availability](https://about.gitlab.com/handbook/product/gitlab-the-product/#limited-availability-la). If you use GitLab SaaS and have a Premium or Ultimate subscription, you can build applications that require macOS in a secure, on-demand GitLab Runner build environment that's fully integrated with GitLab CI/CD. As part of the Limited Availability release, CI jobs that run on the macOS runners will count toward your CI/CD minutes quota at a [cost factor](https://docs.gitlab.com/ee/ci/pipelines/cicd_minutes.html#cost-factor) of 6.
|
||||
GitLab SaaS runners on macOS are now in [Limited Availability](https://about.gitlab.com/handbook/product/gitlab-the-product/#limited-availability-la). If you use GitLab SaaS and have a Premium or Ultimate subscription, you can build applications that require macOS in a secure, on-demand GitLab Runner build environment that's fully integrated with GitLab CI/CD. As part of the Limited Availability release, CI jobs that run on the macOS runners will count toward your CI/CD minutes quota at a [cost factor](https://docs.gitlab.com/ee/ci/pipelines/compute_minutes.html#cost-factor) of 6.
|
||||
stage: verify # String value of the stage that the feature was created in. e.g., Growth
|
||||
self-managed: false # Boolean value (true or false)
|
||||
gitlab-com: true # Boolean value (true or false)
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@
|
|||
[Supply-chain Levels for Software Artifacts (SLSA)](https://github.com/slsa-framework/slsa) is a security framework that helps ensure the security and integrity of your software supply chain. By default, GitLab Runner is now capable of generating and producing SLSA-2 compliant attestation metadata for build artifacts.
|
||||
If the artifact is stored in a registry, then the attestation metadata is stored alongside the artifact in that registry. Otherwise, the metadata is in rendered in a plain text `.json` file that's stored with the artifact.
|
||||
This new attestation information can help you more easily verify that your build artifacts have not been tampered with. To enable this feature, simply set `RUNNER_GENERATE_ARTIFACTS_METADATA = "true"` in your `.gitlab-ci.yml` file.
|
||||
As part of the Limited Availability release, CI jobs that run on the macOS runners will count toward your CI/CD minutes quota at a [cost factor](https://docs.gitlab.com/ee/ci/pipelines/cicd_minutes.html#cost-factor) of 6.
|
||||
As part of the Limited Availability release, CI jobs that run on the macOS runners will count toward your CI/CD minutes quota at a [cost factor](https://docs.gitlab.com/ee/ci/pipelines/compute_minutes.html#cost-factor) of 6.
|
||||
stage: verify
|
||||
self-managed: true
|
||||
gitlab-com: true
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
- name: Upsizing GitLab SaaS runners on Linux
|
||||
description: | # Do not modify this line, instead modify the lines below.
|
||||
You asked, we listened! In our efforts to be best-in-class for CI/CD build speeds, we're doubling the vCPU & RAM for all GitLab SaaS runners on Linux, with no increase in the [cost factor](https://docs.gitlab.com/ee/ci/pipelines/cicd_minutes.html#cost-factor).
|
||||
You asked, we listened! In our efforts to be best-in-class for CI/CD build speeds, we're doubling the vCPU & RAM for all GitLab SaaS runners on Linux, with no increase in the [cost factor](https://docs.gitlab.com/ee/ci/pipelines/compute_minutes.html#cost-factor).
|
||||
|
||||
We're excited to see pipelines run faster and boost productivity.
|
||||
stage: Verify
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
migration_job_name: BackfillRelatedEpicLinksToIssueLinks
|
||||
description: Creates a record on issue_links table for every related_epic_links record
|
||||
feature_category: team_planning
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/152104
|
||||
milestone: '17.1'
|
||||
queued_migration_version: 20240517131715
|
||||
finalize_after: '2024-06-13'
|
||||
finalized_by: # version of the migration that finalized this BBM
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class QueueBackfillRelatedEpicLinksToIssueLinks < Gitlab::Database::Migration[2.2]
|
||||
milestone '17.0'
|
||||
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||
|
||||
MIGRATION = "BackfillRelatedEpicLinksToIssueLinks"
|
||||
DELAY_INTERVAL = 2.minutes
|
||||
BATCH_SIZE = 1000
|
||||
SUB_BATCH_SIZE = 100
|
||||
|
||||
def up
|
||||
queue_batched_background_migration(
|
||||
MIGRATION,
|
||||
:related_epic_links,
|
||||
:id,
|
||||
job_interval: DELAY_INTERVAL,
|
||||
batch_size: BATCH_SIZE,
|
||||
sub_batch_size: SUB_BATCH_SIZE
|
||||
)
|
||||
end
|
||||
|
||||
def down
|
||||
delete_batched_background_migration(MIGRATION, :related_epic_links, :id, [])
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
672eaf2d05154e166aef1d7d9b13100bb33d599172926eb37d07b69933bc33bc
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
status: ongoing
|
||||
creation-date: "2023-11-23"
|
||||
authors: [ "@Andysoiron", "@g.hickman" ]
|
||||
authors: [ "@Andysoiron", "@g.hickman", "@mcavoj" ]
|
||||
coach: "@fabiopitino"
|
||||
approvers: [ "@g.hickman" ]
|
||||
owning-stage: "~devops::govern"
|
||||
|
|
@ -53,11 +53,14 @@ Known compliance pipelines issues are:
|
|||
## Proposal
|
||||
|
||||
Currently, security policies can include multiple scan actions. Each scan action will result in a CI job that will be injected in the project CI pipeline.
|
||||
The new feature allows you to define custom CI jobs that will be injected into the project CI pipeline as well. We want to generalize the security policy
|
||||
The new policy type Pipeline Execution Policy allows users to define custom CI jobs that will be injected into the project CI pipeline as well. We want to generalize the security policy
|
||||
approach to provide the same flexibility that [compliance framework](../../../user/group/compliance_frameworks.md) needs. The combination of the 2 features
|
||||
means that security policies can be scope to compliance frameworks and enforce the presence of custom CI jobs.
|
||||
|
||||
Like scan execution policies, custom CI jobs can be scoped to certain branch names, branch types or compliance frameworks applied to the project.
|
||||
Like Scan Execution Policies, Pipeline Execution Policy jobs can be
|
||||
[scoped](../../../user/application_security/policies/scan-execution-policies.md#policy_scope-scope-type)
|
||||
to certain compliance frameworks applied to the project.
|
||||
It should be possible to control when the policy jobs are enforced by using the existing [workflow rules](../../../ci/yaml/workflow.md).
|
||||
Users can leverage one of the predefined security-policy stages to position jobs in the pipeline according to their needs.
|
||||
Transitioning from compliance pipelines to the new feature should be as smooth as possible.
|
||||
|
||||
|
|
@ -67,25 +70,25 @@ Transitioning from compliance pipelines to the new feature should be as smooth a
|
|||
|
||||
The Pipeline Execution Policy MVC will allow the transition from compliance pipelines.
|
||||
|
||||
- It should be possible to add custom CI YAML to a security policy. The CI YAML should follow the same schema as `.gitlab-ci.yml` files. The custom CI will be merged with the project CI when a pipeline starts.
|
||||
- It should be possible to add custom CI YAML to a security policy. For simplicity, the custom CI YAML should support the same configuration and follow the same schema as `.gitlab-ci.yml` files. The custom CI will be merged with the project CI when a pipeline starts.
|
||||
- The security policy schema should allow the custom CI to be defined in an external file by allowing a project and file path to be added.
|
||||
- Scan execution policies should execute custom CI YAML similar to existing policies, by injecting the job into the GitLab CI YAML of the target projects.
|
||||
- At minimum, pipeline execution policy jobs should align with [existing CI variable precedence](../../../ci/variables/index.md#cicd-variable-precedence). Ideally, all CI variables defined in a scan execution policy job should execute as highest precedence. Specifically, scan execution job variables should take precedence over project, group, and instance variables compliance project variables, among others.
|
||||
- Jobs should be executed in a way that is visible to users within the pipeline and that will not allow project jobs to override the SEP jobs. In scan execution policies today, we utilize the index pattern (-0,-1,-2,...) to increment the name of the job if a job of the same name exists. This also gives some minor indication of which jobs are executed by a security policy. For custom YAML jobs, the same pattern should be utilized.
|
||||
- Users should be able to define the stage in which their job will run, and scan execution policies will have a method to handle precedence. For example, a security/compliance team may define want to enforce jobs that run commonly after a build stage. They would be able to use build_after (for example) and scan execution policies would inject the build_after stage after the build stage and enforce the custom CI YAML defined in the pipeline execution policy within this stage. The stage and job cannot be interfered with by development teams once enforced by a scan execution policy. We should define the rules that allow for injecting stages cleanly into all enforced projects but be minimal invasive as to the project CI.
|
||||
- Pipeline execution policies should execute custom CI YAML in projects that do not contain an existing CI configuration, the same as standard scan execution policies work today.
|
||||
- Pipeline Execution Policies should execute custom CI YAML by creating jobs in isolated pipelines which are merged into the pipeline of the target projects.
|
||||
- At minimum, Pipeline Execution Policy jobs should align with [existing CI variable precedence](../../../ci/variables/index.md#cicd-variable-precedence).
|
||||
Ideally, Pipeline Execution Policy jobs should not get any user-defined variables except those defined in the group or project where the policy belongs.
|
||||
- Jobs should be executed in a way that is visible to users within the pipeline and that will not allow project jobs to override the policy jobs. In Scan Execution Policies today, we utilize the index pattern (-0,-1,-2,...) to increment the name of the job if a job of the same name exists. This also gives some minor indication of which jobs are executed by a security policy. For Pipeline Execution Policy jobs, the same pattern should be utilized.
|
||||
- Jobs coming from the policies should be marked as such in the database so that they can be distinguished, for example by using build metadata. This allows for different handling of jobs and the corresponding variables by the runner.
|
||||
- Users should be able to define the stage in which their job will run, and Pipeline Execution Policies will have a method to handle precedence. For example, a security/compliance team may want to enforce jobs that run commonly after a build stage. The stages and jobs must not interfere with those defined by development teams once enforced by a Pipeline Execution Policy.
|
||||
- Pipeline Execution Policies should allow for jobs to be enforced in projects that do not contain an existing CI configuration.
|
||||
|
||||
### Stages management strategy
|
||||
|
||||
We want users to be able to place jobs to run before or after certain CI stages of the project pipeline.
|
||||
To achieve this, we want to introduce 3 reserved stages that can be used only by pipeline execution policies and injected into the project pipeline:
|
||||
To achieve this, we want to introduce 2 reserved stages that can be used only by pipeline execution policies and injected into the project pipeline:
|
||||
|
||||
1. `.pipeline-policy-pre` stage will run at the very beginning of the pipeline, before the `.pre` stage.
|
||||
1. `.pipeline-policy` stage will be injected after the `test` stage. If the `test` stage does not exist, it will be injected after the `build` stage. If the `build` stage does not exist, it will be injected at the beginning of the pipeline after the `.pre` stage.
|
||||
1. `.pipeline-policy-post` stage will run at the very end of the pipeline, after the `.post` stage.
|
||||
|
||||
Injecting jobs in any of these 3 stages is guaranteed to always work. Execution policy jobs can also be assigned to any stage that exists in the project pipeline. In this case, however, it's not guaranteed that the injection always works as it depends whether the project pipeline has declared such stage.
|
||||
It will not be possible to create custom stages in a pipeline execution policy.
|
||||
Injecting jobs in any of these stages is guaranteed to always work. Execution policy jobs can also be assigned to any standard (`build`, `test`, `deploy`) or user-declared stages. However, in this case, the jobs may be ignored depending on the project pipeline configuration.
|
||||
|
||||
We will try this approach as part of the experiment phase. We also discussed the following strategies that we might want to try:
|
||||
|
||||
|
|
@ -133,4 +136,5 @@ If the `test` stage doesn't exist, it will be injected after the `build` stage.
|
|||
|
||||
## Links
|
||||
|
||||
- [Pipeline execution policy MVC epic](https://gitlab.com/groups/gitlab-org/-/epics/7312)
|
||||
- [Pipeline Execution Policy Type](https://gitlab.com/groups/gitlab-org/-/epics/13266#top)
|
||||
- [Pipeline Execution Action (Custom CI YAML Support) for Scan Execution Policy Type](https://gitlab.com/groups/gitlab-org/-/epics/7312)
|
||||
|
|
|
|||
|
|
@ -35,6 +35,43 @@ We are in the process of migrating our CSS utility class setup to [Tailwind CSS]
|
|||
See the [Tailwind CSS blueprint](../../../architecture/blueprints/tailwindcss/index.md) for motivation, proposal,
|
||||
and implementation details.
|
||||
|
||||
#### Tailwind CSS basics
|
||||
|
||||
Below are some Tailwind CSS basics and information about how it has been configured to use the [Pajamas design system](https://design.gitlab.com/). For a more in-depth guide see the [official Tailwind CSS documentation](https://tailwindcss.com/docs/utility-first).
|
||||
|
||||
##### Prefix
|
||||
|
||||
We have configured Tailwind CSS to use a [prefix](https://tailwindcss.com/docs/configuration#prefix) so all utility classes are prefixed with `gl-`.
|
||||
When using responsive utilities or state modifiers the prefix goes after the colon.
|
||||
**Examples:** `gl-mt-5`, `lg:gl-mt-5`.
|
||||
|
||||
##### Responsive CSS utility classes
|
||||
|
||||
[Responsive CSS utility classes](https://tailwindcss.com/docs/responsive-design) are prefixed with the breakpoint name, followed by the `:` character.
|
||||
The available breakpoints are configured in [tailwind.defaults.js#L44](https://gitlab.com/gitlab-org/gitlab-ui/-/blob/6612eaee37cdb4dd0258468c9f415be28c1053f0/tailwind.defaults.js#L44)
|
||||
**Example:** `lg:gl-mt-5`
|
||||
|
||||
##### Hover, focus, and other state modifiers
|
||||
|
||||
[State modifiers](https://tailwindcss.com/docs/hover-focus-and-other-states) can be used to conditionally apply any Tailwind CSS class. Prefix the CSS utility class with the name of the modifier, followed by the `:` character.
|
||||
**Example:** `hover:gl-underline`
|
||||
|
||||
##### `!important` modifier
|
||||
|
||||
You can use the [important modifier](https://tailwindcss.com/docs/configuration#important-modifier) by adding `!` to the beginning of the CSS utility class. When using in conjunction with responsive utility classes or state modifiers the `!` goes after the `:` character.
|
||||
**Examples:** `!gl-mt-5`, `lg:!gl-mt-5`, `hover:!gl-underline`
|
||||
|
||||
##### Spacing and sizing CSS utility classes
|
||||
|
||||
Spacing and sizing CSS utility classes (e.g. `margin`, `padding`, `width`, `height`) use our spacing scale defined in
|
||||
[tailwind.defaults.js#L4](https://gitlab.com/gitlab-org/gitlab-ui/-/blob/6612eaee37cdb4dd0258468c9f415be28c1053f0/tailwind.defaults.js#L4). They will use the naming conventions documented in the [official Tailwind CSS documentation](https://tailwindcss.com/docs/installation) but the scale will not match. When using the [Tailwind CSS autocomplete](#tailwind-css-autocomplete) our configured spacing scale will be shown.
|
||||
**Example:** `gl-mt-5` will be `margin-top: 1rem;`
|
||||
|
||||
##### Color CSS utility classes
|
||||
|
||||
Color CSS utility classes (e.g. `color` and `background-color`) use colors defined in [src/tokens/build/tailwind/tokens.cjs](https://gitlab.com/gitlab-org/gitlab-ui/-/blob/24a08b50da6bd3d34fb3f8d24f84436d90d165f6/src/tokens/build/tailwind/tokens.cjs). They will use the naming conventions documented in the [official Tailwind CSS documentation](https://tailwindcss.com/docs/installation) but the color names will not match. When using the [Tailwind CSS autocomplete](#tailwind-css-autocomplete) our configured colors will be shown.
|
||||
**Example:** `gl-text-red-500` will be `color: var(--red-500, #dd2b0e);`
|
||||
|
||||
#### Building the Tailwind CSS bundle
|
||||
|
||||
When using Vite or Webpack with the GitLab Development Kit, Tailwind CSS watches for file changes to
|
||||
|
|
@ -92,7 +129,7 @@ For full HAML and custom `*-class` prop support these are the recommended update
|
|||
|
||||
#### Official Tailwind CSS documentation
|
||||
|
||||
GitLab defines its own Tailwind CSS config in [https://gitlab.com/gitlab-org/gitlab-ui/-/blob/main/tailwind.defaults.js](https://gitlab.com/gitlab-org/gitlab-ui/-/blob/main/tailwind.defaults.js) to match the Pajamas design system and to prefix CSS utility classes with `gl-`. This means that in the [official Tailwind CSS documentation](https://tailwindcss.com/docs/installation) the spacing, sizing, and color CSS utility classes may not match. Also, the `gl-` prefix will not be shown. Here is our [spacing scale](https://gitlab.com/gitlab-org/gitlab-ui/-/blob/main/tailwind.defaults.js#L51) and [colors](https://gitlab.com/gitlab-org/gitlab-ui/-/blob/main/tailwind.defaults.js#L8). In the future we plan to utilize [Tailwind config viewer](https://github.com/rogden/tailwind-config-viewer) to have a Tailwind CSS documentation site specific to GitLab.
|
||||
GitLab defines its own Tailwind CSS config in [tailwind.defaults.js](https://gitlab.com/gitlab-org/gitlab-ui/-/blob/6612eaee37cdb4dd0258468c9f415be28c1053f0/tailwind.defaults.js) to match the Pajamas design system and to prefix CSS utility classes with `gl-`. This means that in the [official Tailwind CSS documentation](https://tailwindcss.com/docs/installation) the spacing, sizing, and color CSS utility classes may not match. Also, the `gl-` prefix will not be shown. Here is our [spacing scale](https://gitlab.com/gitlab-org/gitlab-ui/-/blob/6612eaee37cdb4dd0258468c9f415be28c1053f0/tailwind.defaults.js#L4) and [colors](https://gitlab.com/gitlab-org/gitlab-ui/-/blob/24a08b50da6bd3d34fb3f8d24f84436d90d165f6/src/tokens/build/tailwind/tokens.cjs). In the future we plan to utilize [Tailwind config viewer](https://github.com/rogden/tailwind-config-viewer) to have a Tailwind CSS documentation site specific to GitLab.
|
||||
|
||||
### Where should you put new utility classes?
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module BackgroundMigration
|
||||
class BackfillRelatedEpicLinksToIssueLinks < BatchedMigrationJob
|
||||
operation_name :backfill_issue_links_with_related_epic_links
|
||||
feature_category :team_planning
|
||||
|
||||
def perform
|
||||
each_sub_batch do |sub_batch|
|
||||
values_subquery = sub_batch.select(select_fields_to_insert_sql)
|
||||
values_subquery = values_subquery.joins(joins_target_and_source_epic_sql)
|
||||
|
||||
connection.execute(<<~SQL)
|
||||
INSERT INTO issue_links (source_id, target_id, link_type, created_at, updated_at)
|
||||
#{values_subquery.to_sql}
|
||||
ON CONFLICT (source_id, target_id)
|
||||
DO UPDATE SET
|
||||
link_type = EXCLUDED.link_type,
|
||||
created_at = EXCLUDED.created_at,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
SQL
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def select_fields_to_insert_sql
|
||||
<<~SQL
|
||||
source_epics.issue_id AS source_id,
|
||||
target_epics.issue_id AS target_id,
|
||||
related_epic_links.link_type,
|
||||
related_epic_links.created_at AT TIME ZONE '#{Time.zone.tzinfo.name}' AS created_at,
|
||||
related_epic_links.updated_at AT TIME ZONE '#{Time.zone.tzinfo.name}' AS updated_at
|
||||
SQL
|
||||
end
|
||||
|
||||
def joins_target_and_source_epic_sql
|
||||
<<~SQL
|
||||
INNER JOIN epics source_epics ON related_epic_links.source_id = source_epics.id
|
||||
INNER JOIN epics target_epics ON related_epic_links.target_id = target_epics.id
|
||||
SQL
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -361,8 +361,8 @@ module Gitlab
|
|||
clusters: distinct_count(::Clusters::Cluster.where(time_period), :user_id),
|
||||
clusters_integrations_prometheus: cluster_integrations_user_distinct_count(::Clusters::Integrations::Prometheus, time_period),
|
||||
operations_dashboard_default_dashboard: count(::User.active.with_dashboard('operations').where(time_period),
|
||||
start: minimum_id(User),
|
||||
finish: maximum_id(User)),
|
||||
start: minimum_id(User),
|
||||
finish: maximum_id(User)),
|
||||
projects_with_error_tracking_enabled: distinct_count(::Project.with_enabled_error_tracking.where(time_period), :creator_id),
|
||||
projects_with_incidents: distinct_count(::Issue.with_issue_type(:incident).where(time_period), :project_id),
|
||||
# We are making an assumption here that all alert_management_alerts are associated with an issue of type
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ module Gitlab
|
|||
property: MR_APPROVE_ACTION,
|
||||
label: 'redis_hll_counters.code_review.i_code_review_user_approve_mr_monthly',
|
||||
context: [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll,
|
||||
event: MR_APPROVE_ACTION).to_context]
|
||||
event: MR_APPROVE_ACTION).to_context]
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ module Peek
|
|||
duration = (call[:duration] * 1000).round(3)
|
||||
|
||||
call.merge(duration: duration,
|
||||
warnings: warning_for(duration, self.class.thresholds[:individual_call]))
|
||||
warnings: warning_for(duration, self.class.thresholds[:individual_call]))
|
||||
end
|
||||
|
||||
def warning_for(actual, threshold, label: nil)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ module Peek
|
|||
cmd = call[:commands].map { |command| command.join(' ') }.join(', ')
|
||||
|
||||
super.merge(cmd: cmd,
|
||||
instance: call[:storage])
|
||||
instance: call[:storage])
|
||||
end
|
||||
|
||||
def format_command(cmd)
|
||||
|
|
|
|||
|
|
@ -36,12 +36,12 @@ namespace :gems do
|
|||
user_id = File.stat(vendor_gem_dir).uid
|
||||
|
||||
Kernel.system('docker', 'run',
|
||||
"--user=#{user_id}", '--rm', "--volume=#{vendor_gem_dir}:/code", docker_image,
|
||||
'generate',
|
||||
'--input-spec', api_url,
|
||||
'--generator-name', 'ruby',
|
||||
'--output', "/code/#{gem_name}",
|
||||
"--additional-properties=moduleName=#{module_name}"
|
||||
"--user=#{user_id}", '--rm', "--volume=#{vendor_gem_dir}:/code", docker_image,
|
||||
'generate',
|
||||
'--input-spec', api_url,
|
||||
'--generator-name', 'ruby',
|
||||
'--output', "/code/#{gem_name}",
|
||||
"--additional-properties=moduleName=#{module_name}"
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -55,9 +55,9 @@ namespace :gitlab do
|
|||
end
|
||||
|
||||
puts YAML.dump('controller_actions' => controller_actions,
|
||||
'api_endpoints' => endpoints,
|
||||
'sidekiq_workers' => workers,
|
||||
'database_tables' => database_tables)
|
||||
'api_endpoints' => endpoints,
|
||||
'sidekiq_workers' => workers,
|
||||
'database_tables' => database_tables)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ namespace :gitlab do
|
|||
puts "\nSeeding issues for the '#{project.full_path}' project"
|
||||
seeder = Quality::Seeders::Issues.new(project: project)
|
||||
issues_created = seeder.seed(backfill_weeks: args.backfill_weeks.to_i,
|
||||
average_issues_per_week: args.average_issues_per_week.to_i)
|
||||
average_issues_per_week: args.average_issues_per_week.to_i)
|
||||
puts "\n#{issues_created} issues created!"
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ namespace :gitlab do
|
|||
namespace :seed do
|
||||
desc 'Seed groups with sub-groups/projects/runners/jobs for Runner Fleet testing'
|
||||
task :runner_fleet,
|
||||
[:username, :registration_prefix, :runner_count, :job_count] => :gitlab_environment do |_t, args|
|
||||
[:username, :registration_prefix, :runner_count, :job_count] => :gitlab_environment do |_t, args|
|
||||
timings = Benchmark.measure do
|
||||
projects_to_runners = Gitlab::Seeders::Ci::Runner::RunnerFleetSeeder.new(
|
||||
Gitlab::AppLogger,
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@ namespace :gitlab do
|
|||
|
||||
sanitizer = Gitlab::Sanitizers::Exif.new(logger: logger)
|
||||
sanitizer.batch_clean(start_id: args.start_id, stop_id: args.stop_id,
|
||||
dry_run: args.dry_run != 'false',
|
||||
sleep_time: args.sleep_time.to_f,
|
||||
uploader: args.uploader,
|
||||
since: args.since)
|
||||
dry_run: args.dry_run != 'false',
|
||||
sleep_time: args.sleep_time.to_f,
|
||||
uploader: args.uploader,
|
||||
since: args.since)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9212,7 +9212,7 @@ msgstr ""
|
|||
msgid "BranchRules|Update target branch"
|
||||
msgstr ""
|
||||
|
||||
msgid "BranchRules|Users"
|
||||
msgid "BranchRules|Users & groups"
|
||||
msgstr ""
|
||||
|
||||
msgid "BranchRules|View details"
|
||||
|
|
@ -24685,9 +24685,6 @@ msgstr ""
|
|||
msgid "Group %{group_name} was exported successfully."
|
||||
msgstr ""
|
||||
|
||||
msgid "Group %{group_name} was scheduled for deletion."
|
||||
msgstr ""
|
||||
|
||||
msgid "Group %{group_name} was successfully created."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -24697,6 +24694,9 @@ msgstr ""
|
|||
msgid "Group '%{group_name}' has been successfully restored."
|
||||
msgstr ""
|
||||
|
||||
msgid "Group '%{group_name}' is being deleted."
|
||||
msgstr ""
|
||||
|
||||
msgid "Group '%{group_name}' was successfully updated."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -33148,6 +33148,9 @@ msgstr ""
|
|||
msgid "MlModelRegistry|Create model version & import artifacts"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Create model, version & import artifacts"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Creating models is also possible through the MLflow client. %{linkStart}Follow the documentation to learn more.%{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -33178,9 +33181,15 @@ msgstr ""
|
|||
msgid "MlModelRegistry|Enter some description"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Enter some model description"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Error creating model version and uploading artifacts. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Error creating model, version and uploading artifacts. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Error importing artifact. Please try again."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -33202,18 +33211,27 @@ msgstr ""
|
|||
msgid "MlModelRegistry|For example 1.0.0"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|For example my-model"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Info"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Initial version name. Must be a semantic version."
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Latest version"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Leave empty to auto increment."
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Leave empty to skip version creation."
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|MLflow run ID"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -33226,6 +33244,15 @@ msgstr ""
|
|||
msgid "MlModelRegistry|Model deleted successfully"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Model has been created but version or artifacts could not be uploaded. Try creating model version."
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Model name"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Model name must not contain spaces or upper case letter."
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Model performance"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -33277,6 +33304,9 @@ msgstr ""
|
|||
msgid "MlModelRegistry|Use versions to track performance, parameters, and metadata"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Version Description"
|
||||
msgstr ""
|
||||
|
||||
msgid "MlModelRegistry|Version candidates"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -40264,10 +40294,10 @@ msgstr ""
|
|||
msgid "Project '%{project_name}' has been successfully restored."
|
||||
msgstr ""
|
||||
|
||||
msgid "Project '%{project_name}' is being imported."
|
||||
msgid "Project '%{project_name}' is being deleted."
|
||||
msgstr ""
|
||||
|
||||
msgid "Project '%{project_name}' is in the process of being deleted."
|
||||
msgid "Project '%{project_name}' is being imported."
|
||||
msgstr ""
|
||||
|
||||
msgid "Project '%{project_name}' queued for deletion."
|
||||
|
|
@ -44433,12 +44463,6 @@ msgid_plural "Runners|%{highlightStart}%{duration}%{highlightEnd} seconds"
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "Runners|%{installLinkStart}Upgrade GitLab Runner%{installLinkEnd} to match your GitLab version. %{versionLinkStart}Major and minor versions%{versionLinkEnd} must match."
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|%{installLinkStart}Upgrade GitLab Runner%{installLinkEnd} to match your GitLab version. This upgrade is highly recommended for this runner and may contain security or compatibilty fixes. %{versionLinkStart}Major and minor versions%{versionLinkEnd} must match."
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|%{linkStart}Create a new runner%{linkEnd} to get started."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -44464,6 +44488,12 @@ msgid_plural "Runners|%{strongStart}%{count}%{strongEnd} runners will be permane
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "Runners|%{upgradeLinkStart}Upgrade GitLab Runner%{upgradeLinkEnd} to match your GitLab version. %{versionLinkStart}Major and minor versions%{versionLinkEnd} must match."
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|%{upgradeLinkStart}Upgrade GitLab Runner%{upgradeLinkEnd} to match your GitLab version. This upgrade is highly recommended for this runner and may contain security or compatibilty fixes. %{versionLinkStart}Major and minor versions%{versionLinkEnd} must match."
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|1. Configure your Google Cloud project"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ module RuboCop
|
|||
before_execute = node.descendants[1].source_range
|
||||
range_to_remove = node.source_range
|
||||
.with(begin_pos: before_execute.end_pos,
|
||||
end_pos: upto_including_execute.end_pos)
|
||||
end_pos: upto_including_execute.end_pos)
|
||||
|
||||
corrector.remove(range_to_remove)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ module RuboCop
|
|||
return unless ambiguous_page?(node)
|
||||
|
||||
add_offense(node.arguments.each_node(:arg).first,
|
||||
message: MESSAGE % page_object_name(node))
|
||||
message: MESSAGE % page_object_name(node))
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -111,6 +111,7 @@ RSpec.describe Admin::GroupsController, feature_category: :groups_and_projects d
|
|||
it 'redirects to the admin group path' do
|
||||
delete :destroy, params: { id: project.group.path }
|
||||
|
||||
expect(flash[:toast]).to eq(format(_("Group '%{group_name}' is being deleted."), group_name: group.full_name))
|
||||
expect(response).to redirect_to(admin_groups_path)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Admin::ProjectsController do
|
||||
let!(:project) { create(:project, :public) }
|
||||
RSpec.describe Admin::ProjectsController, feature_category: :groups_and_projects do
|
||||
let_it_be(:project) { create(:project, :public) }
|
||||
|
||||
before do
|
||||
sign_in(create(:admin))
|
||||
|
|
@ -107,4 +107,13 @@ RSpec.describe Admin::ProjectsController do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE #destroy' do
|
||||
it 'redirects to the admin projects path and displays the flash toast' do
|
||||
delete :destroy, params: { namespace_id: project.namespace, id: project }
|
||||
|
||||
expect(flash[:toast]).to eq(format(_("Project '%{project_name}' is being deleted."), project_name: project.full_name))
|
||||
expect(response).to redirect_to(admin_projects_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -698,6 +698,7 @@ RSpec.describe GroupsController, factory_default: :keep, feature_category: :code
|
|||
it 'redirects to the root path' do
|
||||
delete :destroy, params: { id: group.to_param }
|
||||
|
||||
expect(flash[:toast]).to eq(format(_("Group '%{group_name}' is being deleted."), group_name: group.full_name))
|
||||
expect(response).to redirect_to(root_path)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1083,6 +1083,7 @@ RSpec.describe ProjectsController, feature_category: :groups_and_projects do
|
|||
|
||||
expect { Project.find(orig_id) }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
expect(response).to have_gitlab_http_status(:found)
|
||||
expect(flash[:toast]).to eq(format(_("Project '%{project_name}' is being deleted."), project_name: project.full_name))
|
||||
expect(response).to redirect_to(dashboard_projects_path)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -416,7 +416,7 @@ RSpec.describe 'Group', feature_category: :groups_and_projects do
|
|||
it 'removes group', :sidekiq_might_not_need_inline do
|
||||
expect { remove_with_confirm('Delete group', group.path) }.to change { Group.count }.by(-1)
|
||||
expect(group.members.all.count).to be_zero
|
||||
expect(page).to have_content "scheduled for deletion"
|
||||
expect(page).to have_content "is being deleted"
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -286,7 +286,7 @@ RSpec.describe 'Project', feature_category: :source_code_management do
|
|||
|
||||
it 'deletes a project', :sidekiq_inline do
|
||||
expect { remove_with_confirm('Delete project', project.path_with_namespace, 'Yes, delete project') }.to change { Project.count }.by(-1)
|
||||
expect(page).to have_content "Project '#{project.full_name}' is in the process of being deleted."
|
||||
expect(page).to have_content "Project '#{project.full_name}' is being deleted."
|
||||
expect(Project.all.count).to be_zero
|
||||
expect(project.issues).to be_empty
|
||||
expect(project.merge_requests).to be_empty
|
||||
|
|
|
|||
|
|
@ -205,6 +205,19 @@ describe('Api', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('groupSubgroups', () => {
|
||||
it('fetches group subgroups', () => {
|
||||
const groupId = '54321';
|
||||
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/subgroups`;
|
||||
const expectedData = [{ id: 7 }];
|
||||
mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, expectedData);
|
||||
|
||||
return Api.groupSubgroups(groupId).then(({ data }) => {
|
||||
expect(data).toEqual(expectedData);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('inviteGroupMembers', () => {
|
||||
it('invites a new email address to create a new User and become a Group Member', () => {
|
||||
const groupId = 1;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import VueApollo from 'vue-apollo';
|
|||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { IndexMlModels } from '~/ml/model_registry/apps';
|
||||
import ModelRow from '~/ml/model_registry/components/model_row.vue';
|
||||
import ModelCreate from '~/ml/model_registry/components/model_create.vue';
|
||||
import { MODEL_ENTITIES } from '~/ml/model_registry/constants';
|
||||
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
|
||||
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
|
||||
|
|
@ -58,7 +59,7 @@ describe('ml/model_registry/apps/index_ml_models', () => {
|
|||
const findTitleArea = () => wrapper.findComponent(TitleArea);
|
||||
const findModelCountMetadataItem = () => findTitleArea().findComponent(MetadataItem);
|
||||
const findBadge = () => wrapper.findComponent(GlExperimentBadge);
|
||||
const findCreateButton = () => wrapper.findByTestId('create-model-button');
|
||||
const findModelCreate = () => wrapper.findComponent(ModelCreate);
|
||||
const findActionsDropdown = () => wrapper.findComponent(ActionsDropdown);
|
||||
const findSearchableList = () => wrapper.findComponent(SearchableList);
|
||||
|
||||
|
|
@ -99,7 +100,7 @@ describe('ml/model_registry/apps/index_ml_models', () => {
|
|||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findCreateButton().exists()).toBe(false);
|
||||
expect(findModelCreate().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -112,7 +113,7 @@ describe('ml/model_registry/apps/index_ml_models', () => {
|
|||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findCreateButton().attributes().href).toBe('path/to/create');
|
||||
expect(findModelCreate().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,298 @@
|
|||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { GlAlert, GlModal } from '@gitlab/ui';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import * as Sentry from '~/sentry/sentry_browser_wrapper';
|
||||
import { visitUrl } from '~/lib/utils/url_utility';
|
||||
import ModelCreate from '~/ml/model_registry/components/model_create.vue';
|
||||
import ImportArtifactZone from '~/ml/model_registry/components/import_artifact_zone.vue';
|
||||
import { uploadModel } from '~/ml/model_registry/services/upload_model';
|
||||
import createModelMutation from '~/ml/model_registry/graphql/mutations/create_model.mutation.graphql';
|
||||
import createModelVersionMutation from '~/ml/model_registry/graphql/mutations/create_model_version.mutation.graphql';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
|
||||
import { createModelResponses, createModelVersionResponses } from '../graphql_mock_data';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
jest.mock('~/lib/utils/url_utility', () => ({
|
||||
...jest.requireActual('~/lib/utils/url_utility'),
|
||||
visitUrl: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('~/ml/model_registry/services/upload_model', () => ({
|
||||
uploadModel: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('ModelCreate', () => {
|
||||
let wrapper;
|
||||
let apolloProvider;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(Sentry, 'captureException').mockImplementation();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
apolloProvider = null;
|
||||
});
|
||||
|
||||
const createWrapper = (
|
||||
createModelResolver = jest.fn().mockResolvedValue(createModelResponses.success),
|
||||
createModelVersionResolver = jest.fn().mockResolvedValue(createModelVersionResponses.success),
|
||||
) => {
|
||||
const requestHandlers = [
|
||||
[createModelMutation, createModelResolver],
|
||||
[createModelVersionMutation, createModelVersionResolver],
|
||||
];
|
||||
apolloProvider = createMockApollo(requestHandlers);
|
||||
|
||||
wrapper = shallowMountExtended(ModelCreate, {
|
||||
provide: {
|
||||
projectPath: 'some/project',
|
||||
},
|
||||
apolloProvider,
|
||||
});
|
||||
};
|
||||
|
||||
const findModalButton = () => wrapper.findByText('Create model');
|
||||
const findNameInput = () => wrapper.findByTestId('nameId');
|
||||
const findVersionInput = () => wrapper.findByTestId('versionId');
|
||||
const findDescriptionInput = () => wrapper.findByTestId('descriptionId');
|
||||
const findVersionDescriptionInput = () => wrapper.findByTestId('versionDescriptionId');
|
||||
const findImportArtifactZone = () => wrapper.findComponent(ImportArtifactZone);
|
||||
const findGlModal = () => wrapper.findComponent(GlModal);
|
||||
const findGlAlert = () => wrapper.findComponent(GlAlert);
|
||||
const submitForm = async () => {
|
||||
findGlModal().vm.$emit('primary', new Event('primary'));
|
||||
await waitForPromises();
|
||||
};
|
||||
|
||||
describe('Initial state', () => {
|
||||
beforeEach(() => {
|
||||
createWrapper();
|
||||
});
|
||||
|
||||
it('renders the modal button', () => {
|
||||
expect(findModalButton().text()).toBe('Create model');
|
||||
});
|
||||
|
||||
describe('Modal open', () => {
|
||||
beforeEach(() => {
|
||||
findModalButton().trigger('click');
|
||||
});
|
||||
|
||||
it('renders the name input', () => {
|
||||
expect(findNameInput().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders the version input', () => {
|
||||
expect(findVersionInput().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders the description input', () => {
|
||||
expect(findDescriptionInput().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders the version description input', () => {
|
||||
expect(findVersionDescriptionInput().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders the import artifact zone input', () => {
|
||||
expect(findImportArtifactZone().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('renders the import artifact zone input with version entered', async () => {
|
||||
findNameInput().vm.$emit('input', 'gpt-alice-1');
|
||||
findVersionInput().vm.$emit('input', '1.0.0');
|
||||
await waitForPromises();
|
||||
expect(findImportArtifactZone().props()).toEqual({
|
||||
path: null,
|
||||
submitOnSelect: false,
|
||||
value: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the import modal', () => {
|
||||
expect(findGlModal().props()).toMatchObject({
|
||||
modalId: 'create-model-modal',
|
||||
title: 'Create model, version & import artifacts',
|
||||
size: 'sm',
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the cancel button in the modal', () => {
|
||||
expect(findGlModal().props('actionCancel')).toEqual({ text: 'Cancel' });
|
||||
});
|
||||
|
||||
it('renders the create button in the modal', () => {
|
||||
expect(findGlModal().props('actionPrimary')).toEqual({
|
||||
attributes: { variant: 'confirm' },
|
||||
text: 'Create',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not render the alert by default', () => {
|
||||
expect(findGlAlert().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Successful flow with version', () => {
|
||||
beforeEach(async () => {
|
||||
createWrapper();
|
||||
findNameInput().vm.$emit('input', 'gpt-alice-1');
|
||||
findVersionInput().vm.$emit('input', '1.0.0');
|
||||
findDescriptionInput().vm.$emit('input', 'My model description');
|
||||
findVersionDescriptionInput().vm.$emit('input', 'My version description');
|
||||
jest.spyOn(apolloProvider.defaultClient, 'mutate');
|
||||
|
||||
await submitForm();
|
||||
});
|
||||
|
||||
it('Makes a create model mutation upon confirm', () => {
|
||||
expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
mutation: createModelMutation,
|
||||
variables: {
|
||||
projectPath: 'some/project',
|
||||
name: 'gpt-alice-1',
|
||||
description: 'My model description',
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('Makes a create model version mutation upon confirm', () => {
|
||||
expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
mutation: createModelVersionMutation,
|
||||
variables: {
|
||||
modelId: 'gid://gitlab/Ml::Model/1',
|
||||
projectPath: 'some/project',
|
||||
version: '1.0.0',
|
||||
description: 'My version description',
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('Uploads a file mutation upon confirm', () => {
|
||||
expect(uploadModel).toHaveBeenCalledWith({
|
||||
file: null,
|
||||
importPath: '/api/v4/projects/1/packages/ml_models/1/files/',
|
||||
});
|
||||
});
|
||||
|
||||
it('Visits the model versions page upon successful create mutation', async () => {
|
||||
createWrapper();
|
||||
await submitForm();
|
||||
expect(visitUrl).toHaveBeenCalledWith('/some/project/-/ml/models/1/versions/1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Successful flow without version', () => {
|
||||
beforeEach(async () => {
|
||||
createWrapper();
|
||||
findNameInput().vm.$emit('input', 'gpt-alice-1');
|
||||
findDescriptionInput().vm.$emit('input', 'My model description');
|
||||
jest.spyOn(apolloProvider.defaultClient, 'mutate');
|
||||
|
||||
await submitForm();
|
||||
});
|
||||
|
||||
it('Visits the model page upon successful create mutation without a version', async () => {
|
||||
createWrapper();
|
||||
await submitForm();
|
||||
expect(visitUrl).toHaveBeenCalledWith('/some/project/-/ml/models/1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Failed flow with version', () => {
|
||||
beforeEach(async () => {
|
||||
const failedCreateModelVersionResolver = jest
|
||||
.fn()
|
||||
.mockResolvedValue(createModelVersionResponses.failure);
|
||||
createWrapper(undefined, failedCreateModelVersionResolver);
|
||||
jest.spyOn(apolloProvider.defaultClient, 'mutate');
|
||||
|
||||
findNameInput().vm.$emit('input', 'gpt-alice-1');
|
||||
findVersionInput().vm.$emit('input', '1.0.0');
|
||||
findVersionDescriptionInput().vm.$emit('input', 'My version description');
|
||||
await submitForm();
|
||||
});
|
||||
|
||||
it('Displays an alert upon failed model create mutation', () => {
|
||||
expect(findGlAlert().text()).toBe('Version is invalid');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Failed flow with version retried', () => {
|
||||
beforeEach(async () => {
|
||||
const failedCreateModelVersionResolver = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce(createModelVersionResponses.failure);
|
||||
createWrapper(undefined, failedCreateModelVersionResolver);
|
||||
jest.spyOn(apolloProvider.defaultClient, 'mutate');
|
||||
|
||||
findNameInput().vm.$emit('input', 'gpt-alice-1');
|
||||
findVersionInput().vm.$emit('input', '1.0.0');
|
||||
findVersionDescriptionInput().vm.$emit('input', 'My retried version description');
|
||||
await submitForm();
|
||||
});
|
||||
|
||||
it('Displays an alert upon failed model create mutation', async () => {
|
||||
expect(findGlAlert().text()).toBe('Version is invalid');
|
||||
|
||||
await submitForm();
|
||||
|
||||
expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
mutation: createModelVersionMutation,
|
||||
variables: {
|
||||
modelId: 'gid://gitlab/Ml::Model/1',
|
||||
projectPath: 'some/project',
|
||||
version: '1.0.0',
|
||||
description: 'My retried version description',
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Failed flow without version', () => {
|
||||
describe('Mutation errors', () => {
|
||||
beforeEach(async () => {
|
||||
const failedCreateModelResolver = jest
|
||||
.fn()
|
||||
.mockResolvedValue(createModelResponses.validationFailure);
|
||||
createWrapper(failedCreateModelResolver);
|
||||
jest.spyOn(apolloProvider.defaultClient, 'mutate');
|
||||
|
||||
findNameInput().vm.$emit('input', 'gpt-alice-1');
|
||||
await submitForm();
|
||||
});
|
||||
|
||||
it('Displays an alert upon failed model create mutation', () => {
|
||||
expect(findGlAlert().text()).toBe("Name is invalid, Name can't be blank");
|
||||
});
|
||||
|
||||
it('Displays an alert upon an exception', () => {
|
||||
expect(findGlAlert().text()).toBe("Name is invalid, Name can't be blank");
|
||||
});
|
||||
});
|
||||
|
||||
it('Logs to sentry upon an exception', async () => {
|
||||
const error = new Error('Runtime error');
|
||||
createWrapper();
|
||||
jest.spyOn(apolloProvider.defaultClient, 'mutate').mockImplementation(() => {
|
||||
throw error;
|
||||
});
|
||||
|
||||
findNameInput().vm.$emit('input', 'gpt-alice-1');
|
||||
await submitForm();
|
||||
|
||||
expect(Sentry.captureException).toHaveBeenCalledWith(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -69,6 +69,7 @@ export const protectionEmptyStatePropsMock = {
|
|||
export const protectionRowPropsMock = {
|
||||
title: 'Test title',
|
||||
users: usersMock,
|
||||
groups: groupsMock,
|
||||
accessLevels: accessLevelsMock,
|
||||
approvalsRequired,
|
||||
statusCheckUrl: statusChecksRulesMock[0].externalUrl,
|
||||
|
|
@ -106,6 +107,10 @@ export const branchProtectionsMockResponse = {
|
|||
project: {
|
||||
id: 'gid://gitlab/Project/1',
|
||||
__typename: 'Project',
|
||||
group: {
|
||||
id: 'gid://gitlab/Group/1',
|
||||
__typename: 'Group',
|
||||
},
|
||||
branchRules: {
|
||||
__typename: 'BranchRuleConnection',
|
||||
nodes: [
|
||||
|
|
@ -158,6 +163,10 @@ export const predefinedBranchRulesMockResponse = {
|
|||
project: {
|
||||
id: 'gid://gitlab/Project/1',
|
||||
__typename: 'Project',
|
||||
group: {
|
||||
id: 'gid://gitlab/Group/1',
|
||||
__typename: 'Group',
|
||||
},
|
||||
branchRules: {
|
||||
__typename: 'BranchRuleConnection',
|
||||
nodes: [
|
||||
|
|
|
|||
|
|
@ -30,7 +30,10 @@ describe('Branch rule protection row', () => {
|
|||
});
|
||||
|
||||
it('renders an avatars-inline component', () => {
|
||||
expect(findAvatarsInline().props('avatars')).toMatchObject(protectionRowPropsMock.users);
|
||||
expect(findAvatarsInline().props('avatars')).toMatchObject([
|
||||
...protectionRowPropsMock.users,
|
||||
...protectionRowPropsMock.groups,
|
||||
]);
|
||||
expect(findAvatarsInline().props('badgeSrOnlyText')).toBe('1 additional user');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -58,31 +58,24 @@ describe('Branch rule protection', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('renders a protection row for users', () => {
|
||||
it('renders a protection row for users and groups', () => {
|
||||
expect(findProtectionRows().at(1).props()).toMatchObject({
|
||||
showDivider: true,
|
||||
groups: protectionPropsMock.groups,
|
||||
users: protectionPropsMock.users,
|
||||
showDivider: true,
|
||||
title: i18n.usersTitle,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders a protection row for groups', () => {
|
||||
expect(findProtectionRows().at(2).props()).toMatchObject({
|
||||
accessLevels: protectionPropsMock.groups,
|
||||
showDivider: true,
|
||||
title: i18n.groupsTitle,
|
||||
title: i18n.usersAndGroupsTitle,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders a protection row for status checks', () => {
|
||||
const statusCheck = protectionPropsMock.statusChecks[0];
|
||||
expect(findProtectionRows().at(3).props()).toMatchObject({
|
||||
expect(findProtectionRows().at(2).props()).toMatchObject({
|
||||
title: statusCheck.name,
|
||||
showDivider: false,
|
||||
statusCheckUrl: statusCheck.externalUrl,
|
||||
});
|
||||
|
||||
expect(findProtectionRows().at(4).props('showDivider')).toBe(true);
|
||||
expect(findProtectionRows().at(3).props('showDivider')).toBe(true);
|
||||
});
|
||||
|
||||
describe('When `isEditAvailable` prop is set to true', () => {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,12 @@ jest.mock('~/rest_api', () => ({
|
|||
{ name: 'Project 2', id: '2' },
|
||||
],
|
||||
}),
|
||||
getSubgroups: jest.fn().mockResolvedValue({
|
||||
data: [
|
||||
{ name: 'Subgroup 1', id: '1' },
|
||||
{ name: 'Subgroup 2', id: '2' },
|
||||
],
|
||||
}),
|
||||
}));
|
||||
Vue.use(VueApollo);
|
||||
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ RSpec.describe BulkImports::Projects::Pipelines::SnippetsRepositoryPipeline, fea
|
|||
end
|
||||
|
||||
describe '#run', :clean_gitlab_redis_shared_state do
|
||||
let(:validation_response) { double(Hash, 'error?': false) }
|
||||
let(:validation_response) { double(Hash, error?: false) }
|
||||
|
||||
before do
|
||||
allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor|
|
||||
|
|
@ -172,7 +172,7 @@ RSpec.describe BulkImports::Projects::Pipelines::SnippetsRepositoryPipeline, fea
|
|||
end
|
||||
|
||||
context 'when snippet is invalid' do
|
||||
let(:validation_response) { double(Hash, 'error?': true) }
|
||||
let(:validation_response) { double(Hash, error?: true) }
|
||||
|
||||
before do
|
||||
allow_next_instance_of(Repository) do |repository|
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ RSpec.describe Constraints::GroupUrlConstrainer do
|
|||
|
||||
def build_request(path, method = 'GET')
|
||||
double(:request,
|
||||
'get?': (method == 'GET'),
|
||||
get?: (method == 'GET'),
|
||||
params: { id: path })
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ RSpec.describe Constraints::ProjectUrlConstrainer do
|
|||
|
||||
def build_request(namespace, project, method = 'GET')
|
||||
double(:request,
|
||||
'get?': (method == 'GET'),
|
||||
get?: (method == 'GET'),
|
||||
params: { namespace_id: namespace, id: project })
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ RSpec.describe Constraints::UserUrlConstrainer do
|
|||
|
||||
def build_request(username, method = 'GET')
|
||||
double(:request,
|
||||
'get?': (method == 'GET'),
|
||||
get?: (method == 'GET'),
|
||||
params: { username: username })
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -273,16 +273,16 @@ RSpec.describe ContainerRegistry::GitlabApiClient, feature_category: :container_
|
|||
let(:response) do
|
||||
[
|
||||
{
|
||||
"name": "docker-alpine",
|
||||
"path": "gitlab-org/build/cng/docker-alpine",
|
||||
"created_at": "2022-06-07T12:11:13.633+00:00",
|
||||
"updated_at": "2022-06-07T14:37:49.251+00:00"
|
||||
name: "docker-alpine",
|
||||
path: "gitlab-org/build/cng/docker-alpine",
|
||||
created_at: "2022-06-07T12:11:13.633+00:00",
|
||||
updated_at: "2022-06-07T14:37:49.251+00:00"
|
||||
},
|
||||
{
|
||||
"name": "git-base",
|
||||
"path": "gitlab-org/build/cng/git-base",
|
||||
"created_at": "2022-06-07T12:11:13.633+00:00",
|
||||
"updated_at": "2022-06-07T14:37:49.251+00:00"
|
||||
name: "git-base",
|
||||
path: "gitlab-org/build/cng/git-base",
|
||||
created_at: "2022-06-07T12:11:13.633+00:00",
|
||||
updated_at: "2022-06-07T14:37:49.251+00:00"
|
||||
}
|
||||
]
|
||||
end
|
||||
|
|
@ -468,7 +468,7 @@ RSpec.describe ContainerRegistry::GitlabApiClient, feature_category: :container_
|
|||
|
||||
describe '.deduplicated_size' do
|
||||
let(:path) { 'foo/bar' }
|
||||
let(:response) { { 'size_bytes': 555 } }
|
||||
let(:response) { { size_bytes: 555 } }
|
||||
let(:registry_enabled) { true }
|
||||
|
||||
subject { described_class.deduplicated_size(path) }
|
||||
|
|
@ -685,16 +685,16 @@ RSpec.describe ContainerRegistry::GitlabApiClient, feature_category: :container_
|
|||
let(:client_response_repositories) do
|
||||
[
|
||||
{
|
||||
"name": "docker-alpine",
|
||||
"path": "gitlab-org/build/cng/docker-alpine",
|
||||
"created_at": "2022-06-07T12:11:13.633+00:00",
|
||||
"updated_at": "2022-06-07T14:37:49.251+00:00"
|
||||
name: "docker-alpine",
|
||||
path: "gitlab-org/build/cng/docker-alpine",
|
||||
created_at: "2022-06-07T12:11:13.633+00:00",
|
||||
updated_at: "2022-06-07T14:37:49.251+00:00"
|
||||
},
|
||||
{
|
||||
"name": "git-base",
|
||||
"path": "gitlab-org/build/cng/git-base",
|
||||
"created_at": "2022-06-07T12:11:13.633+00:00",
|
||||
"updated_at": "2022-06-07T14:37:49.251+00:00"
|
||||
name: "git-base",
|
||||
path: "gitlab-org/build/cng/git-base",
|
||||
created_at: "2022-06-07T12:11:13.633+00:00",
|
||||
updated_at: "2022-06-07T14:37:49.251+00:00"
|
||||
}
|
||||
]
|
||||
end
|
||||
|
|
@ -707,16 +707,16 @@ RSpec.describe ContainerRegistry::GitlabApiClient, feature_category: :container_
|
|||
let(:client_response_repositories1) do
|
||||
[
|
||||
{
|
||||
"name": "docker-alpine",
|
||||
"path": "gitlab-org/build/cng/docker-alpine",
|
||||
"created_at": "2022-06-07T12:11:13.633+00:00",
|
||||
"updated_at": "2022-06-07T14:37:49.251+00:00"
|
||||
name: "docker-alpine",
|
||||
path: "gitlab-org/build/cng/docker-alpine",
|
||||
created_at: "2022-06-07T12:11:13.633+00:00",
|
||||
updated_at: "2022-06-07T14:37:49.251+00:00"
|
||||
},
|
||||
{
|
||||
"name": "git-base",
|
||||
"path": "gitlab-org/build/cng/git-base",
|
||||
"created_at": "2022-06-07T12:11:13.633+00:00",
|
||||
"updated_at": "2022-06-07T14:37:49.251+00:00"
|
||||
name: "git-base",
|
||||
path: "gitlab-org/build/cng/git-base",
|
||||
created_at: "2022-06-07T12:11:13.633+00:00",
|
||||
updated_at: "2022-06-07T14:37:49.251+00:00"
|
||||
}
|
||||
]
|
||||
end
|
||||
|
|
@ -725,16 +725,16 @@ RSpec.describe ContainerRegistry::GitlabApiClient, feature_category: :container_
|
|||
let(:client_response_repositories2) do
|
||||
[
|
||||
{
|
||||
"name": "docker-alpine1",
|
||||
"path": "gitlab-org/build/cng/docker-alpine",
|
||||
"created_at": "2022-06-07T12:11:13.633+00:00",
|
||||
"updated_at": "2022-06-07T14:37:49.251+00:00"
|
||||
name: "docker-alpine1",
|
||||
path: "gitlab-org/build/cng/docker-alpine",
|
||||
created_at: "2022-06-07T12:11:13.633+00:00",
|
||||
updated_at: "2022-06-07T14:37:49.251+00:00"
|
||||
},
|
||||
{
|
||||
"name": "git-base1",
|
||||
"path": "gitlab-org/build/cng/git-base",
|
||||
"created_at": "2022-06-07T12:11:13.633+00:00",
|
||||
"updated_at": "2022-06-07T14:37:49.251+00:00"
|
||||
name: "git-base1",
|
||||
path: "gitlab-org/build/cng/git-base",
|
||||
created_at: "2022-06-07T12:11:13.633+00:00",
|
||||
updated_at: "2022-06-07T14:37:49.251+00:00"
|
||||
}
|
||||
]
|
||||
end
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ RSpec.describe Gitlab::UsageMetricDefinition::RedisHllGenerator, :silence_stdout
|
|||
end
|
||||
|
||||
it 'creates metric definition files' do
|
||||
described_class.new(args, { 'ee': true }).invoke_all
|
||||
described_class.new(args, { ee: true }).invoke_all
|
||||
|
||||
expect(weekly_metric_definition).to include("key_path" => "redis_hll_counters.test_category.i_test_event_weekly")
|
||||
expect(weekly_metric_definition["distribution"]).to include('ee')
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ RSpec.describe Gitlab::UsageMetricDefinitionGenerator, :silence_stdout, feature_
|
|||
end
|
||||
|
||||
it 'creates a metric definition file using the template' do
|
||||
described_class.new([key_path], { 'dir' => dir, 'class_name' => class_name, 'ee': true }).invoke_all
|
||||
described_class.new([key_path], { 'dir' => dir, 'class_name' => class_name, ee: true }).invoke_all
|
||||
expect(YAML.safe_load(File.read(metric_definition_path))).to eq(sample_metric)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,120 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::BackgroundMigration::BackfillRelatedEpicLinksToIssueLinks, feature_category: :team_planning do
|
||||
let!(:epic_type_id) { table(:work_item_types).find_by(base_type: 7).id }
|
||||
let!(:author) { table(:users).create!(username: 'tester', projects_limit: 100) }
|
||||
let!(:namespace) { table(:namespaces).create!(name: 'my test group1', path: 'my-test-group1') }
|
||||
|
||||
let(:epics) { table(:epics) }
|
||||
let(:issues) { table(:issues) }
|
||||
let(:related_epic_links) { table(:related_epic_links) }
|
||||
let(:issue_links) { table(:issue_links) }
|
||||
let(:start_id) { related_epic_links.minimum(:id) }
|
||||
let(:end_id) { related_epic_links.maximum(:id) }
|
||||
|
||||
# Source epics
|
||||
let(:source_epic_1) { create_epic_with_work_item(title: 'Epic 1', iid: 1) }
|
||||
let(:source_epic_2) { create_epic_with_work_item(title: 'Epic 2', iid: 2) }
|
||||
let(:source_epic_3) { create_epic_with_work_item(title: 'Epic 3', iid: 3) }
|
||||
let(:source_epic_4) { create_epic_with_work_item(title: 'Epic 4', iid: 4) }
|
||||
# Target epics
|
||||
let(:target_epic_1) { create_epic_with_work_item(title: 'Epic 5', iid: 5) }
|
||||
let(:target_epic_2) { create_epic_with_work_item(title: 'Epic 6', iid: 6) }
|
||||
let(:target_epic_3) { create_epic_with_work_item(title: 'Epic 7', iid: 7) }
|
||||
let(:target_epic_4) { create_epic_with_work_item(title: 'Epic 8', iid: 8) }
|
||||
|
||||
# Epic links not in sync(without a corresponding issue links record)
|
||||
let!(:related_epic_link_1) { create_related_epic_link(source: source_epic_1, target: target_epic_1, link_type: 0) }
|
||||
let!(:related_epic_link_2) { create_related_epic_link(source: source_epic_2, target: target_epic_2, link_type: 1) }
|
||||
# Epic link in sync
|
||||
let!(:related_epic_link_3) { create_related_epic_link(source: source_epic_3, target: target_epic_3, link_type: 0) }
|
||||
let!(:synced_issue_link_1) do
|
||||
issue_links.create!(
|
||||
source_id: source_epic_3.issue_id,
|
||||
target_id: target_epic_3.issue_id,
|
||||
link_type: 0
|
||||
)
|
||||
end
|
||||
|
||||
# Epic link in sync but with outdated value
|
||||
let!(:related_epic_link_4) { create_related_epic_link(source: source_epic_4, target: target_epic_4, link_type: 0) }
|
||||
let!(:synced_issue_link_2) do
|
||||
issue_links.create!(
|
||||
source_id: source_epic_4.issue_id,
|
||||
target_id: target_epic_4.issue_id,
|
||||
link_type: 1,
|
||||
created_at: 1.day.ago,
|
||||
updated_at: 1.hour.ago
|
||||
)
|
||||
end
|
||||
|
||||
subject(:migration) do
|
||||
described_class.new(
|
||||
start_id: start_id,
|
||||
end_id: end_id,
|
||||
batch_table: :related_epic_links,
|
||||
batch_column: :id,
|
||||
sub_batch_size: 2,
|
||||
pause_ms: 0,
|
||||
connection: ApplicationRecord.connection
|
||||
)
|
||||
end
|
||||
|
||||
RSpec::Matchers.define :have_synced_issue_link do
|
||||
match do |epic_link|
|
||||
source_work_item_id = epics.find(epic_link.source_id).issue_id
|
||||
target_work_item_id = epics.find(epic_link.target_id).issue_id
|
||||
|
||||
issue_links.find_by(
|
||||
source_id: source_work_item_id,
|
||||
target_id: target_work_item_id,
|
||||
link_type: epic_link.link_type
|
||||
).present?
|
||||
end
|
||||
end
|
||||
|
||||
it 'backfills data correctly' do
|
||||
expect do
|
||||
migration.perform
|
||||
end.to change { issue_links.count }.from(2).to(4)
|
||||
.and not_change { synced_issue_link_1.reload }
|
||||
.and change { synced_issue_link_2.reload.link_type }.from(1).to(0)
|
||||
.and change { synced_issue_link_2.reload.created_at }.to(related_epic_link_4.reload.created_at)
|
||||
.and change { synced_issue_link_2.reload.updated_at }.to(related_epic_link_4.reload.updated_at)
|
||||
|
||||
expect(related_epic_link_1).to have_synced_issue_link
|
||||
expect(related_epic_link_2).to have_synced_issue_link
|
||||
expect(related_epic_link_3).to have_synced_issue_link
|
||||
expect(related_epic_link_4).to have_synced_issue_link
|
||||
end
|
||||
|
||||
def create_epic_with_work_item(iid:, title:)
|
||||
wi = issues.create!(
|
||||
iid: iid,
|
||||
author_id: author.id,
|
||||
work_item_type_id: epic_type_id,
|
||||
namespace_id: namespace.id,
|
||||
lock_version: 1,
|
||||
title: title
|
||||
)
|
||||
|
||||
epics.create!(
|
||||
iid: iid,
|
||||
title: title,
|
||||
title_html: title,
|
||||
group_id: namespace.id,
|
||||
author_id: author.id,
|
||||
issue_id: wi.id
|
||||
)
|
||||
end
|
||||
|
||||
def create_related_epic_link(source:, target:, link_type:)
|
||||
related_epic_links.create!(
|
||||
source_id: source.id,
|
||||
target_id: target.id,
|
||||
link_type: link_type
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -113,7 +113,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job, feature_category: :pipeline_compo
|
|||
end
|
||||
|
||||
context 'when job name is empty' do
|
||||
let(:entry) { described_class.new(config, name: ''.to_sym) }
|
||||
let(:entry) { described_class.new(config, name: :"") }
|
||||
|
||||
it 'reports error' do
|
||||
expect(entry.errors).to include "job name can't be blank"
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ RSpec.describe Gitlab::Ci::Config::Entry::Jobs do
|
|||
|
||||
let(:config) do
|
||||
{
|
||||
'.hidden_job'.to_sym => { script: 'something' },
|
||||
'.hidden_bridge'.to_sym => { trigger: 'my/project' },
|
||||
".hidden_job": { script: 'something' },
|
||||
".hidden_bridge": { trigger: 'my/project' },
|
||||
regular_job: { script: 'something' },
|
||||
my_trigger: { trigger: 'my/project' }
|
||||
}
|
||||
|
|
@ -81,7 +81,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Jobs do
|
|||
end
|
||||
|
||||
context 'when no visible jobs present' do
|
||||
let(:config) { { '.hidden'.to_sym => { script: [] } } }
|
||||
let(:config) { { ".hidden": { script: [] } } }
|
||||
|
||||
it 'returns error about no visible jobs defined' do
|
||||
expect(entry.errors)
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable, feature_category: :pipeli
|
|||
end
|
||||
|
||||
context 'when job name is empty' do
|
||||
let(:entry) { node_class.new(config, name: ''.to_sym) }
|
||||
let(:entry) { node_class.new(config, name: :"") }
|
||||
|
||||
it 'reports error' do
|
||||
expect(entry.errors).to include "job name can't be blank"
|
||||
|
|
|
|||
|
|
@ -12,13 +12,13 @@ RSpec.describe Gitlab::Ci::Parsers::Accessibility::Pa11y do
|
|||
context "when there are no URLs provided" do
|
||||
let(:pa11y) do
|
||||
{
|
||||
"total": 1,
|
||||
"passes": 0,
|
||||
"errors": 0,
|
||||
"results": {
|
||||
total: 1,
|
||||
passes: 0,
|
||||
errors: 0,
|
||||
results: {
|
||||
"": [
|
||||
{
|
||||
"message": "Protocol error (Page.navigate): Cannot navigate to invalid URL"
|
||||
message: "Protocol error (Page.navigate): Cannot navigate to invalid URL"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -39,10 +39,10 @@ RSpec.describe Gitlab::Ci::Parsers::Accessibility::Pa11y do
|
|||
context "when there are no errors" do
|
||||
let(:pa11y) do
|
||||
{
|
||||
"total": 1,
|
||||
"passes": 1,
|
||||
"errors": 0,
|
||||
"results": {
|
||||
total: 1,
|
||||
passes: 1,
|
||||
errors: 0,
|
||||
results: {
|
||||
"http://pa11y.org/": []
|
||||
}
|
||||
}.to_json
|
||||
|
|
@ -61,20 +61,20 @@ RSpec.describe Gitlab::Ci::Parsers::Accessibility::Pa11y do
|
|||
context "when there are errors" do
|
||||
let(:pa11y) do
|
||||
{
|
||||
"total": 1,
|
||||
"passes": 0,
|
||||
"errors": 1,
|
||||
"results": {
|
||||
total: 1,
|
||||
passes: 0,
|
||||
errors: 1,
|
||||
results: {
|
||||
"https://about.gitlab.com/": [
|
||||
{
|
||||
"code": "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent",
|
||||
"type": "error",
|
||||
"typeCode": 1,
|
||||
"message": "Anchor element found with a valid href attribute, but no link content has been supplied.",
|
||||
"context": "<a href=\"/\" class=\"navbar-brand animated\"><svg height=\"36\" viewBox=\"0 0 1...</a>",
|
||||
"selector": "#main-nav > div:nth-child(1) > a",
|
||||
"runner": "htmlcs",
|
||||
"runnerExtras": {}
|
||||
code: "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent",
|
||||
type: "error",
|
||||
typeCode: 1,
|
||||
message: "Anchor element found with a valid href attribute, but no link content has been supplied.",
|
||||
context: "<a href=\"/\" class=\"navbar-brand animated\"><svg height=\"36\" viewBox=\"0 0 1...</a>",
|
||||
selector: "#main-nav > div:nth-child(1) > a",
|
||||
runner: "htmlcs",
|
||||
runnerExtras: {}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -96,10 +96,10 @@ RSpec.describe Gitlab::Ci::Parsers::Accessibility::Pa11y do
|
|||
context "when data is not a valid JSON string" do
|
||||
let(:pa11y) do
|
||||
{
|
||||
"total": 1,
|
||||
"passes": 1,
|
||||
"errors": 0,
|
||||
"results": {
|
||||
total: 1,
|
||||
passes: 1,
|
||||
errors: 0,
|
||||
results: {
|
||||
"http://pa11y.org/": []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,27 +10,27 @@ RSpec.describe Gitlab::Ci::Parsers::Codequality::CodeClimate do
|
|||
let(:code_climate) do
|
||||
[
|
||||
{
|
||||
"categories": [
|
||||
categories: [
|
||||
"Complexity"
|
||||
],
|
||||
"check_name": "argument_count",
|
||||
"content": {
|
||||
"body": ""
|
||||
check_name: "argument_count",
|
||||
content: {
|
||||
body: ""
|
||||
},
|
||||
"description": "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.",
|
||||
"fingerprint": "15cdb5c53afd42bc22f8ca366a08d547",
|
||||
"location": {
|
||||
"path": "foo.rb",
|
||||
"lines": {
|
||||
"begin": 10,
|
||||
"end": 10
|
||||
description: "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.",
|
||||
fingerprint: "15cdb5c53afd42bc22f8ca366a08d547",
|
||||
location: {
|
||||
path: "foo.rb",
|
||||
lines: {
|
||||
begin: 10,
|
||||
end: 10
|
||||
}
|
||||
},
|
||||
"other_locations": [],
|
||||
"remediation_points": 900000,
|
||||
"severity": "major",
|
||||
"type": "issue",
|
||||
"engine_name": "structure"
|
||||
other_locations: [],
|
||||
remediation_points: 900000,
|
||||
severity: "major",
|
||||
type: "issue",
|
||||
engine_name: "structure"
|
||||
}
|
||||
].to_json
|
||||
end
|
||||
|
|
@ -68,27 +68,27 @@ RSpec.describe Gitlab::Ci::Parsers::Codequality::CodeClimate do
|
|||
let(:code_climate) do
|
||||
[
|
||||
{
|
||||
"categories": [
|
||||
categories: [
|
||||
"Complexity"
|
||||
],
|
||||
"check_name": "argument_count",
|
||||
"content": {
|
||||
"body": ""
|
||||
check_name: "argument_count",
|
||||
content: {
|
||||
body: ""
|
||||
},
|
||||
"description": "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.",
|
||||
"fingerprint": "15cdb5c53afd42bc22f8ca366a08d547",
|
||||
"location": {
|
||||
"path": "foo.rb",
|
||||
"lines": {
|
||||
"begin": 10,
|
||||
"end": 10
|
||||
description: "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.",
|
||||
fingerprint: "15cdb5c53afd42bc22f8ca366a08d547",
|
||||
location: {
|
||||
path: "foo.rb",
|
||||
lines: {
|
||||
begin: 10,
|
||||
end: 10
|
||||
}
|
||||
},
|
||||
"other_locations": [],
|
||||
"remediation_points": 900000,
|
||||
"severity": "major",
|
||||
"type": "issue",
|
||||
"engine_name": "structure"
|
||||
other_locations: [],
|
||||
remediation_points: 900000,
|
||||
severity: "major",
|
||||
type: "issue",
|
||||
engine_name: "structure"
|
||||
}
|
||||
]
|
||||
end
|
||||
|
|
@ -104,34 +104,34 @@ RSpec.describe Gitlab::Ci::Parsers::Codequality::CodeClimate do
|
|||
let(:code_climate) do
|
||||
[
|
||||
{
|
||||
"type": "Issue",
|
||||
"check_name": "Rubocop/Metrics/ParameterLists",
|
||||
"description": "Avoid parameter lists longer than 5 parameters. [12/5]",
|
||||
"fingerprint": "ab5f8b935886b942d621399aefkaehfiaehf",
|
||||
"severity": "minor"
|
||||
type: "Issue",
|
||||
check_name: "Rubocop/Metrics/ParameterLists",
|
||||
description: "Avoid parameter lists longer than 5 parameters. [12/5]",
|
||||
fingerprint: "ab5f8b935886b942d621399aefkaehfiaehf",
|
||||
severity: "minor"
|
||||
},
|
||||
{
|
||||
"categories": [
|
||||
categories: [
|
||||
"Complexity"
|
||||
],
|
||||
"check_name": "argument_count",
|
||||
"content": {
|
||||
"body": ""
|
||||
check_name: "argument_count",
|
||||
content: {
|
||||
body: ""
|
||||
},
|
||||
"description": "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.",
|
||||
"fingerprint": "15cdb5c53afd42bc22f8ca366a08d547",
|
||||
"location": {
|
||||
"path": "foo.rb",
|
||||
"lines": {
|
||||
"begin": 10,
|
||||
"end": 10
|
||||
description: "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.",
|
||||
fingerprint: "15cdb5c53afd42bc22f8ca366a08d547",
|
||||
location: {
|
||||
path: "foo.rb",
|
||||
lines: {
|
||||
begin: 10,
|
||||
end: 10
|
||||
}
|
||||
},
|
||||
"other_locations": [],
|
||||
"remediation_points": 900000,
|
||||
"severity": "major",
|
||||
"type": "issue",
|
||||
"engine_name": "structure"
|
||||
other_locations: [],
|
||||
remediation_points: 900000,
|
||||
severity: "major",
|
||||
type: "issue",
|
||||
engine_name: "structure"
|
||||
}
|
||||
].to_json
|
||||
end
|
||||
|
|
@ -147,27 +147,27 @@ RSpec.describe Gitlab::Ci::Parsers::Codequality::CodeClimate do
|
|||
let(:code_climate) do
|
||||
[
|
||||
{
|
||||
"categories": [
|
||||
categories: [
|
||||
"Complexity"
|
||||
],
|
||||
"check_name": "argument_count",
|
||||
"content": {
|
||||
"body": ""
|
||||
check_name: "argument_count",
|
||||
content: {
|
||||
body: ""
|
||||
},
|
||||
"description": "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.",
|
||||
"fingerprint": "15cdb5c53afd42bc22f8ca366a08d547",
|
||||
"location": {
|
||||
"path": "foo.rb",
|
||||
"lines": {
|
||||
"begin": 10,
|
||||
"end": 10
|
||||
description: "Method `new_array` has 12 arguments (exceeds 4 allowed). Consider refactoring.",
|
||||
fingerprint: "15cdb5c53afd42bc22f8ca366a08d547",
|
||||
location: {
|
||||
path: "foo.rb",
|
||||
lines: {
|
||||
begin: 10,
|
||||
end: 10
|
||||
}
|
||||
},
|
||||
"other_locations": [],
|
||||
"remediation_points": 900000,
|
||||
"severity": "major",
|
||||
"type": "issue",
|
||||
"engine_name": "structure"
|
||||
other_locations: [],
|
||||
remediation_points: 900000,
|
||||
severity: "major",
|
||||
type: "issue",
|
||||
engine_name: "structure"
|
||||
}
|
||||
].to_json
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,24 +8,24 @@ RSpec.describe Gitlab::Ci::Reports::AccessibilityReports do
|
|||
let(:data) do
|
||||
[
|
||||
{
|
||||
"code": "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent",
|
||||
"type": "error",
|
||||
"typeCode": 1,
|
||||
"message": "Anchor element found with a valid href attribute, but no link content has been supplied.",
|
||||
"context": %(<a href="/customers/worldline"><svg viewBox="0 0 509 89" xmln...</a>),
|
||||
"selector": "html > body > div:nth-child(9) > div:nth-child(2) > a:nth-child(17)",
|
||||
"runner": "htmlcs",
|
||||
"runnerExtras": {}
|
||||
code: "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent",
|
||||
type: "error",
|
||||
typeCode: 1,
|
||||
message: "Anchor element found with a valid href attribute, but no link content has been supplied.",
|
||||
context: %(<a href="/customers/worldline"><svg viewBox="0 0 509 89" xmln...</a>),
|
||||
selector: "html > body > div:nth-child(9) > div:nth-child(2) > a:nth-child(17)",
|
||||
runner: "htmlcs",
|
||||
runnerExtras: {}
|
||||
},
|
||||
{
|
||||
"code": "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent",
|
||||
"type": "error",
|
||||
"typeCode": 1,
|
||||
"message": "Anchor element found with a valid href attribute, but no link content has been supplied.",
|
||||
"context": %(<a href="/customers/equinix"><svg xmlns="http://www.w3.org/...</a>),
|
||||
"selector": "html > body > div:nth-child(9) > div:nth-child(2) > a:nth-child(18)",
|
||||
"runner": "htmlcs",
|
||||
"runnerExtras": {}
|
||||
code: "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent",
|
||||
type: "error",
|
||||
typeCode: 1,
|
||||
message: "Anchor element found with a valid href attribute, but no link content has been supplied.",
|
||||
context: %(<a href="/customers/equinix"><svg xmlns="http://www.w3.org/...</a>),
|
||||
selector: "html > body > div:nth-child(9) > div:nth-child(2) > a:nth-child(18)",
|
||||
runner: "htmlcs",
|
||||
runnerExtras: {}
|
||||
}
|
||||
]
|
||||
end
|
||||
|
|
|
|||
|
|
@ -30,11 +30,11 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReports do
|
|||
context 'when a required property is missing in the degradation' do
|
||||
let(:invalid_degradation) do
|
||||
{
|
||||
"type": "Issue",
|
||||
"check_name": "Rubocop/Metrics/ParameterLists",
|
||||
"description": "Avoid parameter lists longer than 5 parameters. [12/5]",
|
||||
"fingerprint": "ab5f8b935886b942d621399aefkaehfiaehf",
|
||||
"severity": "minor"
|
||||
type: "Issue",
|
||||
check_name: "Rubocop/Metrics/ParameterLists",
|
||||
description: "Avoid parameter lists longer than 5 parameters. [12/5]",
|
||||
fingerprint: "ab5f8b935886b942d621399aefkaehfiaehf",
|
||||
severity: "minor"
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -2815,12 +2815,12 @@ module Gitlab
|
|||
build1: {
|
||||
stage: 'build',
|
||||
script: 'build',
|
||||
parallel: { matrix: [{ 'PROVIDER': ['aws'], 'STACK': %w[monitoring app1 app2] }] }
|
||||
parallel: { matrix: [{ PROVIDER: ['aws'], STACK: %w[monitoring app1 app2] }] }
|
||||
},
|
||||
test1: {
|
||||
stage: 'test',
|
||||
script: 'test',
|
||||
needs: [{ job: 'build1', parallel: { matrix: [{ 'PROVIDER': ['aws'], 'STACK': ['app1'] }] } }]
|
||||
needs: [{ job: 'build1', parallel: { matrix: [{ PROVIDER: ['aws'], STACK: ['app1'] }] } }]
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
@ -3255,7 +3255,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
context 'returns errors if there are no visible jobs defined' do
|
||||
let(:config) { YAML.dump({ before_script: ["bundle update"], '.hidden'.to_sym => { script: 'ls' } }) }
|
||||
let(:config) { YAML.dump({ before_script: ["bundle update"], ".hidden": { script: 'ls' } }) }
|
||||
|
||||
it_behaves_like 'returns errors', 'jobs config should contain at least one visible job'
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe QueueBackfillRelatedEpicLinksToIssueLinks, feature_category: :team_planning do
|
||||
let!(:batched_migration) { described_class::MIGRATION }
|
||||
|
||||
it 'schedules a new batched migration' do
|
||||
reversible_migration do |migration|
|
||||
migration.before -> {
|
||||
expect(batched_migration).not_to have_scheduled_batched_migration
|
||||
}
|
||||
|
||||
migration.after -> {
|
||||
expect(batched_migration).to have_scheduled_batched_migration(
|
||||
table_name: :related_epic_links,
|
||||
column_name: :id,
|
||||
interval: described_class::DELAY_INTERVAL,
|
||||
batch_size: described_class::BATCH_SIZE,
|
||||
sub_batch_size: described_class::SUB_BATCH_SIZE
|
||||
)
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -6,19 +6,19 @@ RSpec.describe Ci::RetryPipelineWorker, feature_category: :continuous_integratio
|
|||
describe '#perform' do
|
||||
subject(:perform) { described_class.new.perform(pipeline_id, user_id) }
|
||||
|
||||
let(:pipeline) { create(:ci_pipeline) }
|
||||
let_it_be(:pipeline) { create(:ci_pipeline) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
before_all do
|
||||
pipeline.project.add_maintainer(user)
|
||||
end
|
||||
|
||||
context 'when pipeline exists' do
|
||||
let(:pipeline_id) { pipeline.id }
|
||||
|
||||
context 'when user exists' do
|
||||
let(:user) { create(:user) }
|
||||
let(:user_id) { user.id }
|
||||
|
||||
before do
|
||||
pipeline.project.add_maintainer(user)
|
||||
end
|
||||
|
||||
it 'retries the pipeline' do
|
||||
expect(::Ci::Pipeline).to receive(:find_by_id).with(pipeline.id).and_return(pipeline)
|
||||
expect(pipeline).to receive(:retry_failed).with(having_attributes(id: user_id))
|
||||
|
|
@ -28,7 +28,7 @@ RSpec.describe Ci::RetryPipelineWorker, feature_category: :continuous_integratio
|
|||
end
|
||||
|
||||
context 'when user does not exist' do
|
||||
let(:user_id) { 1234 }
|
||||
let(:user_id) { non_existing_record_id }
|
||||
|
||||
it 'does not retry the pipeline' do
|
||||
expect(::Ci::Pipeline).to receive(:find_by_id).with(pipeline_id).and_return(pipeline)
|
||||
|
|
@ -40,8 +40,8 @@ RSpec.describe Ci::RetryPipelineWorker, feature_category: :continuous_integratio
|
|||
end
|
||||
|
||||
context 'when pipeline does not exist' do
|
||||
let(:pipeline_id) { 1234 }
|
||||
let(:user_id) { 1234 }
|
||||
let(:pipeline_id) { non_existing_record_id }
|
||||
let(:user_id) { user.id }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(perform).to be_nil
|
||||
|
|
|
|||
Loading…
Reference in New Issue