summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/registry
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-10-21 07:08:36 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-10-21 07:08:36 +0000
commit48aff82709769b098321c738f3444b9bdaa694c6 (patch)
treee00c7c43e2d9b603a5a6af576b1685e400410dee /app/assets/javascripts/registry
parent879f5329ee916a948223f8f43d77fba4da6cd028 (diff)
downloadgitlab-ce-48aff82709769b098321c738f3444b9bdaa694c6.tar.gz
Add latest changes from gitlab-org/gitlab@13-5-stable-eev13.5.0-rc42
Diffstat (limited to 'app/assets/javascripts/registry')
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/partial_cleanup_alert.vue38
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue14
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue18
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue79
-rw-r--r--app/assets/javascripts/registry/explorer/constants/expiration_policies.js7
-rw-r--r--app/assets/javascripts/registry/explorer/pages/details.vue20
-rw-r--r--app/assets/javascripts/registry/settings/components/registry_settings_app.vue50
-rw-r--r--app/assets/javascripts/registry/settings/components/settings_form.vue145
-rw-r--r--app/assets/javascripts/registry/settings/graphql/fragments/container_expiration_policy.fragment.graphql8
-rw-r--r--app/assets/javascripts/registry/settings/graphql/index.js14
-rw-r--r--app/assets/javascripts/registry/settings/graphql/mutations/update_container_expiration_policy.graphql10
-rw-r--r--app/assets/javascripts/registry/settings/graphql/queries/get_expiration_policy.graphql9
-rw-r--r--app/assets/javascripts/registry/settings/graphql/utils/cache_update.js22
-rw-r--r--app/assets/javascripts/registry/settings/registry_settings_bundle.js13
-rw-r--r--app/assets/javascripts/registry/settings/store/actions.js30
-rw-r--r--app/assets/javascripts/registry/settings/store/getters.js26
-rw-r--r--app/assets/javascripts/registry/settings/store/index.js18
-rw-r--r--app/assets/javascripts/registry/settings/store/mutation_types.js5
-rw-r--r--app/assets/javascripts/registry/settings/store/mutations.js29
-rw-r--r--app/assets/javascripts/registry/settings/store/state.js42
-rw-r--r--app/assets/javascripts/registry/shared/components/expiration_policy_fields.vue32
-rw-r--r--app/assets/javascripts/registry/shared/constants.js24
-rw-r--r--app/assets/javascripts/registry/shared/utils.js27
23 files changed, 385 insertions, 295 deletions
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/partial_cleanup_alert.vue b/app/assets/javascripts/registry/explorer/components/details_page/partial_cleanup_alert.vue
new file mode 100644
index 00000000000..d13d815a59e
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/components/details_page/partial_cleanup_alert.vue
@@ -0,0 +1,38 @@
+<script>
+import { GlSprintf, GlAlert, GlLink } from '@gitlab/ui';
+
+import { DELETE_ALERT_TITLE, DELETE_ALERT_LINK_TEXT } from '../../constants/index';
+
+export default {
+ components: {
+ GlSprintf,
+ GlAlert,
+ GlLink,
+ },
+ props: {
+ runCleanupPoliciesHelpPagePath: { type: String, required: false, default: '' },
+ cleanupPoliciesHelpPagePath: { type: String, required: false, default: '' },
+ },
+ i18n: {
+ DELETE_ALERT_TITLE,
+ DELETE_ALERT_LINK_TEXT,
+ },
+};
+</script>
+
+<template>
+ <gl-alert variant="warning" :title="$options.i18n.DELETE_ALERT_TITLE" @dismiss="$emit('dismiss')">
+ <gl-sprintf :message="$options.i18n.DELETE_ALERT_LINK_TEXT">
+ <template #adminLink="{content}">
+ <gl-link data-testid="run-link" :href="runCleanupPoliciesHelpPagePath" target="_blank">{{
+ content
+ }}</gl-link>
+ </template>
+ <template #docLink="{content}">
+ <gl-link data-testid="help-link" :href="cleanupPoliciesHelpPagePath" target="_blank">{{
+ content
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </gl-alert>
+</template>
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue b/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue
index 661213733ac..0f6297ca406 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue
+++ b/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue
@@ -123,7 +123,7 @@ export default {
v-if="tag.location"
:title="tag.location"
:text="tag.location"
- css-class="btn-default btn-transparent btn-clipboard"
+ category="tertiary"
/>
<gl-icon
@@ -171,7 +171,7 @@ export default {
/>
</template>
- <template v-if="!invalidTag" #details_published>
+ <template v-if="!invalidTag" #details-published>
<details-row icon="clock" data-testid="published-date-detail">
<gl-sprintf :message="$options.i18n.PUBLISHED_DETAILS_ROW_TEXT">
<template #repositoryPath>
@@ -186,7 +186,7 @@ export default {
</gl-sprintf>
</details-row>
</template>
- <template v-if="!invalidTag" #details_manifest_digest>
+ <template v-if="!invalidTag" #details-manifest-digest>
<details-row icon="log" data-testid="manifest-detail">
<gl-sprintf :message="$options.i18n.MANIFEST_DETAILS_ROW_TEST">
<template #digest>
@@ -197,11 +197,12 @@ export default {
v-if="tag.digest"
:title="tag.digest"
:text="tag.digest"
- css-class="btn-default btn-transparent btn-clipboard gl-p-0"
+ category="tertiary"
+ size="small"
/>
</details-row>
</template>
- <template v-if="!invalidTag" #details_configuration_digest>
+ <template v-if="!invalidTag" #details-configuration-digest>
<details-row icon="cloud-gear" data-testid="configuration-detail">
<gl-sprintf :message="$options.i18n.CONFIGURATION_DETAILS_ROW_TEST">
<template #digest>
@@ -212,7 +213,8 @@ export default {
v-if="formattedRevision"
:title="formattedRevision"
:text="formattedRevision"
- css-class="btn-default btn-transparent btn-clipboard gl-p-0"
+ category="tertiary"
+ size="small"
/>
</details-row>
</template>
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue b/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue
index 32bf27f1143..cfd787b3f52 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue
+++ b/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue
@@ -10,6 +10,7 @@ import {
LIST_DELETE_BUTTON_DISABLED,
REMOVE_REPOSITORY_LABEL,
ROW_SCHEDULED_FOR_DELETION,
+ CLEANUP_TIMED_OUT_ERROR_MESSAGE,
} from '../../constants/index';
export default {
@@ -34,7 +35,6 @@ export default {
LIST_DELETE_BUTTON_DISABLED,
REMOVE_REPOSITORY_LABEL,
ROW_SCHEDULED_FOR_DELETION,
- ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
},
computed: {
encodedItem() {
@@ -42,6 +42,7 @@ export default {
name: this.item.path,
tags_path: this.item.tags_path,
id: this.item.id,
+ cleanup_policy_started_at: this.item.cleanup_policy_started_at,
});
return window.btoa(params);
},
@@ -55,6 +56,14 @@ export default {
this.item.tags_count,
);
},
+ warningIconText() {
+ if (this.item.failedDelete) {
+ return ASYNC_DELETE_IMAGE_ERROR_MESSAGE;
+ } else if (this.item.cleanup_policy_started_at) {
+ return CLEANUP_TIMED_OUT_ERROR_MESSAGE;
+ }
+ return null;
+ },
},
};
</script>
@@ -82,11 +91,12 @@ export default {
:disabled="item.deleting"
:text="item.location"
:title="item.location"
- css-class="btn-default btn-transparent btn-clipboard gl-text-gray-300"
+ category="tertiary"
/>
<gl-icon
- v-if="item.failedDelete"
- v-gl-tooltip="{ title: $options.i18n.ASYNC_DELETE_IMAGE_ERROR_MESSAGE }"
+ v-if="warningIconText"
+ v-gl-tooltip="{ title: warningIconText }"
+ data-testid="warning-icon"
name="warning"
class="gl-text-orange-500"
/>
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue b/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue
index 7be68e77def..c2bd01701df 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue
+++ b/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue
@@ -1,5 +1,4 @@
<script>
-import { GlSprintf, GlLink } from '@gitlab/ui';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
import { n__, sprintf } from '~/locale';
@@ -15,8 +14,6 @@ import {
export default {
components: {
- GlSprintf,
- GlLink,
TitleArea,
MetadataItem,
},
@@ -54,8 +51,6 @@ export default {
},
i18n: {
CONTAINER_REGISTRY_TITLE,
- LIST_INTRO_TEXT,
- EXPIRATION_POLICY_DISABLED_MESSAGE,
},
computed: {
imagesCountText() {
@@ -83,52 +78,40 @@ export default {
!this.expirationPolicyEnabled && this.imagesCount > 0 && !this.hideExpirationPolicyData
);
},
+ infoMessages() {
+ const base = [{ text: LIST_INTRO_TEXT, link: this.helpPagePath }];
+ return this.showExpirationPolicyTip
+ ? [
+ ...base,
+ { text: EXPIRATION_POLICY_DISABLED_MESSAGE, link: this.expirationPolicyHelpPagePath },
+ ]
+ : base;
+ },
},
};
</script>
<template>
- <div>
- <title-area :title="$options.i18n.CONTAINER_REGISTRY_TITLE">
- <template #right-actions>
- <slot name="commands"></slot>
- </template>
- <template #metadata_count>
- <metadata-item
- v-if="imagesCount"
- data-testid="images-count"
- icon="container-image"
- :text="imagesCountText"
- />
- </template>
- <template #metadata_exp_policies>
- <metadata-item
- v-if="!hideExpirationPolicyData"
- data-testid="expiration-policy"
- icon="expire"
- :text="expirationPolicyText"
- size="xl"
- />
- </template>
- </title-area>
-
- <div data-testid="info-area">
- <p>
- <span data-testid="default-intro">
- <gl-sprintf :message="$options.i18n.LIST_INTRO_TEXT">
- <template #docLink="{content}">
- <gl-link :href="helpPagePath" target="_blank">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </span>
- <span v-if="showExpirationPolicyTip" data-testid="expiration-disabled-message">
- <gl-sprintf :message="$options.i18n.EXPIRATION_POLICY_DISABLED_MESSAGE">
- <template #docLink="{content}">
- <gl-link :href="expirationPolicyHelpPagePath" target="_blank">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </span>
- </p>
- </div>
- </div>
+ <title-area :title="$options.i18n.CONTAINER_REGISTRY_TITLE" :info-messages="infoMessages">
+ <template #right-actions>
+ <slot name="commands"></slot>
+ </template>
+ <template #metadata-count>
+ <metadata-item
+ v-if="imagesCount"
+ data-testid="images-count"
+ icon="container-image"
+ :text="imagesCountText"
+ />
+ </template>
+ <template #metadata-exp-policies>
+ <metadata-item
+ v-if="!hideExpirationPolicyData"
+ data-testid="expiration-policy"
+ icon="expire"
+ :text="expirationPolicyText"
+ size="xl"
+ />
+ </template>
+ </title-area>
</template>
diff --git a/app/assets/javascripts/registry/explorer/constants/expiration_policies.js b/app/assets/javascripts/registry/explorer/constants/expiration_policies.js
index 8af25ca6ecc..48a6a015461 100644
--- a/app/assets/javascripts/registry/explorer/constants/expiration_policies.js
+++ b/app/assets/javascripts/registry/explorer/constants/expiration_policies.js
@@ -9,3 +9,10 @@ export const EXPIRATION_POLICY_DISABLED_TEXT = s__(
export const EXPIRATION_POLICY_DISABLED_MESSAGE = s__(
'ContainerRegistry|Expiration policies help manage the storage space used by the Container Registry, but the expiration policies for this registry are disabled. Contact your administrator to enable. %{docLinkStart}More information%{docLinkEnd}',
);
+export const DELETE_ALERT_TITLE = s__('ContainerRegistry|Some tags were not deleted');
+export const DELETE_ALERT_LINK_TEXT = s__(
+ '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}',
+);
+export const CLEANUP_TIMED_OUT_ERROR_MESSAGE = s__(
+ 'ContainerRegistry|Cleanup timed out before it could delete all tags',
+);
diff --git a/app/assets/javascripts/registry/explorer/pages/details.vue b/app/assets/javascripts/registry/explorer/pages/details.vue
index b697bca6259..d2fb695dbfa 100644
--- a/app/assets/javascripts/registry/explorer/pages/details.vue
+++ b/app/assets/javascripts/registry/explorer/pages/details.vue
@@ -4,6 +4,7 @@ import { GlPagination, GlResizeObserverDirective } from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import Tracking from '~/tracking';
import DeleteAlert from '../components/details_page/delete_alert.vue';
+import PartialCleanupAlert from '../components/details_page/partial_cleanup_alert.vue';
import DeleteModal from '../components/details_page/delete_modal.vue';
import DetailsHeader from '../components/details_page/details_header.vue';
import TagsList from '../components/details_page/tags_list.vue';
@@ -21,6 +22,7 @@ import {
export default {
components: {
DeleteAlert,
+ PartialCleanupAlert,
DetailsHeader,
GlPagination,
DeleteModal,
@@ -37,13 +39,16 @@ export default {
itemsToBeDeleted: [],
isDesktop: true,
deleteAlertType: null,
+ dismissPartialCleanupWarning: false,
};
},
computed: {
...mapState(['tagsPagination', 'isLoading', 'config', 'tags']),
- imageName() {
- const { name } = decodeAndParse(this.$route.params.id);
- return name;
+ queryParameters() {
+ return decodeAndParse(this.$route.params.id);
+ },
+ showPartialCleanupWarning() {
+ return this.queryParameters.cleanup_policy_started_at && !this.dismissPartialCleanupWarning;
},
tracking() {
return {
@@ -120,7 +125,14 @@ export default {
class="gl-my-2"
/>
- <details-header :image-name="imageName" />
+ <partial-cleanup-alert
+ v-if="showPartialCleanupWarning"
+ :run-cleanup-policies-help-page-path="config.runCleanupPoliciesHelpPagePath"
+ :cleanup-policies-help-page-path="config.cleanupPoliciesHelpPagePath"
+ @dismiss="dismissPartialCleanupWarning = true"
+ />
+
+ <details-header :image-name="queryParameters.name" />
<tags-loader v-if="isLoading" />
<template v-else>
diff --git a/app/assets/javascripts/registry/settings/components/registry_settings_app.vue b/app/assets/javascripts/registry/settings/components/registry_settings_app.vue
index 2ee7bbef4c6..264d39a406a 100644
--- a/app/assets/javascripts/registry/settings/components/registry_settings_app.vue
+++ b/app/assets/javascripts/registry/settings/components/registry_settings_app.vue
@@ -1,7 +1,7 @@
<script>
-import { mapActions, mapGetters, mapState } from 'vuex';
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
-
+import { isEqual, get } from 'lodash';
+import expirationPolicyQuery from '../graphql/queries/get_expiration_policy.graphql';
import { FETCH_SETTINGS_ERROR_MESSAGE } from '../../shared/constants';
import SettingsForm from './settings_form.vue';
@@ -19,21 +19,39 @@ export default {
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: {
- ...mapState(['isAdmin', 'adminSettingsPath']),
- ...mapGetters({ isDisabled: 'getIsDisabled' }),
- showSettingForm() {
- return !this.isDisabled && !this.fetchSettingsError;
+ isDisabled() {
+ return !(this.containerExpirationPolicy || this.enableHistoricEntries);
},
showDisabledFormMessage() {
return this.isDisabled && !this.fetchSettingsError;
@@ -41,21 +59,27 @@ export default {
unavailableFeatureMessage() {
return this.isAdmin ? UNAVAILABLE_ADMIN_FEATURE_TEXT : UNAVAILABLE_USER_FEATURE_TEXT;
},
- },
- mounted() {
- this.fetchSettings().catch(() => {
- this.fetchSettingsError = true;
- });
+ isEdited() {
+ return !isEqual(this.containerExpirationPolicy, this.workingCopy);
+ },
},
methods: {
- ...mapActions(['fetchSettings']),
+ restoreOriginal() {
+ this.workingCopy = { ...this.containerExpirationPolicy };
+ },
},
};
</script>
<template>
<div>
- <settings-form v-if="showSettingForm" />
+ <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"
diff --git a/app/assets/javascripts/registry/settings/components/settings_form.vue b/app/assets/javascripts/registry/settings/components/settings_form.vue
index 7a26fb5cbee..a9b35d4e29f 100644
--- a/app/assets/javascripts/registry/settings/components/settings_form.vue
+++ b/app/assets/javascripts/registry/settings/components/settings_form.vue
@@ -1,28 +1,45 @@
<script>
-import { get } from 'lodash';
-import { mapActions, mapState, mapGetters } from 'vuex';
-import { GlCard, GlButton, GlLoadingIcon } from '@gitlab/ui';
+import { GlCard, GlButton } from '@gitlab/ui';
import Tracking from '~/tracking';
-import { mapComputed } from '~/vuex_shared/bindings';
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';
+import { formOptionsGenerator } from '~/registry/shared/utils';
+import updateContainerExpirationPolicyMutation from '../graphql/mutations/update_container_expiration_policy.graphql';
+import { updateContainerExpirationPolicy } from '../graphql/utils/cache_update';
export default {
components: {
GlCard,
GlButton,
- GlLoadingIcon,
ExpirationPolicyFields,
},
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,
+ },
+ },
labelsConfig: {
cols: 3,
align: 'right',
},
+ formOptions: formOptionsGenerator(),
i18n: {
CLEANUP_POLICY_CARD_HEADER,
SET_CLEANUP_POLICY_BUTTON,
@@ -34,49 +51,85 @@ export default {
},
fieldsAreValid: true,
apiErrors: null,
+ mutationLoading: false,
};
},
computed: {
- ...mapState(['formOptions', 'isLoading']),
- ...mapGetters({ isEdited: 'getIsEdited' }),
- ...mapComputed([{ key: 'settings', getter: 'getSettings' }], 'updateSettings'),
+ prefilledForm() {
+ return {
+ ...this.value,
+ cadence: this.findDefaultOption('cadence'),
+ keepN: this.findDefaultOption('keepN'),
+ olderThan: this.findDefaultOption('olderThan'),
+ };
+ },
+ showLoadingIcon() {
+ return this.isLoading || this.mutationLoading;
+ },
isSubmitButtonDisabled() {
- return !this.fieldsAreValid || this.isLoading;
+ return !this.fieldsAreValid || this.showLoadingIcon;
},
isCancelButtonDisabled() {
- return !this.isEdited || this.isLoading;
+ return !this.isEdited || this.isLoading || this.mutationLoading;
+ },
+ mutationVariables() {
+ return {
+ projectPath: this.projectPath,
+ enabled: this.value.enabled,
+ cadence: this.value.cadence,
+ olderThan: this.value.olderThan,
+ keepN: this.value.keepN,
+ nameRegex: this.value.nameRegex,
+ nameRegexKeep: this.value.nameRegexKeep,
+ };
},
},
methods: {
- ...mapActions(['resetSettings', 'saveSettings']),
+ findDefaultOption(option) {
+ return this.value[option] || this.$options.formOptions[option].find(f => f.default)?.key;
+ },
reset() {
this.track('reset_form');
this.apiErrors = null;
- this.resetSettings();
+ this.$emit('reset');
},
setApiErrors(response) {
- const messages = get(response, 'data.message', []);
-
- this.apiErrors = Object.keys(messages).reduce((acc, curr) => {
- if (curr.startsWith('container_expiration_policy.')) {
- const key = curr.replace('container_expiration_policy.', '');
- acc[key] = get(messages, [curr, 0], '');
- }
+ this.apiErrors = response.graphQLErrors.reduce((acc, curr) => {
+ curr.extensions.problems.forEach(item => {
+ acc[item.path[0]] = item.message;
+ });
return acc;
}, {});
},
submit() {
this.track('submit_form');
this.apiErrors = null;
- this.saveSettings()
- .then(() => this.$toast.show(UPDATE_SETTINGS_SUCCESS_MESSAGE, { type: 'success' }))
- .catch(({ response }) => {
- this.setApiErrors(response);
+ 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' });
+ }
+ 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(changePayload) {
- this.settings = changePayload.newValue;
+ this.$emit('input', changePayload.newValue);
if (this.apiErrors) {
this.apiErrors[changePayload.modified] = undefined;
}
@@ -93,8 +146,8 @@ export default {
</template>
<template #default>
<expiration-policy-fields
- :value="settings"
- :form-options="formOptions"
+ :value="prefilledForm"
+ :form-options="$options.formOptions"
:is-loading="isLoading"
:api-errors="apiErrors"
@validated="fieldsAreValid = true"
@@ -103,27 +156,25 @@ export default {
/>
</template>
<template #footer>
- <div class="gl-display-flex gl-justify-content-end">
- <gl-button
- ref="cancel-button"
- type="reset"
- class="gl-mr-3 gl-display-block"
- :disabled="isCancelButtonDisabled"
- >
- {{ __('Cancel') }}
- </gl-button>
- <gl-button
- ref="save-button"
- type="submit"
- :disabled="isSubmitButtonDisabled"
- variant="success"
- category="primary"
- class="gl-display-flex gl-justify-content-center gl-align-items-center js-no-auto-disable"
- >
- {{ $options.i18n.SET_CLEANUP_POLICY_BUTTON }}
- <gl-loading-icon v-if="isLoading" class="gl-ml-3" />
- </gl-button>
- </div>
+ <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>
</gl-card>
</form>
diff --git a/app/assets/javascripts/registry/settings/graphql/fragments/container_expiration_policy.fragment.graphql b/app/assets/javascripts/registry/settings/graphql/fragments/container_expiration_policy.fragment.graphql
new file mode 100644
index 00000000000..224e0ed9472
--- /dev/null
+++ b/app/assets/javascripts/registry/settings/graphql/fragments/container_expiration_policy.fragment.graphql
@@ -0,0 +1,8 @@
+fragment ContainerExpirationPolicyFields on ContainerExpirationPolicy {
+ cadence
+ enabled
+ keepN
+ nameRegex
+ nameRegexKeep
+ olderThan
+}
diff --git a/app/assets/javascripts/registry/settings/graphql/index.js b/app/assets/javascripts/registry/settings/graphql/index.js
new file mode 100644
index 00000000000..16152eb81f6
--- /dev/null
+++ b/app/assets/javascripts/registry/settings/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/registry/settings/graphql/mutations/update_container_expiration_policy.graphql b/app/assets/javascripts/registry/settings/graphql/mutations/update_container_expiration_policy.graphql
new file mode 100644
index 00000000000..c40cd115ab0
--- /dev/null
+++ b/app/assets/javascripts/registry/settings/graphql/mutations/update_container_expiration_policy.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/registry/settings/graphql/queries/get_expiration_policy.graphql b/app/assets/javascripts/registry/settings/graphql/queries/get_expiration_policy.graphql
new file mode 100644
index 00000000000..c171be0ad07
--- /dev/null
+++ b/app/assets/javascripts/registry/settings/graphql/queries/get_expiration_policy.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/registry/settings/graphql/utils/cache_update.js b/app/assets/javascripts/registry/settings/graphql/utils/cache_update.js
new file mode 100644
index 00000000000..88067d52b51
--- /dev/null
+++ b/app/assets/javascripts/registry/settings/graphql/utils/cache_update.js
@@ -0,0 +1,22 @@
+import { produce } from 'immer';
+import expirationPolicyQuery from '../queries/get_expiration_policy.graphql';
+
+export const updateContainerExpirationPolicy = projectPath => (client, { data: updatedData }) => {
+ const queryAndParams = {
+ query: expirationPolicyQuery,
+ variables: { projectPath },
+ };
+ const sourceData = client.readQuery(queryAndParams);
+
+ const data = produce(sourceData, draftState => {
+ // eslint-disable-next-line no-param-reassign
+ draftState.project.containerExpirationPolicy = {
+ ...updatedData.updateContainerExpirationPolicy.containerExpirationPolicy,
+ };
+ });
+
+ client.writeQuery({
+ ...queryAndParams,
+ data,
+ });
+};
diff --git a/app/assets/javascripts/registry/settings/registry_settings_bundle.js b/app/assets/javascripts/registry/settings/registry_settings_bundle.js
index a318aa2a694..f7b1c5abd3a 100644
--- a/app/assets/javascripts/registry/settings/registry_settings_bundle.js
+++ b/app/assets/javascripts/registry/settings/registry_settings_bundle.js
@@ -1,8 +1,9 @@
import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
import Translate from '~/vue_shared/translate';
-import store from './store';
+import { parseBoolean } from '~/lib/utils/common_utils';
import RegistrySettingsApp from './components/registry_settings_app.vue';
+import { apolloProvider } from './graphql/index';
Vue.use(GlToast);
Vue.use(Translate);
@@ -12,13 +13,19 @@ export default () => {
if (!el) {
return null;
}
- store.dispatch('setInitialState', el.dataset);
+ const { projectPath, isAdmin, adminSettingsPath, enableHistoricEntries } = el.dataset;
return new Vue({
el,
- store,
+ apolloProvider,
components: {
RegistrySettingsApp,
},
+ provide: {
+ projectPath,
+ isAdmin: parseBoolean(isAdmin),
+ adminSettingsPath,
+ enableHistoricEntries: parseBoolean(enableHistoricEntries),
+ },
render(createElement) {
return createElement('registry-settings-app', {});
},
diff --git a/app/assets/javascripts/registry/settings/store/actions.js b/app/assets/javascripts/registry/settings/store/actions.js
deleted file mode 100644
index 0530a870ecc..00000000000
--- a/app/assets/javascripts/registry/settings/store/actions.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import Api from '~/api';
-import * as types from './mutation_types';
-
-export const setInitialState = ({ commit }, data) => commit(types.SET_INITIAL_STATE, data);
-export const updateSettings = ({ commit }, data) => commit(types.UPDATE_SETTINGS, data);
-export const toggleLoading = ({ commit }) => commit(types.TOGGLE_LOADING);
-export const receiveSettingsSuccess = ({ commit }, data) => {
- commit(types.SET_SETTINGS, data);
-};
-export const resetSettings = ({ commit }) => commit(types.RESET_SETTINGS);
-
-export const fetchSettings = ({ dispatch, state }) => {
- dispatch('toggleLoading');
- return Api.project(state.projectId)
- .then(({ data: { container_expiration_policy } }) =>
- dispatch('receiveSettingsSuccess', container_expiration_policy),
- )
- .finally(() => dispatch('toggleLoading'));
-};
-
-export const saveSettings = ({ dispatch, state }) => {
- dispatch('toggleLoading');
- return Api.updateProject(state.projectId, {
- container_expiration_policy_attributes: state.settings,
- })
- .then(({ data: { container_expiration_policy } }) =>
- dispatch('receiveSettingsSuccess', container_expiration_policy),
- )
- .finally(() => dispatch('toggleLoading'));
-};
diff --git a/app/assets/javascripts/registry/settings/store/getters.js b/app/assets/javascripts/registry/settings/store/getters.js
deleted file mode 100644
index ac1a931d8e0..00000000000
--- a/app/assets/javascripts/registry/settings/store/getters.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import { isEqual } from 'lodash';
-import { findDefaultOption } from '../../shared/utils';
-
-export const getCadence = state =>
- state.settings.cadence || findDefaultOption(state.formOptions.cadence);
-
-export const getKeepN = state =>
- state.settings.keep_n || findDefaultOption(state.formOptions.keepN);
-
-export const getOlderThan = state =>
- state.settings.older_than || findDefaultOption(state.formOptions.olderThan);
-
-export const getSettings = (state, getters) => ({
- enabled: state.settings.enabled,
- cadence: getters.getCadence,
- 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);
-
-export const getIsDisabled = state => {
- return !(state.original || state.enableHistoricEntries);
-};
diff --git a/app/assets/javascripts/registry/settings/store/index.js b/app/assets/javascripts/registry/settings/store/index.js
deleted file mode 100644
index c2500454d8e..00000000000
--- a/app/assets/javascripts/registry/settings/store/index.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import Vue from 'vue';
-import Vuex from 'vuex';
-import * as actions from './actions';
-import mutations from './mutations';
-import * as getters from './getters';
-import state from './state';
-
-Vue.use(Vuex);
-
-export const createStore = () =>
- new Vuex.Store({
- state,
- actions,
- mutations,
- getters,
- });
-
-export default createStore();
diff --git a/app/assets/javascripts/registry/settings/store/mutation_types.js b/app/assets/javascripts/registry/settings/store/mutation_types.js
deleted file mode 100644
index db499ffa761..00000000000
--- a/app/assets/javascripts/registry/settings/store/mutation_types.js
+++ /dev/null
@@ -1,5 +0,0 @@
-export const SET_INITIAL_STATE = 'SET_INITIAL_STATE';
-export const UPDATE_SETTINGS = 'UPDATE_SETTINGS';
-export const TOGGLE_LOADING = 'TOGGLE_LOADING';
-export const SET_SETTINGS = 'SET_SETTINGS';
-export const RESET_SETTINGS = 'RESET_SETTINGS';
diff --git a/app/assets/javascripts/registry/settings/store/mutations.js b/app/assets/javascripts/registry/settings/store/mutations.js
deleted file mode 100644
index 3ba13419b98..00000000000
--- a/app/assets/javascripts/registry/settings/store/mutations.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import { parseBoolean } from '~/lib/utils/common_utils';
-import * as types from './mutation_types';
-
-export default {
- [types.SET_INITIAL_STATE](state, initialState) {
- state.projectId = initialState.projectId;
- state.formOptions = {
- cadence: JSON.parse(initialState.cadenceOptions),
- keepN: JSON.parse(initialState.keepNOptions),
- olderThan: JSON.parse(initialState.olderThanOptions),
- };
- state.enableHistoricEntries = parseBoolean(initialState.enableHistoricEntries);
- state.isAdmin = parseBoolean(initialState.isAdmin);
- state.adminSettingsPath = initialState.adminSettingsPath;
- },
- [types.UPDATE_SETTINGS](state, data) {
- state.settings = { ...state.settings, ...data.settings };
- },
- [types.SET_SETTINGS](state, settings) {
- state.settings = settings ?? state.settings;
- state.original = Object.freeze(settings);
- },
- [types.RESET_SETTINGS](state) {
- state.settings = { ...state.original };
- },
- [types.TOGGLE_LOADING](state) {
- state.isLoading = !state.isLoading;
- },
-};
diff --git a/app/assets/javascripts/registry/settings/store/state.js b/app/assets/javascripts/registry/settings/store/state.js
deleted file mode 100644
index fccc0991c1c..00000000000
--- a/app/assets/javascripts/registry/settings/store/state.js
+++ /dev/null
@@ -1,42 +0,0 @@
-export default () => ({
- /*
- * Project Id used to build the API call
- */
- projectId: '',
- /*
- * Boolean to determine if the UI is loading data from the API
- */
- isLoading: false,
- /*
- * Boolean to determine if the user is an admin
- */
- isAdmin: false,
- /*
- * String containing the full path to the admin config page for CI/CD
- */
- adminSettingsPath: '',
- /*
- * Boolean to determine if project created before 12.8 can use this feature
- */
- enableHistoricEntries: false,
- /*
- * This contains the data shown and manipulated in the UI
- * Has the following structure:
- * {
- * enabled: Boolean
- * cadence: String,
- * older_than: String,
- * keep_n: String,
- * name_regex: String
- * }
- */
- settings: {},
- /*
- * Same structure as settings, above but Frozen object and used only in case the user clicks 'cancel', initialized to null
- */
- original: null,
- /*
- * Contains the options used to populate the form selects
- */
- formOptions: {},
-});
diff --git a/app/assets/javascripts/registry/shared/components/expiration_policy_fields.vue b/app/assets/javascripts/registry/shared/components/expiration_policy_fields.vue
index 1ff2f6f99e5..2b8e9f6ff64 100644
--- a/app/assets/javascripts/registry/shared/components/expiration_policy_fields.vue
+++ b/app/assets/javascripts/registry/shared/components/expiration_policy_fields.vue
@@ -68,34 +68,31 @@ export default {
{
name: 'expiration-policy-interval',
label: EXPIRATION_INTERVAL_LABEL,
- model: 'older_than',
- optionKey: 'olderThan',
+ model: 'olderThan',
},
{
name: 'expiration-policy-schedule',
label: EXPIRATION_SCHEDULE_LABEL,
model: 'cadence',
- optionKey: 'cadence',
},
{
name: 'expiration-policy-latest',
label: KEEP_N_LABEL,
- model: 'keep_n',
- optionKey: 'keepN',
+ model: 'keepN',
},
],
textAreaList: [
{
name: 'expiration-policy-name-matching',
label: NAME_REGEX_LABEL,
- model: 'name_regex',
+ model: 'nameRegex',
placeholder: NAME_REGEX_PLACEHOLDER,
description: NAME_REGEX_DESCRIPTION,
},
{
name: 'expiration-policy-keep-name',
label: NAME_REGEX_KEEP_LABEL,
- model: 'name_regex_keep',
+ model: 'nameRegexKeep',
placeholder: NAME_REGEX_KEEP_PLACEHOLDER,
description: NAME_REGEX_KEEP_DESCRIPTION,
},
@@ -107,17 +104,16 @@ export default {
},
computed: {
...mapComputedToEvent(
- ['enabled', 'cadence', 'older_than', 'keep_n', 'name_regex', 'name_regex_keep'],
+ ['enabled', 'cadence', 'olderThan', 'keepN', 'nameRegex', 'nameRegexKeep'],
'value',
),
policyEnabledText() {
return this.enabled ? ENABLED_TEXT : DISABLED_TEXT;
},
textAreaValidation() {
- const nameRegexErrors =
- this.apiErrors?.name_regex || this.validateRegexLength(this.name_regex);
+ const nameRegexErrors = this.apiErrors?.nameRegex || this.validateRegexLength(this.nameRegex);
const nameKeepRegexErrors =
- this.apiErrors?.name_regex_keep || this.validateRegexLength(this.name_regex_keep);
+ this.apiErrors?.nameRegexKeep || this.validateRegexLength(this.nameRegexKeep);
return {
/*
@@ -127,11 +123,11 @@ export default {
* false: red border, error message
* So in this function we keep null if the are no message otherwise we 'invert' the error message
*/
- name_regex: {
+ nameRegex: {
state: nameRegexErrors === null ? null : !nameRegexErrors,
message: nameRegexErrors,
},
- name_regex_keep: {
+ nameRegexKeep: {
state: nameKeepRegexErrors === null ? null : !nameKeepRegexErrors,
message: nameKeepRegexErrors,
},
@@ -139,8 +135,8 @@ export default {
},
fieldsValidity() {
return (
- this.textAreaValidation.name_regex.state !== false &&
- this.textAreaValidation.name_regex_keep.state !== false
+ this.textAreaValidation.nameRegex.state !== false &&
+ this.textAreaValidation.nameRegexKeep.state !== false
);
},
isFormElementDisabled() {
@@ -216,11 +212,7 @@ export default {
:disabled="isFormElementDisabled"
@input="updateModel($event, select.model)"
>
- <option
- v-for="option in formOptions[select.optionKey]"
- :key="option.key"
- :value="option.key"
- >
+ <option v-for="option in formOptions[select.model]" :key="option.key" :value="option.key">
{{ option.label }}
</option>
</gl-form-select>
diff --git a/app/assets/javascripts/registry/shared/constants.js b/app/assets/javascripts/registry/shared/constants.js
index 36d55c7610e..735d72972e6 100644
--- a/app/assets/javascripts/registry/shared/constants.js
+++ b/app/assets/javascripts/registry/shared/constants.js
@@ -43,3 +43,27 @@ export const NAME_REGEX_KEEP_PLACEHOLDER = '';
export const NAME_REGEX_KEEP_DESCRIPTION = s__(
'ContainerRegistry|Wildcards such as %{codeStart}.*-master%{codeEnd} or %{codeStart}release-.*%{codeEnd} are supported',
);
+
+export const KEEP_N_OPTIONS = [
+ { variable: 1, key: 'ONE_TAG', default: false },
+ { variable: 5, key: 'FIVE_TAGS', default: false },
+ { variable: 10, key: 'TEN_TAGS', default: true },
+ { variable: 25, key: 'TWENTY_FIVE_TAGS', default: false },
+ { variable: 50, key: 'FIFTY_TAGS', default: false },
+ { variable: 100, key: 'ONE_HUNDRED_TAGS', 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 },
+];
diff --git a/app/assets/javascripts/registry/shared/utils.js b/app/assets/javascripts/registry/shared/utils.js
index a7377773842..bdf1ab9507d 100644
--- a/app/assets/javascripts/registry/shared/utils.js
+++ b/app/assets/javascripts/registry/shared/utils.js
@@ -1,3 +1,6 @@
+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;
@@ -17,3 +20,27 @@ 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 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),
+ };
+};