diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-19 15:44:42 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-19 15:44:42 +0000 |
commit | 4555e1b21c365ed8303ffb7a3325d773c9b8bf31 (patch) | |
tree | 5423a1c7516cffe36384133ade12572cf709398d /app/assets/javascripts/packages_and_registries | |
parent | e570267f2f6b326480d284e0164a6464ba4081bc (diff) | |
download | gitlab-ce-4555e1b21c365ed8303ffb7a3325d773c9b8bf31.tar.gz |
Add latest changes from gitlab-org/gitlab@13-12-stable-eev13.12.0-rc42
Diffstat (limited to 'app/assets/javascripts/packages_and_registries')
23 files changed, 1136 insertions, 119 deletions
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list_app_bundle.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list_app_bundle.js index 88ee8a4200e..7e6e98f4fb5 100644 --- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list_app_bundle.js +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list_app_bundle.js @@ -9,7 +9,7 @@ Vue.use(Translate); export default () => { const el = document.getElementById('js-vue-packages-list'); const store = createStore(); - store.dispatch('setInitialState', el.dataset); + store.dispatch('setInitialState', { ...el.dataset, forceTerraform: true }); return new Vue({ el, diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/duplicates_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/duplicates_settings.vue new file mode 100644 index 00000000000..d66a30e7e81 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/settings/group/components/duplicates_settings.vue @@ -0,0 +1,118 @@ +<script> +import { GlSprintf, GlToggle, GlFormGroup, GlFormInput } from '@gitlab/ui'; +import { isEqual } from 'lodash'; + +import { + DUPLICATES_TOGGLE_LABEL, + DUPLICATES_ALLOWED_DISABLED, + DUPLICATES_ALLOWED_ENABLED, + DUPLICATES_SETTING_EXCEPTION_TITLE, + DUPLICATES_SETTINGS_EXCEPTION_LEGEND, +} from '~/packages_and_registries/settings/group/constants'; + +export default { + name: 'DuplicatesSettings', + i18n: { + DUPLICATES_TOGGLE_LABEL, + DUPLICATES_SETTING_EXCEPTION_TITLE, + DUPLICATES_SETTINGS_EXCEPTION_LEGEND, + }, + components: { + GlSprintf, + GlToggle, + GlFormGroup, + GlFormInput, + }, + props: { + loading: { + type: Boolean, + required: false, + default: false, + }, + duplicatesAllowed: { + type: Boolean, + default: false, + required: false, + }, + duplicateExceptionRegex: { + type: String, + default: '', + required: false, + }, + duplicateExceptionRegexError: { + type: String, + default: '', + required: false, + }, + modelNames: { + type: Object, + required: true, + validator(value) { + return isEqual(Object.keys(value), ['allowed', 'exception']); + }, + }, + toggleQaSelector: { + type: String, + required: false, + default: null, + }, + labelQaSelector: { + type: String, + required: false, + default: null, + }, + }, + computed: { + enabledButtonLabel() { + return this.duplicatesAllowed ? DUPLICATES_ALLOWED_ENABLED : DUPLICATES_ALLOWED_DISABLED; + }, + isExceptionRegexValid() { + return !this.duplicateExceptionRegexError; + }, + }, + methods: { + update(type, value) { + this.$emit('update', { [type]: value }); + }, + }, +}; +</script> + +<template> + <form> + <div class="gl-display-flex"> + <gl-toggle + :data-qa-selector="toggleQaSelector" + :label="$options.i18n.DUPLICATES_TOGGLE_LABEL" + label-position="hidden" + :value="duplicatesAllowed" + @change="update(modelNames.allowed, $event)" + /> + <div class="gl-ml-5"> + <div data-testid="toggle-label" :data-qa-selector="labelQaSelector"> + <gl-sprintf :message="enabledButtonLabel"> + <template #bold="{ content }"> + <strong>{{ content }}</strong> + </template> + </gl-sprintf> + </div> + <gl-form-group + v-if="!duplicatesAllowed" + class="gl-mt-4" + :label="$options.i18n.DUPLICATES_SETTING_EXCEPTION_TITLE" + label-size="sm" + :state="isExceptionRegexValid" + :invalid-feedback="duplicateExceptionRegexError" + :description="$options.i18n.DUPLICATES_SETTINGS_EXCEPTION_LEGEND" + label-for="maven-duplicated-settings-regex-input" + > + <gl-form-input + id="maven-duplicated-settings-regex-input" + :value="duplicateExceptionRegex" + @change="update(modelNames.exception, $event)" + /> + </gl-form-group> + </div> + </div> + </form> +</template> diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/generic_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/generic_settings.vue new file mode 100644 index 00000000000..e5f63fe8d0d --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/settings/group/components/generic_settings.vue @@ -0,0 +1,26 @@ +<script> +import { s__ } from '~/locale'; +import SettingsTitles from '~/packages_and_registries/settings/group/components/settings_titles.vue'; + +export default { + name: 'GenericSettings', + components: { + SettingsTitles, + }, + i18n: { + title: s__('PackageRegistry|Generic'), + subTitle: s__('PackageRegistry|Settings for Generic packages'), + }, + modelNames: { + allowed: 'genericDuplicatesAllowed', + exception: 'genericDuplicateExceptionRegex', + }, +}; +</script> + +<template> + <div> + <settings-titles :title="$options.i18n.title" :sub-title="$options.i18n.subTitle" /> + <slot :model-names="$options.modelNames"></slot> + </div> +</template> diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue index 4f5c53ed4a3..01d4861f5c2 100644 --- a/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue +++ b/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue @@ -1,7 +1,8 @@ <script> import { GlSprintf, GlLink, GlAlert } from '@gitlab/ui'; +import DuplicatesSettings from '~/packages_and_registries/settings/group/components/duplicates_settings.vue'; +import GenericSettings from '~/packages_and_registries/settings/group/components/generic_settings.vue'; import MavenSettings from '~/packages_and_registries/settings/group/components/maven_settings.vue'; - import { PACKAGE_SETTINGS_HEADER, PACKAGE_SETTINGS_DESCRIPTION, @@ -30,6 +31,8 @@ export default { GlLink, SettingsBlock, MavenSettings, + GenericSettings, + DuplicatesSettings, }, inject: ['defaultExpanded', 'groupPath'], apollo: { @@ -128,13 +131,32 @@ export default { </span> </template> <template #default> - <maven-settings - :maven-duplicates-allowed="packageSettings.mavenDuplicatesAllowed" - :maven-duplicate-exception-regex="packageSettings.mavenDuplicateExceptionRegex" - :maven-duplicate-exception-regex-error="errors.mavenDuplicateExceptionRegex" - :loading="isLoading" - @update="updateSettings" - /> + <maven-settings data-testid="maven-settings"> + <template #default="{ modelNames }"> + <duplicates-settings + :duplicates-allowed="packageSettings.mavenDuplicatesAllowed" + :duplicate-exception-regex="packageSettings.mavenDuplicateExceptionRegex" + :duplicate-exception-regex-error="errors.mavenDuplicateExceptionRegex" + :model-names="modelNames" + :loading="isLoading" + toggle-qa-selector="allow_duplicates_toggle" + label-qa-selector="allow_duplicates_label" + @update="updateSettings" + /> + </template> + </maven-settings> + <generic-settings class="gl-mt-6" data-testid="generic-settings"> + <template #default="{ modelNames }"> + <duplicates-settings + :duplicates-allowed="packageSettings.genericDuplicatesAllowed" + :duplicate-exception-regex="packageSettings.genericDuplicateExceptionRegex" + :duplicate-exception-regex-error="errors.genericDuplicateExceptionRegex" + :model-names="modelNames" + :loading="isLoading" + @update="updateSettings" + /> + </template> + </generic-settings> </template> </settings-block> </div> diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/maven_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/maven_settings.vue index faacabb44ce..a1cbd695f34 100644 --- a/app/assets/javascripts/packages_and_registries/settings/group/components/maven_settings.vue +++ b/app/assets/javascripts/packages_and_registries/settings/group/components/maven_settings.vue @@ -1,118 +1,26 @@ <script> -import { GlSprintf, GlToggle, GlFormGroup, GlFormInput } from '@gitlab/ui'; - -import { - MAVEN_TOGGLE_LABEL, - MAVEN_TITLE, - MAVEN_SETTINGS_SUBTITLE, - MAVEN_DUPLICATES_ALLOWED_DISABLED, - MAVEN_DUPLICATES_ALLOWED_ENABLED, - MAVEN_SETTING_EXCEPTION_TITLE, - MAVEN_SETTINGS_EXCEPTION_LEGEND, - MAVEN_DUPLICATES_ALLOWED, - MAVEN_DUPLICATE_EXCEPTION_REGEX, -} from '~/packages_and_registries/settings/group/constants'; +import { s__ } from '~/locale'; +import SettingsTitles from '~/packages_and_registries/settings/group/components/settings_titles.vue'; export default { name: 'MavenSettings', - i18n: { - MAVEN_TOGGLE_LABEL, - MAVEN_TITLE, - MAVEN_SETTINGS_SUBTITLE, - MAVEN_SETTING_EXCEPTION_TITLE, - MAVEN_SETTINGS_EXCEPTION_LEGEND, - }, - modelNames: { - MAVEN_DUPLICATES_ALLOWED, - MAVEN_DUPLICATE_EXCEPTION_REGEX, - }, components: { - GlSprintf, - GlToggle, - GlFormGroup, - GlFormInput, - }, - props: { - loading: { - type: Boolean, - required: false, - default: false, - }, - mavenDuplicatesAllowed: { - type: Boolean, - default: false, - required: true, - }, - mavenDuplicateExceptionRegex: { - type: String, - default: '', - required: true, - }, - mavenDuplicateExceptionRegexError: { - type: String, - default: '', - required: false, - }, + SettingsTitles, }, - computed: { - enabledButtonLabel() { - return this.mavenDuplicatesAllowed - ? MAVEN_DUPLICATES_ALLOWED_ENABLED - : MAVEN_DUPLICATES_ALLOWED_DISABLED; - }, - isMavenDuplicateExceptionRegexValid() { - return !this.mavenDuplicateExceptionRegexError; - }, + i18n: { + title: s__('PackageRegistry|Maven'), + subTitle: s__('PackageRegistry|Settings for Maven packages'), }, - methods: { - update(type, value) { - this.$emit('update', { [type]: value }); - }, + modelNames: { + allowed: 'mavenDuplicatesAllowed', + exception: 'mavenDuplicateExceptionRegex', }, }; </script> <template> <div> - <h5 class="gl-border-b-solid gl-border-b-1 gl-border-gray-200"> - {{ $options.i18n.MAVEN_TITLE }} - </h5> - <p>{{ $options.i18n.MAVEN_SETTINGS_SUBTITLE }}</p> - <form> - <div class="gl-display-flex"> - <gl-toggle - data-qa-selector="allow_duplicates_toggle" - :label="$options.i18n.MAVEN_TOGGLE_LABEL" - label-position="hidden" - :value="mavenDuplicatesAllowed" - @change="update($options.modelNames.MAVEN_DUPLICATES_ALLOWED, $event)" - /> - <div class="gl-ml-5"> - <div data-testid="toggle-label" data-qa-selector="allow_duplicates_label"> - <gl-sprintf :message="enabledButtonLabel"> - <template #bold="{ content }"> - <strong>{{ content }}</strong> - </template> - </gl-sprintf> - </div> - <gl-form-group - v-if="!mavenDuplicatesAllowed" - class="gl-mt-4" - :label="$options.i18n.MAVEN_SETTING_EXCEPTION_TITLE" - label-size="sm" - :state="isMavenDuplicateExceptionRegexValid" - :invalid-feedback="mavenDuplicateExceptionRegexError" - :description="$options.i18n.MAVEN_SETTINGS_EXCEPTION_LEGEND" - label-for="maven-duplicated-settings-regex-input" - > - <gl-form-input - id="maven-duplicated-settings-regex-input" - :value="mavenDuplicateExceptionRegex" - @change="update($options.modelNames.MAVEN_DUPLICATE_EXCEPTION_REGEX, $event)" - /> - </gl-form-group> - </div> - </div> - </form> + <settings-titles :title="$options.i18n.title" :sub-title="$options.i18n.subTitle" /> + <slot :model-names="$options.modelNames"></slot> </div> </template> diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/settings_titles.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/settings_titles.vue new file mode 100644 index 00000000000..3f0ab7686e5 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/settings/group/components/settings_titles.vue @@ -0,0 +1,25 @@ +<script> +export default { + name: 'SettingsTitle', + props: { + title: { + type: String, + required: true, + }, + subTitle: { + type: String, + required: true, + }, + }, +}; +</script> + +<template> + <div> + <h5 class="gl-border-b-solid gl-border-b-1 gl-border-gray-200"> + {{ title }} + </h5> + <p>{{ subTitle }}</p> + <slot></slot> + </div> +</template> diff --git a/app/assets/javascripts/packages_and_registries/settings/group/constants.js b/app/assets/javascripts/packages_and_registries/settings/group/constants.js index d52a6a626f9..a2256c5c371 100644 --- a/app/assets/javascripts/packages_and_registries/settings/group/constants.js +++ b/app/assets/javascripts/packages_and_registries/settings/group/constants.js @@ -6,17 +6,15 @@ export const PACKAGE_SETTINGS_DESCRIPTION = s__( 'PackageRegistry|GitLab Packages allows organizations to utilize GitLab as a private repository for a variety of common package formats. %{linkStart}More Information%{linkEnd}', ); -export const MAVEN_TITLE = s__('PackageRegistry|Maven'); -export const MAVEN_SETTINGS_SUBTITLE = s__('PackageRegistry|Settings for Maven packages'); -export const MAVEN_TOGGLE_LABEL = s__('PackageRegistry|Allow duplicates'); -export const MAVEN_DUPLICATES_ALLOWED_DISABLED = s__( +export const DUPLICATES_TOGGLE_LABEL = s__('PackageRegistry|Allow duplicates'); +export const DUPLICATES_ALLOWED_DISABLED = s__( 'PackageRegistry|%{boldStart}Do not allow duplicates%{boldEnd} - Packages with the same name and version are rejected.', ); -export const MAVEN_DUPLICATES_ALLOWED_ENABLED = s__( +export const DUPLICATES_ALLOWED_ENABLED = s__( 'PackageRegistry|%{boldStart}Allow duplicates%{boldEnd} - Packages with the same name and version are accepted.', ); -export const MAVEN_SETTING_EXCEPTION_TITLE = __('Exceptions'); -export const MAVEN_SETTINGS_EXCEPTION_LEGEND = s__( +export const DUPLICATES_SETTING_EXCEPTION_TITLE = __('Exceptions'); +export const DUPLICATES_SETTINGS_EXCEPTION_LEGEND = s__( 'PackageRegistry|Packages can be published if their name or version matches this regex', ); diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql b/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql index 1fc59bd3496..5c245ff9453 100644 --- a/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql +++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql @@ -3,6 +3,8 @@ mutation updateNamespacePackageSettings($input: UpdateNamespacePackageSettingsIn packageSettings { mavenDuplicatesAllowed mavenDuplicateExceptionRegex + genericDuplicatesAllowed + genericDuplicateExceptionRegex } errors } diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql b/app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql index 2011659887d..a1c01300893 100644 --- a/app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql +++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql @@ -3,6 +3,8 @@ query getGroupPackagesSettings($fullPath: ID!) { packageSettings { mavenDuplicatesAllowed mavenDuplicateExceptionRegex + genericDuplicatesAllowed + genericDuplicateExceptionRegex } } } diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_dropdown.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_dropdown.vue new file mode 100644 index 00000000000..d75fb31fd98 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_dropdown.vue @@ -0,0 +1,50 @@ +<script> +import { GlFormGroup, GlFormSelect } from '@gitlab/ui'; + +export default { + components: { + GlFormGroup, + GlFormSelect, + }, + props: { + formOptions: { + type: Array, + required: false, + default: () => [], + }, + disabled: { + type: Boolean, + required: false, + default: false, + }, + value: { + type: String, + required: false, + default: '', + }, + name: { + type: String, + required: true, + }, + label: { + type: String, + required: true, + }, + }, +}; +</script> + +<template> + <gl-form-group :id="`${name}-form-group`" :label-for="name" :label="label"> + <gl-form-select :id="name" :value="value" :disabled="disabled" @input="$emit('input', $event)"> + <option + v-for="option in formOptions" + :key="option.key" + :value="option.key" + data-testid="option" + > + {{ option.label }} + </option> + </gl-form-select> + </gl-form-group> +</template> diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_input.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_input.vue new file mode 100644 index 00000000000..d6d85189792 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_input.vue @@ -0,0 +1,113 @@ +<script> +import { GlFormGroup, GlFormInput, GlSprintf, GlLink } from '@gitlab/ui'; +import { + NAME_REGEX_LENGTH, + TEXT_AREA_INVALID_FEEDBACK, +} from '~/packages_and_registries/settings/project/constants'; + +export default { + components: { + GlFormGroup, + GlFormInput, + GlSprintf, + GlLink, + }, + inject: ['tagsRegexHelpPagePath'], + props: { + error: { + type: String, + required: false, + default: '', + }, + disabled: { + type: Boolean, + required: false, + default: false, + }, + value: { + type: String, + required: false, + default: '', + }, + name: { + type: String, + required: true, + }, + label: { + type: String, + required: true, + }, + placeholder: { + type: String, + required: false, + default: '', + }, + description: { + type: String, + required: true, + }, + }, + computed: { + textAreaLengthErrorMessage() { + return this.isInputValid(this.value) ? '' : TEXT_AREA_INVALID_FEEDBACK; + }, + inputValidation() { + const nameRegexErrors = this.error || this.textAreaLengthErrorMessage; + return { + state: nameRegexErrors === null ? null : !nameRegexErrors, + message: nameRegexErrors, + }; + }, + internalValue: { + get() { + return this.value; + }, + set(value) { + this.$emit('input', value); + this.$emit('validation', this.isInputValid(value)); + }, + }, + }, + methods: { + isInputValid(value) { + return !value || value.length <= NAME_REGEX_LENGTH; + }, + }, +}; +</script> + +<template> + <gl-form-group + :id="`${name}-form-group`" + :label-for="name" + :state="inputValidation.state" + :invalid-feedback="inputValidation.message" + > + <template #label> + <span data-testid="label"> + <gl-sprintf :message="label"> + <template #italic="{ content }"> + <i>{{ content }}</i> + </template> + </gl-sprintf> + </span> + </template> + <gl-form-input + :id="name" + v-model="internalValue" + :placeholder="placeholder" + :state="inputValidation.state" + :disabled="disabled" + trim + /> + <template #description> + <span data-testid="description" class="gl-text-gray-400"> + <gl-sprintf :message="description"> + <template #link="{ content }"> + <gl-link :href="tagsRegexHelpPagePath" target="_blank">{{ content }}</gl-link> + </template> + </gl-sprintf> + </span> + </template> + </gl-form-group> +</template> diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_run_text.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_run_text.vue new file mode 100644 index 00000000000..0c595fa79b4 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_run_text.vue @@ -0,0 +1,49 @@ +<script> +import { GlFormGroup, GlFormInput } from '@gitlab/ui'; +import { + NEXT_CLEANUP_LABEL, + NOT_SCHEDULED_POLICY_TEXT, +} from '~/packages_and_registries/settings/project/constants'; + +export default { + components: { + GlFormGroup, + GlFormInput, + }, + props: { + value: { + type: String, + required: false, + default: NOT_SCHEDULED_POLICY_TEXT, + }, + enabled: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + parsedValue() { + return this.enabled ? this.value : NOT_SCHEDULED_POLICY_TEXT; + }, + }, + i18n: { + NEXT_CLEANUP_LABEL, + }, +}; +</script> + +<template> + <gl-form-group + id="expiration-policy-info-text-group" + :label="$options.i18n.NEXT_CLEANUP_LABEL" + label-for="expiration-policy-info-text" + > + <gl-form-input + id="expiration-policy-info-text" + class="gl-pl-0!" + plaintext + :value="parsedValue" + /> + </gl-form-group> +</template> diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_toggle.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_toggle.vue new file mode 100644 index 00000000000..7a9ea7c0bf7 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_toggle.vue @@ -0,0 +1,65 @@ +<script> +import { GlFormGroup, GlToggle, GlSprintf } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import { + ENABLED_TOGGLE_DESCRIPTION, + DISABLED_TOGGLE_DESCRIPTION, +} from '~/packages_and_registries/settings/project/constants'; + +export default { + i18n: { + toggleLabel: s__('ContainerRegistry|Enable expiration policy'), + }, + components: { + GlFormGroup, + GlToggle, + GlSprintf, + }, + props: { + disabled: { + type: Boolean, + required: false, + default: false, + }, + value: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + enabled: { + get() { + return this.value; + }, + set(value) { + this.$emit('input', value); + }, + }, + toggleText() { + return this.enabled ? ENABLED_TOGGLE_DESCRIPTION : DISABLED_TOGGLE_DESCRIPTION; + }, + }, +}; +</script> + +<template> + <gl-form-group id="expiration-policy-toggle-group" label-for="expiration-policy-toggle"> + <div class="gl-display-flex"> + <gl-toggle + id="expiration-policy-toggle" + v-model="enabled" + :label="$options.i18n.toggleLabel" + label-position="hidden" + :disabled="disabled" + /> + <span class="gl-ml-5 gl-line-height-24" data-testid="description"> + <gl-sprintf :message="toggleText"> + <template #strong="{ content }"> + <strong>{{ content }}</strong> + </template> + </gl-sprintf> + </span> + </div> + </gl-form-group> +</template> diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue new file mode 100644 index 00000000000..edbe9441e57 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue @@ -0,0 +1,106 @@ +<script> +import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui'; +import { isEqual, get, isEmpty } from 'lodash'; +import { + FETCH_SETTINGS_ERROR_MESSAGE, + UNAVAILABLE_FEATURE_TITLE, + UNAVAILABLE_FEATURE_INTRO_TEXT, + UNAVAILABLE_USER_FEATURE_TEXT, + UNAVAILABLE_ADMIN_FEATURE_TEXT, +} from '~/packages_and_registries/settings/project/constants'; +import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql'; + +import SettingsForm from './settings_form.vue'; + +export default { + components: { + SettingsForm, + GlAlert, + GlSprintf, + GlLink, + }, + inject: ['projectPath', 'isAdmin', 'adminSettingsPath', 'enableHistoricEntries'], + i18n: { + UNAVAILABLE_FEATURE_TITLE, + UNAVAILABLE_FEATURE_INTRO_TEXT, + FETCH_SETTINGS_ERROR_MESSAGE, + }, + apollo: { + containerExpirationPolicy: { + query: expirationPolicyQuery, + variables() { + return { + projectPath: this.projectPath, + }; + }, + update: (data) => data.project?.containerExpirationPolicy, + result({ data }) { + this.workingCopy = { ...get(data, 'project.containerExpirationPolicy', {}) }; + }, + error(e) { + this.fetchSettingsError = e; + }, + }, + }, + data() { + return { + fetchSettingsError: false, + containerExpirationPolicy: null, + workingCopy: {}, + }; + }, + computed: { + isDisabled() { + return !(this.containerExpirationPolicy || this.enableHistoricEntries); + }, + showDisabledFormMessage() { + return this.isDisabled && !this.fetchSettingsError; + }, + unavailableFeatureMessage() { + 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); + }, + }, + methods: { + restoreOriginal() { + this.workingCopy = { ...this.containerExpirationPolicy }; + }, + }, +}; +</script> + +<template> + <div> + <settings-form + v-if="!isDisabled" + v-model="workingCopy" + :is-loading="$apollo.queries.containerExpirationPolicy.loading" + :is-edited="isEdited" + @reset="restoreOriginal" + /> + <template v-else> + <gl-alert + v-if="showDisabledFormMessage" + :dismissible="false" + :title="$options.i18n.UNAVAILABLE_FEATURE_TITLE" + variant="tip" + > + {{ $options.i18n.UNAVAILABLE_FEATURE_INTRO_TEXT }} + + <gl-sprintf :message="unavailableFeatureMessage"> + <template #link="{ content }"> + <gl-link :href="adminSettingsPath" target="_blank">{{ content }}</gl-link> + </template> + </gl-sprintf> + </gl-alert> + <gl-alert v-else-if="fetchSettingsError" variant="warning" :dismissible="false"> + <gl-sprintf :message="$options.i18n.FETCH_SETTINGS_ERROR_MESSAGE" /> + </gl-alert> + </template> + </div> +</template> diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/settings_form.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/settings_form.vue new file mode 100644 index 00000000000..41be70a3ad5 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/settings/project/components/settings_form.vue @@ -0,0 +1,313 @@ +<script> +import { GlCard, GlButton, GlSprintf } from '@gitlab/ui'; +import { + UPDATE_SETTINGS_ERROR_MESSAGE, + UPDATE_SETTINGS_SUCCESS_MESSAGE, + 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 '~/packages_and_registries/settings/project/constants'; +import updateContainerExpirationPolicyMutation from '~/packages_and_registries/settings/project/graphql/mutations/update_container_expiration_policy.mutation.graphql'; +import { updateContainerExpirationPolicy } from '~/packages_and_registries/settings/project/graphql/utils/cache_update'; +import { formOptionsGenerator } from '~/packages_and_registries/settings/project/utils'; +import Tracking from '~/tracking'; +import ExpirationDropdown from './expiration_dropdown.vue'; +import ExpirationInput from './expiration_input.vue'; +import ExpirationRunText from './expiration_run_text.vue'; +import ExpirationToggle from './expiration_toggle.vue'; + +export default { + components: { + GlCard, + GlButton, + GlSprintf, + ExpirationDropdown, + ExpirationInput, + ExpirationToggle, + ExpirationRunText, + }, + mixins: [Tracking.mixin()], + inject: ['projectPath'], + props: { + value: { + type: Object, + required: true, + }, + isLoading: { + type: Boolean, + required: false, + default: false, + }, + isEdited: { + type: Boolean, + required: false, + default: false, + }, + }, + + formOptions: formOptionsGenerator(), + i18n: { + 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', + }, + apiErrors: {}, + localErrors: {}, + mutationLoading: false, + }; + }, + computed: { + prefilledForm() { + return { + ...this.value, + cadence: this.findDefaultOption('cadence'), + keepN: this.findDefaultOption('keepN'), + olderThan: this.findDefaultOption('olderThan'), + }; + }, + 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, + enabled: this.prefilledForm.enabled, + cadence: this.prefilledForm.cadence, + olderThan: this.prefilledForm.olderThan, + keepN: this.prefilledForm.keepN, + nameRegex: this.prefilledForm.nameRegex, + nameRegexKeep: this.prefilledForm.nameRegexKeep, + }; + }, + }, + methods: { + findDefaultOption(option) { + return this.value[option] || this.$options.formOptions[option].find((f) => f.default)?.key; + }, + reset() { + this.track('reset_form'); + this.apiErrors = {}; + this.localErrors = {}; + this.$emit('reset'); + }, + setApiErrors(response) { + this.apiErrors = response.graphQLErrors.reduce((acc, curr) => { + curr.extensions.problems.forEach((item) => { + acc[item.path[0]] = item.message; + }); + return acc; + }, {}); + }, + setLocalErrors(state, model) { + this.localErrors = { + ...this.localErrors, + [model]: state, + }; + }, + submit() { + this.track('submit_form'); + this.apiErrors = {}; + this.mutationLoading = true; + return this.$apollo + .mutate({ + mutation: updateContainerExpirationPolicyMutation, + variables: { + input: this.mutationVariables, + }, + update: updateContainerExpirationPolicy(this.projectPath), + }) + .then(({ data }) => { + const errorMessage = data?.updateContainerExpirationPolicy?.errors[0]; + if (errorMessage) { + this.$toast.show(errorMessage, { type: 'error' }); + } else { + this.$toast.show(UPDATE_SETTINGS_SUCCESS_MESSAGE, { type: 'success' }); + } + }) + .catch((error) => { + this.setApiErrors(error); + this.$toast.show(UPDATE_SETTINGS_ERROR_MESSAGE, { type: 'error' }); + }) + .finally(() => { + this.mutationLoading = false; + }); + }, + onModelChange(newValue, model) { + this.$emit('input', { ...this.value, [model]: newValue }); + this.apiErrors[model] = undefined; + }, + }, +}; +</script> + +<template> + <form ref="form-element" @submit.prevent="submit" @reset.prevent="reset"> + <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" + :enabled="prefilledForm.enabled" + class="gl-mb-0!" + /> + </div> + <gl-card class="gl-mt-7"> + <template #header> + {{ $options.i18n.KEEP_HEADER_TEXT }} + </template> + <template #default> + <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-input + 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-input" + @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 #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-input + 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-input" + @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" + category="primary" + variant="confirm" + 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> diff --git a/app/assets/javascripts/packages_and_registries/settings/project/constants.js b/app/assets/javascripts/packages_and_registries/settings/project/constants.js new file mode 100644 index 00000000000..165c4aae3cb --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/settings/project/constants.js @@ -0,0 +1,91 @@ +import { s__, __ } from '~/locale'; + +export const SET_CLEANUP_POLICY_BUTTON = __('Save'); +export const UNAVAILABLE_FEATURE_TITLE = s__( + `ContainerRegistry|Cleanup policy for tags is disabled`, +); +export const UNAVAILABLE_FEATURE_INTRO_TEXT = s__( + `ContainerRegistry|This project's cleanup policy for tags is not enabled.`, +); +export const UNAVAILABLE_USER_FEATURE_TEXT = __(`Please contact your administrator.`); +export const UNAVAILABLE_ADMIN_FEATURE_TEXT = s__( + `ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature.`, +); + +export const TEXT_AREA_INVALID_FEEDBACK = s__( + 'ContainerRegistry|The value of this input should be less than 256 characters', +); + +export const KEEP_HEADER_TEXT = s__('ContainerRegistry|Keep these tags'); +export const KEEP_INFO_TEXT = s__( + '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_DESCRIPTION = s__( + 'ContainerRegistry|Tags with names that match this regex pattern are kept. %{linkStart}View regex examples.%{linkEnd}', +); + +export const REMOVE_HEADER_TEXT = s__('ContainerRegistry|Remove these tags'); +export const REMOVE_INFO_TEXT = s__( + '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 that match this regex pattern are removed. %{linkStart}View regex examples.%{linkEnd}', +); + +export const ENABLED_TOGGLE_DESCRIPTION = s__( + 'ContainerRegistry|%{strongStart}Enabled%{strongEnd} - Tags that match the rules on this page are automatically scheduled for deletion.', +); +export const DISABLED_TOGGLE_DESCRIPTION = s__( + 'ContainerRegistry|%{strongStart}Disabled%{strongEnd} - Tags will not be automatically deleted.', +); + +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'); +export const EXPIRATION_POLICY_FOOTER_NOTE = s__( + 'ContainerRegistry|Note: Any policy update will result in a change to the scheduled run date and time', +); + +export const KEEP_N_OPTIONS = [ + { key: 'ONE_TAG', variable: 1, default: false }, + { key: 'FIVE_TAGS', variable: 5, default: false }, + { key: 'TEN_TAGS', variable: 10, default: true }, + { key: 'TWENTY_FIVE_TAGS', variable: 25, default: false }, + { key: 'FIFTY_TAGS', variable: 50, default: false }, + { key: 'ONE_HUNDRED_TAGS', variable: 100, default: false }, +]; + +export const CADENCE_OPTIONS = [ + { key: 'EVERY_DAY', label: __('Every day'), default: true }, + { key: 'EVERY_WEEK', label: __('Every week'), default: false }, + { key: 'EVERY_TWO_WEEKS', label: __('Every two weeks'), default: false }, + { key: 'EVERY_MONTH', label: __('Every month'), default: false }, + { key: 'EVERY_THREE_MONTHS', label: __('Every three months'), default: false }, +]; + +export const OLDER_THAN_OPTIONS = [ + { key: 'SEVEN_DAYS', variable: 7, default: false }, + { key: 'FOURTEEN_DAYS', variable: 14, default: false }, + { key: 'THIRTY_DAYS', variable: 30, default: false }, + { key: 'NINETY_DAYS', variable: 90, default: true }, +]; + +export const FETCH_SETTINGS_ERROR_MESSAGE = s__( + 'ContainerRegistry|Something went wrong while fetching the cleanup policy.', +); + +export const UPDATE_SETTINGS_ERROR_MESSAGE = s__( + 'ContainerRegistry|Something went wrong while updating the cleanup policy.', +); + +export const UPDATE_SETTINGS_SUCCESS_MESSAGE = s__( + 'ContainerRegistry|Cleanup policy successfully saved.', +); + +export const NAME_REGEX_LENGTH = 255; diff --git a/app/assets/javascripts/packages_and_registries/settings/project/graphql/fragments/container_expiration_policy.fragment.graphql b/app/assets/javascripts/packages_and_registries/settings/project/graphql/fragments/container_expiration_policy.fragment.graphql new file mode 100644 index 00000000000..1d6c89133af --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/settings/project/graphql/fragments/container_expiration_policy.fragment.graphql @@ -0,0 +1,9 @@ +fragment ContainerExpirationPolicyFields on ContainerExpirationPolicy { + cadence + enabled + keepN + nameRegex + nameRegexKeep + olderThan + nextRunAt +} diff --git a/app/assets/javascripts/packages_and_registries/settings/project/graphql/index.js b/app/assets/javascripts/packages_and_registries/settings/project/graphql/index.js new file mode 100644 index 00000000000..16152eb81f6 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/settings/project/graphql/index.js @@ -0,0 +1,14 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; + +Vue.use(VueApollo); + +export const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient( + {}, + { + assumeImmutableResults: true, + }, + ), +}); diff --git a/app/assets/javascripts/packages_and_registries/settings/project/graphql/mutations/update_container_expiration_policy.mutation.graphql b/app/assets/javascripts/packages_and_registries/settings/project/graphql/mutations/update_container_expiration_policy.mutation.graphql new file mode 100644 index 00000000000..c40cd115ab0 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/settings/project/graphql/mutations/update_container_expiration_policy.mutation.graphql @@ -0,0 +1,10 @@ +#import "../fragments/container_expiration_policy.fragment.graphql" + +mutation updateContainerExpirationPolicy($input: UpdateContainerExpirationPolicyInput!) { + updateContainerExpirationPolicy(input: $input) { + containerExpirationPolicy { + ...ContainerExpirationPolicyFields + } + errors + } +} diff --git a/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql b/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql new file mode 100644 index 00000000000..c171be0ad07 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql @@ -0,0 +1,9 @@ +#import "../fragments/container_expiration_policy.fragment.graphql" + +query getProjectExpirationPolicy($projectPath: ID!) { + project(fullPath: $projectPath) { + containerExpirationPolicy { + ...ContainerExpirationPolicyFields + } + } +} diff --git a/app/assets/javascripts/packages_and_registries/settings/project/graphql/utils/cache_update.js b/app/assets/javascripts/packages_and_registries/settings/project/graphql/utils/cache_update.js new file mode 100644 index 00000000000..c4b2af13862 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/settings/project/graphql/utils/cache_update.js @@ -0,0 +1,21 @@ +import { produce } from 'immer'; +import expirationPolicyQuery from '../queries/get_expiration_policy.query.graphql'; + +export const updateContainerExpirationPolicy = (projectPath) => (client, { data: updatedData }) => { + const queryAndParams = { + query: expirationPolicyQuery, + variables: { projectPath }, + }; + const sourceData = client.readQuery(queryAndParams); + + const data = produce(sourceData, (draftState) => { + draftState.project.containerExpirationPolicy = { + ...updatedData.updateContainerExpirationPolicy.containerExpirationPolicy, + }; + }); + + client.writeQuery({ + ...queryAndParams, + data, + }); +}; diff --git a/app/assets/javascripts/packages_and_registries/settings/project/registry_settings_bundle.js b/app/assets/javascripts/packages_and_registries/settings/project/registry_settings_bundle.js new file mode 100644 index 00000000000..65af6f846aa --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/settings/project/registry_settings_bundle.js @@ -0,0 +1,40 @@ +import { GlToast } from '@gitlab/ui'; +import Vue from 'vue'; +import { parseBoolean } from '~/lib/utils/common_utils'; +import Translate from '~/vue_shared/translate'; +import RegistrySettingsApp from './components/registry_settings_app.vue'; +import { apolloProvider } from './graphql/index'; + +Vue.use(GlToast); +Vue.use(Translate); + +export default () => { + const el = document.getElementById('js-registry-settings'); + if (!el) { + return null; + } + const { + isAdmin, + enableHistoricEntries, + projectPath, + adminSettingsPath, + tagsRegexHelpPagePath, + } = el.dataset; + return new Vue({ + el, + apolloProvider, + components: { + RegistrySettingsApp, + }, + provide: { + isAdmin: parseBoolean(isAdmin), + enableHistoricEntries: parseBoolean(enableHistoricEntries), + projectPath, + adminSettingsPath, + tagsRegexHelpPagePath, + }, + render(createElement) { + return createElement('registry-settings-app', {}); + }, + }); +}; diff --git a/app/assets/javascripts/packages_and_registries/settings/project/utils.js b/app/assets/javascripts/packages_and_registries/settings/project/utils.js new file mode 100644 index 00000000000..4a2d7c7d466 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/settings/project/utils.js @@ -0,0 +1,26 @@ +import { n__ } from '~/locale'; +import { KEEP_N_OPTIONS, CADENCE_OPTIONS, OLDER_THAN_OPTIONS } from './constants'; + +export const findDefaultOption = (options) => { + const item = options.find((o) => o.default); + return item ? item.key : null; +}; + +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); + +export const optionLabelGenerator = (collection, translationFn) => + collection.map((option) => ({ + ...option, + label: translationFn(option.variable), + })); + +export const formOptionsGenerator = () => { + return { + olderThan: optionLabelGenerator(OLDER_THAN_OPTIONS, olderThanTranslationGenerator), + cadence: CADENCE_OPTIONS, + keepN: optionLabelGenerator(KEEP_N_OPTIONS, keepNTranslationGenerator), + }; +}; |