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