Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue28
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue539
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue81
-rw-r--r--app/assets/javascripts/alerts_settings/constants.js3
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue66
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/constants.js2
-rw-r--r--app/assets/stylesheets/page_bundles/incident_management_list.scss122
-rw-r--r--app/controllers/projects/ci/pipeline_editor_controller.rb2
-rw-r--r--app/finders/labels_finder.rb7
-rw-r--r--app/mailers/emails/profile.rb2
-rw-r--r--app/models/label.rb16
-rw-r--r--app/serializers/merge_request_serializer.rb2
-rw-r--r--app/services/auth/container_registry_authentication_service.rb22
-rw-r--r--app/services/labels/available_labels_service.rb15
-rw-r--r--app/services/labels/create_service.rb4
-rw-r--r--app/services/labels/update_service.rb10
-rw-r--r--app/services/merge_requests/base_service.rb11
-rw-r--r--app/views/projects/settings/operations/_alert_management.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml2
-rw-r--r--config/feature_flags/ops/enforce_locked_labels_on_merge.yml8
-rw-r--r--config/metrics/counts_28d/20210427102618_code_review_category_monthly_active_users.yml1
-rw-r--r--config/metrics/counts_28d/20210427103119_code_review_group_monthly_active_users.yml1
-rw-r--r--config/metrics/counts_28d/20230809194743_i_code_review_saved_replies_use_in_mr_monthly.yml25
-rw-r--r--config/metrics/counts_7d/20210427103328_code_review_group_monthly_active_users.yml1
-rw-r--r--config/metrics/counts_7d/20210427103407_code_review_category_monthly_active_users.yml1
-rw-r--r--config/metrics/counts_7d/20230809194743_i_code_review_saved_replies_use_in_mr_weekly.yml25
-rw-r--r--config/metrics/counts_all/20230809194308_i_code_review_saved_replies_use_in_mr.yml25
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--doc/development/database_review.md5
-rw-r--r--doc/development/fe_guide/accessibility.md29
-rw-r--r--doc/topics/autodevops/cloud_deployments/auto_devops_with_eks.md3
-rw-r--r--doc/topics/autodevops/cloud_deployments/auto_devops_with_gke.md3
-rw-r--r--doc/topics/autodevops/index.md1
-rw-r--r--doc/topics/autodevops/stages.md17
-rw-r--r--doc/update/versions/gitlab_16_changes.md12
-rw-r--r--doc/user/ai_features.md20
-rw-r--r--doc/user/application_security/index.md1
-rw-r--r--doc/user/compliance/license_compliance/index.md4
-rw-r--r--lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml2
-rw-r--r--locale/gitlab.pot8
-rw-r--r--package.json2
-rw-r--r--qa/lib/gitlab/page/admin/dashboard.rb1
-rw-r--r--qa/lib/gitlab/page/admin/dashboard.stub.rb24
-rw-r--r--qa/lib/gitlab/page/admin/subscription.rb12
-rw-r--r--qa/lib/gitlab/page/admin/subscription.stub.rb186
-rw-r--r--spec/features/issues/user_toggles_subscription_spec.rb4
-rw-r--r--spec/finders/labels_finder_spec.rb22
-rw-r--r--spec/frontend/alerts_settings/components/alerts_integrations_list_spec.js6
-rw-r--r--spec/frontend/artifacts_settings/components/__snapshots__/keep_latest_artifact_checkbox_spec.js.snap76
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js61
-rw-r--r--spec/frontend/ci/pipeline_schedules/mock_data.js13
-rw-r--r--spec/frontend/comment_templates/components/__snapshots__/list_item_spec.js.snap1
-rw-r--r--spec/frontend/notifications/components/notifications_dropdown_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap1
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap1
-rw-r--r--spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js71
-rw-r--r--spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js8
-rw-r--r--spec/models/label_spec.rb79
-rw-r--r--spec/services/issues/update_service_spec.rb1
-rw-r--r--spec/services/labels/available_labels_service_spec.rb69
-rw-r--r--spec/services/labels/create_service_spec.rb36
-rw-r--r--spec/services/labels/update_service_spec.rb36
-rw-r--r--spec/services/merge_requests/update_service_spec.rb29
-rw-r--r--spec/support/helpers/features/runners_helpers.rb8
-rw-r--r--spec/support/shared_examples/features/work_items_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb28
-rw-r--r--spec/support/shared_examples/services/issuable/issuable_update_service_shared_examples.rb11
-rw-r--r--yarn.lock9
70 files changed, 1230 insertions, 709 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 67d80586a98..2ecbd063e8e 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-050416be5af659e666f9c174b00f65b24c5d3ed8
+e8bd279636709887e70420ef64276022f25724f0
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue b/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue
index 3860831169e..38b861a430a 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue
@@ -36,9 +36,6 @@ export const i18n = {
},
};
-const bodyTrClass =
- 'gl-border-1 gl-border-t-solid gl-border-b-solid gl-border-gray-100 gl-hover-cursor-pointer gl-hover-bg-blue-50 gl-hover-border-blue-200';
-
export default {
i18n,
typeSet,
@@ -85,20 +82,23 @@ export default {
{
key: 'active',
label: __('Status'),
+ tdClass: 'gl-vertical-align-middle!',
},
{
key: 'name',
label: s__('AlertsIntegrations|Integration Name'),
+ tdClass: 'gl-vertical-align-middle!',
},
{
key: 'type',
label: __('Type'),
+ tdClass: 'gl-vertical-align-middle!',
formatter: (value) => (value === typeSet.prometheus ? capitalize(value) : value),
},
{
key: 'actions',
- thClass: `gl-text-center`,
- tdClass: `gl-text-center`,
+ thClass: 'gl-text-right',
+ tdClass: 'gl-text-right gl-vertical-align-middle!',
label: __('Actions'),
},
],
@@ -127,12 +127,6 @@ export default {
this.observer.observe(this.$el);
},
methods: {
- tbodyTrClass(item) {
- return {
- [bodyTrClass]: this.integrations?.length,
- 'gl-bg-blue-50': (item !== null && item.id) === this.currentIntegration?.id,
- };
- },
trackPageViews() {
const { category, action } = trackAlertIntegrationsViewsOptions;
Tracking.event(category, action);
@@ -160,7 +154,6 @@ export default {
:fields="$options.fields"
:busy="loading"
stacked="md"
- :tbody-tr-class="tbodyTrClass"
show-empty
>
<template #cell(active)="{ item }">
@@ -187,7 +180,7 @@ export default {
</template>
<template #cell(actions)="{ item }">
- <gl-button-group class="gl-ml-3">
+ <gl-button-group class="gl-ml-3 gl-mt-n2 gl-mb-n2">
<gl-button
icon="settings"
:aria-label="$options.i18n.editIntegration"
@@ -204,17 +197,14 @@ export default {
</template>
<template #table-busy>
- <gl-loading-icon size="lg" color="dark" class="mt-3" />
+ <gl-loading-icon size="sm" />
</template>
<template #empty>
- <div
- class="gl-border-t-solid gl-border-b-solid gl-border-1 gl-border gl-border-gray-100 mt-n3 gl-px-5"
- >
- <p class="gl-text-gray-400 gl-py-3 gl-my-3">{{ $options.i18n.emptyState }}</p>
- </div>
+ <p class="gl-new-card-empty gl-text-center gl-mb-0">{{ $options.i18n.emptyState }}</p>
</template>
</gl-table>
+
<gl-modal
modal-id="deleteIntegration"
:title="$options.i18n.deleteIntegration"
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
index f831208d9f8..56740e436ca 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
@@ -387,306 +387,309 @@ export default {
</script>
<template>
- <gl-form class="gl-mt-6" @submit.prevent="submit" @reset.prevent="reset">
- <gl-tabs v-model="activeTabIndex">
- <gl-tab :title="$options.i18n.integrationTabs.configureDetails" class="gl-mt-3">
- <gl-form-group
- v-if="isCreating"
- id="integration-type"
- :label="
- getLabelWithStepNumber(
- $options.integrationSteps.selectType,
- $options.i18n.integrationFormSteps.selectType.label,
- )
- "
- label-for="integration-type"
- >
- <gl-form-select
- v-model="integrationForm.type"
- :disabled="isSelectDisabled"
- class="gl-max-w-full"
- data-qa-selector="integration_type_dropdown"
- :options="integrationTypesOptions"
- />
-
- <alert-settings-form-help-block
- v-if="!canAddIntegration"
- disabled="true"
- class="gl-display-inline-block gl-my-4"
- :message="$options.i18n.integrationFormSteps.selectType.enterprise"
- :link="pricingLink"
- data-testid="multi-integrations-not-supported"
- />
- </gl-form-group>
- <div class="gl-mt-3">
+ <div class="gl-new-card-add-form gl-py-0 gl-m-3">
+ <gl-form @submit.prevent="submit" @reset.prevent="reset">
+ <gl-tabs v-model="activeTabIndex">
+ <gl-tab :title="$options.i18n.integrationTabs.configureDetails" class="gl-mt-3">
<gl-form-group
- v-if="isHttp"
+ v-if="isCreating"
+ id="integration-type"
:label="
getLabelWithStepNumber(
- $options.integrationSteps.nameIntegration,
- $options.i18n.integrationFormSteps.nameIntegration.label,
+ $options.integrationSteps.selectType,
+ $options.i18n.integrationFormSteps.selectType.label,
)
"
- label-for="name-integration"
- :invalid-feedback="$options.i18n.integrationFormSteps.nameIntegration.error"
- :state="validationState.name"
+ label-for="integration-type"
>
- <gl-form-input
- id="name-integration"
- ref="integrationName"
- v-model="integrationForm.name"
- type="text"
- :placeholder="$options.i18n.integrationFormSteps.nameIntegration.placeholder"
- data-qa-selector="integration_name_field"
- @input="validateName"
+ <gl-form-select
+ v-model="integrationForm.type"
+ :disabled="isSelectDisabled"
+ class="gl-max-w-full"
+ data-qa-selector="integration_type_dropdown"
+ :options="integrationTypesOptions"
+ autofocus
/>
- </gl-form-group>
- <gl-form-group
- v-if="!isNone"
- :label="
- getLabelWithStepNumber(
- isHttp
- ? $options.integrationSteps.enableHttpIntegration
- : $options.integrationSteps.enablePrometheusIntegration,
- $options.i18n.integrationFormSteps.enableIntegration.label,
- )
- "
- >
- <span>{{ $options.i18n.integrationFormSteps.enableIntegration.help }}</span>
-
- <gl-toggle
- id="enable-integration"
- v-model="integrationForm.active"
- :is-loading="loading"
- :label="$options.i18n.integrationFormSteps.nameIntegration.activeToggle"
- data-qa-selector="active_toggle_container"
- class="gl-mt-4 gl-font-weight-normal"
+ <alert-settings-form-help-block
+ v-if="!canAddIntegration"
+ disabled="true"
+ class="gl-display-inline-block gl-my-4"
+ :message="$options.i18n.integrationFormSteps.selectType.enterprise"
+ :link="pricingLink"
+ data-testid="multi-integrations-not-supported"
/>
</gl-form-group>
- <template v-if="showMappingBuilder">
+ <div class="gl-mt-3">
<gl-form-group
- data-testid="sample-payload-section"
+ v-if="isHttp"
:label="
getLabelWithStepNumber(
- $options.integrationSteps.customizeMapping,
- $options.i18n.integrationFormSteps.mapFields.label,
+ $options.integrationSteps.nameIntegration,
+ $options.i18n.integrationFormSteps.nameIntegration.label,
)
"
- label-for="sample-payload"
- class="gl-mb-0!"
- :invalid-feedback="samplePayload.error"
+ label-for="name-integration"
+ :invalid-feedback="$options.i18n.integrationFormSteps.nameIntegration.error"
+ :state="validationState.name"
>
- <span>{{ $options.i18n.integrationFormSteps.mapFields.help }}</span>
-
- <gl-form-textarea
- id="sample-payload"
- v-model="samplePayload.json"
- :disabled="canEditPayload"
- :state="isSampePayloadValid"
- :placeholder="$options.i18n.integrationFormSteps.mapFields.placeholder"
- class="gl-my-3"
- :debounce="$options.JSON_VALIDATE_DELAY"
- rows="6"
- max-rows="10"
- @input="validateJson"
+ <gl-form-input
+ id="name-integration"
+ ref="integrationName"
+ v-model="integrationForm.name"
+ type="text"
+ :placeholder="$options.i18n.integrationFormSteps.nameIntegration.placeholder"
+ data-qa-selector="integration_name_field"
+ @input="validateName"
/>
</gl-form-group>
+ <gl-form-group
+ v-if="!isNone"
+ :label="
+ getLabelWithStepNumber(
+ isHttp
+ ? $options.integrationSteps.enableHttpIntegration
+ : $options.integrationSteps.enablePrometheusIntegration,
+ $options.i18n.integrationFormSteps.enableIntegration.label,
+ )
+ "
+ >
+ <span>{{ $options.i18n.integrationFormSteps.enableIntegration.help }}</span>
+
+ <gl-toggle
+ id="enable-integration"
+ v-model="integrationForm.active"
+ :is-loading="loading"
+ :label="$options.i18n.integrationFormSteps.nameIntegration.activeToggle"
+ data-qa-selector="active_toggle_container"
+ class="gl-mt-4 gl-font-weight-normal"
+ />
+ </gl-form-group>
+ <template v-if="showMappingBuilder">
+ <gl-form-group
+ data-testid="sample-payload-section"
+ :label="
+ getLabelWithStepNumber(
+ $options.integrationSteps.customizeMapping,
+ $options.i18n.integrationFormSteps.mapFields.label,
+ )
+ "
+ label-for="sample-payload"
+ class="gl-mb-0!"
+ :invalid-feedback="samplePayload.error"
+ >
+ <span>{{ $options.i18n.integrationFormSteps.mapFields.help }}</span>
+
+ <gl-form-textarea
+ id="sample-payload"
+ v-model="samplePayload.json"
+ :disabled="canEditPayload"
+ :state="isSampePayloadValid"
+ :placeholder="$options.i18n.integrationFormSteps.mapFields.placeholder"
+ class="gl-my-3"
+ :debounce="$options.JSON_VALIDATE_DELAY"
+ rows="6"
+ max-rows="10"
+ @input="validateJson"
+ />
+ </gl-form-group>
+
+ <gl-button
+ v-if="canEditPayload"
+ v-gl-modal.resetPayloadModal
+ data-testid="payload-action-btn"
+ :disabled="!integrationForm.active"
+ class="gl-mt-3"
+ >
+ {{ $options.i18n.integrationFormSteps.mapFields.editPayload }}
+ </gl-button>
+
+ <gl-button
+ v-else
+ data-testid="payload-action-btn"
+ :class="{ 'gl-mt-3': samplePayload.error }"
+ :disabled="!canParseSamplePayload"
+ :loading="samplePayload.loading"
+ @click="parseSamplePayload"
+ >
+ {{ $options.i18n.integrationFormSteps.mapFields.parsePayload }}
+ </gl-button>
+ <gl-modal
+ modal-id="resetPayloadModal"
+ :title="$options.i18n.integrationFormSteps.mapFields.resetHeader"
+ :ok-title="$options.i18n.integrationFormSteps.mapFields.resetOk"
+ ok-variant="danger"
+ @ok="resetPayloadAndMappingConfirmed = true"
+ >
+ {{ $options.i18n.integrationFormSteps.mapFields.resetBody }}
+ </gl-modal>
+
+ <div class="gl-mt-5">
+ <span>{{ $options.i18n.integrationFormSteps.mapFields.mapIntro }}</span>
+ <mapping-builder
+ :parsed-payload="parsedPayload"
+ :saved-mapping="mapping"
+ :alert-fields="alertFields"
+ @onMappingUpdate="updateMapping"
+ />
+ </div>
+ </template>
+ </div>
+ <div class="gl-display-flex gl-justify-content-start gl-py-3">
<gl-button
- v-if="canEditPayload"
- v-gl-modal.resetPayloadModal
- data-testid="payload-action-btn"
- :disabled="!integrationForm.active"
- class="gl-mt-3"
+ :disabled="!canSubmitForm"
+ variant="confirm"
+ class="js-no-auto-disable"
+ data-testid="integration-form-submit"
+ @click="submit(false)"
>
- {{ $options.i18n.integrationFormSteps.mapFields.editPayload }}
+ {{ $options.i18n.saveIntegration }}
</gl-button>
<gl-button
- v-else
- data-testid="payload-action-btn"
- :class="{ 'gl-mt-3': samplePayload.error }"
- :disabled="!canParseSamplePayload"
- :loading="samplePayload.loading"
- @click="parseSamplePayload"
+ :disabled="!canSubmitForm"
+ variant="confirm"
+ category="secondary"
+ class="gl-ml-3 js-no-auto-disable"
+ data-qa-selector="save_and_create_alert_button"
+ @click="submit(true)"
>
- {{ $options.i18n.integrationFormSteps.mapFields.parsePayload }}
+ {{ $options.i18n.saveAndTestIntegration }}
</gl-button>
- <gl-modal
- modal-id="resetPayloadModal"
- :title="$options.i18n.integrationFormSteps.mapFields.resetHeader"
- :ok-title="$options.i18n.integrationFormSteps.mapFields.resetOk"
- ok-variant="danger"
- @ok="resetPayloadAndMappingConfirmed = true"
- >
- {{ $options.i18n.integrationFormSteps.mapFields.resetBody }}
- </gl-modal>
-
- <div class="gl-mt-5">
- <span>{{ $options.i18n.integrationFormSteps.mapFields.mapIntro }}</span>
- <mapping-builder
- :parsed-payload="parsedPayload"
- :saved-mapping="mapping"
- :alert-fields="alertFields"
- @onMappingUpdate="updateMapping"
- />
- </div>
- </template>
- </div>
- <div class="gl-display-flex gl-justify-content-start gl-py-3">
- <gl-button
- :disabled="!canSubmitForm"
- variant="confirm"
- class="js-no-auto-disable"
- data-testid="integration-form-submit"
- @click="submit(false)"
- >
- {{ $options.i18n.saveIntegration }}
- </gl-button>
-
- <gl-button
- :disabled="!canSubmitForm"
- variant="confirm"
- category="secondary"
- class="gl-ml-3 js-no-auto-disable"
- data-qa-selector="save_and_create_alert_button"
- @click="submit(true)"
- >
- {{ $options.i18n.saveAndTestIntegration }}
- </gl-button>
-
- <gl-button type="reset" class="gl-ml-3 js-no-auto-disable">{{
- $options.i18n.cancelAndClose
- }}</gl-button>
- </div>
- </gl-tab>
-
- <gl-tab
- :title="$options.i18n.integrationTabs.viewCredentials"
- :disabled="isCreating"
- class="gl-mt-3"
- >
- <alert-settings-form-help-block
- :message="viewCredentialsHelpMsg"
- :link="$options.incidentManagementDocsLink"
- />
-
- <gl-form-group id="integration-webhook">
- <div class="gl-my-4">
- <span class="gl-font-weight-bold">
- {{ $options.i18n.integrationFormSteps.setupCredentials.webhookUrl }}
- </span>
-
- <gl-form-input-group id="url" readonly :value="integrationForm.url">
- <template #append>
- <clipboard-button
- :text="integrationForm.url || ''"
- :title="$options.i18n.copy"
- class="gl-m-0!"
- />
- </template>
- </gl-form-input-group>
- </div>
-
- <div class="gl-my-4">
- <span class="gl-font-weight-bold">
- {{ $options.i18n.integrationFormSteps.setupCredentials.authorizationKey }}
- </span>
- <gl-form-input-group
- id="authorization-key"
- class="gl-mb-3"
- readonly
- :value="integrationForm.token"
- >
- <template #append>
- <clipboard-button
- :text="integrationForm.token || ''"
- :title="$options.i18n.copy"
- class="gl-m-0!"
- />
- </template>
- </gl-form-input-group>
+ <gl-button type="reset" class="gl-ml-3 js-no-auto-disable">{{
+ $options.i18n.cancelAndClose
+ }}</gl-button>
</div>
- </gl-form-group>
-
- <div class="gl-display-flex gl-justify-content-start gl-py-3">
- <gl-button v-gl-modal.authKeyModal variant="danger">
- {{ $options.i18n.integrationFormSteps.setupCredentials.reset }}
- </gl-button>
-
- <gl-button type="reset" class="gl-ml-3 js-no-auto-disable">
- {{ $options.i18n.cancelAndClose }}
- </gl-button>
- </div>
-
- <gl-modal
- modal-id="authKeyModal"
- :title="$options.i18n.integrationFormSteps.setupCredentials.reset"
- :ok-title="$options.i18n.integrationFormSteps.setupCredentials.reset"
- ok-variant="danger"
- @ok="resetAuthKey"
+ </gl-tab>
+
+ <gl-tab
+ :title="$options.i18n.integrationTabs.viewCredentials"
+ :disabled="isCreating"
+ class="gl-mt-3"
>
- {{ $options.i18n.integrationFormSteps.restKeyInfo.label }}
- </gl-modal>
- </gl-tab>
-
- <gl-tab
- :title="$options.i18n.integrationTabs.sendTestAlert"
- :disabled="isCreating"
- class="gl-mt-3"
- >
- <gl-form-group id="test-integration" :invalid-feedback="testPayload.error">
<alert-settings-form-help-block
- :message="$options.i18n.integrationFormSteps.testPayload.help"
- :link="alertsUsageUrl"
+ :message="viewCredentialsHelpMsg"
+ :link="$options.incidentManagementDocsLink"
/>
- <gl-form-textarea
- id="test-payload"
- v-model="testPayload.json"
- :state="isTestPayloadValid"
- :placeholder="$options.i18n.integrationFormSteps.testPayload.placeholder"
- class="gl-my-3"
- :debounce="$options.JSON_VALIDATE_DELAY"
- rows="6"
- max-rows="10"
- data-qa-selector="test_payload_field"
- @input="validateJson(false)"
- />
- </gl-form-group>
- <div class="gl-display-flex gl-justify-content-start gl-py-3">
- <gl-button
- v-gl-modal="testAlertModal"
- :disabled="!isTestPayloadValid"
- :loading="loading"
- data-testid="send-test-alert"
- variant="confirm"
- class="js-no-auto-disable"
- data-qa-selector="send_test_alert_button"
- @click="isFormDirty ? null : sendTestAlert()"
+ <gl-form-group id="integration-webhook">
+ <div class="gl-my-4">
+ <span class="gl-font-weight-bold">
+ {{ $options.i18n.integrationFormSteps.setupCredentials.webhookUrl }}
+ </span>
+
+ <gl-form-input-group id="url" readonly :value="integrationForm.url">
+ <template #append>
+ <clipboard-button
+ :text="integrationForm.url || ''"
+ :title="$options.i18n.copy"
+ class="gl-m-0!"
+ />
+ </template>
+ </gl-form-input-group>
+ </div>
+
+ <div class="gl-my-4">
+ <span class="gl-font-weight-bold">
+ {{ $options.i18n.integrationFormSteps.setupCredentials.authorizationKey }}
+ </span>
+
+ <gl-form-input-group
+ id="authorization-key"
+ class="gl-mb-3"
+ readonly
+ :value="integrationForm.token"
+ >
+ <template #append>
+ <clipboard-button
+ :text="integrationForm.token || ''"
+ :title="$options.i18n.copy"
+ class="gl-m-0!"
+ />
+ </template>
+ </gl-form-input-group>
+ </div>
+ </gl-form-group>
+
+ <div class="gl-display-flex gl-justify-content-start gl-py-3">
+ <gl-button v-gl-modal.authKeyModal variant="danger">
+ {{ $options.i18n.integrationFormSteps.setupCredentials.reset }}
+ </gl-button>
+
+ <gl-button type="reset" class="gl-ml-3 js-no-auto-disable">
+ {{ $options.i18n.cancelAndClose }}
+ </gl-button>
+ </div>
+
+ <gl-modal
+ modal-id="authKeyModal"
+ :title="$options.i18n.integrationFormSteps.setupCredentials.reset"
+ :ok-title="$options.i18n.integrationFormSteps.setupCredentials.reset"
+ ok-variant="danger"
+ @ok="resetAuthKey"
>
- {{ $options.i18n.send }}
- </gl-button>
-
- <gl-button type="reset" class="gl-ml-3 js-no-auto-disable">
- {{ $options.i18n.cancelAndClose }}
- </gl-button>
- </div>
-
- <gl-modal
- :modal-id="$options.testAlertModalId"
- :title="$options.i18n.integrationFormSteps.testPayload.modalTitle"
- :action-primary="$options.primaryProps"
- :action-secondary="$options.secondaryProps"
- :action-cancel="$options.cancelProps"
- @primary="saveAndSendTestAlert"
- @secondary="sendTestAlert"
+ {{ $options.i18n.integrationFormSteps.restKeyInfo.label }}
+ </gl-modal>
+ </gl-tab>
+
+ <gl-tab
+ :title="$options.i18n.integrationTabs.sendTestAlert"
+ :disabled="isCreating"
+ class="gl-mt-3"
>
- {{ $options.i18n.integrationFormSteps.testPayload.modalBody }}
- </gl-modal>
- </gl-tab>
- </gl-tabs>
- </gl-form>
+ <gl-form-group id="test-integration" :invalid-feedback="testPayload.error">
+ <alert-settings-form-help-block
+ :message="$options.i18n.integrationFormSteps.testPayload.help"
+ :link="alertsUsageUrl"
+ />
+
+ <gl-form-textarea
+ id="test-payload"
+ v-model="testPayload.json"
+ :state="isTestPayloadValid"
+ :placeholder="$options.i18n.integrationFormSteps.testPayload.placeholder"
+ class="gl-my-3"
+ :debounce="$options.JSON_VALIDATE_DELAY"
+ rows="6"
+ max-rows="10"
+ data-qa-selector="test_payload_field"
+ @input="validateJson(false)"
+ />
+ </gl-form-group>
+ <div class="gl-display-flex gl-justify-content-start gl-py-3">
+ <gl-button
+ v-gl-modal="testAlertModal"
+ :disabled="!isTestPayloadValid"
+ :loading="loading"
+ data-testid="send-test-alert"
+ variant="confirm"
+ class="js-no-auto-disable"
+ data-qa-selector="send_test_alert_button"
+ @click="isFormDirty ? null : sendTestAlert()"
+ >
+ {{ $options.i18n.send }}
+ </gl-button>
+
+ <gl-button type="reset" class="gl-ml-3 js-no-auto-disable">
+ {{ $options.i18n.cancelAndClose }}
+ </gl-button>
+ </div>
+
+ <gl-modal
+ :modal-id="$options.testAlertModalId"
+ :title="$options.i18n.integrationFormSteps.testPayload.modalTitle"
+ :action-primary="$options.primaryProps"
+ :action-secondary="$options.secondaryProps"
+ :action-cancel="$options.cancelProps"
+ @primary="saveAndSendTestAlert"
+ @secondary="sendTestAlert"
+ >
+ {{ $options.i18n.integrationFormSteps.testPayload.modalBody }}
+ </gl-modal>
+ </gl-tab>
+ </gl-tabs>
+ </gl-form>
+ </div>
</template>
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
index bc9fe9543bd..e4fc37f9760 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton, GlAlert, GlTabs, GlTab } from '@gitlab/ui';
+import { GlAlert, GlButton, GlCard, GlTabs, GlTab, GlIcon } from '@gitlab/ui';
import createHttpIntegrationMutation from 'ee_else_ce/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql';
import updateHttpIntegrationMutation from 'ee_else_ce/alerts_settings/graphql/mutations/update_http_integration.mutation.graphql';
import { createAlert, VARIANT_SUCCESS } from '~/alert';
@@ -43,8 +43,10 @@ export default {
AlertSettingsForm,
GlAlert,
GlButton,
+ GlCard,
GlTabs,
GlTab,
+ GlIcon,
},
inject: {
projectPath: {
@@ -364,36 +366,55 @@ export default {
{{ $options.i18n.integrationCreated.successMsg }}
</gl-alert>
- <integrations-list
- :integrations="integrations"
- :loading="loading"
- @edit-integration="editIntegration"
- @delete-integration="deleteIntegration"
- />
- <gl-button
- v-if="canAddIntegration && !formVisible"
- category="secondary"
- variant="confirm"
- data-testid="add-integration-btn"
- data-qa-selector="add_integration_button"
- class="gl-mt-3"
- @click="setFormVisibility(true)"
+ <gl-card
+ class="gl-new-card gl-mt-2"
+ header-class="gl-new-card-header"
+ body-class="gl-new-card-body gl-px-0 gl-overflow-hidden"
>
- {{ $options.i18n.addNewIntegration }}
- </gl-button>
- <alert-settings-form
- v-if="formVisible"
- :loading="isUpdating"
- :can-add-integration="canAddIntegration"
- :alert-fields="alertFields"
- :tab-index="tabIndex"
- @create-new-integration="createNewIntegration"
- @update-integration="updateIntegration"
- @reset-token="resetToken"
- @clear-current-integration="clearCurrentIntegration"
- @test-alert-payload="testAlertPayload"
- @save-and-test-alert-payload="saveAndTestAlertPayload"
- />
+ <template #header>
+ <div class="gl-new-card-title-wrapper">
+ <h5 class="gl-new-card-title">
+ {{ $options.i18n.card.title }}
+ <span class="gl-new-card-count">
+ <gl-icon name="warning" class="gl-mr-2" />
+ {{ integrations.length }}
+ </span>
+ </h5>
+ </div>
+ <div class="gl-new-card-actions">
+ <gl-button
+ v-if="canAddIntegration && !formVisible"
+ size="small"
+ data-testid="add-integration-btn"
+ data-qa-selector="add_integration_button"
+ @click="setFormVisibility(true)"
+ >
+ {{ $options.i18n.addNewIntegration }}
+ </gl-button>
+ </div>
+ </template>
+
+ <alert-settings-form
+ v-if="formVisible"
+ :loading="isUpdating"
+ :can-add-integration="canAddIntegration"
+ :alert-fields="alertFields"
+ :tab-index="tabIndex"
+ @create-new-integration="createNewIntegration"
+ @update-integration="updateIntegration"
+ @reset-token="resetToken"
+ @clear-current-integration="clearCurrentIntegration"
+ @test-alert-payload="testAlertPayload"
+ @save-and-test-alert-payload="saveAndTestAlertPayload"
+ />
+
+ <integrations-list
+ :integrations="integrations"
+ :loading="loading"
+ @edit-integration="editIntegration"
+ @delete-integration="deleteIntegration"
+ />
+ </gl-card>
</gl-tab>
<gl-tab :title="$options.i18n.settingsTabs.integrationSettings">
<alerts-form class="gl-pt-3" />
diff --git a/app/assets/javascripts/alerts_settings/constants.js b/app/assets/javascripts/alerts_settings/constants.js
index 218b09cb1b6..a5f18fda542 100644
--- a/app/assets/javascripts/alerts_settings/constants.js
+++ b/app/assets/javascripts/alerts_settings/constants.js
@@ -1,6 +1,9 @@
import { s__, __ } from '~/locale';
export const i18n = {
+ card: {
+ title: s__('AlertSettings|Active alerts'),
+ },
integrationTabs: {
configureDetails: s__('AlertSettings|Configure details'),
viewCredentials: s__('AlertSettings|View credentials'),
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue
index d84a9a4a4b5..396ff9808f2 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue
@@ -112,6 +112,7 @@ export default {
cronTimezone: '',
variables: [],
schedule: {},
+ showVarValues: false,
};
},
i18n: {
@@ -140,6 +141,8 @@ export default {
scheduleFetchError: s__(
'PipelineSchedules|An error occurred while trying to fetch the pipeline schedule.',
),
+ revealText: __('Reveal values'),
+ hideText: __('Hide values'),
},
typeOptions: {
[VARIABLE_TYPE]: __('Variable'),
@@ -167,11 +170,24 @@ export default {
getEnabledRefTypes() {
return [REF_TYPE_BRANCHES, REF_TYPE_TAGS];
},
+ filledVariables() {
+ return this.variables.filter((variable) => variable.key !== '' && !variable.empty);
+ },
preparedVariablesUpdate() {
- return this.variables.filter((variable) => variable.key !== '');
+ return this.filledVariables.map((variable) => {
+ return {
+ id: variable.id,
+ key: variable.key,
+ value: variable.value,
+ variableType: variable.variableType,
+ destroy: variable.destroy,
+ };
+ });
},
preparedVariablesCreate() {
- return this.preparedVariablesUpdate.map((variable) => {
+ const vars = this.variables.filter((variable) => variable.key !== '');
+
+ return vars.map((variable) => {
return {
key: variable.key,
value: variable.value,
@@ -187,6 +203,15 @@ export default {
? this.$options.i18n.editScheduleBtnText
: this.$options.i18n.createScheduleBtnText;
},
+ varSecurityBtnText() {
+ return this.showVarValues ? this.$options.i18n.hideText : this.$options.i18n.revealText;
+ },
+ hasExistingScheduleVariables() {
+ return this.schedule?.variables?.nodes.length > 0;
+ },
+ showVarSecurityBtn() {
+ return this.editing && this.hasExistingScheduleVariables;
+ },
},
created() {
this.addEmptyVariable();
@@ -204,6 +229,7 @@ export default {
key: '',
value: '',
destroy: false,
+ empty: true,
});
},
setVariableAttribute(key, attribute, value) {
@@ -289,6 +315,14 @@ export default {
setTimezone(timezone) {
this.cronTimezone = timezone.identifier || '';
},
+ displayHiddenChars(variable) {
+ return (
+ this.editing && this.hasExistingScheduleVariables && !this.showVarValues && !variable.empty
+ );
+ },
+ resetVariable(index) {
+ this.variables[index].empty = false;
+ },
},
};
</script>
@@ -342,7 +376,7 @@ export default {
/>
</gl-form-group>
<!--Variable List-->
- <gl-form-group :label="$options.i18n.variables">
+ <gl-form-group class="gl-mb-2" :label="$options.i18n.variables">
<div
v-for="(variable, index) in variables"
:key="`var-${index}`"
@@ -372,10 +406,21 @@ export default {
:class="$options.formElementClasses"
data-testid="pipeline-form-ci-variable-key"
data-qa-selector="ci_variable_key_field"
- @change="addEmptyVariable()"
+ @change="addEmptyVariable(variable)"
/>
<gl-form-textarea
+ v-if="displayHiddenChars(variable)"
+ value="*****************"
+ disabled
+ class="gl-mb-3 gl-h-7!"
+ :style="$options.textAreaStyle"
+ :no-resize="false"
+ data-testid="pipeline-form-ci-variable-hidden-value"
+ />
+
+ <gl-form-textarea
+ v-else
v-model="variable.value"
:placeholder="s__('CiVariables|Input variable value')"
class="gl-mb-3 gl-h-7!"
@@ -383,6 +428,7 @@ export default {
:no-resize="false"
data-testid="pipeline-form-ci-variable-value"
data-qa-selector="ci_variable_value_field"
+ @change="resetVariable(index)"
/>
<template v-if="variables.length > 1">
@@ -406,6 +452,18 @@ export default {
</div>
</div>
</gl-form-group>
+
+ <gl-button
+ v-if="showVarSecurityBtn"
+ class="gl-mb-5"
+ category="secondary"
+ variant="confirm"
+ data-testid="variable-security-btn"
+ @click="showVarValues = !showVarValues"
+ >
+ {{ varSecurityBtnText }}
+ </gl-button>
+
<!--Activated-->
<gl-form-checkbox id="schedule-active" v-model="activated" class="gl-mb-3">
{{ $options.i18n.activated }}
diff --git a/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue b/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue
index abfb92f772d..b1c6f5e6056 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue
@@ -1,8 +1,10 @@
<script>
import { GlCollapsibleListbox, GlTooltip, GlButton } from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import { getDerivedMergeRequestInformation } from '~/diffs/utils/merge_request';
import { InternalEvents } from '~/tracking';
import savedRepliesQuery from './saved_replies.query.graphql';
+import { TRACKING_SAVED_REPLIES_USE, TRACKING_SAVED_REPLIES_USE_IN_MR } from './constants';
export default {
apollo: {
@@ -54,10 +56,14 @@ export default {
this.commentTemplateSearch = search;
},
onSelect(id) {
+ const isInMr = Boolean(getDerivedMergeRequestInformation({ endpoint: window.location }).id);
const savedReply = this.savedReplies.find((r) => r.id === id);
if (savedReply) {
this.$emit('select', savedReply.content);
- this.track_event('i_code_review_saved_replies_use');
+ this.track_event(TRACKING_SAVED_REPLIES_USE);
+ if (isInMr) {
+ this.track_event(TRACKING_SAVED_REPLIES_USE_IN_MR);
+ }
}
},
},
diff --git a/app/assets/javascripts/vue_shared/components/markdown/constants.js b/app/assets/javascripts/vue_shared/components/markdown/constants.js
new file mode 100644
index 00000000000..47ef7cccbc2
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/markdown/constants.js
@@ -0,0 +1,2 @@
+export const TRACKING_SAVED_REPLIES_USE = 'i_code_review_saved_replies_use';
+export const TRACKING_SAVED_REPLIES_USE_IN_MR = 'i_code_review_saved_replies_use_in_mr';
diff --git a/app/assets/stylesheets/page_bundles/incident_management_list.scss b/app/assets/stylesheets/page_bundles/incident_management_list.scss
index 30a75103c30..312d5c2b10c 100644
--- a/app/assets/stylesheets/page_bundles/incident_management_list.scss
+++ b/app/assets/stylesheets/page_bundles/incident_management_list.scss
@@ -5,120 +5,6 @@
background-color: var(--green-50, $green-50);
}
- // these styles need to be deleted once GlTable component looks in GitLab same as in @gitlab/ui
- table {
- color: var(--gray-500, $gray-500);
-
- tbody {
- tr:not(.b-table-busy-slot):not(.b-table-empty-row) {
- &:hover {
- @include gl-border-t-double;
-
- td {
- @include gl-border-b-initial;
- }
- }
- }
- }
-
- tr {
- &:focus {
- @include gl-outline-none;
- }
-
- td,
- th {
- @include gl-py-5;
- @include gl-outline-none;
- @include gl-relative;
- }
-
- th {
- @include gl-bg-transparent;
- @include gl-font-weight-bold;
- color: var(--gray-400, $gray-400);
-
-
- &[aria-sort='none']:hover {
- background-image: url('data:image/svg+xml, %3csvg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="4 0 8 16"%3e %3cpath style="fill: %23BABABA;" fill-rule="evenodd" d="M11.707085,11.7071 L7.999975,15.4142 L4.292875,11.7071 C3.902375,11.3166 3.902375, 10.6834 4.292875,10.2929 C4.683375,9.90237 5.316575,9.90237 5.707075,10.2929 L6.999975, 11.5858 L6.999975,2 C6.999975,1.44771 7.447695,1 7.999975,1 C8.552255,1 8.999975,1.44771 8.999975,2 L8.999975,11.5858 L10.292865,10.2929 C10.683395 ,9.90237 11.316555,9.90237 11.707085,10.2929 C12.097605,10.6834 12.097605,11.3166 11.707085,11.7071 Z"/%3e %3c/svg%3e');
- }
- }
- }
-
- @include media-breakpoint-up(md) {
- tr {
- &:last-child {
- td {
- @include gl-border-0;
- }
- }
- }
-
- .sortable-cell {
- padding-left: calc(0.75rem + 0.65em);
- }
- }
- }
-
- @include media-breakpoint-down(sm) {
- table {
- tr {
- @include gl-border-t-0;
-
- .table-col {
- min-height: 68px;
- }
-
- &:hover {
- background-color: var(--white, $white);
- @include gl-border-none;
- }
-
- th,
- td {
- @include gl-pt-6;
- }
- }
-
- &.alert-management-table {
- .table-col {
- &:last-child {
- background-color: var(--gray-10, $gray-10);
-
- &::before {
- content: none !important;
- }
-
- div:not(.dropdown-title) {
- width: 100% !important;
- padding: 0 !important;
- }
- }
- }
- }
-
- .b-table-empty-row {
- td {
- @include gl-border-b-0;
-
- div {
- text-align: unset !important;
- }
- }
- }
-
- .b-table-busy-slot {
- td {
- @include gl-border-b-0;
-
- div {
- text-align: center !important;
- }
- }
- }
- }
- }
-
.gl-tabs-nav {
@include gl-border-b-0;
@@ -142,12 +28,4 @@
@include gl-w-full;
}
}
-
- .integration-list {
- .b-table-empty-row {
- td {
- @include gl-px-0;
- }
- }
- }
}
diff --git a/app/controllers/projects/ci/pipeline_editor_controller.rb b/app/controllers/projects/ci/pipeline_editor_controller.rb
index 8499bf0ced7..6e7f764c5c1 100644
--- a/app/controllers/projects/ci/pipeline_editor_controller.rb
+++ b/app/controllers/projects/ci/pipeline_editor_controller.rb
@@ -21,3 +21,5 @@ class Projects::Ci::PipelineEditorController < Projects::ApplicationController
render_404 unless can_collaborate_with_project?(@project)
end
end
+
+Projects::Ci::PipelineEditorController.prepend_mod
diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb
index b1387f2a104..1bf2e3b71e4 100644
--- a/app/finders/labels_finder.rb
+++ b/app/finders/labels_finder.rb
@@ -24,6 +24,7 @@ class LabelsFinder < UnionFinder
items = with_title(items)
items = by_subscription(items)
items = by_search(items)
+ items = by_locked_labels(items)
items = items.with_preloaded_container if @preload_parent_association
sort(items)
@@ -94,6 +95,12 @@ class LabelsFinder < UnionFinder
labels.optionally_subscribed_by(subscriber_id)
end
+ def by_locked_labels(items)
+ return items unless params[:locked_labels]
+
+ items.with_lock_on_merge
+ end
+
def subscriber_id
current_user&.id if subscribed?
end
diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb
index e8fa65ee119..25d68d47228 100644
--- a/app/mailers/emails/profile.rb
+++ b/app/mailers/emails/profile.rb
@@ -65,7 +65,7 @@ module Emails
@target_url = profile_personal_access_tokens_url
@token_name = token_name
- mail_with_locale(to: @user.notification_email_or_default, subject: subject(_("A new personal access token has been created")))
+ email_with_layout(to: @user.notification_email_or_default, subject: subject(_("A new personal access token has been created")))
end
def access_token_about_to_expire_email(user, token_names)
diff --git a/app/models/label.rb b/app/models/label.rb
index 0831ba40536..9c37b165ebf 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -25,8 +25,10 @@ class Label < ApplicationRecord
has_many :merge_requests, through: :label_links, source: :target, source_type: 'MergeRequest'
before_validation :strip_whitespace_from_title
+ before_destroy :prevent_locked_label_destroy, prepend: true
validates :color, color: true, presence: true
+ validate :ensure_lock_on_merge_allowed
# Don't allow ',' for label titles
validates :title, presence: true, format: { with: /\A[^,]+\z/ }
@@ -42,6 +44,7 @@ class Label < ApplicationRecord
scope :templates, -> { where(template: true, type: [Label.name, nil]) }
scope :with_title, ->(title) { where(title: title) }
scope :with_lists_and_board, -> { joins(lists: :board).merge(List.movable) }
+ scope :with_lock_on_merge, -> { where(lock_on_merge: true) }
scope :on_project_boards, ->(project_id) { with_lists_and_board.where(boards: { project_id: project_id }) }
scope :on_board, ->(board_id) { with_lists_and_board.where(boards: { id: board_id }) }
scope :order_name_asc, -> { reorder(title: :asc) }
@@ -319,6 +322,19 @@ class Label < ApplicationRecord
def strip_whitespace_from_title
self[:title] = title&.strip
end
+
+ def prevent_locked_label_destroy
+ return unless lock_on_merge
+
+ throw :abort # rubocop:disable Cop/BanCatchThrow
+ end
+
+ def ensure_lock_on_merge_allowed
+ return unless template?
+ return unless lock_on_merge || will_save_change_to_lock_on_merge?
+
+ errors.add(:lock_on_merge, _('can not be set for template labels'))
+ end
end
Label.prepend_mod_with('Label')
diff --git a/app/serializers/merge_request_serializer.rb b/app/serializers/merge_request_serializer.rb
index 9fd50c8c51d..6f83978841d 100644
--- a/app/serializers/merge_request_serializer.rb
+++ b/app/serializers/merge_request_serializer.rb
@@ -27,3 +27,5 @@ class MergeRequestSerializer < BaseSerializer
super(merge_request, opts, entity)
end
end
+
+MergeRequestSerializer.prepend_mod
diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb
index 3827d199325..eaee5ce70fc 100644
--- a/app/services/auth/container_registry_authentication_service.rb
+++ b/app/services/auth/container_registry_authentication_service.rb
@@ -112,6 +112,28 @@ module Auth
token.expire_time = self.class.token_expire_at
token[:auth_type] = params[:auth_type]
token[:access] = accesses.compact
+ token[:user] = user_info_token.encoded
+ end
+ end
+
+ def user_info_token
+ info =
+ if current_user
+ {
+ token_type: params[:auth_type],
+ username: current_user.username,
+ user_id: current_user.id
+ }
+ elsif deploy_token
+ {
+ token_type: params[:auth_type],
+ username: deploy_token.username,
+ deploy_token_id: deploy_token.id
+ }
+ end
+
+ JSONWebToken::RSAToken.new(registry.key).tap do |token|
+ token[:user_info] = info
end
end
diff --git a/app/services/labels/available_labels_service.rb b/app/services/labels/available_labels_service.rb
index ff29358df86..21f92eeaf09 100644
--- a/app/services/labels/available_labels_service.rb
+++ b/app/services/labels/available_labels_service.rb
@@ -40,6 +40,21 @@ module Labels
ids.map(&:to_i) & existing_ids
end
+ def filter_locked_labels_ids_in_param(key)
+ ids = Array.wrap(params[key])
+ return [] if ids.empty?
+
+ params = finder_params
+ params[:locked_labels] = true
+ existing_labels = LabelsFinder.new(current_user, params).execute
+
+ # rubocop:disable CodeReuse/ActiveRecord
+ existing_ids = existing_labels.id_in(ids).pluck(:id)
+ # rubocop:enable CodeReuse/ActiveRecord
+
+ ids.map(&:to_i) & existing_ids
+ end
+
def available_labels
@available_labels ||= LabelsFinder.new(current_user, finder_params).execute
end
diff --git a/app/services/labels/create_service.rb b/app/services/labels/create_service.rb
index 6c070d15cdb..675439b2f64 100644
--- a/app/services/labels/create_service.rb
+++ b/app/services/labels/create_service.rb
@@ -13,6 +13,10 @@ module Labels
project_or_group = target_params[:project] || target_params[:group]
if project_or_group.present?
+ if Feature.disabled?(:enforce_locked_labels_on_merge, project_or_group, type: :ops)
+ params.delete(:lock_on_merge)
+ end
+
project_or_group.labels.create(params)
elsif target_params[:template]
label = Label.new(params)
diff --git a/app/services/labels/update_service.rb b/app/services/labels/update_service.rb
index be33947d0eb..4ac54959e84 100644
--- a/app/services/labels/update_service.rb
+++ b/app/services/labels/update_service.rb
@@ -10,9 +10,19 @@ module Labels
def execute(label)
params[:name] = params.delete(:new_name) if params.key?(:new_name)
params[:color] = convert_color_name_to_hex if params[:color].present?
+ params.delete(:lock_on_merge) unless allow_lock_on_merge?(label)
label.update(params)
label
end
+
+ private
+
+ def allow_lock_on_merge?(label)
+ return if label.template?
+ return unless label.respond_to?(:parent_container)
+
+ Feature.enabled?(:enforce_locked_labels_on_merge, label.parent_container, type: :ops)
+ end
end
end
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index 7074bf5bd29..0fc85675e49 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -176,10 +176,21 @@ module MergeRequests
params.delete(:allow_collaboration)
end
+ filter_locked_labels(merge_request)
filter_reviewer(merge_request)
filter_suggested_reviewers
end
+ # Filter out any locked labels that are requested to be removed.
+ # Only supported for merged MRs.
+ def filter_locked_labels(merge_request)
+ return unless params[:remove_label_ids].present?
+ return unless merge_request.merged?
+ return unless Feature.enabled?(:enforce_locked_labels_on_merge, merge_request.project, type: :ops)
+
+ params[:remove_label_ids] -= labels_service.filter_locked_labels_ids_in_param(:remove_label_ids)
+ end
+
def filter_reviewer(merge_request)
return if params[:reviewer_ids].blank?
diff --git a/app/views/projects/settings/operations/_alert_management.html.haml b/app/views/projects/settings/operations/_alert_management.html.haml
index 7433e81c11c..5ba9276c116 100644
--- a/app/views/projects/settings/operations/_alert_management.html.haml
+++ b/app/views/projects/settings/operations/_alert_management.html.haml
@@ -9,7 +9,7 @@
= _('Alerts')
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= _('Expand')
- %p
+ %p.gl-text-secondary.gl-mb-0
= _('Display alerts from all configured monitoring tools.')
= link_to _('Learn more.'), help_page_path('operations/incident_management/integrations.md'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index fadaeafeaf6..46710081307 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -42,7 +42,7 @@
.js-sidebar-labels-widget-root{ data: sidebar_labels_data(issuable_sidebar, @project) }
- if issuable_sidebar[:supports_milestone]
- .block.milestone{ :class => ("gl-border-b-0!" if in_group_context_with_iterations), data: { qa_selector: 'milestone_block', testid: 'sidebar-milestones' } }
+ .block.milestone{ data: { qa_selector: 'milestone_block', testid: 'sidebar-milestones' } }
.js-sidebar-milestone-widget-root{ data: { can_edit: can_edit_issuable.to_s, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid] } }
- if in_group_context_with_iterations
diff --git a/config/feature_flags/ops/enforce_locked_labels_on_merge.yml b/config/feature_flags/ops/enforce_locked_labels_on_merge.yml
new file mode 100644
index 00000000000..5dd02e8e46e
--- /dev/null
+++ b/config/feature_flags/ops/enforce_locked_labels_on_merge.yml
@@ -0,0 +1,8 @@
+---
+name: enforce_locked_labels_on_merge
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/128928
+rollout_issue_url:
+milestone: '16.3'
+type: ops
+group: group::project management
+default_enabled: false
diff --git a/config/metrics/counts_28d/20210427102618_code_review_category_monthly_active_users.yml b/config/metrics/counts_28d/20210427102618_code_review_category_monthly_active_users.yml
index 8d1f00446d7..18f4ed24837 100644
--- a/config/metrics/counts_28d/20210427102618_code_review_category_monthly_active_users.yml
+++ b/config/metrics/counts_28d/20210427102618_code_review_category_monthly_active_users.yml
@@ -141,3 +141,4 @@ options:
- 'i_code_review_merge_request_widget_security_reports_expand_failed'
- 'i_code_review_saved_replies_create'
- 'i_code_review_saved_replies_use'
+ - 'i_code_review_saved_replies_use_in_mr'
diff --git a/config/metrics/counts_28d/20210427103119_code_review_group_monthly_active_users.yml b/config/metrics/counts_28d/20210427103119_code_review_group_monthly_active_users.yml
index e19e0965e05..796acd20d15 100644
--- a/config/metrics/counts_28d/20210427103119_code_review_group_monthly_active_users.yml
+++ b/config/metrics/counts_28d/20210427103119_code_review_group_monthly_active_users.yml
@@ -146,3 +146,4 @@ options:
- 'i_code_review_merge_request_widget_security_reports_expand_failed'
- 'i_code_review_saved_replies_create'
- 'i_code_review_saved_replies_use'
+ - 'i_code_review_saved_replies_use_in_mr'
diff --git a/config/metrics/counts_28d/20230809194743_i_code_review_saved_replies_use_in_mr_monthly.yml b/config/metrics/counts_28d/20230809194743_i_code_review_saved_replies_use_in_mr_monthly.yml
new file mode 100644
index 00000000000..490dbfadcd2
--- /dev/null
+++ b/config/metrics/counts_28d/20230809194743_i_code_review_saved_replies_use_in_mr_monthly.yml
@@ -0,0 +1,25 @@
+---
+key_path: redis_hll_counters.code_review.i_code_review_saved_replies_use_in_mr_monthly
+description: Monthly unique users who used saved replies from an MR page
+product_section: dev
+product_stage: create
+product_group: code_review
+value_type: number
+status: active
+milestone: "16.3"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/128821
+time_frame: 28d
+data_source: redis_hll
+data_category: optional
+instrumentation_class: RedisHLLMetric
+performance_indicator_type: []
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+options:
+ events:
+ - i_code_review_saved_replies_use_in_mr
diff --git a/config/metrics/counts_7d/20210427103328_code_review_group_monthly_active_users.yml b/config/metrics/counts_7d/20210427103328_code_review_group_monthly_active_users.yml
index 94cf3bcc63c..b7a65d38ab3 100644
--- a/config/metrics/counts_7d/20210427103328_code_review_group_monthly_active_users.yml
+++ b/config/metrics/counts_7d/20210427103328_code_review_group_monthly_active_users.yml
@@ -144,3 +144,4 @@ options:
- 'i_code_review_merge_request_widget_security_reports_expand_failed'
- 'i_code_review_saved_replies_create'
- 'i_code_review_saved_replies_use'
+ - 'i_code_review_saved_replies_use_in_mr'
diff --git a/config/metrics/counts_7d/20210427103407_code_review_category_monthly_active_users.yml b/config/metrics/counts_7d/20210427103407_code_review_category_monthly_active_users.yml
index a15273b156b..c7c7e248ea0 100644
--- a/config/metrics/counts_7d/20210427103407_code_review_category_monthly_active_users.yml
+++ b/config/metrics/counts_7d/20210427103407_code_review_category_monthly_active_users.yml
@@ -139,3 +139,4 @@ options:
- 'i_code_review_merge_request_widget_security_reports_expand_failed'
- 'i_code_review_saved_replies_create'
- 'i_code_review_saved_replies_use'
+ - 'i_code_review_saved_replies_use_in_mr'
diff --git a/config/metrics/counts_7d/20230809194743_i_code_review_saved_replies_use_in_mr_weekly.yml b/config/metrics/counts_7d/20230809194743_i_code_review_saved_replies_use_in_mr_weekly.yml
new file mode 100644
index 00000000000..81dc130397b
--- /dev/null
+++ b/config/metrics/counts_7d/20230809194743_i_code_review_saved_replies_use_in_mr_weekly.yml
@@ -0,0 +1,25 @@
+---
+key_path: redis_hll_counters.code_review.i_code_review_saved_replies_use_in_mr_weekly
+description: Weekly unique users who used saved replies from an MR page
+product_section: dev
+product_stage: create
+product_group: code_review
+value_type: number
+status: active
+milestone: "16.3"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/128821
+time_frame: 7d
+data_source: redis_hll
+data_category: optional
+instrumentation_class: RedisHLLMetric
+performance_indicator_type: []
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+options:
+ events:
+ - i_code_review_saved_replies_use_in_mr
diff --git a/config/metrics/counts_all/20230809194308_i_code_review_saved_replies_use_in_mr.yml b/config/metrics/counts_all/20230809194308_i_code_review_saved_replies_use_in_mr.yml
new file mode 100644
index 00000000000..a12d694306e
--- /dev/null
+++ b/config/metrics/counts_all/20230809194308_i_code_review_saved_replies_use_in_mr.yml
@@ -0,0 +1,25 @@
+---
+key_path: counts.i_code_review_saved_replies_use_in_mr
+description: Total number of times a saved reply was used from an MR page
+product_section: dev
+product_stage: create
+product_group: code_review
+value_type: number
+status: active
+milestone: "16.3"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/128821
+time_frame: all
+data_source: redis
+data_category: optional
+instrumentation_class: RedisMetric
+performance_indicator_type: []
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+options:
+ event: use_in_mr
+ prefix: i_code_review_saved_replies
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index bfe9b80b241..260cdeb27d6 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -445,6 +445,8 @@
- 1
- - package_cleanup
- 1
+- - package_metadata_advisory_scan
+ - 1
- - package_repositories
- 1
- - packages_composer_cache_update
diff --git a/doc/development/database_review.md b/doc/development/database_review.md
index d742fe8a54b..bb0bfbc759b 100644
--- a/doc/development/database_review.md
+++ b/doc/development/database_review.md
@@ -221,8 +221,9 @@ Include in the MR description:
#### Preparation when using `update`, `delete`, `update_all`, `delete_all` or `destroy_all`
Using these ActiveRecord methods requires extra care because they modify data and can perform poorly, or they
-can destroy data if improperly scoped. These methods are also incompatible with Common Table Expression (CTE)
-statements. Danger will comment on a Merge Request Diff when these methods are used.
+can destroy data if improperly scoped. These methods are also
+[incompatible with Common Table Expression (CTE) statements](sql.md#when-to-use-common-table-expressions).
+Danger will comment on a Merge Request Diff when these methods are used.
Follow documentation for [preparation when adding or modifying queries](#preparation-when-adding-or-modifying-queries)
to add the raw SQL query and query plan to the Merge Request description, and request a database review.
diff --git a/doc/development/fe_guide/accessibility.md b/doc/development/fe_guide/accessibility.md
index b00131b12f3..65b50bedb0c 100644
--- a/doc/development/fe_guide/accessibility.md
+++ b/doc/development/fe_guide/accessibility.md
@@ -518,9 +518,26 @@ Proper research and testing should be done to ensure compliance with [WCAG](http
## Automated accessibility testing with axe
-You can use [axe-core](https://github.com/dequelabs/axe-core) [gems](https://github.com/dequelabs/axe-core-gems)
+We use [axe-core](https://github.com/dequelabs/axe-core) [gems](https://github.com/dequelabs/axe-core-gems)
to run automated accessibility tests in feature tests.
+[We aim to conform to level AA of the World Wide Web Consortium (W3C) Web Content Accessibility Guidelines 2.1](https://design.gitlab.com/accessibility/a11y).
+
+### When to add accessibility tests
+
+When adding a new view to the application, make sure to include the accessibility check in your feature test.
+We aim to have full coverage for all the views.
+
+One of the advantages of testing in feature tests is that we can check different states, not only
+single components in isolation.
+
+Make sure to add assertions, when the view you are working on:
+
+- Has an empty state,
+- Has significant changes in page structure, for example an alert is shown, or a new section is rendered.
+
+### How to add accessibility tests
+
Axe provides the custom matcher `be_axe_clean`, which can be used like the following:
```ruby
@@ -530,10 +547,12 @@ it 'passes axe automated accessibility testing', :js do
wait_for_requests # ensures page is fully loaded
- expect(page).to be_axe_clean
+ expect(page).to be_axe_clean.according_to :wcag21aa
end
```
+Make sure to specify the accessibility standard as `:wcag21aa`.
+
If needed, you can scope testing to a specific area of the page by using `within`.
Axe also provides specific [clauses](https://github.com/dequelabs/axe-core-gems/blob/develop/packages/axe-core-rspec/README.md#clauses),
@@ -543,20 +562,22 @@ for example:
expect(page).to be_axe_clean.within '[data-testid="element"]'
# run only WCAG 2.0 Level AA rules
-expect(page).to be_axe_clean.according_to :wcag2aa
+expect(page).to be_axe_clean.according_to :wcag21aa
# specifies which rule to skip
expect(page).to be_axe_clean.skipping :'link-in-text-block'
# clauses can be chained
expect(page).to be_axe_clean.within('[data-testid="element"]')
- .according_to(:wcag2aa)
+ .according_to(:wcag21aa)
```
Axe does not test hidden regions, such as inactive menus or modal windows. To test
hidden regions for accessibility, write tests that activate or render the regions visible
and run the matcher again.
+You can run accessibility tests locally in the same way as you [run any feature tests](../testing_guide/frontend_testing.md#how-to-run-a-feature-test).
+
### Known accessibility violations
This section documents violations where a recommendation differs with the [design system](https://design.gitlab.com/):
diff --git a/doc/topics/autodevops/cloud_deployments/auto_devops_with_eks.md b/doc/topics/autodevops/cloud_deployments/auto_devops_with_eks.md
index 09d27e010c5..d8544f3f382 100644
--- a/doc/topics/autodevops/cloud_deployments/auto_devops_with_eks.md
+++ b/doc/topics/autodevops/cloud_deployments/auto_devops_with_eks.md
@@ -178,9 +178,6 @@ The jobs are separated into stages:
- Jobs suffixed with `-sast` run static analysis on the current code to check for potential
security issues, and are allowed to fail ([Auto SAST](../stages.md#auto-sast))
- The `secret-detection` job checks for leaked secrets and is allowed to fail ([Auto Secret Detection](../stages.md#auto-secret-detection))
- - The `license_scanning` job searches the application's dependencies to determine each of their
- licenses and is allowed to fail
- ([Auto License Compliance](../stages.md#auto-license-compliance))
- **Review** - Pipelines on the default branch include this stage with a `dast_environment_deploy` job.
To learn more, see [Dynamic Application Security Testing (DAST)](../../../user/application_security/dast/index.md).
diff --git a/doc/topics/autodevops/cloud_deployments/auto_devops_with_gke.md b/doc/topics/autodevops/cloud_deployments/auto_devops_with_gke.md
index 74c2ed639bc..0cab8c3e857 100644
--- a/doc/topics/autodevops/cloud_deployments/auto_devops_with_gke.md
+++ b/doc/topics/autodevops/cloud_deployments/auto_devops_with_gke.md
@@ -182,9 +182,6 @@ The jobs are separated into stages:
- Jobs suffixed with `-sast` run static analysis on the current code to check for potential
security issues, and are allowed to fail ([Auto SAST](../stages.md#auto-sast))
- The `secret-detection` job checks for leaked secrets and is allowed to fail ([Auto Secret Detection](../stages.md#auto-secret-detection))
- - The `license_scanning` job searches the application's dependencies to determine each of their
- licenses and is allowed to fail
- ([Auto License Compliance](../stages.md#auto-license-compliance))
- **Review** - Pipelines on the default branch include this stage with a `dast_environment_deploy` job.
For more information, see [Dynamic Application Security Testing (DAST)](../../../user/application_security/dast/index.md).
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index d5ebd4e9fcb..ed7740e5a94 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -37,7 +37,6 @@ Auto DevOps supports development during each of the [DevOps stages](stages.md).
| Test | [Auto Code Intelligence](stages.md#auto-code-intelligence) |
| Test | [Auto Code Quality](stages.md#auto-code-quality) |
| Test | [Auto Container Scanning](stages.md#auto-container-scanning) |
-| Test | [Auto License Compliance](stages.md#auto-license-compliance) |
| Deploy | [Auto Review Apps](stages.md#auto-review-apps) |
| Deploy | [Auto Deploy](stages.md#auto-deploy) |
| Secure | [Auto Dynamic Application Security Testing (DAST)](stages.md#auto-dast) |
diff --git a/doc/topics/autodevops/stages.md b/doc/topics/autodevops/stages.md
index 6be8a71cdbc..c9ac9b640cb 100644
--- a/doc/topics/autodevops/stages.md
+++ b/doc/topics/autodevops/stages.md
@@ -240,20 +240,15 @@ check out. The merge request widget displays any security warnings detected,
For more information, see
[Dependency Scanning](../../user/application_security/dependency_scanning/index.md).
-## Auto License Compliance **(ULTIMATE)**
+<!--- start_remove The following content will be removed on remove_date: '2023-11-22' -->
-> Introduced in GitLab 11.0.
+## Auto License Compliance (removed) **(ULTIMATE)**
-License Compliance uses the
-[License Compliance Docker image](https://gitlab.com/gitlab-org/security-products/analyzers/license-finder)
-to search the project dependencies for their license. The Auto License Compliance stage
-is skipped on licenses other than [Ultimate](https://about.gitlab.com/pricing/).
+This feature was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/387561) in GitLab 15.9
+and [removed](https://gitlab.com/gitlab-org/gitlab/-/issues/421363) in 16.3.
+Use Auto Dependency Scanning instead.
-After creating the report, it's uploaded as an artifact which you can later download and
-check out. The merge request displays any detected licenses.
-
-For more information, see
-[License Compliance](../../user/compliance/license_compliance/index.md).
+<!--- end_remove -->
## Auto Container Scanning
diff --git a/doc/update/versions/gitlab_16_changes.md b/doc/update/versions/gitlab_16_changes.md
index 55648c87712..39307c71312 100644
--- a/doc/update/versions/gitlab_16_changes.md
+++ b/doc/update/versions/gitlab_16_changes.md
@@ -13,6 +13,18 @@ For more information about upgrading GitLab Helm Chart, see [the release notes f
## 16.3.0
+- For Go applications, [`crypto/tls`: verifying certificate chains containing large RSA keys is slow (CVE-2023-29409)](https://github.com/golang/go/issues/61460)
+ introduced a hard limit of 8192 bits for RSA keys. In the context of Go applications at GitLab, RSA keys can be configured for:
+
+ - [Container Registry](../../administration/packages/container_registry.md)
+ - [Gitaly](../../administration/gitaly/configure_gitaly.md#enable-tls-support)
+ - [GitLab Pages](../../user/project/pages/custom_domains_ssl_tls_certification/index.md#manual-addition-of-ssltls-certificates)
+ - [Workhorse](../../development/workhorse/configuration.md#tls-support)
+
+ You should check the size of your RSA keys (`openssl rsa -in <your-key-file> -text -noout | grep "Key:"`)
+ for any of the applications above before
+ upgrading.
+
### Linux package installations
Specific information applies to Linux package installations:
diff --git a/doc/user/ai_features.md b/doc/user/ai_features.md
index 4ec2f7a70ea..4cca6bd5c66 100644
--- a/doc/user/ai_features.md
+++ b/doc/user/ai_features.md
@@ -193,6 +193,26 @@ For example, if you select a 30-day range, a forecast for the following 15 days
Provide feedback on this experimental feature in [issue 416833](https://gitlab.com/gitlab-org/gitlab/-/issues/416833).
+### Generate issue descriptions **(ULTIMATE SAAS)**
+
+> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/10762) in GitLab 16.3 as an [Experiment](../policy/experiment-beta-support.md#experiment).
+
+This feature is an [Experiment](../policy/experiment-beta-support.md) on GitLab.com that is powered by OpenAI's
+GPT-3. It requires the [group-level third-party AI features setting](group/manage.md#enable-third-party-ai-features) to be enabled.
+
+You can generate the description for an issue from a short summary.
+
+1. Create a new issue.
+1. Above the **Description** field, select **AI actions > Generate issue description**.
+1. Write a short description and select **Submit**.
+
+The issue description is replaced with AI-generated text.
+
+Provide feedback on this experimental feature in [issue 409844](https://gitlab.com/gitlab-org/gitlab/-/issues/409844).
+
+**Data usage**: When you use this feature, the text you enter is sent to the large
+language model referenced above.
+
## Data Usage
GitLab AI features leverage generative AI to help increase velocity and aim to help make you more productive. Each feature operates independently of other features and is not required for other features to function.
diff --git a/doc/user/application_security/index.md b/doc/user/application_security/index.md
index 787b0d5a2f0..e3f6982fd6c 100644
--- a/doc/user/application_security/index.md
+++ b/doc/user/application_security/index.md
@@ -124,7 +124,6 @@ To enable all GitLab Security scanning tools, with default settings, enable
- [Auto Secret Detection](../../topics/autodevops/stages.md#auto-secret-detection)
- [Auto DAST](../../topics/autodevops/stages.md#auto-dast)
- [Auto Dependency Scanning](../../topics/autodevops/stages.md#auto-dependency-scanning)
-- [Auto License Compliance](../../topics/autodevops/stages.md#auto-license-compliance)
- [Auto Container Scanning](../../topics/autodevops/stages.md#auto-container-scanning)
While you cannot directly customize Auto DevOps, you can [include the Auto DevOps template in your project's `.gitlab-ci.yml` file](../../topics/autodevops/customize.md#customize-gitlab-ciyml).
diff --git a/doc/user/compliance/license_compliance/index.md b/doc/user/compliance/license_compliance/index.md
index 238cf10cba9..106bd316fd8 100644
--- a/doc/user/compliance/license_compliance/index.md
+++ b/doc/user/compliance/license_compliance/index.md
@@ -51,10 +51,8 @@ License Compliance Scanning does not support run-time installation of compilers
## Enable License Compliance
-To enable License Compliance in your project's pipeline, either:
+To enable License Compliance in your project's pipeline:
-- Enable [Auto License Compliance](../../../topics/autodevops/stages.md#auto-license-compliance)
- (provided by [Auto DevOps](../../../topics/autodevops/index.md)).
- Include the [`License-Scanning.gitlab-ci.yml` template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml) in your `.gitlab-ci.yml` file.
License Compliance is not supported when GitLab is run with FIPS mode enabled.
diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
index 4f12f0cd3b8..2a07530c00d 100644
--- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
@@ -16,7 +16,6 @@
# Test jobs may be disabled by setting environment variables:
# * test: TEST_DISABLED
# * code_quality: CODE_QUALITY_DISABLED
-# * license_management: LICENSE_MANAGEMENT_DISABLED
# * browser_performance: BROWSER_PERFORMANCE_DISABLED
# * load_performance: LOAD_PERFORMANCE_DISABLED
# * sast: SAST_DISABLED
@@ -178,7 +177,6 @@ include:
- template: Security/DAST.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
- template: Jobs/Container-Scanning.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Container-Scanning.gitlab-ci.yml
- template: Jobs/Dependency-Scanning.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Dependency-Scanning.gitlab-ci.yml
- - template: Jobs/License-Scanning.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/License-Scanning.gitlab-ci.yml
- template: Jobs/SAST.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/SAST.gitlab-ci.yml
- template: Jobs/Secret-Detection.gitlab-ci.yml # https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Secret-Detection.gitlab-ci.yml
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index f0ac5014437..98846f0398e 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -4451,6 +4451,9 @@ msgstr ""
msgid "AlertSettings|A webhook URL and authorization key is generated for the integration. After you save the integration, both are visible under the “View credentials” tab."
msgstr ""
+msgid "AlertSettings|Active alerts"
+msgstr ""
+
msgid "AlertSettings|Add new integration"
msgstr ""
@@ -49043,7 +49046,7 @@ msgstr ""
msgid "To remove the %{link_start}read-only%{link_end} state and regain write access, ask your top-level group owner(s) to reduce the number of users in your top-level group to %{free_limit} users or less, or to upgrade to a paid tier which do not have user limits."
msgstr ""
-msgid "To remove the %{link_start}read-only%{link_end} state and regain write access, you can reduce the number of users in your top-level group to %{free_limit} users or less. You can also upgrade to a paid tier, which do not have user limits. If you need additional time, you can start a free 30-day trial which includes unlimited users. To minimize the impact to operations, for a limited time, GitLab is offering a %{promotion_link_start}one-time 70 percent discount%{link_end} off the list price at time of purchase for a new, one year subscription of GitLab Premium SaaS to %{offer_availability_link_start}qualifying top-level groups%{link_end}. The offer is valid until %{offer_date}."
+msgid "To remove the %{link_start}read-only%{link_end} state and regain write access, you can reduce the number of users in your top-level group to %{free_limit} users or less. You can also upgrade to a paid tier, which do not have user limits. If you need additional time, you can start a free 30-day trial which includes unlimited users."
msgstr ""
msgid "To replace phone verification with credit card verification, create a phone verification exemption using the button below."
@@ -55017,6 +55020,9 @@ msgstr ""
msgid "can not be changed when assigned to an epic"
msgstr ""
+msgid "can not be set for template labels"
+msgstr ""
+
msgid "can not be set for this resource"
msgstr ""
diff --git a/package.json b/package.json
index 1bf2e454476..963b94db8b8 100644
--- a/package.json
+++ b/package.json
@@ -59,7 +59,7 @@
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/fonts": "^1.3.0",
"@gitlab/svgs": "3.59.0",
- "@gitlab/ui": "64.20.1",
+ "@gitlab/ui": "65.0.1",
"@gitlab/visual-review-tools": "1.7.3",
"@gitlab/web-ide": "0.0.1-dev-20230807045127",
"@mattiasbuelens/web-streams-adapter": "^0.1.0",
diff --git a/qa/lib/gitlab/page/admin/dashboard.rb b/qa/lib/gitlab/page/admin/dashboard.rb
index f1a732f8fac..b6ae49a0e3b 100644
--- a/qa/lib/gitlab/page/admin/dashboard.rb
+++ b/qa/lib/gitlab/page/admin/dashboard.rb
@@ -8,7 +8,6 @@ module Gitlab
h2 :users_in_license
h2 :billable_users
- h3 :number_of_users
end
end
end
diff --git a/qa/lib/gitlab/page/admin/dashboard.stub.rb b/qa/lib/gitlab/page/admin/dashboard.stub.rb
index 820acf79b9b..da717558133 100644
--- a/qa/lib/gitlab/page/admin/dashboard.stub.rb
+++ b/qa/lib/gitlab/page/admin/dashboard.stub.rb
@@ -51,30 +51,6 @@ module Gitlab
def billable_users?
# This is a stub, used for indexing. The method is dynamically generated.
end
-
- # @note Defined as +h3 :number_of_users+
- # @return [String] The text content or value of +number_of_users+
- def number_of_users
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @example
- # Gitlab::Page::Admin::Dashboard.perform do |dashboard|
- # expect(dashboard.number_of_users_element).to exist
- # end
- # @return [Watir::H3] The raw +H3+ element
- def number_of_users_element
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @example
- # Gitlab::Page::Admin::Dashboard.perform do |dashboard|
- # expect(dashboard).to be_number_of_users
- # end
- # @return [Boolean] true if the +number_of_users+ element is present on the page
- def number_of_users?
- # This is a stub, used for indexing. The method is dynamically generated.
- end
end
end
end
diff --git a/qa/lib/gitlab/page/admin/subscription.rb b/qa/lib/gitlab/page/admin/subscription.rb
index 058cf8d281e..e19f0580007 100644
--- a/qa/lib/gitlab/page/admin/subscription.rb
+++ b/qa/lib/gitlab/page/admin/subscription.rb
@@ -12,15 +12,11 @@ module Gitlab
label :terms_of_services, text: /I agree that/
button :remove_license
button :confirm_remove_license
- p :plan
- p :started
- p :name
- p :company
- p :email
- h2 :billable_users
- h2 :maximum_users
+ td :plan
+ td :name
+ td :company
+ td :email
h2 :users_in_subscription
- h2 :users_over_subscription
table :subscription_history
div :no_valid_license_alert, text: /no longer has a valid license/
diff --git a/qa/lib/gitlab/page/admin/subscription.stub.rb b/qa/lib/gitlab/page/admin/subscription.stub.rb
index 56a063e8978..9ed127f9281 100644
--- a/qa/lib/gitlab/page/admin/subscription.stub.rb
+++ b/qa/lib/gitlab/page/admin/subscription.stub.rb
@@ -4,7 +4,7 @@ module Gitlab
module Page
module Admin
module Subscription
- # @note Defined as +h6 :subscription_details+
+ # @note Defined as +div :subscription_details+
# @return [String] The text content or value of +subscription_details+
def subscription_details
# This is a stub, used for indexing. The method is dynamically generated.
@@ -14,7 +14,7 @@ module Gitlab
# Gitlab::Page::Admin::Subscription.perform do |subscription|
# expect(subscription.subscription_details_element).to exist
# end
- # @return [Watir::H6] The raw +H6+ element
+ # @return [Watir::Div] The raw +Div+ element
def subscription_details_element
# This is a stub, used for indexing. The method is dynamically generated.
end
@@ -62,6 +62,30 @@ module Gitlab
# This is a stub, used for indexing. The method is dynamically generated.
end
+ # @note Defined as +button :activate+
+ # Clicks +activate+
+ def activate
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Admin::Subscription.perform do |subscription|
+ # expect(subscription.activate_element).to exist
+ # end
+ # @return [Watir::Button] The raw +Button+ element
+ def activate_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Admin::Subscription.perform do |subscription|
+ # expect(subscription).to be_activate
+ # end
+ # @return [Boolean] true if the +activate+ element is present on the page
+ def activate?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
# @note Defined as +label :terms_of_services+
# @return [String] The text content or value of +terms_of_services+
def terms_of_services
@@ -86,79 +110,79 @@ module Gitlab
# This is a stub, used for indexing. The method is dynamically generated.
end
- # @note Defined as +button :activate+
- # Clicks +activate+
- def activate
+ # @note Defined as +button :remove_license+
+ # Clicks +remove_license+
+ def remove_license
# This is a stub, used for indexing. The method is dynamically generated.
end
# @example
# Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription.activate_element).to exist
+ # expect(subscription.remove_license_element).to exist
# end
# @return [Watir::Button] The raw +Button+ element
- def activate_element
+ def remove_license_element
# This is a stub, used for indexing. The method is dynamically generated.
end
# @example
# Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription).to be_activate
+ # expect(subscription).to be_remove_license
# end
- # @return [Boolean] true if the +activate+ element is present on the page
- def activate?
+ # @return [Boolean] true if the +remove_license+ element is present on the page
+ def remove_license?
# This is a stub, used for indexing. The method is dynamically generated.
end
- # @note Defined as +p :plan+
- # @return [String] The text content or value of +plan+
- def plan
+ # @note Defined as +button :confirm_remove_license+
+ # Clicks +confirm_remove_license+
+ def confirm_remove_license
# This is a stub, used for indexing. The method is dynamically generated.
end
# @example
# Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription.plan_element).to exist
+ # expect(subscription.confirm_remove_license_element).to exist
# end
- # @return [Watir::P] The raw +P+ element
- def plan_element
+ # @return [Watir::Button] The raw +Button+ element
+ def confirm_remove_license_element
# This is a stub, used for indexing. The method is dynamically generated.
end
# @example
# Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription).to be_plan
+ # expect(subscription).to be_confirm_remove_license
# end
- # @return [Boolean] true if the +plan+ element is present on the page
- def plan?
+ # @return [Boolean] true if the +confirm_remove_license+ element is present on the page
+ def confirm_remove_license?
# This is a stub, used for indexing. The method is dynamically generated.
end
- # @note Defined as +p :started+
- # @return [String] The text content or value of +started+
- def started
+ # @note Defined as +td :plan+
+ # @return [String] The text content or value of +plan+
+ def plan
# This is a stub, used for indexing. The method is dynamically generated.
end
# @example
# Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription.started_element).to exist
+ # expect(subscription.plan_element).to exist
# end
- # @return [Watir::P] The raw +P+ element
- def started_element
+ # @return [Watir::Td] The raw +Td+ element
+ def plan_element
# This is a stub, used for indexing. The method is dynamically generated.
end
# @example
# Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription).to be_started
+ # expect(subscription).to be_plan
# end
- # @return [Boolean] true if the +started+ element is present on the page
- def started?
+ # @return [Boolean] true if the +plan+ element is present on the page
+ def plan?
# This is a stub, used for indexing. The method is dynamically generated.
end
- # @note Defined as +p :name+
+ # @note Defined as +td :name+
# @return [String] The text content or value of +name+
def name
# This is a stub, used for indexing. The method is dynamically generated.
@@ -168,7 +192,7 @@ module Gitlab
# Gitlab::Page::Admin::Subscription.perform do |subscription|
# expect(subscription.name_element).to exist
# end
- # @return [Watir::P] The raw +P+ element
+ # @return [Watir::Td] The raw +Td+ element
def name_element
# This is a stub, used for indexing. The method is dynamically generated.
end
@@ -182,7 +206,7 @@ module Gitlab
# This is a stub, used for indexing. The method is dynamically generated.
end
- # @note Defined as +p :company+
+ # @note Defined as +td :company+
# @return [String] The text content or value of +company+
def company
# This is a stub, used for indexing. The method is dynamically generated.
@@ -192,7 +216,7 @@ module Gitlab
# Gitlab::Page::Admin::Subscription.perform do |subscription|
# expect(subscription.company_element).to exist
# end
- # @return [Watir::P] The raw +P+ element
+ # @return [Watir::Td] The raw +Td+ element
def company_element
# This is a stub, used for indexing. The method is dynamically generated.
end
@@ -206,7 +230,7 @@ module Gitlab
# This is a stub, used for indexing. The method is dynamically generated.
end
- # @note Defined as +p :email+
+ # @note Defined as +td :email+
# @return [String] The text content or value of +email+
def email
# This is a stub, used for indexing. The method is dynamically generated.
@@ -216,7 +240,7 @@ module Gitlab
# Gitlab::Page::Admin::Subscription.perform do |subscription|
# expect(subscription.email_element).to exist
# end
- # @return [Watir::P] The raw +P+ element
+ # @return [Watir::Td] The raw +Td+ element
def email_element
# This is a stub, used for indexing. The method is dynamically generated.
end
@@ -230,123 +254,99 @@ module Gitlab
# This is a stub, used for indexing. The method is dynamically generated.
end
- # @note Defined as +h2 :billable_users+
- # @return [String] The text content or value of +billable_users+
- def billable_users
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @example
- # Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription.billable_users_element).to exist
- # end
- # @return [Watir::H2] The raw +H2+ element
- def billable_users_element
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @example
- # Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription).to be_billable_users
- # end
- # @return [Boolean] true if the +billable_users+ element is present on the page
- def billable_users?
- # This is a stub, used for indexing. The method is dynamically generated.
- end
-
- # @note Defined as +h2 :maximum_users+
- # @return [String] The text content or value of +maximum_users+
- def maximum_users
+ # @note Defined as +h2 :users_in_subscription+
+ # @return [String] The text content or value of +users_in_subscription+
+ def users_in_subscription
# This is a stub, used for indexing. The method is dynamically generated.
end
# @example
# Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription.maximum_users_element).to exist
+ # expect(subscription.users_in_subscription_element).to exist
# end
# @return [Watir::H2] The raw +H2+ element
- def maximum_users_element
+ def users_in_subscription_element
# This is a stub, used for indexing. The method is dynamically generated.
end
# @example
# Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription).to be_maximum_users
+ # expect(subscription).to be_users_in_subscription
# end
- # @return [Boolean] true if the +maximum_users+ element is present on the page
- def maximum_users?
+ # @return [Boolean] true if the +users_in_subscription+ element is present on the page
+ def users_in_subscription?
# This is a stub, used for indexing. The method is dynamically generated.
end
- # @note Defined as +h2 :users_in_subscription+
- # @return [String] The text content or value of +users_in_subscription+
- def users_in_subscription
+ # @note Defined as +table :subscription_history+
+ # @return [String] The text content or value of +subscription_history+
+ def subscription_history
# This is a stub, used for indexing. The method is dynamically generated.
end
# @example
# Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription.users_in_subscription_element).to exist
+ # expect(subscription.subscription_history_element).to exist
# end
- # @return [Watir::H2] The raw +H2+ element
- def users_in_subscription_element
+ # @return [Watir::Table] The raw +Table+ element
+ def subscription_history_element
# This is a stub, used for indexing. The method is dynamically generated.
end
# @example
# Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription).to be_users_in_subscription
+ # expect(subscription).to be_subscription_history
# end
- # @return [Boolean] true if the +users_in_subscription+ element is present on the page
- def users_in_subscription?
+ # @return [Boolean] true if the +subscription_history+ element is present on the page
+ def subscription_history?
# This is a stub, used for indexing. The method is dynamically generated.
end
- # @note Defined as +h2 :users_over_subscription+
- # @return [String] The text content or value of +users_over_subscription+
- def users_over_subscription
+ # @note Defined as +div :no_valid_license_alert+
+ # @return [String] The text content or value of +no_valid_license_alert+
+ def no_valid_license_alert
# This is a stub, used for indexing. The method is dynamically generated.
end
# @example
# Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription.users_over_subscription_element).to exist
+ # expect(subscription.no_valid_license_alert_element).to exist
# end
- # @return [Watir::H2] The raw +H2+ element
- def users_over_subscription_element
+ # @return [Watir::Div] The raw +Div+ element
+ def no_valid_license_alert_element
# This is a stub, used for indexing. The method is dynamically generated.
end
# @example
# Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription).to be_users_over_subscription
+ # expect(subscription).to be_no_valid_license_alert
# end
- # @return [Boolean] true if the +users_over_subscription+ element is present on the page
- def users_over_subscription?
+ # @return [Boolean] true if the +no_valid_license_alert+ element is present on the page
+ def no_valid_license_alert?
# This is a stub, used for indexing. The method is dynamically generated.
end
- # @note Defined as +table :subscription_history+
- # @return [String] The text content or value of +subscription_history+
- def subscription_history
+ # @note Defined as +h3 :no_active_subscription_title+
+ # @return [String] The text content or value of +no_active_subscription_title+
+ def no_active_subscription_title
# This is a stub, used for indexing. The method is dynamically generated.
end
# @example
# Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription.subscription_history_element).to exist
+ # expect(subscription.no_active_subscription_title_element).to exist
# end
- # @return [Watir::Table] The raw +Table+ element
- def subscription_history_element
+ # @return [Watir::H3] The raw +H3+ element
+ def no_active_subscription_title_element
# This is a stub, used for indexing. The method is dynamically generated.
end
# @example
# Gitlab::Page::Admin::Subscription.perform do |subscription|
- # expect(subscription).to be_subscription_history
+ # expect(subscription).to be_no_active_subscription_title
# end
- # @return [Boolean] true if the +subscription_history+ element is present on the page
- def subscription_history?
+ # @return [Boolean] true if the +no_active_subscription_title+ element is present on the page
+ def no_active_subscription_title?
# This is a stub, used for indexing. The method is dynamically generated.
end
end
diff --git a/spec/features/issues/user_toggles_subscription_spec.rb b/spec/features/issues/user_toggles_subscription_spec.rb
index 0ddf49578a0..af8a31afd5f 100644
--- a/spec/features/issues/user_toggles_subscription_spec.rb
+++ b/spec/features/issues/user_toggles_subscription_spec.rb
@@ -34,7 +34,7 @@ RSpec.describe "User toggles subscription", :js, feature_category: :team_plannin
expect(subscription_button).to have_css("button.is-checked")
# Toggle subscription.
- find('[data-testid="subscription-toggle"]').click
+ subscription_button.find('button').click
wait_for_requests
# Check we're unsubscribed.
@@ -66,7 +66,7 @@ RSpec.describe "User toggles subscription", :js, feature_category: :team_plannin
expect(subscription_button).to have_css("button:not(.is-checked)")
# Toggle subscription.
- find('[data-testid="subscription-toggle"]').click
+ subscription_button.find('button').click
wait_for_requests
# Check we're subscribed.
diff --git a/spec/finders/labels_finder_spec.rb b/spec/finders/labels_finder_spec.rb
index e344591dd5d..41224f0a5c5 100644
--- a/spec/finders/labels_finder_spec.rb
+++ b/spec/finders/labels_finder_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe LabelsFinder do
+RSpec.describe LabelsFinder, feature_category: :team_planning do
describe '#execute' do
let_it_be(:group_1) { create(:group) }
let_it_be(:group_2) { create(:group) }
@@ -20,10 +20,12 @@ RSpec.describe LabelsFinder do
let_it_be(:project_label_2) { create(:label, project: project_2, title: 'Label 2') }
let_it_be(:project_label_4) { create(:label, project: project_4, title: 'Label 4') }
let_it_be(:project_label_5) { create(:label, project: project_5, title: 'Label 5') }
+ let_it_be(:project_label_locked) { create(:label, project: project_1, title: 'Label Locked', lock_on_merge: true) }
let_it_be(:group_label_1) { create(:group_label, group: group_1, title: 'Label 1 (group)') }
let_it_be(:group_label_2) { create(:group_label, group: group_1, title: 'Group Label 2') }
let_it_be(:group_label_3) { create(:group_label, group: group_2, title: 'Group Label 3') }
+ let_it_be(:group_label_locked) { create(:group_label, group: group_1, title: 'Group Label Locked', lock_on_merge: true) }
let_it_be(:private_group_label_1) { create(:group_label, group: private_group_1, title: 'Private Group Label 1') }
let_it_be(:private_subgroup_label_1) { create(:group_label, group: private_subgroup_1, title: 'Private Sub Group Label 1') }
@@ -42,7 +44,7 @@ RSpec.describe LabelsFinder do
finder = described_class.new(user)
- expect(finder.execute).to match_array([group_label_2, group_label_3, project_label_1, group_label_1, project_label_2, project_label_4])
+ expect(finder.execute).to match_array([group_label_2, group_label_3, group_label_locked, project_label_1, group_label_1, project_label_2, project_label_4, project_label_locked])
end
it 'returns labels available if nil title is supplied' do
@@ -50,7 +52,7 @@ RSpec.describe LabelsFinder do
# params[:title] will return `nil` regardless whether it is specified
finder = described_class.new(user, title: nil)
- expect(finder.execute).to match_array([group_label_2, group_label_3, project_label_1, group_label_1, project_label_2, project_label_4])
+ expect(finder.execute).to match_array([group_label_2, group_label_3, group_label_locked, project_label_1, group_label_1, project_label_2, project_label_4, project_label_locked])
end
end
@@ -60,7 +62,7 @@ RSpec.describe LabelsFinder do
::Projects::UpdateService.new(project_1, user, archived: true).execute
finder = described_class.new(user, **group_params(group_1))
- expect(finder.execute).to match_array([group_label_2, group_label_1, project_label_5])
+ expect(finder.execute).to match_array([group_label_2, group_label_1, project_label_5, group_label_locked])
end
context 'when only_group_labels is true' do
@@ -69,7 +71,7 @@ RSpec.describe LabelsFinder do
finder = described_class.new(user, only_group_labels: true, **group_params(group_1))
- expect(finder.execute).to match_array([group_label_2, group_label_1])
+ expect(finder.execute).to match_array([group_label_2, group_label_1, group_label_locked])
end
end
@@ -249,7 +251,7 @@ RSpec.describe LabelsFinder do
it 'returns labels available for the project' do
finder = described_class.new(user, project_id: project_1.id)
- expect(finder.execute).to match_array([group_label_2, project_label_1, group_label_1])
+ expect(finder.execute).to match_array([group_label_2, group_label_locked, project_label_1, project_label_locked, group_label_1])
end
context 'as an administrator' do
@@ -330,6 +332,14 @@ RSpec.describe LabelsFinder do
end
end
+ context 'filter by locked labels' do
+ it 'returns labels that are locked' do
+ finder = described_class.new(user, locked_labels: true)
+
+ expect(finder.execute).to match_array([project_label_locked, group_label_locked])
+ end
+ end
+
context 'external authorization' do
it_behaves_like 'a finder with external authorization service' do
let!(:subject) { create(:label, project: project) }
diff --git a/spec/frontend/alerts_settings/components/alerts_integrations_list_spec.js b/spec/frontend/alerts_settings/components/alerts_integrations_list_spec.js
index 76d0c12e434..0453bf0b0f8 100644
--- a/spec/frontend/alerts_settings/components/alerts_integrations_list_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_integrations_list_spec.js
@@ -47,7 +47,6 @@ describe('AlertIntegrationsList', () => {
});
const findTableComponent = () => wrapper.findComponent(GlTable);
- const findTableComponentRows = () => wrapper.findComponent(GlTable).findAll('table tbody tr');
const finsStatusCell = () => wrapper.findAll('[data-testid="integration-activated-status"]');
it('renders a table', () => {
@@ -63,11 +62,6 @@ describe('AlertIntegrationsList', () => {
expect(findTableComponent().findAllComponents(GlButton).length).toBe(4);
});
- it('renders an highlighted row when a current integration is selected to edit', () => {
- mountComponent({ data: { currentIntegration: { id: '1' } } });
- expect(findTableComponentRows().at(0).classes()).toContain('gl-bg-blue-50');
- });
-
describe('integration status', () => {
it('enabled', () => {
const cell = finsStatusCell().at(0);
diff --git a/spec/frontend/artifacts_settings/components/__snapshots__/keep_latest_artifact_checkbox_spec.js.snap b/spec/frontend/artifacts_settings/components/__snapshots__/keep_latest_artifact_checkbox_spec.js.snap
index 0bee37dbf15..58aee76e381 100644
--- a/spec/frontend/artifacts_settings/components/__snapshots__/keep_latest_artifact_checkbox_spec.js.snap
+++ b/spec/frontend/artifacts_settings/components/__snapshots__/keep_latest_artifact_checkbox_spec.js.snap
@@ -5,52 +5,60 @@ exports[`Keep latest artifact toggle when application keep latest artifact setti
<!---->
<div
- class="gl-toggle-wrapper gl-display-flex gl-mb-0 gl-flex-direction-column"
+ class="gl-toggle-wrapper gl-display-flex gl-mb-0 flex-grow-1 gl-flex-direction-column"
data-testid="toggle-wrapper"
>
<span
- class="gl-toggle-label gl-flex-shrink-0 gl-mb-3"
- data-testid="toggle-label"
- id="toggle-label-4"
- >
- Keep artifacts from most recent successful jobs
- </span>
-
- <!---->
-
- <!---->
-
- <button
- aria-checked="true"
- aria-describedby="toggle-help-2"
- aria-labelledby="toggle-label-4"
- class="gl-flex-shrink-0 gl-toggle is-checked"
- role="switch"
- type="button"
+ class="gl-toggle-label-container gl-mb-3"
>
<span
- class="toggle-icon"
+ class="gl-toggle-label"
+ data-testid="toggle-label"
+ id="toggle-label-4"
>
- <gl-icon-stub
- name="mobile-issue-close"
- size="16"
- />
+ Keep artifacts from most recent successful jobs
</span>
- </button>
+
+ <!---->
+ </span>
<span
- class="gl-help-label"
- data-testid="toggle-help"
- id="toggle-help-2"
+ class="gl-toggle-switch-container"
>
-
+ <!---->
+
+ <button
+ aria-checked="true"
+ aria-describedby="toggle-help-2"
+ aria-labelledby="toggle-label-4"
+ class="gl-flex-shrink-0 gl-toggle is-checked"
+ role="switch"
+ type="button"
+ >
+ <span
+ class="toggle-icon"
+ >
+ <gl-icon-stub
+ name="mobile-issue-close"
+ size="16"
+ />
+ </span>
+ </button>
+
+ <span
+ class="gl-help-label"
+ data-testid="toggle-help"
+ id="toggle-help-2"
+ >
+
The latest artifacts created by jobs in the most recent successful pipeline will be stored.
- <gl-link-stub
- href="/help/ci/pipelines/job_artifacts"
- >
- Learn more.
- </gl-link-stub>
+ <gl-link-stub
+ href="/help/ci/pipelines/job_artifacts"
+ >
+ Learn more.
+ </gl-link-stub>
+ </span>
</span>
</div>
</div>
diff --git a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js
index bb48d4dc38d..79a0cfa0dc9 100644
--- a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js
@@ -21,6 +21,7 @@ import {
createScheduleMutationResponse,
updateScheduleMutationResponse,
mockSinglePipelineScheduleNode,
+ mockSinglePipelineScheduleNodeNoVars,
} from '../mock_data';
Vue.use(VueApollo);
@@ -51,6 +52,9 @@ describe('Pipeline schedules form', () => {
const dailyLimit = '';
const querySuccessHandler = jest.fn().mockResolvedValue(mockSinglePipelineScheduleNode);
+ const querySuccessEmptyVarsHandler = jest
+ .fn()
+ .mockResolvedValue(mockSinglePipelineScheduleNodeNoVars);
const queryFailedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
const createMutationHandlerSuccess = jest.fn().mockResolvedValue(createScheduleMutationResponse);
@@ -95,6 +99,10 @@ describe('Pipeline schedules form', () => {
const findVariableRows = () => wrapper.findAllByTestId('ci-variable-row');
const findKeyInputs = () => wrapper.findAllByTestId('pipeline-form-ci-variable-key');
const findValueInputs = () => wrapper.findAllByTestId('pipeline-form-ci-variable-value');
+ const findHiddenValueInputs = () =>
+ wrapper.findAllByTestId('pipeline-form-ci-variable-hidden-value');
+ const findVariableSecurityBtn = () => wrapper.findByTestId('variable-security-btn');
+
const findRemoveIcons = () => wrapper.findAllByTestId('remove-ci-variable-row');
const addVariableToForm = () => {
@@ -241,6 +249,12 @@ describe('Pipeline schedules form', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
+ it('does not show variable security button', () => {
+ createComponent();
+
+ expect(findVariableSecurityBtn().exists()).toBe(false);
+ });
+
describe('schedule creation success', () => {
let mock;
@@ -336,6 +350,26 @@ describe('Pipeline schedules form', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
+ it('shows variable security button', async () => {
+ createComponent(shallowMountExtended, true, [
+ [getPipelineSchedulesQuery, querySuccessHandler],
+ ]);
+
+ await waitForPromises();
+
+ expect(findVariableSecurityBtn().exists()).toBe(true);
+ });
+
+ it('does not show variable security button with no present variables', async () => {
+ createComponent(shallowMountExtended, true, [
+ [getPipelineSchedulesQuery, querySuccessEmptyVarsHandler],
+ ]);
+
+ await waitForPromises();
+
+ expect(findVariableSecurityBtn().exists()).toBe(false);
+ });
+
describe('schedule fetch success', () => {
it('fetches schedule and sets form data correctly', async () => {
createComponent(mountExtended, true, [[getPipelineSchedulesQuery, querySuccessHandler]]);
@@ -351,8 +385,13 @@ describe('Pipeline schedules form', () => {
expect(findVariableRows()).toHaveLength(3);
expect(findKeyInputs().at(0).element.value).toBe(variables[0].key);
expect(findKeyInputs().at(1).element.value).toBe(variables[1].key);
- expect(findValueInputs().at(0).element.value).toBe(variables[0].value);
- expect(findValueInputs().at(1).element.value).toBe(variables[1].value);
+ // values are hidden on load when editing a schedule
+ expect(findHiddenValueInputs().at(0).element.value).toBe('*****************');
+ expect(findHiddenValueInputs().at(1).element.value).toBe('*****************');
+ expect(findHiddenValueInputs().at(0).attributes('disabled')).toBe('disabled');
+ expect(findHiddenValueInputs().at(1).attributes('disabled')).toBe('disabled');
+ // empty placeholder to create a new variable
+ expect(findValueInputs()).toHaveLength(1);
});
});
@@ -432,5 +471,23 @@ describe('Pipeline schedules form', () => {
message: 'An error occurred while updating the pipeline schedule.',
});
});
+
+ it('hides/shows variable values', async () => {
+ createComponent(mountExtended, true, [[getPipelineSchedulesQuery, querySuccessHandler]]);
+
+ await waitForPromises();
+
+ // shows two hidden values and one placeholder
+ expect(findHiddenValueInputs()).toHaveLength(2);
+ expect(findValueInputs()).toHaveLength(1);
+
+ findVariableSecurityBtn().vm.$emit('click');
+
+ await nextTick();
+
+ // shows all variable values
+ expect(findHiddenValueInputs()).toHaveLength(0);
+ expect(findValueInputs()).toHaveLength(3);
+ });
});
});
diff --git a/spec/frontend/ci/pipeline_schedules/mock_data.js b/spec/frontend/ci/pipeline_schedules/mock_data.js
index 0a4f233f199..8d4e0f1bea6 100644
--- a/spec/frontend/ci/pipeline_schedules/mock_data.js
+++ b/spec/frontend/ci/pipeline_schedules/mock_data.js
@@ -35,6 +35,19 @@ export const mockPipelineScheduleAsGuestNodes = guestNodes;
export const mockTakeOwnershipNodes = takeOwnershipNodes;
export const mockSinglePipelineScheduleNode = mockGetSinglePipelineScheduleGraphQLResponse;
+export const mockSinglePipelineScheduleNodeNoVars = {
+ data: {
+ currentUser: mockGetPipelineSchedulesGraphQLResponse.data.currentUser,
+ project: {
+ id: mockGetPipelineSchedulesGraphQLResponse.data.project.id,
+ pipelineSchedules: {
+ count: 1,
+ nodes: [mockGetPipelineSchedulesGraphQLResponse.data.project.pipelineSchedules.nodes[1]],
+ },
+ },
+ },
+};
+
export const emptyPipelineSchedulesResponse = {
data: {
currentUser: {
diff --git a/spec/frontend/comment_templates/components/__snapshots__/list_item_spec.js.snap b/spec/frontend/comment_templates/components/__snapshots__/list_item_spec.js.snap
index c5704d68660..60c87aa10eb 100644
--- a/spec/frontend/comment_templates/components/__snapshots__/list_item_spec.js.snap
+++ b/spec/frontend/comment_templates/components/__snapshots__/list_item_spec.js.snap
@@ -26,7 +26,6 @@ exports[`Comment templates list item component renders list item 1`] = `
class="btn btn-default btn-md gl-button btn-default-tertiary gl-new-dropdown-toggle gl-new-dropdown-icon-only gl-new-dropdown-toggle-no-caret"
data-testid="base-dropdown-toggle"
id="actions-toggle-3"
- listeners="[object Object]"
type="button"
>
<!---->
diff --git a/spec/frontend/notifications/components/notifications_dropdown_spec.js b/spec/frontend/notifications/components/notifications_dropdown_spec.js
index bae9b028cf7..1b6a1d2898d 100644
--- a/spec/frontend/notifications/components/notifications_dropdown_spec.js
+++ b/spec/frontend/notifications/components/notifications_dropdown_spec.js
@@ -200,7 +200,7 @@ describe('NotificationsDropdown', () => {
noFlip: true,
});
- expect(findDropdown().attributes('no-flip')).toBe('true');
+ expect(findDropdown().props('noFlip')).toBe(true);
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap
index 4ef3bb24259..99ee6ce01b2 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap
@@ -22,7 +22,6 @@ exports[`PypiInstallation renders all the messages 1`] = `
class="btn btn-default btn-md gl-button gl-new-dropdown-toggle"
data-testid="base-dropdown-toggle"
id="dropdown-toggle-btn-8"
- listeners="[object Object]"
type="button"
>
<!---->
diff --git a/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap
index 2b89e36344d..62d75fbdc5f 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap
@@ -12,6 +12,7 @@ exports[`SplitButton renders actionItems 1`] = `
menu-class=""
size="medium"
split="true"
+ splithref=""
text="professor"
variant="default"
>
diff --git a/spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js b/spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js
index e4689a84900..cd9f27dccbd 100644
--- a/spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js
@@ -5,9 +5,14 @@ import savedRepliesResponse from 'test_fixtures/graphql/comment_templates/saved_
import { mockTracking } from 'helpers/tracking_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
+import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
import CommentTemplatesDropdown from '~/vue_shared/components/markdown/comment_templates_dropdown.vue';
import savedRepliesQuery from '~/vue_shared/components/markdown/saved_replies.query.graphql';
+import {
+ TRACKING_SAVED_REPLIES_USE,
+ TRACKING_SAVED_REPLIES_USE_IN_MR,
+} from '~/vue_shared/components/markdown/constants';
let wrapper;
let savedRepliesResp;
@@ -37,6 +42,18 @@ function findDropdownComponent() {
return wrapper.findComponent(GlCollapsibleListbox);
}
+async function selectSavedReply() {
+ const dropdown = findDropdownComponent();
+
+ dropdown.vm.$emit('shown');
+
+ await waitForPromises();
+
+ dropdown.vm.$emit('select', savedRepliesResponse.data.currentUser.savedReplies.nodes[0].id);
+}
+
+useMockLocationHelper();
+
describe('Comment templates dropdown', () => {
it('fetches data when dropdown gets opened', async () => {
const mockApollo = createMockApolloProvider(savedRepliesResponse);
@@ -69,22 +86,44 @@ describe('Comment templates dropdown', () => {
expect(wrapper.emitted().select[0]).toEqual(['Saved Reply Content']);
});
- it('tracks the usage of the saved comment', async () => {
- const dropdown = findDropdownComponent();
-
- dropdown.vm.$emit('shown');
-
- await waitForPromises();
-
- dropdown.vm.$emit('select', savedRepliesResponse.data.currentUser.savedReplies.nodes[0].id);
-
- await waitForPromises();
-
- expect(trackingSpy).toHaveBeenCalledWith(
- expect.any(String),
- 'i_code_review_saved_replies_use',
- expect.any(Object),
- );
+ describe('tracking', () => {
+ it('tracks overall usage', async () => {
+ await selectSavedReply();
+
+ expect(trackingSpy).toHaveBeenCalledWith(
+ expect.any(String),
+ TRACKING_SAVED_REPLIES_USE,
+ expect.any(Object),
+ );
+ });
+
+ describe('MR-specific usage event', () => {
+ it('is sent when in an MR', async () => {
+ window.location.toString.mockReturnValue('this/looks/like/a/-/merge_requests/1');
+
+ await selectSavedReply();
+
+ expect(trackingSpy).toHaveBeenCalledWith(
+ expect.any(String),
+ TRACKING_SAVED_REPLIES_USE_IN_MR,
+ expect.any(Object),
+ );
+ expect(trackingSpy).toHaveBeenCalledTimes(2);
+ });
+
+ it('is not sent when not in an MR', async () => {
+ window.location.toString.mockReturnValue('this/looks/like/a/-/issues/1');
+
+ await selectSavedReply();
+
+ expect(trackingSpy).not.toHaveBeenCalledWith(
+ expect.any(String),
+ TRACKING_SAVED_REPLIES_USE_IN_MR,
+ expect.any(Object),
+ );
+ expect(trackingSpy).toHaveBeenCalledTimes(1);
+ });
+ });
});
});
});
diff --git a/spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js b/spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js
index f04e1976a5f..7efc0e162b8 100644
--- a/spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js
@@ -138,7 +138,7 @@ describe('NewResourceDropdown component', () => {
});
it('dropdown button is not a link', () => {
- expect(findDropdown().attributes('split-href')).toBeUndefined();
+ expect(findDropdown().props('splitHref')).toBe('');
});
it('displays default text on the dropdown button', () => {
@@ -162,7 +162,7 @@ describe('NewResourceDropdown component', () => {
it('dropdown button is a link', () => {
const href = joinPaths(project1.webUrl, DASH_SCOPE, expectedPath);
- expect(findDropdown().attributes('split-href')).toBe(href);
+ expect(findDropdown().props('splitHref')).toBe(href);
});
it('displays project name on the dropdown button', () => {
@@ -199,7 +199,7 @@ describe('NewResourceDropdown component', () => {
await nextTick();
const dropdown = findDropdown();
- expect(dropdown.attributes('split-href')).toBe(
+ expect(dropdown.props('splitHref')).toBe(
joinPaths(project1.webUrl, DASH_SCOPE, 'issues/new'),
);
expect(dropdown.props('text')).toBe(`New issue in ${project1.name}`);
@@ -217,7 +217,7 @@ describe('NewResourceDropdown component', () => {
await nextTick();
const dropdown = findDropdown();
- expect(dropdown.attributes('split-href')).toBe(
+ expect(dropdown.props('splitHref')).toBe(
joinPaths(project1.webUrl, DASH_SCOPE, 'issues/new'),
);
expect(dropdown.props('text')).toBe(`New issue in ${project1.name}`);
diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb
index 65e02da2b5d..ee63433965e 100644
--- a/spec/models/label_spec.rb
+++ b/spec/models/label_spec.rb
@@ -2,7 +2,9 @@
require 'spec_helper'
-RSpec.describe Label do
+RSpec.describe Label, feature_category: :team_planning do
+ using RSpec::Parameterized::TableSyntax
+
let_it_be(:project) { create(:project) }
describe 'modules' do
@@ -162,11 +164,54 @@ RSpec.describe Label do
end
end
+ describe 'ensure_lock_on_merge_allowed' do
+ let(:validation_error) { 'can not be set for template labels' }
+
+ # rubocop:disable Rails/SaveBang
+ context 'when creating a label' do
+ let(:label) { described_class.create(title: 'test', template: template, lock_on_merge: lock_on_merge) }
+
+ where(:template, :lock_on_merge, :valid, :errors) do
+ false | false | true | []
+ false | true | true | []
+ true | false | true | []
+ true | true | false | [validation_error]
+ false | true | true | []
+ end
+
+ with_them do
+ it 'validates lock_on_merge on label creation' do
+ expect(label.valid?).to be(valid)
+ expect(label.errors[:lock_on_merge]).to eq(errors)
+ end
+ end
+ end
+ # rubocop:enable Rails/SaveBang
+
+ context 'when updating a label' do
+ let_it_be(:template_label) { create(:label, template: true) }
+
+ where(:lock_on_merge, :valid, :errors) do
+ true | false | [validation_error]
+ false | true | []
+ end
+
+ with_them do
+ it 'validates lock_on_merge value if label is a template' do
+ template_label.update_column(:lock_on_merge, lock_on_merge)
+
+ expect(template_label.valid?).to be(valid)
+ expect(template_label.errors[:lock_on_merge]).to eq(errors)
+ end
+ end
+ end
+ end
+
describe 'scopes' do
describe '.on_board' do
let(:board) { create(:board, project: project) }
- let!(:list1) { create(:list, board: board, label: development) }
- let!(:list2) { create(:list, board: board, label: testing) }
+ let!(:list1) { create(:list, board: board, label: development) }
+ let!(:list2) { create(:list, board: board, label: testing) }
let!(:development) { create(:label, project: project, name: 'Development') }
let!(:testing) { create(:label, project: project, name: 'Testing') }
@@ -176,6 +221,34 @@ RSpec.describe Label do
expect(described_class.on_board(board.id)).to match_array([development, testing])
end
end
+
+ describe '.with_lock_on_merge' do
+ let(:label) { create(:label, project: project, name: 'Label') }
+ let(:label_locked) { create(:label, project: project, name: 'Label locked', lock_on_merge: true) }
+
+ it 'return only locked labels' do
+ expect(described_class.with_lock_on_merge).to match_array([label_locked])
+ end
+ end
+ end
+
+ describe 'destroying labels' do
+ context 'when lock_on_merge is true' do
+ it 'prevents label from being destroyed' do
+ label = create(:label, lock_on_merge: true)
+
+ expect(label.destroy).to be false
+ end
+ end
+
+ context 'when lock_on_merge is false' do
+ it 'allows label to be destroyed' do
+ label = create(:label, lock_on_merge: false)
+
+ expect(label.destroy).to eq label
+ expect(label.destroyed?).to be_truthy
+ end
+ end
end
describe '#color' do
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index bfcc5a565c7..c677dc0315c 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -996,6 +996,7 @@ RSpec.describe Issues::UpdateService, :mailer, feature_category: :team_planning
let(:label_a) { label }
let(:label_b) { label2 }
let(:label_c) { label3 }
+ let(:label_locked) { create(:label, title: 'locked', project: project, lock_on_merge: true) }
let(:issuable) { issue }
it_behaves_like 'updating issuable labels'
diff --git a/spec/services/labels/available_labels_service_spec.rb b/spec/services/labels/available_labels_service_spec.rb
index 51314c2c226..c9f75283c75 100644
--- a/spec/services/labels/available_labels_service_spec.rb
+++ b/spec/services/labels/available_labels_service_spec.rb
@@ -2,15 +2,18 @@
require 'spec_helper'
RSpec.describe Labels::AvailableLabelsService, feature_category: :team_planning do
- let(:user) { create(:user) }
+ let_it_be(:user) { create(:user) }
let(:project) { create(:project, :public, group: group) }
let(:group) { create(:group) }
let(:project_label) { create(:label, project: project) }
+ let(:project_label_locked) { create(:label, project: project, lock_on_merge: true) }
let(:other_project_label) { create(:label) }
+ let(:other_project_label_locked) { create(:label, lock_on_merge: true) }
let(:group_label) { create(:group_label, group: group) }
+ let(:group_label_locked) { create(:group_label, group: group, lock_on_merge: true) }
let(:other_group_label) { create(:group_label) }
- let!(:labels) { [project_label, other_project_label, group_label, other_group_label] }
+ let!(:labels) { [project_label, other_project_label, group_label, other_group_label, project_label_locked, other_project_label_locked, group_label_locked] }
describe '#find_or_create_by_titles' do
let(:label_titles) { labels.map(&:title).push('non existing title') }
@@ -20,7 +23,7 @@ RSpec.describe Labels::AvailableLabelsService, feature_category: :team_planning
it 'returns only relevant label ids' do
result = described_class.new(user, project, labels: label_titles).find_or_create_by_titles
- expect(result).to match_array([project_label, group_label])
+ expect(result).to match_array([project_label, group_label, project_label_locked, group_label_locked])
end
end
@@ -32,7 +35,7 @@ RSpec.describe Labels::AvailableLabelsService, feature_category: :team_planning
it 'creates new labels for not found titles' do
result = described_class.new(user, project, labels: label_titles).find_or_create_by_titles
- expect(result.count).to eq(5)
+ expect(result.count).to eq(8)
expect(result).to include(project_label, group_label)
expect(result).not_to include(other_project_label, other_group_label)
end
@@ -53,7 +56,7 @@ RSpec.describe Labels::AvailableLabelsService, feature_category: :team_planning
it 'returns only relevant label ids' do
result = described_class.new(user, group, labels: label_titles).find_or_create_by_titles
- expect(result).to match_array([group_label])
+ expect(result).to match_array([group_label, group_label_locked])
end
end
@@ -65,9 +68,9 @@ RSpec.describe Labels::AvailableLabelsService, feature_category: :team_planning
it 'creates new labels for not found titles' do
result = described_class.new(user, group, labels: label_titles).find_or_create_by_titles
- expect(result.count).to eq(5)
- expect(result).to include(group_label)
- expect(result).not_to include(project_label, other_project_label, other_group_label)
+ expect(result.count).to eq(8)
+ expect(result).to include(group_label, group_label_locked)
+ expect(result).not_to include(project_label, other_project_label, other_group_label, project_label_locked, other_project_label_locked)
end
end
end
@@ -80,13 +83,13 @@ RSpec.describe Labels::AvailableLabelsService, feature_category: :team_planning
it 'returns only relevant label ids' do
result = described_class.new(user, project, ids: label_ids).filter_labels_ids_in_param(:ids)
- expect(result).to match_array([project_label.id, group_label.id])
+ expect(result).to match_array([project_label.id, group_label.id, project_label_locked.id, group_label_locked.id])
end
it 'returns labels in preserved order' do
result = described_class.new(user, project, ids: label_ids.reverse).filter_labels_ids_in_param(:ids)
- expect(result).to eq([group_label.id, project_label.id])
+ expect(result).to eq([group_label_locked.id, project_label_locked.id, group_label.id, project_label.id])
end
end
@@ -94,7 +97,7 @@ RSpec.describe Labels::AvailableLabelsService, feature_category: :team_planning
it 'returns only relevant label ids' do
result = described_class.new(user, group, ids: label_ids).filter_labels_ids_in_param(:ids)
- expect(result).to match_array([group_label.id])
+ expect(result).to match_array([group_label.id, group_label_locked.id])
end
end
@@ -105,14 +108,46 @@ RSpec.describe Labels::AvailableLabelsService, feature_category: :team_planning
end
end
+ describe '#filter_locked_labels_ids_in_param' do
+ let(:label_ids) { labels.map(&:id).push(non_existing_record_id) }
+
+ context 'when parent is a project' do
+ it 'returns only locked label ids' do
+ result = described_class.new(user, project, ids: label_ids).filter_locked_labels_ids_in_param(:ids)
+
+ expect(result).to match_array([project_label_locked.id, group_label_locked.id])
+ end
+
+ it 'returns labels in preserved order' do
+ result = described_class.new(user, project, ids: label_ids.reverse).filter_locked_labels_ids_in_param(:ids)
+
+ expect(result).to eq([group_label_locked.id, project_label_locked.id])
+ end
+ end
+
+ context 'when parent is a group' do
+ it 'returns only locked label ids' do
+ result = described_class.new(user, group, ids: label_ids).filter_locked_labels_ids_in_param(:ids)
+
+ expect(result).to match_array([group_label_locked.id])
+ end
+ end
+
+ it 'accepts a single id parameter' do
+ result = described_class.new(user, project, label_id: project_label_locked.id).filter_locked_labels_ids_in_param(:label_id)
+
+ expect(result).to match_array([project_label_locked.id])
+ end
+ end
+
describe '#available_labels' do
context 'when parent is a project' do
it 'returns only relevant labels' do
result = described_class.new(user, project, {}).available_labels
- expect(result.count).to eq(2)
- expect(result).to include(project_label, group_label)
- expect(result).not_to include(other_project_label, other_group_label)
+ expect(result.count).to eq(4)
+ expect(result).to include(project_label, group_label, project_label_locked, group_label_locked)
+ expect(result).not_to include(other_project_label, other_group_label, other_project_label_locked)
end
end
@@ -120,9 +155,9 @@ RSpec.describe Labels::AvailableLabelsService, feature_category: :team_planning
it 'returns only relevant labels' do
result = described_class.new(user, group, {}).available_labels
- expect(result.count).to eq(1)
- expect(result).to include(group_label)
- expect(result).not_to include(project_label, other_project_label, other_group_label)
+ expect(result.count).to eq(2)
+ expect(result).to include(group_label, group_label_locked)
+ expect(result).not_to include(project_label, other_project_label, other_group_label, project_label_locked, other_project_label_locked)
end
end
end
diff --git a/spec/services/labels/create_service_spec.rb b/spec/services/labels/create_service_spec.rb
index 9be611490cf..8dbe050990c 100644
--- a/spec/services/labels/create_service_spec.rb
+++ b/spec/services/labels/create_service_spec.rb
@@ -176,6 +176,42 @@ RSpec.describe Labels::CreateService, feature_category: :team_planning do
end
end
end
+
+ describe 'lock_on_merge' do
+ let_it_be(:params) { { title: 'Locked label', lock_on_merge: true } }
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(enforce_locked_labels_on_merge: false)
+ end
+
+ it 'does not allow setting lock_on_merge' do
+ label = described_class.new(params).execute(project: project)
+ label2 = described_class.new(params).execute(group: group)
+ label3 = described_class.new(params).execute(template: true)
+
+ expect(label.lock_on_merge).to be_falsey
+ expect(label2.lock_on_merge).to be_falsey
+ expect(label3).not_to be_persisted
+ end
+ end
+
+ context 'when feature flag is enabled' do
+ it 'allows setting lock_on_merge' do
+ label = described_class.new(params).execute(project: project)
+ label2 = described_class.new(params).execute(group: group)
+
+ expect(label.lock_on_merge).to be_truthy
+ expect(label2.lock_on_merge).to be_truthy
+ end
+
+ it 'does not alow setting lock_on_merge for templates' do
+ label = described_class.new(params).execute(template: true)
+
+ expect(label).not_to be_persisted
+ end
+ end
+ end
end
def params_with(color)
diff --git a/spec/services/labels/update_service_spec.rb b/spec/services/labels/update_service_spec.rb
index b9ac5282d10..9a8868dac10 100644
--- a/spec/services/labels/update_service_spec.rb
+++ b/spec/services/labels/update_service_spec.rb
@@ -71,6 +71,42 @@ RSpec.describe Labels::UpdateService, feature_category: :team_planning do
expect(label).not_to be_valid
end
end
+
+ describe 'lock_on_merge' do
+ let_it_be(:params) { { lock_on_merge: true } }
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(enforce_locked_labels_on_merge: false)
+ end
+
+ it 'does not allow setting lock_on_merge' do
+ label = described_class.new(params).execute(@label)
+
+ expect(label.reload.lock_on_merge).to be_falsey
+
+ template_label = Labels::CreateService.new(title: 'Initial').execute(template: true)
+ label = described_class.new(params).execute(template_label)
+
+ expect(label.reload.lock_on_merge).to be_falsey
+ end
+ end
+
+ context 'when feature flag is enabled' do
+ it 'allows setting lock_on_merge' do
+ label = described_class.new(params).execute(@label)
+
+ expect(label.reload.lock_on_merge).to be_truthy
+ end
+
+ it 'does not allow setting lock_on_merge for templates' do
+ template_label = Labels::CreateService.new(title: 'Initial').execute(template: true)
+ label = described_class.new(params).execute(template_label)
+
+ expect(label.reload.lock_on_merge).to be_falsey
+ end
+ end
+ end
end
def params_with(color)
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 7e0bd340fcd..2f6db13a041 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -1304,12 +1304,41 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re
let(:label_a) { label }
let(:label_b) { create(:label, title: 'b', project: project) }
let(:label_c) { create(:label, title: 'c', project: project) }
+ let(:label_locked) { create(:label, title: 'locked', project: project, lock_on_merge: true) }
let(:issuable) { merge_request }
it_behaves_like 'updating issuable labels'
it_behaves_like 'keeps issuable labels sorted after update'
it_behaves_like 'broadcasting issuable labels updates'
+ context 'when merge request has been merged' do
+ context 'when remove_label_ids contains a locked label' do
+ let(:params) { { remove_label_ids: [label_locked.id] } }
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(enforce_locked_labels_on_merge: false)
+ end
+
+ it 'removes locked labels' do
+ merge_request.update!(state: 'merged', labels: [label_a, label_locked])
+ update_issuable(params)
+
+ expect(merge_request.label_ids).to contain_exactly(label_a.id)
+ end
+ end
+
+ context 'when feature flag is enabled' do
+ it 'does not remove locked labels' do
+ merge_request.update!(state: 'merged', labels: [label_a, label_locked])
+ update_issuable(params)
+
+ expect(merge_request.label_ids).to contain_exactly(label_a.id, label_locked.id)
+ end
+ end
+ end
+ end
+
def update_issuable(update_params)
update_merge_request(update_params)
end
diff --git a/spec/support/helpers/features/runners_helpers.rb b/spec/support/helpers/features/runners_helpers.rb
index d5622885a69..dbd1edade8c 100644
--- a/spec/support/helpers/features/runners_helpers.rb
+++ b/spec/support/helpers/features/runners_helpers.rb
@@ -23,11 +23,11 @@ module Features
def input_filtered_search_keys(search_term)
focus_filtered_search
- page.within(search_bar_selector) do
- page.find('input').send_keys(search_term)
- click_on 'Search'
- end
+ page.find(search_bar_selector).find('input').send_keys(search_term)
+ # blur input
+ find('body').click
+ page.click_on 'Search'
wait_for_requests
end
diff --git a/spec/support/shared_examples/features/work_items_shared_examples.rb b/spec/support/shared_examples/features/work_items_shared_examples.rb
index 97407d93cb0..d3863c9a675 100644
--- a/spec/support/shared_examples/features/work_items_shared_examples.rb
+++ b/spec/support/shared_examples/features/work_items_shared_examples.rb
@@ -316,7 +316,7 @@ end
RSpec.shared_examples 'work items notifications' do
let(:actions_dropdown_selector) { '[data-testid="work-item-actions-dropdown"]' }
- let(:notifications_toggle_selector) { '[data-testid="notifications-toggle-action"] > button' }
+ let(:notifications_toggle_selector) { '[data-testid="notifications-toggle-action"] button[role="switch"]' }
it 'displays toast when notification is toggled' do
find(actions_dropdown_selector).click
diff --git a/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb
index 493a96b8dae..34188a8d18a 100644
--- a/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb
@@ -58,6 +58,12 @@ RSpec.shared_examples 'with auth_type' do
let(:current_params) { super().merge(auth_type: :foo) }
it { expect(payload['auth_type']).to eq('foo') }
+
+ it "contains the auth_type as part of the encoded user information in the payload" do
+ user_info = decode_user_info_from_payload(payload)
+
+ expect(user_info["token_type"]).to eq("foo")
+ end
end
RSpec.shared_examples 'a browsable' do
@@ -971,7 +977,16 @@ RSpec.shared_examples 'a container registry auth service' do
let(:authentication_abilities) { [:read_container_image] }
it_behaves_like 'an authenticated'
+
it { expect(payload['auth_type']).to eq('deploy_token') }
+
+ it "has encoded user information in the payload" do
+ user_info = decode_user_info_from_payload(payload)
+
+ expect(user_info["token_type"]).to eq('deploy_token')
+ expect(user_info["username"]).to eq(deploy_token.username)
+ expect(user_info["deploy_token_id"]).to eq(deploy_token.id)
+ end
end
end
@@ -1198,6 +1213,15 @@ RSpec.shared_examples 'a container registry auth service' do
it_behaves_like 'a pushable'
it_behaves_like 'container repository factory'
end
+
+ it "has encoded user information in the payload" do
+ user_info = decode_user_info_from_payload(payload)
+
+ expect(user_info["username"]).to eq(current_user.username)
+ expect(user_info["user_id"]).to eq(current_user.id)
+ end
+
+ it_behaves_like 'with auth_type'
end
end
@@ -1293,4 +1317,8 @@ RSpec.shared_examples 'a container registry auth service' do
end
end
end
+
+ def decode_user_info_from_payload(payload)
+ JWT.decode(payload["user"], nil, false)[0]["user_info"]
+ end
end
diff --git a/spec/support/shared_examples/services/issuable/issuable_update_service_shared_examples.rb b/spec/support/shared_examples/services/issuable/issuable_update_service_shared_examples.rb
index 77e4f9a98bb..3f95d6060ea 100644
--- a/spec/support/shared_examples/services/issuable/issuable_update_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/issuable/issuable_update_service_shared_examples.rb
@@ -132,6 +132,17 @@ RSpec.shared_examples 'updating issuable labels' do
expect(issuable.labels).to contain_exactly(label_c)
end
end
+
+ context 'when remove_label_ids contains a locked label' do
+ let(:params) { { remove_label_ids: [label_locked.id] } }
+
+ it 'removes locked labels for non-merged issuables' do
+ issuable.update!(labels: [label_a, label_locked])
+ update_issuable(params)
+
+ expect(issuable.label_ids).to contain_exactly(label_a.id)
+ end
+ end
end
RSpec.shared_examples 'keeps issuable labels sorted after update' do
diff --git a/yarn.lock b/yarn.lock
index 145eb3e665e..4a254c2fe77 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1132,14 +1132,13 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.59.0.tgz#21090154aa7987e059264e13182c4c60e6d0d4b3"
integrity sha512-5+FZ0Clwtf2X6oHEEVCwbhqhmnxT8Ds1CGFxHzzWsvQ5Hkdt658BVAicsbvQSU+TuEIhnKOK3BfooyleMUwLlQ==
-"@gitlab/ui@64.20.1":
- version "64.20.1"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-64.20.1.tgz#4ca5ca7a854be5c15afcc464d16498b9b833ad6e"
- integrity sha512-yB4gRRzQCIQxxHnD50j3uOAcg5mhSBw9+tRi9kDqn+CxozDUNC/SNfTy+v7cTLJBn2mdxHfw++cuJcH3gRajxQ==
+"@gitlab/ui@65.0.1":
+ version "65.0.1"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-65.0.1.tgz#4ce7ff6cc9c1ceb54111ba095bb64944f577390d"
+ integrity sha512-cVMYYJPRwtNviReOVU3U08wFUt8k8Lo/ypbTgpWdio42kldr2foj5xRTN3jsKu5WwlePA6WEGOryl4MsGaQXhg==
dependencies:
"@floating-ui/dom" "1.2.9"
bootstrap-vue "2.23.1"
- dompurify "^2.4.5"
echarts "^5.3.2"
iframe-resizer "^4.3.2"
lodash "^4.17.20"