Commit bc94b1ff authored by Olena Horal-Koretska's avatar Olena Horal-Koretska

Merge branch '222885-update-design-of-the-container-registry-cleanup-policy-for-tags' into 'master'

Update Design of the Container Registry Cleanup Policy for tags

See merge request gitlab-org/gitlab!48243
parents 06e65234 6f1d629f
...@@ -36,7 +36,8 @@ export default { ...@@ -36,7 +36,8 @@ export default {
}, },
placeholder: { placeholder: {
type: String, type: String,
required: true, required: false,
default: '',
}, },
description: { description: {
type: String, type: String,
......
<script> <script>
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui'; import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
import { isEqual, get } from 'lodash'; import { isEqual, get, isEmpty } from 'lodash';
import expirationPolicyQuery from '../graphql/queries/get_expiration_policy.graphql'; import expirationPolicyQuery from '../graphql/queries/get_expiration_policy.graphql';
import { FETCH_SETTINGS_ERROR_MESSAGE } from '../../shared/constants'; import { FETCH_SETTINGS_ERROR_MESSAGE } from '../../shared/constants';
...@@ -60,6 +60,9 @@ export default { ...@@ -60,6 +60,9 @@ export default {
return this.isAdmin ? UNAVAILABLE_ADMIN_FEATURE_TEXT : UNAVAILABLE_USER_FEATURE_TEXT; return this.isAdmin ? UNAVAILABLE_ADMIN_FEATURE_TEXT : UNAVAILABLE_USER_FEATURE_TEXT;
}, },
isEdited() { isEdited() {
if (isEmpty(this.containerExpirationPolicy) && isEmpty(this.workingCopy)) {
return false;
}
return !isEqual(this.containerExpirationPolicy, this.workingCopy); return !isEqual(this.containerExpirationPolicy, this.workingCopy);
}, },
}, },
......
<script> <script>
import { GlCard, GlButton } from '@gitlab/ui'; import { GlCard, GlButton, GlSprintf } from '@gitlab/ui';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import { import {
UPDATE_SETTINGS_ERROR_MESSAGE, UPDATE_SETTINGS_ERROR_MESSAGE,
UPDATE_SETTINGS_SUCCESS_MESSAGE, UPDATE_SETTINGS_SUCCESS_MESSAGE,
} from '../../shared/constants'; } from '~/registry/shared/constants';
import ExpirationPolicyFields from '../../shared/components/expiration_policy_fields.vue'; import {
import { SET_CLEANUP_POLICY_BUTTON, CLEANUP_POLICY_CARD_HEADER } from '../constants'; SET_CLEANUP_POLICY_BUTTON,
KEEP_HEADER_TEXT,
KEEP_INFO_TEXT,
KEEP_N_LABEL,
NAME_REGEX_KEEP_LABEL,
NAME_REGEX_KEEP_DESCRIPTION,
REMOVE_HEADER_TEXT,
REMOVE_INFO_TEXT,
EXPIRATION_SCHEDULE_LABEL,
NAME_REGEX_LABEL,
NAME_REGEX_PLACEHOLDER,
NAME_REGEX_DESCRIPTION,
CADENCE_LABEL,
EXPIRATION_POLICY_FOOTER_NOTE,
} from '~/registry/settings/constants';
import { formOptionsGenerator } from '~/registry/shared/utils'; import { formOptionsGenerator } from '~/registry/shared/utils';
import updateContainerExpirationPolicyMutation from '../graphql/mutations/update_container_expiration_policy.graphql'; import updateContainerExpirationPolicyMutation from '~/registry/settings/graphql/mutations/update_container_expiration_policy.graphql';
import { updateContainerExpirationPolicy } from '../graphql/utils/cache_update'; import { updateContainerExpirationPolicy } from '~/registry/settings/graphql/utils/cache_update';
import ExpirationDropdown from './expiration_dropdown.vue';
import ExpirationTextarea from './expiration_textarea.vue';
import ExpirationToggle from './expiration_toggle.vue';
import ExpirationRunText from './expiration_run_text.vue';
export default { export default {
components: { components: {
GlCard, GlCard,
GlButton, GlButton,
ExpirationPolicyFields, GlSprintf,
ExpirationDropdown,
ExpirationTextarea,
ExpirationToggle,
ExpirationRunText,
}, },
mixins: [Tracking.mixin()], mixins: [Tracking.mixin()],
inject: ['projectPath'], inject: ['projectPath'],
...@@ -35,22 +57,31 @@ export default { ...@@ -35,22 +57,31 @@ export default {
default: false, default: false,
}, },
}, },
labelsConfig: {
cols: 3,
align: 'right',
},
formOptions: formOptionsGenerator(), formOptions: formOptionsGenerator(),
i18n: { i18n: {
CLEANUP_POLICY_CARD_HEADER, KEEP_HEADER_TEXT,
KEEP_INFO_TEXT,
KEEP_N_LABEL,
NAME_REGEX_KEEP_LABEL,
SET_CLEANUP_POLICY_BUTTON, SET_CLEANUP_POLICY_BUTTON,
NAME_REGEX_KEEP_DESCRIPTION,
REMOVE_HEADER_TEXT,
REMOVE_INFO_TEXT,
EXPIRATION_SCHEDULE_LABEL,
NAME_REGEX_LABEL,
NAME_REGEX_PLACEHOLDER,
NAME_REGEX_DESCRIPTION,
CADENCE_LABEL,
EXPIRATION_POLICY_FOOTER_NOTE,
}, },
data() { data() {
return { return {
tracking: { tracking: {
label: 'docker_container_retention_and_expiration_policies', label: 'docker_container_retention_and_expiration_policies',
}, },
fieldsAreValid: true, apiErrors: {},
apiErrors: null, localErrors: {},
mutationLoading: false, mutationLoading: false,
}; };
}, },
...@@ -66,12 +97,18 @@ export default { ...@@ -66,12 +97,18 @@ export default {
showLoadingIcon() { showLoadingIcon() {
return this.isLoading || this.mutationLoading; return this.isLoading || this.mutationLoading;
}, },
fieldsAreValid() {
return Object.values(this.localErrors).every(error => error);
},
isSubmitButtonDisabled() { isSubmitButtonDisabled() {
return !this.fieldsAreValid || this.showLoadingIcon; return !this.fieldsAreValid || this.showLoadingIcon;
}, },
isCancelButtonDisabled() { isCancelButtonDisabled() {
return !this.isEdited || this.isLoading || this.mutationLoading; return !this.isEdited || this.isLoading || this.mutationLoading;
}, },
isFieldDisabled() {
return this.showLoadingIcon || !this.value.enabled;
},
mutationVariables() { mutationVariables() {
return { return {
projectPath: this.projectPath, projectPath: this.projectPath,
...@@ -90,7 +127,8 @@ export default { ...@@ -90,7 +127,8 @@ export default {
}, },
reset() { reset() {
this.track('reset_form'); this.track('reset_form');
this.apiErrors = null; this.apiErrors = {};
this.localErrors = {};
this.$emit('reset'); this.$emit('reset');
}, },
setApiErrors(response) { setApiErrors(response) {
...@@ -101,9 +139,15 @@ export default { ...@@ -101,9 +139,15 @@ export default {
return acc; return acc;
}, {}); }, {});
}, },
setLocalErrors(state, model) {
this.localErrors = {
...this.localErrors,
[model]: state,
};
},
submit() { submit() {
this.track('submit_form'); this.track('submit_form');
this.apiErrors = null; this.apiErrors = {};
this.mutationLoading = true; this.mutationLoading = true;
return this.$apollo return this.$apollo
.mutate({ .mutate({
...@@ -129,11 +173,9 @@ export default { ...@@ -129,11 +173,9 @@ export default {
this.mutationLoading = false; this.mutationLoading = false;
}); });
}, },
onModelChange(changePayload) { onModelChange(newValue, model) {
this.$emit('input', changePayload.newValue); this.$emit('input', { ...this.value, [model]: newValue });
if (this.apiErrors) { this.apiErrors[model] = undefined;
this.apiErrors[changePayload.modified] = undefined;
}
}, },
}, },
}; };
...@@ -141,42 +183,129 @@ export default { ...@@ -141,42 +183,129 @@ export default {
<template> <template>
<form ref="form-element" @submit.prevent="submit" @reset.prevent="reset"> <form ref="form-element" @submit.prevent="submit" @reset.prevent="reset">
<gl-card> <expiration-toggle
:value="prefilledForm.enabled"
:disabled="showLoadingIcon"
class="gl-mb-0!"
data-testid="enable-toggle"
@input="onModelChange($event, 'enabled')"
/>
<div class="gl-display-flex gl-mt-7">
<expiration-dropdown
v-model="prefilledForm.cadence"
:disabled="isFieldDisabled"
:form-options="$options.formOptions.cadence"
:label="$options.i18n.CADENCE_LABEL"
name="cadence"
class="gl-mr-7 gl-mb-0!"
data-testid="cadence-dropdown"
@input="onModelChange($event, 'cadence')"
/>
<expiration-run-text :value="prefilledForm.nextRunAt" class="gl-mb-0!" />
</div>
<gl-card class="gl-mt-7">
<template #header> <template #header>
{{ $options.i18n.CLEANUP_POLICY_CARD_HEADER }} {{ $options.i18n.KEEP_HEADER_TEXT }}
</template> </template>
<template #default> <template #default>
<expiration-policy-fields <div>
:value="prefilledForm" <p>
:form-options="$options.formOptions" <gl-sprintf :message="$options.i18n.KEEP_INFO_TEXT">
:is-loading="isLoading" <template #strong="{content}">
:api-errors="apiErrors" <strong>{{ content }}</strong>
@validated="fieldsAreValid = true" </template>
@invalidated="fieldsAreValid = false" <template #secondStrong="{content}">
@input="onModelChange" <strong>{{ content }}</strong>
/> </template>
</gl-sprintf>
</p>
<expiration-dropdown
v-model="prefilledForm.keepN"
:disabled="isFieldDisabled"
:form-options="$options.formOptions.keepN"
:label="$options.i18n.KEEP_N_LABEL"
name="keep-n"
data-testid="keep-n-dropdown"
@input="onModelChange($event, 'keepN')"
/>
<expiration-textarea
v-model="prefilledForm.nameRegexKeep"
:error="apiErrors.nameRegexKeep"
:disabled="isFieldDisabled"
:label="$options.i18n.NAME_REGEX_KEEP_LABEL"
:description="$options.i18n.NAME_REGEX_KEEP_DESCRIPTION"
name="keep-regex"
data-testid="keep-regex-textarea"
@input="onModelChange($event, 'nameRegexKeep')"
@validation="setLocalErrors($event, 'nameRegexKeep')"
/>
</div>
</template>
</gl-card>
<gl-card class="gl-mt-7">
<template #header>
{{ $options.i18n.REMOVE_HEADER_TEXT }}
</template> </template>
<template #footer> <template #default>
<gl-button <div>
ref="cancel-button" <p>
type="reset" <gl-sprintf :message="$options.i18n.REMOVE_INFO_TEXT">
class="gl-mr-3 gl-display-block float-right" <template #strong="{content}">
:disabled="isCancelButtonDisabled" <strong>{{ content }}</strong>
> </template>
{{ __('Cancel') }} <template #secondStrong="{content}">
</gl-button> <strong>{{ content }}</strong>
<gl-button </template>
ref="save-button" </gl-sprintf>
type="submit" </p>
:disabled="isSubmitButtonDisabled" <expiration-dropdown
:loading="showLoadingIcon" v-model="prefilledForm.olderThan"
variant="success" :disabled="isFieldDisabled"
category="primary" :form-options="$options.formOptions.olderThan"
class="js-no-auto-disable" :label="$options.i18n.EXPIRATION_SCHEDULE_LABEL"
> name="older-than"
{{ $options.i18n.SET_CLEANUP_POLICY_BUTTON }} data-testid="older-than-dropdown"
</gl-button> @input="onModelChange($event, 'olderThan')"
/>
<expiration-textarea
v-model="prefilledForm.nameRegex"
:error="apiErrors.nameRegex"
:disabled="isFieldDisabled"
:label="$options.i18n.NAME_REGEX_LABEL"
:placeholder="$options.i18n.NAME_REGEX_PLACEHOLDER"
:description="$options.i18n.NAME_REGEX_DESCRIPTION"
name="remove-regex"
data-testid="remove-regex-textarea"
@input="onModelChange($event, 'nameRegex')"
@validation="setLocalErrors($event, 'nameRegex')"
/>
</div>
</template> </template>
</gl-card> </gl-card>
<div class="gl-mt-7 gl-display-flex gl-align-items-center">
<gl-button
data-testid="save-button"
type="submit"
:disabled="isSubmitButtonDisabled"
:loading="showLoadingIcon"
variant="success"
category="primary"
class="js-no-auto-disable gl-mr-4"
>
{{ $options.i18n.SET_CLEANUP_POLICY_BUTTON }}
</gl-button>
<gl-button
data-testid="cancel-button"
type="reset"
:disabled="isCancelButtonDisabled"
class="gl-mr-4"
>
{{ __('Cancel') }}
</gl-button>
<span class="gl-font-style-italic gl-text-gray-400">{{
$options.i18n.EXPIRATION_POLICY_FOOTER_NOTE
}}</span>
</div>
</form> </form>
</template> </template>
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
export const SET_CLEANUP_POLICY_BUTTON = __('Save'); export const SET_CLEANUP_POLICY_BUTTON = __('Save');
export const CLEANUP_POLICY_CARD_HEADER = s__('ContainerRegistry|Tag expiration policy');
export const UNAVAILABLE_FEATURE_TITLE = s__( export const UNAVAILABLE_FEATURE_TITLE = s__(
`ContainerRegistry|Cleanup policy for tags is disabled`, `ContainerRegistry|Cleanup policy for tags is disabled`,
); );
...@@ -19,34 +18,33 @@ export const TEXT_AREA_INVALID_FEEDBACK = s__( ...@@ -19,34 +18,33 @@ export const TEXT_AREA_INVALID_FEEDBACK = s__(
export const KEEP_HEADER_TEXT = s__('ContainerRegistry|Keep these tags'); export const KEEP_HEADER_TEXT = s__('ContainerRegistry|Keep these tags');
export const KEEP_INFO_TEXT = s__( export const KEEP_INFO_TEXT = s__(
'ContainerRegistry|Tags that match these rules will always be %{strongStart}kept%{strongEnd}, even if they match a removal rule below. The %{secondStrongStart}latest%{secondStrongEnd} tag will always be kept.', 'ContainerRegistry|Tags that match these rules are %{strongStart}kept%{strongEnd}, even if they match a removal rule below. The %{secondStrongStart}latest%{secondStrongEnd} tag is always kept.',
); );
export const KEEP_N_LABEL = s__('ContainerRegistry|Keep the most recent:'); export const KEEP_N_LABEL = s__('ContainerRegistry|Keep the most recent:');
export const NAME_REGEX_KEEP_LABEL = s__('ContainerRegistry|Keep tags matching:'); export const NAME_REGEX_KEEP_LABEL = s__('ContainerRegistry|Keep tags matching:');
export const NAME_REGEX_KEEP_PLACEHOLDER = 'production-v.*';
export const NAME_REGEX_KEEP_DESCRIPTION = s__( export const NAME_REGEX_KEEP_DESCRIPTION = s__(
'ContainerRegistry|Tags with names matching this regex pattern will be kept. %{linkStart}More information%{linkEnd}', 'ContainerRegistry|Tags with names that match this regex pattern are kept. %{linkStart}More information%{linkEnd}',
); );
export const REMOVE_HEADER_TEXT = s__('ContainerRegistry|Remove these tags'); export const REMOVE_HEADER_TEXT = s__('ContainerRegistry|Remove these tags');
export const REMOVE_INFO_TEXT = s__( export const REMOVE_INFO_TEXT = s__(
'ContainerRegistry|Tags that match these rules will be %{strongStart}removed%{strongEnd}, unless kept by a rule above.', 'ContainerRegistry|Tags that match these rules are %{strongStart}removed%{strongEnd}, unless a rule above says to keep them.',
); );
export const EXPIRATION_SCHEDULE_LABEL = s__('ContainerRegistry|Remove tags older than:'); export const EXPIRATION_SCHEDULE_LABEL = s__('ContainerRegistry|Remove tags older than:');
export const NAME_REGEX_LABEL = s__('ContainerRegistry|Remove tags matching:'); export const NAME_REGEX_LABEL = s__('ContainerRegistry|Remove tags matching:');
export const NAME_REGEX_PLACEHOLDER = '.*'; export const NAME_REGEX_PLACEHOLDER = '.*';
export const NAME_REGEX_DESCRIPTION = s__( export const NAME_REGEX_DESCRIPTION = s__(
'ContainerRegistry|Tags with names matching this regex pattern will be removed. %{linkStart}More information%{linkEnd}', 'ContainerRegistry|Tags with names that match this regex pattern are removed. %{linkStart}More information%{linkEnd}',
); );
export const ENABLED_TEXT = __('Enabled'); export const ENABLED_TEXT = __('Enabled');
export const DISABLED_TEXT = __('Disabled'); export const DISABLED_TEXT = __('Disabled');
export const ENABLE_TOGGLE_DESCRIPTION = s__( export const ENABLE_TOGGLE_DESCRIPTION = s__(
'ContainerRegistry|%{toggleStatus} - Tags matching the rules defined below will be automatically scheduled for deletion.', 'ContainerRegistry|%{toggleStatus} - Tags that match the rules on this page are automatically scheduled for deletion.',
); );
export const CADENCE_LABEL = s__('ContainerRegistry|Run cleanup every:'); export const CADENCE_LABEL = s__('ContainerRegistry|Run cleanup:');
export const NEXT_CLEANUP_LABEL = s__('ContainerRegistry|Next cleanup scheduled to run on:'); export const NEXT_CLEANUP_LABEL = s__('ContainerRegistry|Next cleanup scheduled to run on:');
export const NOT_SCHEDULED_POLICY_TEXT = s__('ContainerRegistry|Not yet scheduled'); export const NOT_SCHEDULED_POLICY_TEXT = s__('ContainerRegistry|Not yet scheduled');
......
...@@ -21,12 +21,7 @@ export const mapComputedToEvent = (list, root) => { ...@@ -21,12 +21,7 @@ export const mapComputedToEvent = (list, root) => {
return result; return result;
}; };
export const olderThanTranslationGenerator = variable => export const olderThanTranslationGenerator = variable => n__('%d day', '%d days', variable);
n__(
'%d day until tags are automatically removed',
'%d days until tags are automatically removed',
variable,
);
export const keepNTranslationGenerator = variable => export const keepNTranslationGenerator = variable =>
n__('%d tag per image name', '%d tags per image name', variable); n__('%d tag per image name', '%d tags per image name', variable);
......
...@@ -66,11 +66,11 @@ ...@@ -66,11 +66,11 @@
%section.settings.no-animate#js-registry-policies{ class: ('expanded' if expanded) } %section.settings.no-animate#js-registry-policies{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4 %h4
= _("Cleanup policy for tags") = _("Clean up image tags")
%button.btn.js-settings-toggle{ type: 'button' } %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand') = expanded ? _('Collapse') : _('Expand')
%p %p
= _("Save space and find tags in the Container Registry more easily. Enable the cleanup policy to remove stale tags and keep only the ones you need.") = _("Save space and find images in the Container Registry. Remove unneeded tags and keep only the ones you want.")
= link_to _('More information'), help_page_path('user/packages/container_registry/index', anchor: 'cleanup-policy', target: '_blank', rel: 'noopener noreferrer') = link_to _('More information'), help_page_path('user/packages/container_registry/index', anchor: 'cleanup-policy', target: '_blank', rel: 'noopener noreferrer')
.settings-content .settings-content
= render 'projects/registry/settings/index' = render 'projects/registry/settings/index'
......
---
title: Update Design of the Container Registry Cleanup Policy for tags
merge_request: 48243
author:
type: changed
...@@ -513,24 +513,24 @@ You can create a cleanup policy in [the API](#use-the-cleanup-policy-api) or the ...@@ -513,24 +513,24 @@ You can create a cleanup policy in [the API](#use-the-cleanup-policy-api) or the
To create a cleanup policy in the UI: To create a cleanup policy in the UI:
1. For your project, go to **Settings > CI/CD**. 1. For your project, go to **Settings > CI/CD**.
1. Expand the **Cleanup policy for tags** section. 1. Expand the **Clean up image tags** section.
1. Complete the fields. 1. Complete the fields.
| Field | Description | | Field | Description |
|---------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------| |---------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------|
| **Cleanup policy** | Turn the policy on or off. | | **Toggle** | Turn the policy on or off. |
| **Expiration interval** | How long tags are exempt from being deleted. | | **Run cleanup** | How often the policy should run. |
| **Expiration schedule** | How often the policy should run. | | **Keep the most recent** | How many tags to _always_ keep for each image. |
| **Number of tags to retain** | How many tags to _always_ keep for each image. | | **Keep tags matching** | The regex pattern that determines which tags to preserve. The `latest` tag is always preserved. For all tags, use `.*`. See other [regex pattern examples](#regex-pattern-examples). |
| **Tags with names matching this regex pattern expire:** | The regex pattern that determines which tags to remove. This value cannot be blank. For all tags, use `.*`. See other [regex pattern examples](#regex-pattern-examples). | | **Remove tags older than** | Remove only tags older than X days. |
| **Tags with names matching this regex pattern are preserved:** | The regex pattern that determines which tags to preserve. The `latest` tag is always preserved. For all tags, use `.*`. See other [regex pattern examples](#regex-pattern-examples). | | **Remove tags matching** | The regex pattern that determines which tags to remove. This value cannot be blank. For all tags, use `.*`. See other [regex pattern examples](#regex-pattern-examples). |
1. Click **Set cleanup policy**. 1. Click **Save**.
Depending on the interval you chose, the policy is scheduled to run. Depending on the interval you chose, the policy is scheduled to run.
NOTE: **Note:** NOTE: **Note:**
If you edit the policy and click **Set cleanup policy** again, the interval is reset. If you edit the policy and click **Save** again, the interval is reset.
### Regex pattern examples ### Regex pattern examples
......
...@@ -170,11 +170,6 @@ msgid_plural "%d days" ...@@ -170,11 +170,6 @@ msgid_plural "%d days"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "%d day until tags are automatically removed"
msgid_plural "%d days until tags are automatically removed"
msgstr[0] ""
msgstr[1] ""
msgid "%d error" msgid "%d error"
msgid_plural "%d errors" msgid_plural "%d errors"
msgstr[0] "" msgstr[0] ""
...@@ -5571,7 +5566,7 @@ msgstr "" ...@@ -5571,7 +5566,7 @@ msgstr ""
msgid "ClassificationLabelUnavailable|is unavailable: %{reason}" msgid "ClassificationLabelUnavailable|is unavailable: %{reason}"
msgstr "" msgstr ""
msgid "Cleanup policy for tags" msgid "Clean up image tags"
msgstr "" msgstr ""
msgid "Cleanup policy maximum processing time (seconds)" msgid "Cleanup policy maximum processing time (seconds)"
...@@ -7266,7 +7261,7 @@ msgstr "" ...@@ -7266,7 +7261,7 @@ msgstr ""
msgid "ContainerRegistry|%{toggleStatus} - Tags matching the patterns defined below will be scheduled for deletion" msgid "ContainerRegistry|%{toggleStatus} - Tags matching the patterns defined below will be scheduled for deletion"
msgstr "" msgstr ""
msgid "ContainerRegistry|%{toggleStatus} - Tags matching the rules defined below will be automatically scheduled for deletion." msgid "ContainerRegistry|%{toggleStatus} - Tags that match the rules on this page are automatically scheduled for deletion."
msgstr "" msgstr ""
msgid "ContainerRegistry|Build an image" msgid "ContainerRegistry|Build an image"
...@@ -7403,7 +7398,7 @@ msgstr "" ...@@ -7403,7 +7398,7 @@ msgstr ""
msgid "ContainerRegistry|Remove these tags" msgid "ContainerRegistry|Remove these tags"
msgstr "" msgstr ""
msgid "ContainerRegistry|Run cleanup every:" msgid "ContainerRegistry|Run cleanup:"
msgstr "" msgstr ""
msgid "ContainerRegistry|Some tags were not deleted" msgid "ContainerRegistry|Some tags were not deleted"
...@@ -7436,19 +7431,16 @@ msgstr "" ...@@ -7436,19 +7431,16 @@ msgstr ""
msgid "ContainerRegistry|Sorry, your filter produced no results." msgid "ContainerRegistry|Sorry, your filter produced no results."
msgstr "" msgstr ""
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag successfully marked for deletion." msgid "ContainerRegistry|Tag successfully marked for deletion."
msgstr "" msgstr ""
msgid "ContainerRegistry|Tags successfully marked for deletion." msgid "ContainerRegistry|Tags successfully marked for deletion."
msgstr "" msgstr ""
msgid "ContainerRegistry|Tags that match these rules will always be %{strongStart}kept%{strongEnd}, even if they match a removal rule below. The %{secondStrongStart}latest%{secondStrongEnd} tag will always be kept." msgid "ContainerRegistry|Tags that match these rules are %{strongStart}kept%{strongEnd}, even if they match a removal rule below. The %{secondStrongStart}latest%{secondStrongEnd} tag is always kept."
msgstr "" msgstr ""
msgid "ContainerRegistry|Tags that match these rules will be %{strongStart}removed%{strongEnd}, unless kept by a rule above." msgid "ContainerRegistry|Tags that match these rules are %{strongStart}removed%{strongEnd}, unless a rule above says to keep them."
msgstr "" msgstr ""
msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}" msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
...@@ -7457,10 +7449,10 @@ msgstr "" ...@@ -7457,10 +7449,10 @@ msgstr ""
msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}" msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
msgstr "" msgstr ""
msgid "ContainerRegistry|Tags with names matching this regex pattern will be kept. %{linkStart}More information%{linkEnd}" msgid "ContainerRegistry|Tags with names that match this regex pattern are kept. %{linkStart}More information%{linkEnd}"
msgstr "" msgstr ""
msgid "ContainerRegistry|Tags with names matching this regex pattern will be removed. %{linkStart}More information%{linkEnd}" msgid "ContainerRegistry|Tags with names that match this regex pattern are removed. %{linkStart}More information%{linkEnd}"
msgstr "" msgstr ""
msgid "ContainerRegistry|The cleanup policy timed out before it could delete all tags. An administrator can %{adminLinkStart}manually run cleanup now%{adminLinkEnd} or you can wait for the cleanup policy to automatically run again. %{docLinkStart}More information%{docLinkEnd}" msgid "ContainerRegistry|The cleanup policy timed out before it could delete all tags. An administrator can %{adminLinkStart}manually run cleanup now%{adminLinkEnd} or you can wait for the cleanup policy to automatically run again. %{docLinkStart}More information%{docLinkEnd}"
...@@ -23757,7 +23749,7 @@ msgstr "" ...@@ -23757,7 +23749,7 @@ msgstr ""
msgid "Save pipeline schedule" msgid "Save pipeline schedule"
msgstr "" msgstr ""
msgid "Save space and find tags in the Container Registry more easily. Enable the cleanup policy to remove stale tags and keep only the ones you need." msgid "Save space and find images in the Container Registry. Remove unneeded tags and keep only the ones you want."
msgstr "" msgstr ""
msgid "Saved scan settings and target site settings which are reusable." msgid "Saved scan settings and target site settings which are reusable."
......
...@@ -26,20 +26,20 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p ...@@ -26,20 +26,20 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
subject subject
settings_block = find('#js-registry-policies') settings_block = find('#js-registry-policies')
expect(settings_block).to have_text 'Cleanup policy for tags' expect(settings_block).to have_text 'Clean up image tags'
end end
it 'saves cleanup policy submit the form' do it 'saves cleanup policy submit the form' do
subject subject
within '#js-registry-policies' do within '#js-registry-policies' do
within '.gl-card-body' do select('Every day', from: 'Run cleanup')
select('7 days until tags are automatically removed', from: 'Expiration interval:') select('50 tags per image name', from: 'Keep the most recent:')
select('Every day', from: 'Expiration schedule:') fill_in('Keep tags matching:', with: 'stable')
select('50 tags per image name', from: 'Number of tags to retain:') select('7 days', from: 'Remove tags older than:')
fill_in('Tags with names matching this regex pattern will expire:', with: '.*-production') fill_in('Remove tags matching:', with: '.*-production')
end
submit_button = find('.gl-card-footer .btn.btn-success') submit_button = find('.btn.btn-success')
expect(submit_button).not_to be_disabled expect(submit_button).not_to be_disabled
submit_button.click submit_button.click
end end
...@@ -51,10 +51,9 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p ...@@ -51,10 +51,9 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
subject subject
within '#js-registry-policies' do within '#js-registry-policies' do
within '.gl-card-body' do fill_in('Remove tags matching:', with: '*-production')
fill_in('Tags with names matching this regex pattern will expire:', with: '*-production')
end submit_button = find('.btn.btn-success')
submit_button = find('.gl-card-footer .btn.btn-success')
expect(submit_button).not_to be_disabled expect(submit_button).not_to be_disabled
submit_button.click submit_button.click
end end
...@@ -85,7 +84,7 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p ...@@ -85,7 +84,7 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
within '#js-registry-policies' do within '#js-registry-policies' do
case result case result
when :available_section when :available_section
expect(find('.gl-card-header')).to have_content('Tag expiration policy') expect(find('[data-testid="enable-toggle"]')).to have_content('Tags that match the rules on this page are automatically scheduled for deletion.')
when :disabled_message when :disabled_message
expect(find('.gl-alert-title')).to have_content('Cleanup policy for tags is disabled') expect(find('.gl-alert-title')).to have_content('Cleanup policy for tags is disabled')
end end
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Settings Form Cadence matches snapshot 1`] = `
<expiration-dropdown-stub
class="gl-mr-7 gl-mb-0!"
data-testid="cadence-dropdown"
formoptions="[object Object],[object Object],[object Object],[object Object],[object Object]"
label="Run cleanup:"
name="cadence"
value="EVERY_DAY"
/>
`;
exports[`Settings Form Enable matches snapshot 1`] = `
<expiration-toggle-stub
class="gl-mb-0!"
data-testid="enable-toggle"
value="true"
/>
`;
exports[`Settings Form Keep N matches snapshot 1`] = `
<expiration-dropdown-stub
data-testid="keep-n-dropdown"
formoptions="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]"
label="Keep the most recent:"
name="keep-n"
value="TEN_TAGS"
/>
`;
exports[`Settings Form Keep Regex matches snapshot 1`] = `
<expiration-textarea-stub
data-testid="keep-regex-textarea"
description="Tags with names that match this regex pattern are kept. %{linkStart}More information%{linkEnd}"
error=""
label="Keep tags matching:"
name="keep-regex"
placeholder=""
value="sss"
/>
`;
exports[`Settings Form OlderThan matches snapshot 1`] = `
<expiration-dropdown-stub
data-testid="older-than-dropdown"
formoptions="[object Object],[object Object],[object Object],[object Object]"
label="Remove tags older than:"
name="older-than"
value="FOURTEEN_DAYS"
/>
`;
exports[`Settings Form Remove regex matches snapshot 1`] = `
<expiration-textarea-stub
data-testid="remove-regex-textarea"
description="Tags with names that match this regex pattern are removed. %{linkStart}More information%{linkEnd}"
error=""
label="Remove tags matching:"
name="remove-regex"
placeholder=".*"
value="asdasdssssdfdf"
/>
`;
...@@ -11,7 +11,11 @@ import { ...@@ -11,7 +11,11 @@ import {
UNAVAILABLE_USER_FEATURE_TEXT, UNAVAILABLE_USER_FEATURE_TEXT,
} from '~/registry/settings/constants'; } from '~/registry/settings/constants';
import { expirationPolicyPayload, emptyExpirationPolicyPayload } from '../mock_data'; import {
expirationPolicyPayload,
emptyExpirationPolicyPayload,
containerExpirationPolicyData,
} from '../mock_data';
const localVue = createLocalVue(); const localVue = createLocalVue();
...@@ -62,6 +66,29 @@ describe('Registry Settings App', () => { ...@@ -62,6 +66,29 @@ describe('Registry Settings App', () => {
wrapper.destroy(); wrapper.destroy();
}); });
describe('isEdited status', () => {
it.each`
description | apiResponse | workingCopy | result
${'empty response and no changes from user'} | ${emptyExpirationPolicyPayload()} | ${{}} | ${false}
${'empty response and changes from user'} | ${emptyExpirationPolicyPayload()} | ${{ enabled: true }} | ${true}
${'response and no changes'} | ${expirationPolicyPayload()} | ${containerExpirationPolicyData()} | ${false}
${'response and changes'} | ${expirationPolicyPayload()} | ${{ ...containerExpirationPolicyData(), nameRegex: '12345' }} | ${true}
${'response and empty'} | ${expirationPolicyPayload()} | ${{}} | ${true}
`('$description', async ({ apiResponse, workingCopy, result }) => {
const requests = mountComponentWithApollo({
provide: { ...defaultProvidedValues, enableHistoricEntries: true },
resolver: jest.fn().mockResolvedValue(apiResponse),
});
await Promise.all(requests);
findSettingsComponent().vm.$emit('input', workingCopy);
await wrapper.vm.$nextTick();
expect(findSettingsComponent().props('isEdited')).toBe(result);
});
});
it('renders the setting form', async () => { it('renders the setting form', async () => {
const requests = mountComponentWithApollo({ const requests = mountComponentWithApollo({
resolver: jest.fn().mockResolvedValue(expirationPolicyPayload()), resolver: jest.fn().mockResolvedValue(expirationPolicyPayload()),
......
...@@ -76,25 +76,25 @@ Array [ ...@@ -76,25 +76,25 @@ Array [
Object { Object {
"default": false, "default": false,
"key": "SEVEN_DAYS", "key": "SEVEN_DAYS",
"label": "7 days until tags are automatically removed", "label": "7 days",
"variable": 7, "variable": 7,
}, },
Object { Object {
"default": false, "default": false,
"key": "FOURTEEN_DAYS", "key": "FOURTEEN_DAYS",
"label": "14 days until tags are automatically removed", "label": "14 days",
"variable": 14, "variable": 14,
}, },
Object { Object {
"default": false, "default": false,
"key": "THIRTY_DAYS", "key": "THIRTY_DAYS",
"label": "30 days until tags are automatically removed", "label": "30 days",
"variable": 30, "variable": 30,
}, },
Object { Object {
"default": true, "default": true,
"key": "NINETY_DAYS", "key": "NINETY_DAYS",
"label": "90 days until tags are automatically removed", "label": "90 days",
"variable": 90, "variable": 90,
}, },
] ]
......
...@@ -11,10 +11,7 @@ describe('Utils', () => { ...@@ -11,10 +11,7 @@ describe('Utils', () => {
[{ variable: 1 }, { variable: 2 }], [{ variable: 1 }, { variable: 2 }],
olderThanTranslationGenerator, olderThanTranslationGenerator,
); );
expect(result).toEqual([ expect(result).toEqual([{ variable: 1, label: '1 day' }, { variable: 2, label: '2 days' }]);
{ variable: 1, label: '1 day until tags are automatically removed' },
{ variable: 2, label: '2 days until tags are automatically removed' },
]);
}); });
}); });
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment