Commit 06870a25 authored by Martin Wortschack's avatar Martin Wortschack

Merge branch '207267-regex-keep-frontend' into 'master'

Add Keep Regex to expiration policy UI

See merge request gitlab-org/gitlab!29940
parents 57df92ae cd82a73c
...@@ -16,6 +16,7 @@ export const getSettings = (state, getters) => ({ ...@@ -16,6 +16,7 @@ export const getSettings = (state, getters) => ({
older_than: getters.getOlderThan, older_than: getters.getOlderThan,
keep_n: getters.getKeepN, keep_n: getters.getKeepN,
name_regex: state.settings.name_regex, name_regex: state.settings.name_regex,
name_regex_keep: state.settings.name_regex_keep,
}); });
export const getIsEdited = state => !isEqual(state.original, state.settings); export const getIsEdited = state => !isEqual(state.original, state.settings);
......
<script> <script>
import { uniqueId } from 'lodash'; import { uniqueId } from 'lodash';
import { GlFormGroup, GlToggle, GlFormSelect, GlFormTextarea, GlSprintf } from '@gitlab/ui'; import { GlFormGroup, GlToggle, GlFormSelect, GlFormTextarea, GlSprintf } from '@gitlab/ui';
import { s__, __ } from '~/locale'; import {
import { NAME_REGEX_LENGTH } from '../constants'; NAME_REGEX_LENGTH,
ENABLED_TEXT,
DISABLED_TEXT,
TEXT_AREA_INVALID_FEEDBACK,
EXPIRATION_INTERVAL_LABEL,
EXPIRATION_SCHEDULE_LABEL,
KEEP_N_LABEL,
NAME_REGEX_LABEL,
NAME_REGEX_PLACEHOLDER,
NAME_REGEX_DESCRIPTION,
NAME_REGEX_KEEP_LABEL,
NAME_REGEX_KEEP_PLACEHOLDER,
NAME_REGEX_KEEP_DESCRIPTION,
ENABLE_TOGGLE_LABEL,
ENABLE_TOGGLE_DESCRIPTION,
} from '../constants';
import { mapComputedToEvent } from '../utils'; import { mapComputedToEvent } from '../utils';
export default { export default {
...@@ -40,42 +55,73 @@ export default { ...@@ -40,42 +55,73 @@ export default {
default: 'right', default: 'right',
}, },
}, },
nameRegexPlaceholder: '.*', i18n: {
textAreaInvalidFeedback: TEXT_AREA_INVALID_FEEDBACK,
enableToggleLabel: ENABLE_TOGGLE_LABEL,
enableToggleDescription: ENABLE_TOGGLE_DESCRIPTION,
},
selectList: [ selectList: [
{ {
name: 'expiration-policy-interval', name: 'expiration-policy-interval',
label: s__('ContainerRegistry|Expiration interval:'), label: EXPIRATION_INTERVAL_LABEL,
model: 'older_than', model: 'older_than',
optionKey: 'olderThan', optionKey: 'olderThan',
}, },
{ {
name: 'expiration-policy-schedule', name: 'expiration-policy-schedule',
label: s__('ContainerRegistry|Expiration schedule:'), label: EXPIRATION_SCHEDULE_LABEL,
model: 'cadence', model: 'cadence',
optionKey: 'cadence', optionKey: 'cadence',
}, },
{ {
name: 'expiration-policy-latest', name: 'expiration-policy-latest',
label: s__('ContainerRegistry|Number of tags to retain:'), label: KEEP_N_LABEL,
model: 'keep_n', model: 'keep_n',
optionKey: 'keepN', optionKey: 'keepN',
}, },
], ],
textAreaList: [
{
name: 'expiration-policy-name-matching',
label: NAME_REGEX_LABEL,
model: 'name_regex',
placeholder: NAME_REGEX_PLACEHOLDER,
stateVariable: 'nameRegexState',
description: NAME_REGEX_DESCRIPTION,
},
{
name: 'expiration-policy-keep-name',
label: NAME_REGEX_KEEP_LABEL,
model: 'name_regex_keep',
placeholder: NAME_REGEX_KEEP_PLACEHOLDER,
stateVariable: 'nameKeepRegexState',
description: NAME_REGEX_KEEP_DESCRIPTION,
},
],
data() { data() {
return { return {
uniqueId: uniqueId(), uniqueId: uniqueId(),
}; };
}, },
computed: { computed: {
...mapComputedToEvent(['enabled', 'cadence', 'older_than', 'keep_n', 'name_regex'], 'value'), ...mapComputedToEvent(
['enabled', 'cadence', 'older_than', 'keep_n', 'name_regex', 'name_regex_keep'],
'value',
),
policyEnabledText() { policyEnabledText() {
return this.enabled ? __('enabled') : __('disabled'); return this.enabled ? ENABLED_TEXT : DISABLED_TEXT;
}, },
nameRegexState() { textAreaState() {
return this.name_regex ? this.name_regex.length <= NAME_REGEX_LENGTH : null; return {
nameRegexState: this.validateNameRegex(this.name_regex),
nameKeepRegexState: this.validateNameRegex(this.name_regex_keep),
};
}, },
fieldsValidity() { fieldsValidity() {
return this.nameRegexState !== false; return (
this.textAreaState.nameRegexState !== false &&
this.textAreaState.nameKeepRegexState !== false
);
}, },
isFormElementDisabled() { isFormElementDisabled() {
return !this.enabled || this.isLoading; return !this.enabled || this.isLoading;
...@@ -94,6 +140,9 @@ export default { ...@@ -94,6 +140,9 @@ export default {
}, },
}, },
methods: { methods: {
validateNameRegex(value) {
return value ? value.length <= NAME_REGEX_LENGTH : null;
},
idGenerator(id) { idGenerator(id) {
return `${id}_${this.uniqueId}`; return `${id}_${this.uniqueId}`;
}, },
...@@ -111,7 +160,7 @@ export default { ...@@ -111,7 +160,7 @@ export default {
:label-cols="labelCols" :label-cols="labelCols"
:label-align="labelAlign" :label-align="labelAlign"
:label-for="idGenerator('expiration-policy-toggle')" :label-for="idGenerator('expiration-policy-toggle')"
:label="s__('ContainerRegistry|Expiration policy:')" :label="$options.i18n.enableToggleLabel"
> >
<div class="d-flex align-items-start"> <div class="d-flex align-items-start">
<gl-toggle <gl-toggle
...@@ -120,9 +169,7 @@ export default { ...@@ -120,9 +169,7 @@ export default {
:disabled="isLoading" :disabled="isLoading"
/> />
<span class="mb-2 ml-1 lh-2"> <span class="mb-2 ml-1 lh-2">
<gl-sprintf <gl-sprintf :message="$options.i18n.enableToggleDescription">
:message="s__('ContainerRegistry|Docker tag expiration policy is %{toggleStatus}')"
>
<template #toggleStatus> <template #toggleStatus>
<strong>{{ policyEnabledText }}</strong> <strong>{{ policyEnabledText }}</strong>
</template> </template>
...@@ -157,35 +204,34 @@ export default { ...@@ -157,35 +204,34 @@ export default {
</gl-form-group> </gl-form-group>
<gl-form-group <gl-form-group
:id="idGenerator('expiration-policy-name-matching-group')" v-for="textarea in $options.textAreaList"
:id="idGenerator(`${textarea.name}-group`)"
:key="textarea.name"
:label-cols="labelCols" :label-cols="labelCols"
:label-align="labelAlign" :label-align="labelAlign"
:label-for="idGenerator('expiration-policy-name-matching')" :label-for="idGenerator(textarea.name)"
:label=" :state="textAreaState[textarea.stateVariable]"
s__('ContainerRegistry|Docker tags with names matching this regex pattern will expire:') :invalid-feedback="$options.i18n.textAreaInvalidFeedback"
"
:state="nameRegexState"
:invalid-feedback="
s__('ContainerRegistry|The value of this input should be less than 255 characters')
"
> >
<template #label>
<gl-sprintf :message="textarea.label">
<template #italic="{content}">
<i>{{ content }}</i>
</template>
</gl-sprintf>
</template>
<gl-form-textarea <gl-form-textarea
:id="idGenerator('expiration-policy-name-matching')" :id="idGenerator(textarea.name)"
v-model="name_regex" :value="value[textarea.model]"
:placeholder="$options.nameRegexPlaceholder" :placeholder="textarea.placeholder"
:state="nameRegexState" :state="textAreaState[textarea.stateVariable]"
:disabled="isFormElementDisabled" :disabled="isFormElementDisabled"
trim trim
@input="updateModel($event, textarea.model)"
/> />
<template #description> <template #description>
<span ref="regex-description"> <span ref="regex-description">
<gl-sprintf <gl-sprintf :message="textarea.description">
:message="
s__(
'ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}',
)
"
>
<template #code="{content}"> <template #code="{content}">
<code>{{ content }}</code> <code>{{ content }}</code>
</template> </template>
......
import { s__ } from '~/locale'; import { s__, __ } from '~/locale';
export const FETCH_SETTINGS_ERROR_MESSAGE = s__( export const FETCH_SETTINGS_ERROR_MESSAGE = s__(
'ContainerRegistry|Something went wrong while fetching the expiration policy.', 'ContainerRegistry|Something went wrong while fetching the expiration policy.',
...@@ -13,3 +13,33 @@ export const UPDATE_SETTINGS_SUCCESS_MESSAGE = s__( ...@@ -13,3 +13,33 @@ export const UPDATE_SETTINGS_SUCCESS_MESSAGE = s__(
); );
export const NAME_REGEX_LENGTH = 255; export const NAME_REGEX_LENGTH = 255;
export const ENABLED_TEXT = __('enabled');
export const DISABLED_TEXT = __('disabled');
export const ENABLE_TOGGLE_LABEL = s__('ContainerRegistry|Expiration policy:');
export const ENABLE_TOGGLE_DESCRIPTION = s__(
'ContainerRegistry|Docker tag expiration policy is %{toggleStatus}',
);
export const TEXT_AREA_INVALID_FEEDBACK = s__(
'ContainerRegistry|The value of this input should be less than 255 characters',
);
export const EXPIRATION_INTERVAL_LABEL = s__('ContainerRegistry|Expiration interval:');
export const EXPIRATION_SCHEDULE_LABEL = s__('ContainerRegistry|Expiration schedule:');
export const KEEP_N_LABEL = s__('ContainerRegistry|Number of tags to retain:');
export const NAME_REGEX_LABEL = s__(
'ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}',
);
export const NAME_REGEX_PLACEHOLDER = '.*';
export const NAME_REGEX_DESCRIPTION = s__(
'ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}',
);
export const NAME_REGEX_KEEP_LABEL = s__(
'ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}',
);
export const NAME_REGEX_KEEP_PLACEHOLDER = '';
export const NAME_REGEX_KEEP_DESCRIPTION = s__(
'ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported',
);
---
title: Add new keep regex to expiration policy settings ui
merge_request: 29940
author:
type: changed
...@@ -5606,9 +5606,6 @@ msgstr "" ...@@ -5606,9 +5606,6 @@ msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}" msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr "" msgstr ""
msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
msgstr ""
msgid "ContainerRegistry|Edit Settings" msgid "ContainerRegistry|Edit Settings"
msgstr "" msgstr ""
...@@ -5654,6 +5651,9 @@ msgstr "" ...@@ -5654,6 +5651,9 @@ msgstr ""
msgid "ContainerRegistry|Quick Start" msgid "ContainerRegistry|Quick Start"
msgstr "" msgstr ""
msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported"
msgstr ""
msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}" msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
msgstr "" msgstr ""
...@@ -5707,6 +5707,12 @@ msgstr "" ...@@ -5707,6 +5707,12 @@ msgstr ""
msgid "ContainerRegistry|Tags deleted successfully" msgid "ContainerRegistry|Tags deleted successfully"
msgstr "" msgstr ""
msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
msgstr ""
msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
msgstr ""
msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled." msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
msgstr "" msgstr ""
......
...@@ -29,7 +29,7 @@ describe 'Project > Settings > CI/CD > Container registry tag expiration policy' ...@@ -29,7 +29,7 @@ describe 'Project > Settings > CI/CD > Container registry tag expiration policy'
select('7 days until tags are automatically removed', from: 'Expiration interval:') select('7 days until tags are automatically removed', from: 'Expiration interval:')
select('Every day', from: 'Expiration schedule:') select('Every day', from: 'Expiration schedule:')
select('50 tags per image name', from: 'Number of tags to retain:') select('50 tags per image name', from: 'Number of tags to retain:')
fill_in('Docker tags with names matching this regex pattern will expire:', with: '*-production') fill_in('Tags with names matching this regex pattern will expire:', with: '*-production')
end end
submit_button = find('.card-footer .btn.btn-success') submit_button = find('.card-footer .btn.btn-success')
expect(submit_button).not_to be_disabled expect(submit_button).not_to be_disabled
......
...@@ -4,9 +4,12 @@ import { formOptions } from '../../shared/mock_data'; ...@@ -4,9 +4,12 @@ import { formOptions } from '../../shared/mock_data';
describe('Getters registry settings store', () => { describe('Getters registry settings store', () => {
const settings = { const settings = {
enabled: true,
cadence: 'foo', cadence: 'foo',
keep_n: 'bar', keep_n: 'bar',
older_than: 'baz', older_than: 'baz',
name_regex: 'name-foo',
name_regex_keep: 'name-keep-bar',
}; };
describe.each` describe.each`
...@@ -29,6 +32,17 @@ describe('Getters registry settings store', () => { ...@@ -29,6 +32,17 @@ describe('Getters registry settings store', () => {
}); });
}); });
describe('getSettings', () => {
it('returns the content of settings', () => {
const computedGetters = {
getCadence: settings.cadence,
getOlderThan: settings.older_than,
getKeepN: settings.keep_n,
};
expect(getters.getSettings({ settings }, computedGetters)).toEqual(settings);
});
});
describe('getIsEdited', () => { describe('getIsEdited', () => {
it('returns false when original is equal to settings', () => { it('returns false when original is equal to settings', () => {
const same = { foo: 'bar' }; const same = { foo: 'bar' };
......
...@@ -117,11 +117,11 @@ exports[`Expiration Policy Form renders 1`] = ` ...@@ -117,11 +117,11 @@ exports[`Expiration Policy Form renders 1`] = `
<gl-form-group-stub <gl-form-group-stub
id="expiration-policy-name-matching-group" id="expiration-policy-name-matching-group"
invalid-feedback="The value of this input should be less than 255 characters" invalid-feedback="The value of this input should be less than 255 characters"
label="Docker tags with names matching this regex pattern will expire:"
label-align="right" label-align="right"
label-cols="3" label-cols="3"
label-for="expiration-policy-name-matching" label-for="expiration-policy-name-matching"
> >
<gl-form-textarea-stub <gl-form-textarea-stub
disabled="true" disabled="true"
id="expiration-policy-name-matching" id="expiration-policy-name-matching"
...@@ -130,5 +130,21 @@ exports[`Expiration Policy Form renders 1`] = ` ...@@ -130,5 +130,21 @@ exports[`Expiration Policy Form renders 1`] = `
value="" value=""
/> />
</gl-form-group-stub> </gl-form-group-stub>
<gl-form-group-stub
id="expiration-policy-keep-name-group"
invalid-feedback="The value of this input should be less than 255 characters"
label-align="right"
label-cols="3"
label-for="expiration-policy-keep-name"
>
<gl-form-textarea-stub
disabled="true"
id="expiration-policy-keep-name"
placeholder=""
trim=""
value=""
/>
</gl-form-group-stub>
</div> </div>
`; `;
...@@ -46,6 +46,7 @@ describe('Expiration Policy Form', () => { ...@@ -46,6 +46,7 @@ describe('Expiration Policy Form', () => {
${'schedule'} | ${'cadence'} | ${'foo'} | ${'disabled'} ${'schedule'} | ${'cadence'} | ${'foo'} | ${'disabled'}
${'latest'} | ${'keep_n'} | ${'foo'} | ${'disabled'} ${'latest'} | ${'keep_n'} | ${'foo'} | ${'disabled'}
${'name-matching'} | ${'name_regex'} | ${'foo'} | ${'disabled'} ${'name-matching'} | ${'name_regex'} | ${'foo'} | ${'disabled'}
${'keep-name'} | ${'name_regex_keep'} | ${'bar'} | ${'disabled'}
`( `(
`${FORM_ELEMENTS_ID_PREFIX}-$elementName form element`, `${FORM_ELEMENTS_ID_PREFIX}-$elementName form element`,
({ elementName, modelName, value, disabledByToggle }) => { ({ elementName, modelName, value, disabledByToggle }) => {
...@@ -118,21 +119,26 @@ describe('Expiration Policy Form', () => { ...@@ -118,21 +119,26 @@ describe('Expiration Policy Form', () => {
${'schedule'} ${'schedule'}
${'latest'} ${'latest'}
${'name-matching'} ${'name-matching'}
${'keep-name'}
`(`${FORM_ELEMENTS_ID_PREFIX}-$elementName is disabled`, ({ elementName }) => { `(`${FORM_ELEMENTS_ID_PREFIX}-$elementName is disabled`, ({ elementName }) => {
expect(findFormElements(elementName).attributes('disabled')).toBe('true'); expect(findFormElements(elementName).attributes('disabled')).toBe('true');
}); });
}); });
describe('form validation', () => { describe.each`
modelName | elementName | stateVariable
${'name_regex'} | ${'name-matching'} | ${'nameRegexState'}
${'name_regex_keep'} | ${'keep-name'} | ${'nameKeepRegexState'}
`('regex textarea validation', ({ modelName, elementName, stateVariable }) => {
describe(`when name regex is longer than ${NAME_REGEX_LENGTH}`, () => { describe(`when name regex is longer than ${NAME_REGEX_LENGTH}`, () => {
const invalidString = new Array(NAME_REGEX_LENGTH + 2).join(','); const invalidString = new Array(NAME_REGEX_LENGTH + 2).join(',');
beforeEach(() => { beforeEach(() => {
mountComponent({ value: { name_regex: invalidString } }); mountComponent({ value: { [modelName]: invalidString } });
}); });
it('nameRegexState is false', () => { it(`${stateVariable} is false`, () => {
expect(wrapper.vm.nameRegexState).toBe(false); expect(wrapper.vm.textAreaState[stateVariable]).toBe(false);
}); });
it('emit the @invalidated event', () => { it('emit the @invalidated event', () => {
...@@ -141,17 +147,20 @@ describe('Expiration Policy Form', () => { ...@@ -141,17 +147,20 @@ describe('Expiration Policy Form', () => {
}); });
it('if the user did not type validation is null', () => { it('if the user did not type validation is null', () => {
mountComponent({ value: { name_regex: '' } }); mountComponent({ value: { [modelName]: '' } });
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.nameRegexState).toBe(null); expect(wrapper.vm.textAreaState[stateVariable]).toBe(null);
expect(wrapper.emitted('validated')).toBeTruthy(); expect(wrapper.emitted('validated')).toBeTruthy();
}); });
}); });
it(`if the user typed and is less than ${NAME_REGEX_LENGTH} state is true`, () => { it(`if the user typed and is less than ${NAME_REGEX_LENGTH} state is true`, () => {
mountComponent({ value: { name_regex: 'foo' } }); mountComponent({ value: { [modelName]: 'foo' } });
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.nameRegexState).toBe(true); const formGroup = findFormGroup(elementName);
const formElement = findFormElements(elementName, formGroup);
expect(formGroup.attributes('state')).toBeTruthy();
expect(formElement.attributes('state')).toBeTruthy();
}); });
}); });
}); });
......
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