Commit 6f1d629f authored by Nicolò Maria Mezzopera's avatar Nicolò Maria Mezzopera Committed by Olena Horal-Koretska

Refactor isEmpty computed property

- source
- tests
parent 15e99cd6
......@@ -36,7 +36,8 @@ export default {
},
placeholder: {
type: String,
required: true,
required: false,
default: '',
},
description: {
type: String,
......
<script>
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 { FETCH_SETTINGS_ERROR_MESSAGE } from '../../shared/constants';
......@@ -60,6 +60,9 @@ export default {
return this.isAdmin ? UNAVAILABLE_ADMIN_FEATURE_TEXT : UNAVAILABLE_USER_FEATURE_TEXT;
},
isEdited() {
if (isEmpty(this.containerExpirationPolicy) && isEmpty(this.workingCopy)) {
return false;
}
return !isEqual(this.containerExpirationPolicy, this.workingCopy);
},
},
......
<script>
import { GlCard, GlButton } from '@gitlab/ui';
import { GlCard, GlButton, GlSprintf } from '@gitlab/ui';
import Tracking from '~/tracking';
import {
UPDATE_SETTINGS_ERROR_MESSAGE,
UPDATE_SETTINGS_SUCCESS_MESSAGE,
} from '../../shared/constants';
import ExpirationPolicyFields from '../../shared/components/expiration_policy_fields.vue';
import { SET_CLEANUP_POLICY_BUTTON, CLEANUP_POLICY_CARD_HEADER } from '../constants';
} from '~/registry/shared/constants';
import {
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 updateContainerExpirationPolicyMutation from '../graphql/mutations/update_container_expiration_policy.graphql';
import { updateContainerExpirationPolicy } from '../graphql/utils/cache_update';
import updateContainerExpirationPolicyMutation from '~/registry/settings/graphql/mutations/update_container_expiration_policy.graphql';
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 {
components: {
GlCard,
GlButton,
ExpirationPolicyFields,
GlSprintf,
ExpirationDropdown,
ExpirationTextarea,
ExpirationToggle,
ExpirationRunText,
},
mixins: [Tracking.mixin()],
inject: ['projectPath'],
......@@ -35,22 +57,31 @@ export default {
default: false,
},
},
labelsConfig: {
cols: 3,
align: 'right',
},
formOptions: formOptionsGenerator(),
i18n: {
CLEANUP_POLICY_CARD_HEADER,
KEEP_HEADER_TEXT,
KEEP_INFO_TEXT,
KEEP_N_LABEL,
NAME_REGEX_KEEP_LABEL,
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() {
return {
tracking: {
label: 'docker_container_retention_and_expiration_policies',
},
fieldsAreValid: true,
apiErrors: null,
apiErrors: {},
localErrors: {},
mutationLoading: false,
};
},
......@@ -66,12 +97,18 @@ export default {
showLoadingIcon() {
return this.isLoading || this.mutationLoading;
},
fieldsAreValid() {
return Object.values(this.localErrors).every(error => error);
},
isSubmitButtonDisabled() {
return !this.fieldsAreValid || this.showLoadingIcon;
},
isCancelButtonDisabled() {
return !this.isEdited || this.isLoading || this.mutationLoading;
},
isFieldDisabled() {
return this.showLoadingIcon || !this.value.enabled;
},
mutationVariables() {
return {
projectPath: this.projectPath,
......@@ -90,7 +127,8 @@ export default {
},
reset() {
this.track('reset_form');
this.apiErrors = null;
this.apiErrors = {};
this.localErrors = {};
this.$emit('reset');
},
setApiErrors(response) {
......@@ -101,9 +139,15 @@ export default {
return acc;
}, {});
},
setLocalErrors(state, model) {
this.localErrors = {
...this.localErrors,
[model]: state,
};
},
submit() {
this.track('submit_form');
this.apiErrors = null;
this.apiErrors = {};
this.mutationLoading = true;
return this.$apollo
.mutate({
......@@ -129,11 +173,9 @@ export default {
this.mutationLoading = false;
});
},
onModelChange(changePayload) {
this.$emit('input', changePayload.newValue);
if (this.apiErrors) {
this.apiErrors[changePayload.modified] = undefined;
}
onModelChange(newValue, model) {
this.$emit('input', { ...this.value, [model]: newValue });
this.apiErrors[model] = undefined;
},
},
};
......@@ -141,42 +183,129 @@ export default {
<template>
<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>
{{ $options.i18n.CLEANUP_POLICY_CARD_HEADER }}
{{ $options.i18n.KEEP_HEADER_TEXT }}
</template>
<template #default>
<expiration-policy-fields
:value="prefilledForm"
:form-options="$options.formOptions"
:is-loading="isLoading"
:api-errors="apiErrors"
@validated="fieldsAreValid = true"
@invalidated="fieldsAreValid = false"
@input="onModelChange"
/>
<div>
<p>
<gl-sprintf :message="$options.i18n.KEEP_INFO_TEXT">
<template #strong="{content}">
<strong>{{ content }}</strong>
</template>
<template #secondStrong="{content}">
<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 #footer>
<gl-button
ref="cancel-button"
type="reset"
class="gl-mr-3 gl-display-block float-right"
:disabled="isCancelButtonDisabled"
>
{{ __('Cancel') }}
</gl-button>
<gl-button
ref="save-button"
type="submit"
:disabled="isSubmitButtonDisabled"
:loading="showLoadingIcon"
variant="success"
category="primary"
class="js-no-auto-disable"
>
{{ $options.i18n.SET_CLEANUP_POLICY_BUTTON }}
</gl-button>
<template #default>
<div>
<p>
<gl-sprintf :message="$options.i18n.REMOVE_INFO_TEXT">
<template #strong="{content}">
<strong>{{ content }}</strong>
</template>
<template #secondStrong="{content}">
<strong>{{ content }}</strong>
</template>
</gl-sprintf>
</p>
<expiration-dropdown
v-model="prefilledForm.olderThan"
:disabled="isFieldDisabled"
:form-options="$options.formOptions.olderThan"
:label="$options.i18n.EXPIRATION_SCHEDULE_LABEL"
name="older-than"
data-testid="older-than-dropdown"
@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>
</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>
</template>
import { s__, __ } from '~/locale';
export const SET_CLEANUP_POLICY_BUTTON = __('Save');
export const CLEANUP_POLICY_CARD_HEADER = s__('ContainerRegistry|Tag expiration policy');
export const UNAVAILABLE_FEATURE_TITLE = s__(
`ContainerRegistry|Cleanup policy for tags is disabled`,
);
......@@ -19,34 +18,33 @@ export const TEXT_AREA_INVALID_FEEDBACK = s__(
export const KEEP_HEADER_TEXT = s__('ContainerRegistry|Keep these tags');
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 NAME_REGEX_KEEP_LABEL = s__('ContainerRegistry|Keep tags matching:');
export const NAME_REGEX_KEEP_PLACEHOLDER = 'production-v.*';
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_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 NAME_REGEX_LABEL = s__('ContainerRegistry|Remove tags matching:');
export const NAME_REGEX_PLACEHOLDER = '.*';
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 DISABLED_TEXT = __('Disabled');
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 NOT_SCHEDULED_POLICY_TEXT = s__('ContainerRegistry|Not yet scheduled');
......
......@@ -21,12 +21,7 @@ export const mapComputedToEvent = (list, root) => {
return result;
};
export const olderThanTranslationGenerator = variable =>
n__(
'%d day until tags are automatically removed',
'%d days until tags are automatically removed',
variable,
);
export const olderThanTranslationGenerator = variable => n__('%d day', '%d days', variable);
export const keepNTranslationGenerator = variable =>
n__('%d tag per image name', '%d tags per image name', variable);
......
......@@ -66,11 +66,11 @@
%section.settings.no-animate#js-registry-policies{ class: ('expanded' if expanded) }
.settings-header
%h4
= _("Cleanup policy for tags")
= _("Clean up image tags")
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%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')
.settings-content
= 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
To create a cleanup policy in the UI:
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.
| Field | Description |
|---------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------|
| **Cleanup policy** | Turn the policy on or off. |
| **Expiration interval** | How long tags are exempt from being deleted. |
| **Expiration schedule** | How often the policy should run. |
| **Number of tags to retain** | How many tags to _always_ keep for each image. |
| **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). |
| **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). |
| **Toggle** | Turn the policy on or off. |
| **Run cleanup** | How often the policy should run. |
| **Keep the most recent** | 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). |
| **Remove tags older than** | Remove only tags older than X days. |
| **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.
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
......
......@@ -170,11 +170,6 @@ msgid_plural "%d days"
msgstr[0] ""
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_plural "%d errors"
msgstr[0] ""
......@@ -5571,7 +5566,7 @@ msgstr ""
msgid "ClassificationLabelUnavailable|is unavailable: %{reason}"
msgstr ""
msgid "Cleanup policy for tags"
msgid "Clean up image tags"
msgstr ""
msgid "Cleanup policy maximum processing time (seconds)"
......@@ -7266,7 +7261,7 @@ msgstr ""
msgid "ContainerRegistry|%{toggleStatus} - Tags matching the patterns defined below will be scheduled for deletion"
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 ""
msgid "ContainerRegistry|Build an image"
......@@ -7403,7 +7398,7 @@ msgstr ""
msgid "ContainerRegistry|Remove these tags"
msgstr ""
msgid "ContainerRegistry|Run cleanup every:"
msgid "ContainerRegistry|Run cleanup:"
msgstr ""
msgid "ContainerRegistry|Some tags were not deleted"
......@@ -7436,19 +7431,16 @@ msgstr ""
msgid "ContainerRegistry|Sorry, your filter produced no results."
msgstr ""
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag successfully marked for deletion."
msgstr ""
msgid "ContainerRegistry|Tags successfully marked for deletion."
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 ""
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 ""
msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}"
......@@ -7457,10 +7449,10 @@ msgstr ""
msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
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 ""
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 ""
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 ""
msgid "Save pipeline schedule"
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 ""
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
subject
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
it 'saves cleanup policy submit the form' do
subject
within '#js-registry-policies' do
within '.gl-card-body' do
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('Tags with names matching this regex pattern will expire:', with: '.*-production')
end
submit_button = find('.gl-card-footer .btn.btn-success')
select('Every day', from: 'Run cleanup')
select('50 tags per image name', from: 'Keep the most recent:')
fill_in('Keep tags matching:', with: 'stable')
select('7 days', from: 'Remove tags older than:')
fill_in('Remove tags matching:', with: '.*-production')
submit_button = find('.btn.btn-success')
expect(submit_button).not_to be_disabled
submit_button.click
end
......@@ -51,10 +51,9 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
subject
within '#js-registry-policies' do
within '.gl-card-body' do
fill_in('Tags with names matching this regex pattern will expire:', with: '*-production')
end
submit_button = find('.gl-card-footer .btn.btn-success')
fill_in('Remove tags matching:', with: '*-production')
submit_button = find('.btn.btn-success')
expect(submit_button).not_to be_disabled
submit_button.click
end
......@@ -85,7 +84,7 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
within '#js-registry-policies' do
case result
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
expect(find('.gl-alert-title')).to have_content('Cleanup policy for tags is disabled')
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 {
UNAVAILABLE_USER_FEATURE_TEXT,
} from '~/registry/settings/constants';
import { expirationPolicyPayload, emptyExpirationPolicyPayload } from '../mock_data';
import {
expirationPolicyPayload,
emptyExpirationPolicyPayload,
containerExpirationPolicyData,
} from '../mock_data';
const localVue = createLocalVue();
......@@ -62,6 +66,29 @@ describe('Registry Settings App', () => {
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 () => {
const requests = mountComponentWithApollo({
resolver: jest.fn().mockResolvedValue(expirationPolicyPayload()),
......
......@@ -4,7 +4,6 @@ import createMockApollo from 'jest/helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import Tracking from '~/tracking';
import component from '~/registry/settings/components/settings_form.vue';
import expirationPolicyFields from '~/registry/shared/components/expiration_policy_fields.vue';
import updateContainerExpirationPolicyMutation from '~/registry/settings/graphql/mutations/update_container_expiration_policy.graphql';
import expirationPolicyQuery from '~/registry/settings/graphql/queries/get_expiration_policy.graphql';
import {
......@@ -39,9 +38,15 @@ describe('Settings Form', () => {
};
const findForm = () => wrapper.find({ ref: 'form-element' });
const findFields = () => wrapper.find(expirationPolicyFields);
const findCancelButton = () => wrapper.find({ ref: 'cancel-button' });
const findSaveButton = () => wrapper.find({ ref: 'save-button' });
const findCancelButton = () => wrapper.find('[data-testid="cancel-button"');
const findSaveButton = () => wrapper.find('[data-testid="save-button"');
const findEnableToggle = () => wrapper.find('[data-testid="enable-toggle"]');
const findCadenceDropdown = () => wrapper.find('[data-testid="cadence-dropdown"]');
const findKeepNDropdown = () => wrapper.find('[data-testid="keep-n-dropdown"]');
const findKeepRegexTextarea = () => wrapper.find('[data-testid="keep-regex-textarea"]');
const findOlderThanDropdown = () => wrapper.find('[data-testid="older-than-dropdown"]');
const findRemoveRegexTextarea = () => wrapper.find('[data-testid="remove-regex-textarea"]');
const mountComponent = ({
props = defaultProps,
......@@ -109,45 +114,136 @@ describe('Settings Form', () => {
wrapper.destroy();
});
describe('data binding', () => {
it('v-model change update the settings property', () => {
describe.each`
model | finder | fieldName | type | defaultValue
${'enabled'} | ${findEnableToggle} | ${'Enable'} | ${'toggle'} | ${false}
${'cadence'} | ${findCadenceDropdown} | ${'Cadence'} | ${'dropdown'} | ${'EVERY_DAY'}
${'keepN'} | ${findKeepNDropdown} | ${'Keep N'} | ${'dropdown'} | ${'TEN_TAGS'}
${'nameRegexKeep'} | ${findKeepRegexTextarea} | ${'Keep Regex'} | ${'textarea'} | ${''}
${'olderThan'} | ${findOlderThanDropdown} | ${'OlderThan'} | ${'dropdown'} | ${'NINETY_DAYS'}
${'nameRegex'} | ${findRemoveRegexTextarea} | ${'Remove regex'} | ${'textarea'} | ${''}
`('$fieldName', ({ model, finder, type, defaultValue }) => {
it('matches snapshot', () => {
mountComponent();
findFields().vm.$emit('input', { newValue: 'foo' });
expect(wrapper.emitted('input')).toEqual([['foo']]);
expect(finder().element).toMatchSnapshot();
});
it('v-model change update the api error property', () => {
const apiErrors = { baz: 'bar' };
mountComponent({ data: { apiErrors } });
expect(findFields().props('apiErrors')).toEqual(apiErrors);
findFields().vm.$emit('input', { newValue: 'foo', modified: 'baz' });
expect(findFields().props('apiErrors')).toEqual({});
it('input event triggers a model update', () => {
mountComponent();
finder().vm.$emit('input', 'foo');
expect(wrapper.emitted('input')[0][0]).toMatchObject({
[model]: 'foo',
});
});
it('shows the default option when none are selected', () => {
mountComponent({ props: { value: {} } });
expect(findFields().props('value')).toEqual({
cadence: 'EVERY_DAY',
keepN: 'TEN_TAGS',
olderThan: 'NINETY_DAYS',
});
expect(finder().props('value')).toEqual(defaultValue);
});
if (type !== 'toggle') {
it.each`
isLoading | mutationLoading | enabledValue
${false} | ${false} | ${false}
${true} | ${false} | ${false}
${true} | ${true} | ${true}
${false} | ${true} | ${true}
${false} | ${false} | ${false}
`(
'is disabled when is loading is $isLoading, mutationLoading is $mutationLoading and enabled is $enabledValue',
({ isLoading, mutationLoading, enabledValue }) => {
mountComponent({
props: { isLoading, value: { enabled: enabledValue } },
data: { mutationLoading },
});
expect(finder().props('disabled')).toEqual(true);
},
);
} else {
it.each`
isLoading | mutationLoading
${true} | ${false}
${true} | ${true}
${false} | ${true}
`(
'is disabled when is loading is $isLoading and mutationLoading is $mutationLoading',
({ isLoading, mutationLoading }) => {
mountComponent({
props: { isLoading, value: {} },
data: { mutationLoading },
});
expect(finder().props('disabled')).toEqual(true);
},
);
}
if (type === 'textarea') {
it('input event updates the api error property', async () => {
const apiErrors = { [model]: 'bar' };
mountComponent({ data: { apiErrors } });
finder().vm.$emit('input', 'foo');
expect(finder().props('error')).toEqual('bar');
await wrapper.vm.$nextTick();
expect(finder().props('error')).toEqual('');
});
it('validation event updates buttons disabled state', async () => {
mountComponent();
expect(findSaveButton().props('disabled')).toBe(false);
finder().vm.$emit('validation', false);
await wrapper.vm.$nextTick();
expect(findSaveButton().props('disabled')).toBe(true);
});
}
if (type === 'dropdown') {
it('has the correct formOptions', () => {
mountComponent();
expect(finder().props('formOptions')).toEqual(wrapper.vm.$options.formOptions[model]);
});
}
});
describe('form', () => {
describe('form reset event', () => {
beforeEach(() => {
it('calls the appropriate function', () => {
mountComponent();
findForm().trigger('reset');
});
it('calls the appropriate function', () => {
expect(wrapper.emitted('reset')).toEqual([[]]);
});
it('tracks the reset event', () => {
mountComponent();
findForm().trigger('reset');
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'reset_form', trackingPayload);
});
it('resets the errors objects', async () => {
mountComponent({
data: { apiErrors: { nameRegex: 'bar' }, localErrors: { nameRegexKeep: false } },
});
findForm().trigger('reset');
await wrapper.vm.$nextTick();
expect(findKeepRegexTextarea().props('error')).toBe('');
expect(findRemoveRegexTextarea().props('error')).toBe('');
expect(findSaveButton().props('disabled')).toBe(false);
});
});
describe('form submit event ', () => {
......@@ -209,6 +305,7 @@ describe('Settings Form', () => {
});
});
});
describe('global errors', () => {
it('shows an error', async () => {
const handlers = mountComponentWithApollo({
......@@ -230,7 +327,7 @@ describe('Settings Form', () => {
graphQLErrors: [
{
extensions: {
problems: [{ path: ['name'], message: 'baz' }],
problems: [{ path: ['nameRegexKeep'], message: 'baz' }],
},
},
],
......@@ -241,7 +338,7 @@ describe('Settings Form', () => {
await waitForPromises();
await wrapper.vm.$nextTick();
expect(findFields().props('apiErrors')).toEqual({ name: 'baz' });
expect(findKeepRegexTextarea().props('error')).toEqual('baz');
});
});
});
......@@ -257,23 +354,21 @@ describe('Settings Form', () => {
});
it.each`
isLoading | isEdited | mutationLoading | isDisabled
${true} | ${true} | ${true} | ${true}
${false} | ${true} | ${true} | ${true}
${false} | ${false} | ${true} | ${true}
${true} | ${false} | ${false} | ${true}
${false} | ${false} | ${false} | ${true}
${false} | ${true} | ${false} | ${false}
isLoading | isEdited | mutationLoading
${true} | ${true} | ${true}
${false} | ${true} | ${true}
${false} | ${false} | ${true}
${true} | ${false} | ${false}
${false} | ${false} | ${false}
`(
'when isLoading is $isLoading and isEdited is $isEdited and mutationLoading is $mutationLoading is $isDisabled that the is disabled',
({ isEdited, isLoading, mutationLoading, isDisabled }) => {
'when isLoading is $isLoading, isEdited is $isEdited and mutationLoading is $mutationLoading is disabled',
({ isEdited, isLoading, mutationLoading }) => {
mountComponent({
props: { ...defaultProps, isEdited, isLoading },
data: { mutationLoading },
});
const expectation = isDisabled ? 'true' : undefined;
expect(findCancelButton().attributes('disabled')).toBe(expectation);
expect(findCancelButton().props('disabled')).toBe(true);
},
);
});
......@@ -284,24 +379,24 @@ describe('Settings Form', () => {
expect(findSaveButton().attributes('type')).toBe('submit');
});
it.each`
isLoading | fieldsAreValid | mutationLoading | isDisabled
${true} | ${true} | ${true} | ${true}
${false} | ${true} | ${true} | ${true}
${false} | ${false} | ${true} | ${true}
${true} | ${false} | ${false} | ${true}
${false} | ${false} | ${false} | ${true}
${false} | ${true} | ${false} | ${false}
isLoading | localErrors | mutationLoading
${true} | ${{}} | ${true}
${true} | ${{}} | ${false}
${false} | ${{}} | ${true}
${false} | ${{ foo: false }} | ${true}
${true} | ${{ foo: false }} | ${false}
${false} | ${{ foo: false }} | ${false}
`(
'when isLoading is $isLoading and fieldsAreValid is $fieldsAreValid and mutationLoading is $mutationLoading is $isDisabled that the is disabled',
({ fieldsAreValid, isLoading, mutationLoading, isDisabled }) => {
'when isLoading is $isLoading, localErrors is $localErrors and mutationLoading is $mutationLoading is disabled',
({ localErrors, isLoading, mutationLoading }) => {
mountComponent({
props: { ...defaultProps, isLoading },
data: { mutationLoading, fieldsAreValid },
data: { mutationLoading, localErrors },
});
const expectation = isDisabled ? 'true' : undefined;
expect(findSaveButton().attributes('disabled')).toBe(expectation);
expect(findSaveButton().props('disabled')).toBe(true);
},
);
......
......@@ -76,25 +76,25 @@ Array [
Object {
"default": false,
"key": "SEVEN_DAYS",
"label": "7 days until tags are automatically removed",
"label": "7 days",
"variable": 7,
},
Object {
"default": false,
"key": "FOURTEEN_DAYS",
"label": "14 days until tags are automatically removed",
"label": "14 days",
"variable": 14,
},
Object {
"default": false,
"key": "THIRTY_DAYS",
"label": "30 days until tags are automatically removed",
"label": "30 days",
"variable": 30,
},
Object {
"default": true,
"key": "NINETY_DAYS",
"label": "90 days until tags are automatically removed",
"label": "90 days",
"variable": 90,
},
]
......
......@@ -11,10 +11,7 @@ describe('Utils', () => {
[{ variable: 1 }, { variable: 2 }],
olderThanTranslationGenerator,
);
expect(result).toEqual([
{ variable: 1, label: '1 day until tags are automatically removed' },
{ variable: 2, label: '2 days until tags are automatically removed' },
]);
expect(result).toEqual([{ variable: 1, label: '1 day' }, { variable: 2, label: '2 days' }]);
});
});
......
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