Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-12-17 14:59:07 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-12-17 14:59:07 +0300
commit8b573c94895dc0ac0e1d9d59cf3e8745e8b539ca (patch)
tree544930fb309b30317ae9797a9683768705d664c4 /app/assets/javascripts/registry
parent4b1de649d0168371549608993deac953eb692019 (diff)
Add latest changes from gitlab-org/gitlab@13-7-stable-eev13.7.0-rc42
Diffstat (limited to 'app/assets/javascripts/registry')
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/details_header.vue36
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue2
-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/cli_commands.vue5
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/group_empty_state.vue5
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/image_list.vue33
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue34
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/project_empty_state.vue6
-rw-r--r--app/assets/javascripts/registry/explorer/components/registry_breadcrumb.vue21
-rw-r--r--app/assets/javascripts/registry/explorer/constants/details.js2
-rw-r--r--app/assets/javascripts/registry/explorer/constants/list.js5
-rw-r--r--app/assets/javascripts/registry/explorer/graphql/fragments/container_repository.fragment.graphql11
-rw-r--r--app/assets/javascripts/registry/explorer/graphql/index.js14
-rw-r--r--app/assets/javascripts/registry/explorer/graphql/mutations/delete_container_repository.mutation.graphql9
-rw-r--r--app/assets/javascripts/registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql5
-rw-r--r--app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_details.query.graphql41
-rw-r--r--app/assets/javascripts/registry/explorer/graphql/queries/get_group_container_repositories.query.graphql23
-rw-r--r--app/assets/javascripts/registry/explorer/graphql/queries/get_project_container_repositories.query.graphql23
-rw-r--r--app/assets/javascripts/registry/explorer/index.js37
-rw-r--r--app/assets/javascripts/registry/explorer/pages/details.vue177
-rw-r--r--app/assets/javascripts/registry/explorer/pages/index.vue4
-rw-r--r--app/assets/javascripts/registry/explorer/pages/list.vue133
-rw-r--r--app/assets/javascripts/registry/explorer/router.js4
-rw-r--r--app/assets/javascripts/registry/explorer/stores/actions.js119
-rw-r--r--app/assets/javascripts/registry/explorer/stores/getters.js18
-rw-r--r--app/assets/javascripts/registry/explorer/stores/index.js16
-rw-r--r--app/assets/javascripts/registry/explorer/stores/mutation_types.js10
-rw-r--r--app/assets/javascripts/registry/explorer/stores/mutations.js54
-rw-r--r--app/assets/javascripts/registry/explorer/stores/state.js10
-rw-r--r--app/assets/javascripts/registry/explorer/utils.js25
-rw-r--r--app/assets/javascripts/registry/settings/components/expiration_dropdown.vue50
-rw-r--r--app/assets/javascripts/registry/settings/components/expiration_input.vue110
-rw-r--r--app/assets/javascripts/registry/settings/components/expiration_run_text.vue46
-rw-r--r--app/assets/javascripts/registry/settings/components/expiration_toggle.vue52
-rw-r--r--app/assets/javascripts/registry/settings/components/registry_settings_app.vue13
-rw-r--r--app/assets/javascripts/registry/settings/components/settings_form.vue237
-rw-r--r--app/assets/javascripts/registry/settings/constants.js81
-rw-r--r--app/assets/javascripts/registry/settings/graphql/fragments/container_expiration_policy.fragment.graphql1
-rw-r--r--app/assets/javascripts/registry/settings/graphql/mutations/update_container_expiration_policy.mutation.graphql (renamed from app/assets/javascripts/registry/settings/graphql/mutations/update_container_expiration_policy.graphql)0
-rw-r--r--app/assets/javascripts/registry/settings/graphql/queries/get_expiration_policy.query.graphql (renamed from app/assets/javascripts/registry/settings/graphql/queries/get_expiration_policy.graphql)0
-rw-r--r--app/assets/javascripts/registry/settings/graphql/utils/cache_update.js2
-rw-r--r--app/assets/javascripts/registry/settings/registry_settings_bundle.js13
-rw-r--r--app/assets/javascripts/registry/settings/utils.js (renamed from app/assets/javascripts/registry/shared/utils.js)22
-rw-r--r--app/assets/javascripts/registry/shared/components/expiration_policy_fields.vue258
-rw-r--r--app/assets/javascripts/registry/shared/constants.js69
45 files changed, 1018 insertions, 832 deletions
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue b/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue
index ff613daf7fa..3eeb7b29386 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue
+++ b/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue
@@ -1,15 +1,29 @@
<script>
import { GlSprintf } from '@gitlab/ui';
+import { sprintf } from '~/locale';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
-import { DETAILS_PAGE_TITLE } from '../../constants/index';
+import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
+import { DETAILS_PAGE_TITLE, UPDATED_AT } from '../../constants/index';
export default {
- components: { GlSprintf, TitleArea },
+ components: { GlSprintf, TitleArea, MetadataItem },
+ mixins: [timeagoMixin],
props: {
- imageName: {
- type: String,
- required: false,
- default: '',
+ image: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ visibilityIcon() {
+ return this.image?.project?.visibility === 'public' ? 'eye' : 'eye-slash';
+ },
+ timeAgo() {
+ return this.timeFormatted(this.image.updatedAt);
+ },
+ updatedText() {
+ return sprintf(UPDATED_AT, { time: this.timeAgo });
},
},
i18n: {
@@ -23,9 +37,17 @@ export default {
<template #title>
<gl-sprintf :message="$options.i18n.DETAILS_PAGE_TITLE">
<template #imageName>
- {{ imageName }}
+ {{ image.name }}
</template>
</gl-sprintf>
</template>
+ <template #metadata-updated>
+ <metadata-item
+ :icon="visibilityIcon"
+ :text="updatedText"
+ size="xl"
+ data-testid="updated-and-visibility"
+ />
+ </template>
</title-area>
</template>
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue b/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue
index 2844b4ffde3..ad39a898e7b 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue
+++ b/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue
@@ -34,7 +34,7 @@ export default {
return this.tags.some(tag => this.selectedItems[tag.name]);
},
showMultiDeleteButton() {
- return this.tags.some(tag => tag.destroy_path) && !this.isMobile;
+ return this.tags.some(tag => tag.canDelete) && !this.isMobile;
},
},
methods: {
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 2edeac1144f..5aeafd318aa 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
@@ -63,7 +63,7 @@ export default {
},
computed: {
formattedSize() {
- return this.tag.total_size ? numberToHumanSize(this.tag.total_size) : NOT_AVAILABLE_SIZE;
+ return this.tag.totalSize ? numberToHumanSize(this.tag.totalSize) : NOT_AVAILABLE_SIZE;
},
layers() {
return this.tag.layers ? n__('%d layer', '%d layers', this.tag.layers) : '';
@@ -76,10 +76,10 @@ export default {
return this.tag.digest?.substring(7, 14) ?? NOT_AVAILABLE_TEXT;
},
publishedDate() {
- return formatDate(this.tag.created_at, 'isoDate');
+ return formatDate(this.tag.createdAt, 'isoDate');
},
publishedTime() {
- return formatDate(this.tag.created_at, 'hh:MM Z');
+ return formatDate(this.tag.createdAt, 'hh:MM Z');
},
formattedRevision() {
// to be removed when API response is adjusted
@@ -101,7 +101,7 @@ export default {
<list-item v-bind="$attrs" :selected="selected">
<template #left-action>
<gl-form-checkbox
- v-if="Boolean(tag.destroy_path)"
+ v-if="tag.canDelete"
:disabled="invalidTag"
class="gl-m-0"
:checked="selected"
@@ -148,7 +148,7 @@ export default {
<span data-testid="time">
<gl-sprintf :message="$options.i18n.CREATED_AT_LABEL">
<template #timeInfo>
- <time-ago-tooltip :time="tag.created_at" />
+ <time-ago-tooltip :time="tag.createdAt" />
</template>
</gl-sprintf>
</span>
@@ -162,10 +162,10 @@ export default {
</template>
<template #right-action>
<delete-button
- :disabled="!tag.destroy_path || invalidTag"
+ :disabled="!tag.canDelete || invalidTag"
:title="$options.i18n.REMOVE_TAG_BUTTON_TITLE"
:tooltip-title="$options.i18n.REMOVE_TAG_BUTTON_DISABLE_TOOLTIP"
- :tooltip-disabled="Boolean(tag.destroy_path)"
+ :tooltip-disabled="tag.canDelete"
data-testid="single-delete-button"
@delete="$emit('delete')"
/>
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue b/app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue
index ba55822f0ca..319666210d6 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue
+++ b/app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue
@@ -1,6 +1,5 @@
<script>
import { GlDropdown } from '@gitlab/ui';
-import { mapGetters } from 'vuex';
import Tracking from '~/tracking';
import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
import {
@@ -20,6 +19,7 @@ export default {
GlDropdown,
CodeInstruction,
},
+ inject: ['config', 'dockerBuildCommand', 'dockerPushCommand', 'dockerLoginCommand'],
mixins: [Tracking.mixin({ label: trackingLabel })],
trackingLabel,
i18n: {
@@ -31,9 +31,6 @@ export default {
PUSH_COMMAND_LABEL,
COPY_PUSH_TITLE,
},
- computed: {
- ...mapGetters(['dockerBuildCommand', 'dockerPushCommand', 'dockerLoginCommand']),
- },
};
</script>
<template>
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/group_empty_state.vue b/app/assets/javascripts/registry/explorer/components/list_page/group_empty_state.vue
index 80cc392f86a..26e9fee63af 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/group_empty_state.vue
+++ b/app/assets/javascripts/registry/explorer/components/list_page/group_empty_state.vue
@@ -1,17 +1,14 @@
<script>
import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
-import { mapState } from 'vuex';
export default {
name: 'GroupEmptyState',
+ inject: ['config'],
components: {
GlEmptyState,
GlSprintf,
GlLink,
},
- computed: {
- ...mapState(['config']),
- },
};
</script>
<template>
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/image_list.vue b/app/assets/javascripts/registry/explorer/components/list_page/image_list.vue
index d1b9894da0e..f8b3233438f 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/image_list.vue
+++ b/app/assets/javascripts/registry/explorer/components/list_page/image_list.vue
@@ -1,11 +1,11 @@
<script>
-import { GlPagination } from '@gitlab/ui';
+import { GlKeysetPagination } from '@gitlab/ui';
import ImageListRow from './image_list_row.vue';
export default {
name: 'ImageList',
components: {
- GlPagination,
+ GlKeysetPagination,
ImageListRow,
},
props: {
@@ -13,19 +13,14 @@ export default {
type: Array,
required: true,
},
- pagination: {
+ pageInfo: {
type: Object,
required: true,
},
},
computed: {
- currentPage: {
- get() {
- return this.pagination.page;
- },
- set(page) {
- this.$emit('pageChange', page);
- },
+ showPagination() {
+ return this.pageInfo.hasPreviousPage || this.pageInfo.hasNextPage;
},
},
};
@@ -40,13 +35,15 @@ export default {
:first="index === 0"
@delete="$emit('delete', $event)"
/>
-
- <gl-pagination
- v-model="currentPage"
- :per-page="pagination.perPage"
- :total-items="pagination.total"
- align="center"
- class="w-100 gl-mt-3"
- />
+ <div class="gl-display-flex gl-justify-content-center">
+ <gl-keyset-pagination
+ v-if="showPagination"
+ :has-next-page="pageInfo.hasNextPage"
+ :has-previous-page="pageInfo.hasPreviousPage"
+ class="gl-mt-3"
+ @prev="$emit('prev-page')"
+ @next="$emit('next-page')"
+ />
+ </div>
</div>
</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 b0a7c4824bd..3fe61dc231a 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
@@ -1,6 +1,8 @@
<script>
import { GlTooltipDirective, GlIcon, GlSprintf } from '@gitlab/ui';
import { n__ } from '~/locale';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import DeleteButton from '../delete_button.vue';
@@ -11,6 +13,8 @@ import {
REMOVE_REPOSITORY_LABEL,
ROW_SCHEDULED_FOR_DELETION,
CLEANUP_TIMED_OUT_ERROR_MESSAGE,
+ IMAGE_DELETE_SCHEDULED_STATUS,
+ IMAGE_FAILED_DELETED_STATUS,
} from '../../constants/index';
export default {
@@ -38,19 +42,29 @@ export default {
},
computed: {
disabledDelete() {
- return !this.item.destroy_path || this.item.deleting;
+ return !this.item.canDelete || this.deleting;
+ },
+ id() {
+ return getIdFromGraphQLId(this.item.id);
+ },
+ deleting() {
+ return this.item.status === IMAGE_DELETE_SCHEDULED_STATUS;
+ },
+ failedDelete() {
+ return this.item.status === IMAGE_FAILED_DELETED_STATUS;
},
tagsCountText() {
return n__(
'ContainerRegistry|%{count} Tag',
'ContainerRegistry|%{count} Tags',
- this.item.tags_count,
+ this.item.tagsCount,
);
},
warningIconText() {
- if (this.item.failedDelete) {
+ if (this.failedDelete) {
return ASYNC_DELETE_IMAGE_ERROR_MESSAGE;
- } else if (this.item.cleanup_policy_started_at) {
+ }
+ if (this.item.expirationPolicyStartedAt) {
return CLEANUP_TIMED_OUT_ERROR_MESSAGE;
}
return null;
@@ -63,23 +77,23 @@ export default {
<list-item
v-gl-tooltip="{
placement: 'left',
- disabled: !item.deleting,
+ disabled: !deleting,
title: $options.i18n.ROW_SCHEDULED_FOR_DELETION,
}"
v-bind="$attrs"
- :disabled="item.deleting"
+ :disabled="deleting"
>
<template #left-primary>
<router-link
class="gl-text-body gl-font-weight-bold"
data-testid="details-link"
- :to="{ name: 'details', params: { id: item.id } }"
+ :to="{ name: 'details', params: { id } }"
>
{{ item.path }}
</router-link>
<clipboard-button
v-if="item.location"
- :disabled="item.deleting"
+ :disabled="deleting"
:text="item.location"
:title="item.location"
category="tertiary"
@@ -97,7 +111,7 @@ export default {
<gl-icon name="tag" class="gl-mr-2" />
<gl-sprintf :message="tagsCountText">
<template #count>
- {{ item.tags_count }}
+ {{ item.tagsCount }}
</template>
</gl-sprintf>
</span>
@@ -106,7 +120,7 @@ export default {
<delete-button
:title="$options.i18n.REMOVE_REPOSITORY_LABEL"
:disabled="disabledDelete"
- :tooltip-disabled="Boolean(item.destroy_path)"
+ :tooltip-disabled="item.canDelete"
:tooltip-title="$options.i18n.LIST_DELETE_BUTTON_DISABLED"
@delete="$emit('delete', item)"
/>
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/project_empty_state.vue b/app/assets/javascripts/registry/explorer/components/list_page/project_empty_state.vue
index 35eb0b11e40..5308b025cc0 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/project_empty_state.vue
+++ b/app/assets/javascripts/registry/explorer/components/list_page/project_empty_state.vue
@@ -1,6 +1,5 @@
<script>
import { GlEmptyState, GlSprintf, GlLink, GlFormInputGroup, GlFormInput } from '@gitlab/ui';
-import { mapState, mapGetters } from 'vuex';
import { s__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import {
@@ -20,6 +19,7 @@ export default {
GlFormInputGroup,
GlFormInput,
},
+ inject: ['config', 'dockerBuildCommand', 'dockerPushCommand', 'dockerLoginCommand'],
i18n: {
quickStart: QUICK_START,
copyLoginTitle: COPY_LOGIN_TITLE,
@@ -35,10 +35,6 @@ export default {
'ContainerRegistry|You can add an image to this registry with the following commands:',
),
},
- computed: {
- ...mapState(['config']),
- ...mapGetters(['dockerBuildCommand', 'dockerPushCommand', 'dockerLoginCommand']),
- },
};
</script>
<template>
diff --git a/app/assets/javascripts/registry/explorer/components/registry_breadcrumb.vue b/app/assets/javascripts/registry/explorer/components/registry_breadcrumb.vue
index 666d8b042da..1cedcc41b2b 100644
--- a/app/assets/javascripts/registry/explorer/components/registry_breadcrumb.vue
+++ b/app/assets/javascripts/registry/explorer/components/registry_breadcrumb.vue
@@ -1,9 +1,11 @@
<script>
+/* eslint-disable vue/no-v-html */
+// We are forced to use `v-html` untill this gitlab-ui issue is resolved: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1079
+// then we can re-write this to use gl-breadcrumb
import { initial, first, last } from 'lodash';
-import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { sanitize } from '~/lib/dompurify';
export default {
- directives: { SafeHtml },
props: {
crumbs: {
type: Array,
@@ -11,6 +13,9 @@ export default {
},
},
computed: {
+ parsedCrumbs() {
+ return this.crumbs.map(c => ({ ...c, innerHTML: sanitize(c.innerHTML) }));
+ },
rootRoute() {
return this.$router.options.routes.find(r => r.meta.root);
},
@@ -18,11 +23,11 @@ export default {
return this.$route.name === this.rootRoute.name;
},
rootCrumbs() {
- return initial(this.crumbs);
+ return initial(this.parsedCrumbs);
},
divider() {
const { classList, tagName, innerHTML } = first(this.crumbs).querySelector('svg');
- return { classList: [...classList], tagName, innerHTML };
+ return { classList: [...classList], tagName, innerHTML: sanitize(innerHTML) };
},
lastCrumb() {
const { children } = last(this.crumbs);
@@ -30,7 +35,7 @@ export default {
return {
tagName,
className,
- text: this.$route.meta.nameGenerator(this.$store.state),
+ text: this.$route.meta.nameGenerator(),
path: { to: this.$route.name },
};
},
@@ -43,14 +48,14 @@ export default {
<li
v-for="(crumb, index) in rootCrumbs"
:key="index"
- v-safe-html="crumb.innerHTML"
:class="crumb.className"
+ v-html="crumb.innerHTML"
></li>
<li v-if="!isRootRoute">
<router-link ref="rootRouteLink" :to="rootRoute.path">
- {{ rootRoute.meta.nameGenerator($store.state) }}
+ {{ rootRoute.meta.nameGenerator() }}
</router-link>
- <component :is="divider.tagName" v-safe-html="divider.innerHTML" :class="divider.classList" />
+ <component :is="divider.tagName" :class="divider.classList" v-html="divider.innerHTML" />
</li>
<li>
<component :is="lastCrumb.tagName" ref="lastCrumb" :class="lastCrumb.className">
diff --git a/app/assets/javascripts/registry/explorer/constants/details.js b/app/assets/javascripts/registry/explorer/constants/details.js
index 306e6903a4f..1babaaa93da 100644
--- a/app/assets/javascripts/registry/explorer/constants/details.js
+++ b/app/assets/javascripts/registry/explorer/constants/details.js
@@ -56,6 +56,8 @@ export const MISSING_MANIFEST_WARNING_TOOLTIP = s__(
'ContainerRegistry|Invalid tag: missing manifest digest',
);
+export const UPDATED_AT = s__('ContainerRegistry|Last updated %{time}');
+
export const NOT_AVAILABLE_TEXT = __('N/A');
export const NOT_AVAILABLE_SIZE = __('0 bytes');
// Parameters
diff --git a/app/assets/javascripts/registry/explorer/constants/list.js b/app/assets/javascripts/registry/explorer/constants/list.js
index 39f63d2a153..37ced72861e 100644
--- a/app/assets/javascripts/registry/explorer/constants/list.js
+++ b/app/assets/javascripts/registry/explorer/constants/list.js
@@ -44,5 +44,6 @@ export const EMPTY_RESULT_MESSAGE = s__(
// Parameters
-export const IMAGE_DELETE_SCHEDULED_STATUS = 'delete_scheduled';
-export const IMAGE_FAILED_DELETED_STATUS = 'delete_failed';
+export const IMAGE_DELETE_SCHEDULED_STATUS = 'DELETE_SCHEDULED';
+export const IMAGE_FAILED_DELETED_STATUS = 'DELETE_FAILED';
+export const GRAPHQL_PAGE_SIZE = 10;
diff --git a/app/assets/javascripts/registry/explorer/graphql/fragments/container_repository.fragment.graphql b/app/assets/javascripts/registry/explorer/graphql/fragments/container_repository.fragment.graphql
new file mode 100644
index 00000000000..9a3579ee8e0
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/graphql/fragments/container_repository.fragment.graphql
@@ -0,0 +1,11 @@
+fragment ContainerRepositoryFields on ContainerRepository {
+ id
+ name
+ path
+ status
+ location
+ canDelete
+ createdAt
+ tagsCount
+ expirationPolicyStartedAt
+}
diff --git a/app/assets/javascripts/registry/explorer/graphql/index.js b/app/assets/javascripts/registry/explorer/graphql/index.js
new file mode 100644
index 00000000000..16152eb81f6
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/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/explorer/graphql/mutations/delete_container_repository.mutation.graphql b/app/assets/javascripts/registry/explorer/graphql/mutations/delete_container_repository.mutation.graphql
new file mode 100644
index 00000000000..4c88b726ee5
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/graphql/mutations/delete_container_repository.mutation.graphql
@@ -0,0 +1,9 @@
+mutation destroyContainerRepository($id: ContainerRepositoryID!) {
+ destroyContainerRepository(input: { id: $id }) {
+ containerRepository {
+ id
+ status
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql b/app/assets/javascripts/registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql
new file mode 100644
index 00000000000..a31f2829e13
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql
@@ -0,0 +1,5 @@
+mutation destroyContainerRepositoryTags($id: ContainerRepositoryID!, $tagNames: [String!]!) {
+ destroyContainerRepositoryTags(input: { id: $id, tagNames: $tagNames }) {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_details.query.graphql b/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_details.query.graphql
new file mode 100644
index 00000000000..b40200e020b
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_details.query.graphql
@@ -0,0 +1,41 @@
+#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
+
+query getContainerRepositoryDetails(
+ $id: ID!
+ $first: Int
+ $last: Int
+ $after: String
+ $before: String
+) {
+ containerRepository(id: $id) {
+ id
+ name
+ path
+ status
+ location
+ canDelete
+ createdAt
+ updatedAt
+ tagsCount
+ expirationPolicyStartedAt
+ tags(after: $after, before: $before, first: $first, last: $last) {
+ nodes {
+ digest
+ location
+ path
+ name
+ revision
+ shortRevision
+ createdAt
+ totalSize
+ canDelete
+ }
+ pageInfo {
+ ...PageInfo
+ }
+ }
+ project {
+ visibility
+ }
+ }
+}
diff --git a/app/assets/javascripts/registry/explorer/graphql/queries/get_group_container_repositories.query.graphql b/app/assets/javascripts/registry/explorer/graphql/queries/get_group_container_repositories.query.graphql
new file mode 100644
index 00000000000..348eda97ea7
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/graphql/queries/get_group_container_repositories.query.graphql
@@ -0,0 +1,23 @@
+#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
+#import "../fragments/container_repository.fragment.graphql"
+
+query getGroupContainerRepositories(
+ $fullPath: ID!
+ $name: String
+ $first: Int
+ $last: Int
+ $after: String
+ $before: String
+) {
+ group(fullPath: $fullPath) {
+ containerRepositoriesCount
+ containerRepositories(name: $name, after: $after, before: $before, first: $first, last: $last) {
+ nodes {
+ ...ContainerRepositoryFields
+ }
+ pageInfo {
+ ...PageInfo
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/registry/explorer/graphql/queries/get_project_container_repositories.query.graphql b/app/assets/javascripts/registry/explorer/graphql/queries/get_project_container_repositories.query.graphql
new file mode 100644
index 00000000000..338e27745f7
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/graphql/queries/get_project_container_repositories.query.graphql
@@ -0,0 +1,23 @@
+#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
+#import "../fragments/container_repository.fragment.graphql"
+
+query getProjectContainerRepositories(
+ $fullPath: ID!
+ $name: String
+ $first: Int
+ $last: Int
+ $after: String
+ $before: String
+) {
+ project(fullPath: $fullPath) {
+ containerRepositoriesCount
+ containerRepositories(name: $name, after: $after, before: $before, first: $first, last: $last) {
+ nodes {
+ ...ContainerRepositoryFields
+ }
+ pageInfo {
+ ...PageInfo
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/registry/explorer/index.js b/app/assets/javascripts/registry/explorer/index.js
index 2bba3ee4ff9..d887b6a1b15 100644
--- a/app/assets/javascripts/registry/explorer/index.js
+++ b/app/assets/javascripts/registry/explorer/index.js
@@ -1,10 +1,11 @@
import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
import Translate from '~/vue_shared/translate';
+import { parseBoolean } from '~/lib/utils/common_utils';
import RegistryExplorer from './pages/index.vue';
import RegistryBreadcrumb from './components/registry_breadcrumb.vue';
-import { createStore } from './stores';
import createRouter from './router';
+import { apolloProvider } from './graphql/index';
Vue.use(Translate);
Vue.use(GlToast);
@@ -16,20 +17,42 @@ export default () => {
return null;
}
- const { endpoint } = el.dataset;
+ const { endpoint, expirationPolicy, isGroupPage, isAdmin, ...config } = el.dataset;
- const store = createStore();
- const router = createRouter(endpoint);
- store.dispatch('setInitialState', el.dataset);
+ // This is a mini state to help the breadcrumb have the correct name in the details page
+ const breadCrumbState = Vue.observable({
+ name: '',
+ updateName(value) {
+ this.name = value;
+ },
+ });
+
+ const router = createRouter(endpoint, breadCrumbState);
const attachMainComponent = () =>
new Vue({
el,
- store,
router,
+ apolloProvider,
components: {
RegistryExplorer,
},
+ provide() {
+ return {
+ breadCrumbState,
+ config: {
+ ...config,
+ expirationPolicy: expirationPolicy ? JSON.parse(expirationPolicy) : undefined,
+ isGroupPage: parseBoolean(isGroupPage),
+ isAdmin: parseBoolean(isAdmin),
+ },
+ /* eslint-disable @gitlab/require-i18n-strings */
+ dockerBuildCommand: `docker build -t ${config.repositoryUrl} .`,
+ dockerPushCommand: `docker push ${config.repositoryUrl}`,
+ dockerLoginCommand: `docker login ${config.registryHostUrlWithPort}`,
+ /* eslint-enable @gitlab/require-i18n-strings */
+ };
+ },
render(createElement) {
return createElement('registry-explorer');
},
@@ -40,8 +63,8 @@ export default () => {
const crumbs = [...document.querySelectorAll('.js-breadcrumbs-list li')];
return new Vue({
el: breadCrumbEl,
- store,
router,
+ apolloProvider,
components: {
RegistryBreadcrumb,
},
diff --git a/app/assets/javascripts/registry/explorer/pages/details.vue b/app/assets/javascripts/registry/explorer/pages/details.vue
index a60ef5c4982..540f02d58d4 100644
--- a/app/assets/javascripts/registry/explorer/pages/details.vue
+++ b/app/assets/javascripts/registry/explorer/pages/details.vue
@@ -1,8 +1,9 @@
<script>
-import { mapState, mapActions } from 'vuex';
-import { GlPagination, GlResizeObserverDirective } from '@gitlab/ui';
+import { GlKeysetPagination, GlResizeObserverDirective } from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
+import createFlash from '~/flash';
import Tracking from '~/tracking';
+import { joinPaths } from '~/lib/utils/url_utility';
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';
@@ -11,11 +12,16 @@ import TagsList from '../components/details_page/tags_list.vue';
import TagsLoader from '../components/details_page/tags_loader.vue';
import EmptyTagsState from '../components/details_page/empty_tags_state.vue';
+import getContainerRepositoryDetailsQuery from '../graphql/queries/get_container_repository_details.query.graphql';
+import deleteContainerRepositoryTagsMutation from '../graphql/mutations/delete_container_repository_tags.mutation.graphql';
+
import {
ALERT_SUCCESS_TAG,
ALERT_DANGER_TAG,
ALERT_SUCCESS_TAGS,
ALERT_DANGER_TAGS,
+ GRAPHQL_PAGE_SIZE,
+ FETCH_IMAGES_LIST_ERROR_MESSAGE,
} from '../constants/index';
export default {
@@ -23,28 +29,61 @@ export default {
DeleteAlert,
PartialCleanupAlert,
DetailsHeader,
- GlPagination,
+ GlKeysetPagination,
DeleteModal,
TagsList,
TagsLoader,
EmptyTagsState,
},
+ inject: ['breadCrumbState', 'config'],
directives: {
GlResizeObserver: GlResizeObserverDirective,
},
mixins: [Tracking.mixin()],
+ apollo: {
+ image: {
+ query: getContainerRepositoryDetailsQuery,
+ variables() {
+ return this.queryVariables;
+ },
+ update(data) {
+ return data.containerRepository;
+ },
+ result({ data }) {
+ this.tagsPageInfo = data.containerRepository?.tags?.pageInfo;
+ this.breadCrumbState.updateName(data.containerRepository?.name);
+ },
+ error() {
+ createFlash({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE });
+ },
+ },
+ },
data() {
return {
+ image: {},
+ tagsPageInfo: {},
itemsToBeDeleted: [],
isMobile: false,
+ mutationLoading: false,
deleteAlertType: null,
dismissPartialCleanupWarning: false,
};
},
computed: {
- ...mapState(['tagsPagination', 'isLoading', 'config', 'tags', 'imageDetails']),
+ queryVariables() {
+ return {
+ id: joinPaths(this.config.gidPrefix, `${this.$route.params.id}`),
+ first: GRAPHQL_PAGE_SIZE,
+ };
+ },
+ isLoading() {
+ return this.$apollo.queries.image.loading || this.mutationLoading;
+ },
+ tags() {
+ return this.image?.tags?.nodes || [];
+ },
showPartialCleanupWarning() {
- return this.imageDetails?.cleanup_policy_started_at && !this.dismissPartialCleanupWarning;
+ return this.image?.expirationPolicyStartedAt && !this.dismissPartialCleanupWarning;
},
tracking() {
return {
@@ -52,66 +91,78 @@ export default {
this.itemsToBeDeleted?.length > 1 ? 'bulk_registry_tag_delete' : 'registry_tag_delete',
};
},
- currentPage: {
- get() {
- return this.tagsPagination.page;
- },
- set(page) {
- this.requestTagsList({ page });
- },
+ showPagination() {
+ return this.tagsPageInfo.hasPreviousPage || this.tagsPageInfo.hasNextPage;
},
},
- mounted() {
- this.requestImageDetailsAndTagsList(this.$route.params.id);
- },
methods: {
- ...mapActions([
- 'requestTagsList',
- 'requestDeleteTag',
- 'requestDeleteTags',
- 'requestImageDetailsAndTagsList',
- ]),
deleteTags(toBeDeleted) {
this.itemsToBeDeleted = this.tags.filter(tag => toBeDeleted[tag.name]);
this.track('click_button');
this.$refs.deleteModal.show();
},
- handleSingleDelete() {
- const [itemToDelete] = this.itemsToBeDeleted;
- this.itemsToBeDeleted = [];
- return this.requestDeleteTag({ tag: itemToDelete })
- .then(() => {
- this.deleteAlertType = ALERT_SUCCESS_TAG;
- })
- .catch(() => {
- this.deleteAlertType = ALERT_DANGER_TAG;
- });
- },
- handleMultipleDelete() {
+ async handleDelete() {
+ this.track('confirm_delete');
const { itemsToBeDeleted } = this;
this.itemsToBeDeleted = [];
-
- return this.requestDeleteTags({
- ids: itemsToBeDeleted.map(x => x.name),
- })
- .then(() => {
- this.deleteAlertType = ALERT_SUCCESS_TAGS;
- })
- .catch(() => {
- this.deleteAlertType = ALERT_DANGER_TAGS;
+ this.mutationLoading = true;
+ try {
+ const { data } = await this.$apollo.mutate({
+ mutation: deleteContainerRepositoryTagsMutation,
+ variables: {
+ id: this.queryVariables.id,
+ tagNames: itemsToBeDeleted.map(i => i.name),
+ },
+ awaitRefetchQueries: true,
+ refetchQueries: [
+ {
+ query: getContainerRepositoryDetailsQuery,
+ variables: this.queryVariables,
+ },
+ ],
});
- },
- onDeletionConfirmed() {
- this.track('confirm_delete');
- if (this.itemsToBeDeleted.length > 1) {
- this.handleMultipleDelete();
- } else {
- this.handleSingleDelete();
+
+ if (data?.destroyContainerRepositoryTags?.errors[0]) {
+ throw new Error();
+ }
+ this.deleteAlertType =
+ itemsToBeDeleted.length === 0 ? ALERT_SUCCESS_TAG : ALERT_SUCCESS_TAGS;
+ } catch (e) {
+ this.deleteAlertType = itemsToBeDeleted.length === 0 ? ALERT_DANGER_TAG : ALERT_DANGER_TAGS;
}
+
+ this.mutationLoading = false;
},
handleResize() {
this.isMobile = GlBreakpointInstance.getBreakpointSize() === 'xs';
},
+ fetchNextPage() {
+ if (this.tagsPageInfo?.hasNextPage) {
+ this.$apollo.queries.image.fetchMore({
+ variables: {
+ after: this.tagsPageInfo?.endCursor,
+ first: GRAPHQL_PAGE_SIZE,
+ },
+ updateQuery(previousResult, { fetchMoreResult }) {
+ return fetchMoreResult;
+ },
+ });
+ }
+ },
+ fetchPreviousPage() {
+ if (this.tagsPageInfo?.hasPreviousPage) {
+ this.$apollo.queries.image.fetchMore({
+ variables: {
+ first: null,
+ before: this.tagsPageInfo?.startCursor,
+ last: GRAPHQL_PAGE_SIZE,
+ },
+ updateQuery(previousResult, { fetchMoreResult }) {
+ return fetchMoreResult;
+ },
+ });
+ }
+ },
},
};
</script>
@@ -132,28 +183,30 @@ export default {
@dismiss="dismissPartialCleanupWarning = true"
/>
- <details-header :image-name="imageDetails.name" />
+ <details-header :image="image" />
<tags-loader v-if="isLoading" />
<template v-else>
<empty-tags-state v-if="tags.length === 0" :no-containers-image="config.noContainersImage" />
- <tags-list v-else :tags="tags" :is-mobile="isMobile" @delete="deleteTags" />
+ <template v-else>
+ <tags-list :tags="tags" :is-mobile="isMobile" @delete="deleteTags" />
+ <div class="gl-display-flex gl-justify-content-center">
+ <gl-keyset-pagination
+ v-if="showPagination"
+ :has-next-page="tagsPageInfo.hasNextPage"
+ :has-previous-page="tagsPageInfo.hasPreviousPage"
+ class="gl-mt-3"
+ @prev="fetchPreviousPage"
+ @next="fetchNextPage"
+ />
+ </div>
+ </template>
</template>
- <gl-pagination
- v-if="!isLoading"
- ref="pagination"
- v-model="currentPage"
- :per-page="tagsPagination.perPage"
- :total-items="tagsPagination.total"
- align="center"
- class="gl-w-full gl-mt-3"
- />
-
<delete-modal
ref="deleteModal"
:items-to-be-deleted="itemsToBeDeleted"
- @confirmDelete="onDeletionConfirmed"
+ @confirmDelete="handleDelete"
@cancel="track('cancel_delete')"
/>
</div>
diff --git a/app/assets/javascripts/registry/explorer/pages/index.vue b/app/assets/javascripts/registry/explorer/pages/index.vue
index 4ac0bca84c1..dca63e1a569 100644
--- a/app/assets/javascripts/registry/explorer/pages/index.vue
+++ b/app/assets/javascripts/registry/explorer/pages/index.vue
@@ -1,7 +1,3 @@
-<script>
-export default {};
-</script>
-
<template>
<div>
<router-view ref="router-view" />
diff --git a/app/assets/javascripts/registry/explorer/pages/list.vue b/app/assets/javascripts/registry/explorer/pages/list.vue
index 81e47073fe9..3192ba82db8 100644
--- a/app/assets/javascripts/registry/explorer/pages/list.vue
+++ b/app/assets/javascripts/registry/explorer/pages/list.vue
@@ -1,5 +1,4 @@
<script>
-import { mapState, mapActions } from 'vuex';
import {
GlEmptyState,
GlTooltipDirective,
@@ -11,6 +10,7 @@ import {
GlSearchBoxByClick,
} from '@gitlab/ui';
import Tracking from '~/tracking';
+import createFlash from '~/flash';
import ProjectEmptyState from '../components/list_page/project_empty_state.vue';
import GroupEmptyState from '../components/list_page/group_empty_state.vue';
@@ -18,6 +18,10 @@ import RegistryHeader from '../components/list_page/registry_header.vue';
import ImageList from '../components/list_page/image_list.vue';
import CliCommands from '../components/list_page/cli_commands.vue';
+import getProjectContainerRepositoriesQuery from '../graphql/queries/get_project_container_repositories.query.graphql';
+import getGroupContainerRepositoriesQuery from '../graphql/queries/get_group_container_repositories.query.graphql';
+import deleteContainerRepositoryMutation from '../graphql/mutations/delete_container_repository.mutation.graphql';
+
import {
DELETE_IMAGE_SUCCESS_MESSAGE,
DELETE_IMAGE_ERROR_MESSAGE,
@@ -29,6 +33,8 @@ import {
IMAGE_REPOSITORY_LIST_LABEL,
EMPTY_RESULT_TITLE,
EMPTY_RESULT_MESSAGE,
+ GRAPHQL_PAGE_SIZE,
+ FETCH_IMAGES_LIST_ERROR_MESSAGE,
} from '../constants/index';
export default {
@@ -47,6 +53,7 @@ export default {
RegistryHeader,
CliCommands,
},
+ inject: ['config'],
directives: {
GlTooltip: GlTooltipDirective,
},
@@ -66,21 +73,62 @@ export default {
EMPTY_RESULT_TITLE,
EMPTY_RESULT_MESSAGE,
},
+ apollo: {
+ images: {
+ query() {
+ return this.graphQlQuery;
+ },
+ variables() {
+ return this.queryVariables;
+ },
+ update(data) {
+ return data[this.graphqlResource]?.containerRepositories.nodes;
+ },
+ result({ data }) {
+ this.pageInfo = data[this.graphqlResource]?.containerRepositories?.pageInfo;
+ this.containerRepositoriesCount = data[this.graphqlResource]?.containerRepositoriesCount;
+ },
+ error() {
+ createFlash({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE });
+ },
+ },
+ },
data() {
return {
+ images: [],
+ pageInfo: {},
+ containerRepositoriesCount: 0,
itemToDelete: {},
deleteAlertType: null,
- search: null,
- isEmpty: false,
+ searchValue: null,
+ name: null,
+ mutationLoading: false,
};
},
computed: {
- ...mapState(['config', 'isLoading', 'images', 'pagination']),
+ graphqlResource() {
+ return this.config.isGroupPage ? 'group' : 'project';
+ },
+ graphQlQuery() {
+ return this.config.isGroupPage
+ ? getGroupContainerRepositoriesQuery
+ : getProjectContainerRepositoriesQuery;
+ },
+ queryVariables() {
+ return {
+ name: this.name,
+ fullPath: this.config.isGroupPage ? this.config.groupPath : this.config.projectPath,
+ first: GRAPHQL_PAGE_SIZE,
+ };
+ },
tracking() {
return {
label: 'registry_repository_delete',
};
},
+ isLoading() {
+ return this.$apollo.queries.images.loading || this.mutationLoading;
+ },
showCommands() {
return Boolean(!this.isLoading && !this.config?.isGroupPage && this.images?.length);
},
@@ -93,19 +141,7 @@ export default {
: DELETE_IMAGE_ERROR_MESSAGE;
},
},
- mounted() {
- this.loadImageList(this.$route.name);
- },
methods: {
- ...mapActions(['requestImagesList', 'requestDeleteImage']),
- loadImageList(fromName) {
- if (!fromName || !this.images?.length) {
- return this.requestImagesList().then(() => {
- this.isEmpty = this.images.length === 0;
- });
- }
- return Promise.resolve();
- },
deleteImage(item) {
this.track('click_button');
this.itemToDelete = item;
@@ -113,18 +149,59 @@ export default {
},
handleDeleteImage() {
this.track('confirm_delete');
- return this.requestDeleteImage(this.itemToDelete)
- .then(() => {
- this.deleteAlertType = 'success';
+ this.mutationLoading = true;
+ return this.$apollo
+ .mutate({
+ mutation: deleteContainerRepositoryMutation,
+ variables: {
+ id: this.itemToDelete.id,
+ },
+ })
+ .then(({ data }) => {
+ if (data?.destroyContainerRepository?.errors[0]) {
+ this.deleteAlertType = 'danger';
+ } else {
+ this.deleteAlertType = 'success';
+ }
})
.catch(() => {
this.deleteAlertType = 'danger';
+ })
+ .finally(() => {
+ this.mutationLoading = false;
});
},
dismissDeleteAlert() {
this.deleteAlertType = null;
this.itemToDelete = {};
},
+ fetchNextPage() {
+ if (this.pageInfo?.hasNextPage) {
+ this.$apollo.queries.images.fetchMore({
+ variables: {
+ after: this.pageInfo?.endCursor,
+ first: GRAPHQL_PAGE_SIZE,
+ },
+ updateQuery(previousResult, { fetchMoreResult }) {
+ return fetchMoreResult;
+ },
+ });
+ }
+ },
+ fetchPreviousPage() {
+ if (this.pageInfo?.hasPreviousPage) {
+ this.$apollo.queries.images.fetchMore({
+ variables: {
+ first: null,
+ before: this.pageInfo?.startCursor,
+ last: GRAPHQL_PAGE_SIZE,
+ },
+ updateQuery(previousResult, { fetchMoreResult }) {
+ return fetchMoreResult;
+ },
+ });
+ }
+ },
},
};
</script>
@@ -134,7 +211,7 @@ export default {
<gl-alert
v-if="showDeleteAlert"
:variant="deleteAlertType"
- class="mt-2"
+ class="gl-mt-5"
dismissible
@dismiss="dismissDeleteAlert"
>
@@ -165,7 +242,7 @@ export default {
<template v-else>
<registry-header
- :images-count="pagination.total"
+ :images-count="containerRepositoriesCount"
:expiration-policy="config.expirationPolicy"
:help-page-path="config.helpPagePath"
:expiration-policy-help-page-path="config.expirationPolicyHelpPagePath"
@@ -176,7 +253,7 @@ export default {
</template>
</registry-header>
- <div v-if="isLoading" class="mt-2">
+ <div v-if="isLoading" class="gl-mt-5">
<gl-skeleton-loader
v-for="index in $options.loader.repeat"
:key="index"
@@ -190,16 +267,17 @@ export default {
</gl-skeleton-loader>
</div>
<template v-else>
- <template v-if="!isEmpty">
+ <template v-if="images.length > 0 || name">
<div class="gl-display-flex gl-p-1 gl-mt-3" data-testid="listHeader">
<div class="gl-flex-fill-1">
<h5>{{ $options.i18n.IMAGE_REPOSITORY_LIST_LABEL }}</h5>
</div>
<div>
<gl-search-box-by-click
- v-model="search"
+ v-model="searchValue"
:placeholder="$options.i18n.SEARCH_PLACEHOLDER_TEXT"
- @submit="requestImagesList({ name: $event })"
+ @clear="name = null"
+ @submit="name = $event"
/>
</div>
</div>
@@ -207,9 +285,10 @@ export default {
<image-list
v-if="images.length"
:images="images"
- :pagination="pagination"
- @pageChange="requestImagesList({ pagination: { page: $event }, name: search })"
+ :page-info="pageInfo"
@delete="deleteImage"
+ @prev-page="fetchPreviousPage"
+ @next-page="fetchNextPage"
/>
<gl-empty-state
diff --git a/app/assets/javascripts/registry/explorer/router.js b/app/assets/javascripts/registry/explorer/router.js
index dcf1c77329d..d8903cf0931 100644
--- a/app/assets/javascripts/registry/explorer/router.js
+++ b/app/assets/javascripts/registry/explorer/router.js
@@ -6,7 +6,7 @@ import { CONTAINER_REGISTRY_TITLE } from './constants/index';
Vue.use(VueRouter);
-export default function createRouter(base) {
+export default function createRouter(base, breadCrumbState) {
const router = new VueRouter({
base,
mode: 'history',
@@ -25,7 +25,7 @@ export default function createRouter(base) {
path: '/:id',
component: Details,
meta: {
- nameGenerator: ({ imageDetails }) => imageDetails?.name,
+ nameGenerator: () => breadCrumbState.name,
},
},
],
diff --git a/app/assets/javascripts/registry/explorer/stores/actions.js b/app/assets/javascripts/registry/explorer/stores/actions.js
deleted file mode 100644
index c1883095097..00000000000
--- a/app/assets/javascripts/registry/explorer/stores/actions.js
+++ /dev/null
@@ -1,119 +0,0 @@
-import axios from '~/lib/utils/axios_utils';
-import createFlash from '~/flash';
-import Api from '~/api';
-import * as types from './mutation_types';
-import {
- FETCH_IMAGES_LIST_ERROR_MESSAGE,
- DEFAULT_PAGE,
- DEFAULT_PAGE_SIZE,
- FETCH_TAGS_LIST_ERROR_MESSAGE,
- FETCH_IMAGE_DETAILS_ERROR_MESSAGE,
-} from '../constants/index';
-import { pathGenerator } from '../utils';
-
-export const setInitialState = ({ commit }, data) => commit(types.SET_INITIAL_STATE, data);
-export const setShowGarbageCollectionTip = ({ commit }, data) =>
- commit(types.SET_SHOW_GARBAGE_COLLECTION_TIP, data);
-
-export const receiveImagesListSuccess = ({ commit }, { data, headers }) => {
- commit(types.SET_IMAGES_LIST_SUCCESS, data);
- commit(types.SET_PAGINATION, headers);
-};
-
-export const receiveTagsListSuccess = ({ commit }, { data, headers }) => {
- commit(types.SET_TAGS_LIST_SUCCESS, data);
- commit(types.SET_TAGS_PAGINATION, headers);
-};
-
-export const requestImagesList = (
- { commit, dispatch, state },
- { pagination = {}, name = null } = {},
-) => {
- commit(types.SET_MAIN_LOADING, true);
- const { page = DEFAULT_PAGE, perPage = DEFAULT_PAGE_SIZE } = pagination;
-
- return axios
- .get(state.config.endpoint, { params: { page, per_page: perPage, name } })
- .then(({ data, headers }) => {
- dispatch('receiveImagesListSuccess', { data, headers });
- })
- .catch(() => {
- createFlash({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE });
- })
- .finally(() => {
- commit(types.SET_MAIN_LOADING, false);
- });
-};
-
-export const requestTagsList = ({ commit, dispatch, state: { imageDetails } }, pagination = {}) => {
- commit(types.SET_MAIN_LOADING, true);
- const tagsPath = pathGenerator(imageDetails);
-
- const { page = DEFAULT_PAGE, perPage = DEFAULT_PAGE_SIZE } = pagination;
- return axios
- .get(tagsPath, { params: { page, per_page: perPage } })
- .then(({ data, headers }) => {
- dispatch('receiveTagsListSuccess', { data, headers });
- })
- .catch(() => {
- createFlash({ message: FETCH_TAGS_LIST_ERROR_MESSAGE });
- })
- .finally(() => {
- commit(types.SET_MAIN_LOADING, false);
- });
-};
-
-export const requestImageDetailsAndTagsList = ({ dispatch, commit }, id) => {
- commit(types.SET_MAIN_LOADING, true);
- return Api.containerRegistryDetails(id)
- .then(({ data }) => {
- commit(types.SET_IMAGE_DETAILS, data);
- dispatch('requestTagsList');
- })
- .catch(() => {
- createFlash({ message: FETCH_IMAGE_DETAILS_ERROR_MESSAGE });
- commit(types.SET_MAIN_LOADING, false);
- });
-};
-
-export const requestDeleteTag = ({ commit, dispatch, state }, { tag }) => {
- commit(types.SET_MAIN_LOADING, true);
- return axios
- .delete(tag.destroy_path)
- .then(() => {
- dispatch('setShowGarbageCollectionTip', true);
-
- return dispatch('requestTagsList', state.tagsPagination);
- })
- .finally(() => {
- commit(types.SET_MAIN_LOADING, false);
- });
-};
-
-export const requestDeleteTags = ({ commit, dispatch, state }, { ids }) => {
- commit(types.SET_MAIN_LOADING, true);
-
- const tagsPath = pathGenerator(state.imageDetails, '/bulk_destroy');
-
- return axios
- .delete(tagsPath, { params: { ids } })
- .then(() => {
- dispatch('setShowGarbageCollectionTip', true);
- return dispatch('requestTagsList', state.tagsPagination);
- })
- .finally(() => {
- commit(types.SET_MAIN_LOADING, false);
- });
-};
-
-export const requestDeleteImage = ({ commit }, image) => {
- commit(types.SET_MAIN_LOADING, true);
- return axios
- .delete(image.destroy_path)
- .then(() => {
- commit(types.UPDATE_IMAGE, { ...image, deleting: true });
- })
- .finally(() => {
- commit(types.SET_MAIN_LOADING, false);
- });
-};
diff --git a/app/assets/javascripts/registry/explorer/stores/getters.js b/app/assets/javascripts/registry/explorer/stores/getters.js
deleted file mode 100644
index 7b5d1bd6da3..00000000000
--- a/app/assets/javascripts/registry/explorer/stores/getters.js
+++ /dev/null
@@ -1,18 +0,0 @@
-export const dockerBuildCommand = state => {
- /* eslint-disable @gitlab/require-i18n-strings */
- return `docker build -t ${state.config.repositoryUrl} .`;
-};
-
-export const dockerPushCommand = state => {
- /* eslint-disable @gitlab/require-i18n-strings */
- return `docker push ${state.config.repositoryUrl}`;
-};
-
-export const dockerLoginCommand = state => {
- /* eslint-disable @gitlab/require-i18n-strings */
- return `docker login ${state.config.registryHostUrlWithPort}`;
-};
-
-export const showGarbageCollection = state => {
- return state.showGarbageCollectionTip && state.config.isAdmin;
-};
diff --git a/app/assets/javascripts/registry/explorer/stores/index.js b/app/assets/javascripts/registry/explorer/stores/index.js
deleted file mode 100644
index 18e3351ed13..00000000000
--- a/app/assets/javascripts/registry/explorer/stores/index.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import Vue from 'vue';
-import Vuex from 'vuex';
-import * as actions from './actions';
-import * as getters from './getters';
-import mutations from './mutations';
-import state from './state';
-
-Vue.use(Vuex);
-
-export const createStore = () =>
- new Vuex.Store({
- state,
- getters,
- actions,
- mutations,
- });
diff --git a/app/assets/javascripts/registry/explorer/stores/mutation_types.js b/app/assets/javascripts/registry/explorer/stores/mutation_types.js
deleted file mode 100644
index 5dd0cec52eb..00000000000
--- a/app/assets/javascripts/registry/explorer/stores/mutation_types.js
+++ /dev/null
@@ -1,10 +0,0 @@
-export const SET_INITIAL_STATE = 'SET_INITIAL_STATE';
-
-export const SET_IMAGES_LIST_SUCCESS = 'SET_PACKAGE_LIST_SUCCESS';
-export const UPDATE_IMAGE = 'UPDATE_IMAGE';
-export const SET_PAGINATION = 'SET_PAGINATION';
-export const SET_MAIN_LOADING = 'SET_MAIN_LOADING';
-export const SET_TAGS_PAGINATION = 'SET_TAGS_PAGINATION';
-export const SET_TAGS_LIST_SUCCESS = 'SET_TAGS_LIST_SUCCESS';
-export const SET_SHOW_GARBAGE_COLLECTION_TIP = 'SET_SHOW_GARBAGE_COLLECTION_TIP';
-export const SET_IMAGE_DETAILS = 'SET_IMAGE_DETAILS';
diff --git a/app/assets/javascripts/registry/explorer/stores/mutations.js b/app/assets/javascripts/registry/explorer/stores/mutations.js
deleted file mode 100644
index 5bdb431ad2e..00000000000
--- a/app/assets/javascripts/registry/explorer/stores/mutations.js
+++ /dev/null
@@ -1,54 +0,0 @@
-import * as types from './mutation_types';
-import { parseIntPagination, normalizeHeaders, parseBoolean } from '~/lib/utils/common_utils';
-import { IMAGE_DELETE_SCHEDULED_STATUS, IMAGE_FAILED_DELETED_STATUS } from '../constants/index';
-
-export default {
- [types.SET_INITIAL_STATE](state, config) {
- state.config = {
- ...config,
- expirationPolicy: config.expirationPolicy ? JSON.parse(config.expirationPolicy) : undefined,
- isGroupPage: parseBoolean(config.isGroupPage),
- isAdmin: parseBoolean(config.isAdmin),
- };
- },
-
- [types.SET_IMAGES_LIST_SUCCESS](state, images) {
- state.images = images.map(i => ({
- ...i,
- status: undefined,
- deleting: i.status === IMAGE_DELETE_SCHEDULED_STATUS,
- failedDelete: i.status === IMAGE_FAILED_DELETED_STATUS,
- }));
- },
-
- [types.UPDATE_IMAGE](state, image) {
- const index = state.images.findIndex(i => i.id === image.id);
- state.images.splice(index, 1, { ...image });
- },
-
- [types.SET_TAGS_LIST_SUCCESS](state, tags) {
- state.tags = tags;
- },
-
- [types.SET_MAIN_LOADING](state, isLoading) {
- state.isLoading = isLoading;
- },
-
- [types.SET_SHOW_GARBAGE_COLLECTION_TIP](state, showGarbageCollectionTip) {
- state.showGarbageCollectionTip = showGarbageCollectionTip;
- },
-
- [types.SET_PAGINATION](state, headers) {
- const normalizedHeaders = normalizeHeaders(headers);
- state.pagination = parseIntPagination(normalizedHeaders);
- },
-
- [types.SET_TAGS_PAGINATION](state, headers) {
- const normalizedHeaders = normalizeHeaders(headers);
- state.tagsPagination = parseIntPagination(normalizedHeaders);
- },
-
- [types.SET_IMAGE_DETAILS](state, details) {
- state.imageDetails = details;
- },
-};
diff --git a/app/assets/javascripts/registry/explorer/stores/state.js b/app/assets/javascripts/registry/explorer/stores/state.js
deleted file mode 100644
index 66ee56eb47b..00000000000
--- a/app/assets/javascripts/registry/explorer/stores/state.js
+++ /dev/null
@@ -1,10 +0,0 @@
-export default () => ({
- isLoading: false,
- showGarbageCollectionTip: false,
- config: {},
- images: [],
- imageDetails: {},
- tags: [],
- pagination: {},
- tagsPagination: {},
-});
diff --git a/app/assets/javascripts/registry/explorer/utils.js b/app/assets/javascripts/registry/explorer/utils.js
deleted file mode 100644
index a48da51caae..00000000000
--- a/app/assets/javascripts/registry/explorer/utils.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import { joinPaths } from '~/lib/utils/url_utility';
-
-export const pathGenerator = (imageDetails, ending = '?format=json') => {
- // this method is a temporary workaround, to be removed with graphql implementation
- // https://gitlab.com/gitlab-org/gitlab/-/issues/276432
-
- const splitPath = imageDetails.path.split('/').reverse();
- const splitName = imageDetails.name ? imageDetails.name.split('/').reverse() : [];
- const basePath = splitPath
- .reduce((acc, curr, index) => {
- if (splitPath[index] !== splitName[index]) {
- acc.unshift(curr);
- }
- return acc;
- }, [])
- .join('/');
-
- return joinPaths(
- window.gon.relative_url_root,
- `/${basePath}`,
- '/registry/repository/',
- `${imageDetails.id}`,
- `tags${ending}`,
- );
-};
diff --git a/app/assets/javascripts/registry/settings/components/expiration_dropdown.vue b/app/assets/javascripts/registry/settings/components/expiration_dropdown.vue
new file mode 100644
index 00000000000..d75fb31fd98
--- /dev/null
+++ b/app/assets/javascripts/registry/settings/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/registry/settings/components/expiration_input.vue b/app/assets/javascripts/registry/settings/components/expiration_input.vue
new file mode 100644
index 00000000000..2dbd9d26f60
--- /dev/null
+++ b/app/assets/javascripts/registry/settings/components/expiration_input.vue
@@ -0,0 +1,110 @@
+<script>
+import { GlFormGroup, GlFormInput, GlSprintf, GlLink } from '@gitlab/ui';
+import { NAME_REGEX_LENGTH, TEXT_AREA_INVALID_FEEDBACK } from '../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/registry/settings/components/expiration_run_text.vue b/app/assets/javascripts/registry/settings/components/expiration_run_text.vue
new file mode 100644
index 00000000000..fd9ca6a54c5
--- /dev/null
+++ b/app/assets/javascripts/registry/settings/components/expiration_run_text.vue
@@ -0,0 +1,46 @@
+<script>
+import { GlFormGroup, GlFormInput } from '@gitlab/ui';
+import { NEXT_CLEANUP_LABEL, NOT_SCHEDULED_POLICY_TEXT } from '~/registry/settings/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/registry/settings/components/expiration_toggle.vue b/app/assets/javascripts/registry/settings/components/expiration_toggle.vue
new file mode 100644
index 00000000000..7f045244926
--- /dev/null
+++ b/app/assets/javascripts/registry/settings/components/expiration_toggle.vue
@@ -0,0 +1,52 @@
+<script>
+import { GlFormGroup, GlToggle, GlSprintf } from '@gitlab/ui';
+import { ENABLED_TOGGLE_DESCRIPTION, DISABLED_TOGGLE_DESCRIPTION } from '../constants';
+
+export default {
+ 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" :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/registry/settings/components/registry_settings_app.vue b/app/assets/javascripts/registry/settings/components/registry_settings_app.vue
index 264d39a406a..35c7a8be4ea 100644
--- a/app/assets/javascripts/registry/settings/components/registry_settings_app.vue
+++ b/app/assets/javascripts/registry/settings/components/registry_settings_app.vue
@@ -1,17 +1,17 @@
<script>
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';
+import { isEqual, get, isEmpty } from 'lodash';
+import expirationPolicyQuery from '../graphql/queries/get_expiration_policy.query.graphql';
import {
+ FETCH_SETTINGS_ERROR_MESSAGE,
UNAVAILABLE_FEATURE_TITLE,
UNAVAILABLE_FEATURE_INTRO_TEXT,
UNAVAILABLE_USER_FEATURE_TEXT,
UNAVAILABLE_ADMIN_FEATURE_TEXT,
} from '../constants';
+import SettingsForm from './settings_form.vue';
+
export default {
components: {
SettingsForm,
@@ -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);
},
},
diff --git a/app/assets/javascripts/registry/settings/components/settings_form.vue b/app/assets/javascripts/registry/settings/components/settings_form.vue
index fe4aee6806e..1f374c7b60e 100644
--- a/app/assets/javascripts/registry/settings/components/settings_form.vue
+++ b/app/assets/javascripts/registry/settings/components/settings_form.vue
@@ -1,21 +1,41 @@
<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';
-import { formOptionsGenerator } from '~/registry/shared/utils';
-import updateContainerExpirationPolicyMutation from '../graphql/mutations/update_container_expiration_policy.graphql';
-import { updateContainerExpirationPolicy } from '../graphql/utils/cache_update';
+ 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/settings/utils';
+import updateContainerExpirationPolicyMutation from '~/registry/settings/graphql/mutations/update_container_expiration_policy.mutation.graphql';
+import { updateContainerExpirationPolicy } from '~/registry/settings/graphql/utils/cache_update';
+import ExpirationDropdown from './expiration_dropdown.vue';
+import ExpirationInput from './expiration_input.vue';
+import ExpirationToggle from './expiration_toggle.vue';
+import ExpirationRunText from './expiration_run_text.vue';
export default {
components: {
GlCard,
GlButton,
- ExpirationPolicyFields,
+ GlSprintf,
+ ExpirationDropdown,
+ ExpirationInput,
+ ExpirationToggle,
+ ExpirationRunText,
},
mixins: [Tracking.mixin()],
inject: ['projectPath'],
@@ -35,22 +55,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 +95,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 +125,8 @@ export default {
},
reset() {
this.track('reset_form');
- this.apiErrors = null;
+ this.apiErrors = {};
+ this.localErrors = {};
this.$emit('reset');
},
setApiErrors(response) {
@@ -101,9 +137,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 +171,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 +181,133 @@ 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"
+ :enabled="prefilledForm.enabled"
+ 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-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>
- <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>
+ </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"
+ 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>
diff --git a/app/assets/javascripts/registry/settings/constants.js b/app/assets/javascripts/registry/settings/constants.js
index e790658f491..21c54299632 100644
--- a/app/assets/javascripts/registry/settings/constants.js
+++ b/app/assets/javascripts/registry/settings/constants.js
@@ -1,7 +1,6 @@
import { s__, __ } from '~/locale';
-export const SET_CLEANUP_POLICY_BUTTON = s__('ContainerRegistry|Set cleanup policy');
-export const CLEANUP_POLICY_CARD_HEADER = s__('ContainerRegistry|Tag expiration policy');
+export const SET_CLEANUP_POLICY_BUTTON = __('Save');
export const UNAVAILABLE_FEATURE_TITLE = s__(
`ContainerRegistry|Cleanup policy for tags is disabled`,
);
@@ -12,3 +11,81 @@ export const UNAVAILABLE_USER_FEATURE_TEXT = __(`Please contact your administrat
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}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 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}More information%{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/registry/settings/graphql/fragments/container_expiration_policy.fragment.graphql b/app/assets/javascripts/registry/settings/graphql/fragments/container_expiration_policy.fragment.graphql
index 224e0ed9472..1d6c89133af 100644
--- 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
@@ -5,4 +5,5 @@ fragment ContainerExpirationPolicyFields on ContainerExpirationPolicy {
nameRegex
nameRegexKeep
olderThan
+ nextRunAt
}
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.mutation.graphql
index c40cd115ab0..c40cd115ab0 100644
--- 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.mutation.graphql
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.query.graphql
index c171be0ad07..c171be0ad07 100644
--- a/app/assets/javascripts/registry/settings/graphql/queries/get_expiration_policy.graphql
+++ b/app/assets/javascripts/registry/settings/graphql/queries/get_expiration_policy.query.graphql
diff --git a/app/assets/javascripts/registry/settings/graphql/utils/cache_update.js b/app/assets/javascripts/registry/settings/graphql/utils/cache_update.js
index 88067d52b51..05b4125a2fc 100644
--- a/app/assets/javascripts/registry/settings/graphql/utils/cache_update.js
+++ b/app/assets/javascripts/registry/settings/graphql/utils/cache_update.js
@@ -1,5 +1,5 @@
import { produce } from 'immer';
-import expirationPolicyQuery from '../queries/get_expiration_policy.graphql';
+import expirationPolicyQuery from '../queries/get_expiration_policy.query.graphql';
export const updateContainerExpirationPolicy = projectPath => (client, { data: updatedData }) => {
const queryAndParams = {
diff --git a/app/assets/javascripts/registry/settings/registry_settings_bundle.js b/app/assets/javascripts/registry/settings/registry_settings_bundle.js
index f7b1c5abd3a..6a4584b1b28 100644
--- a/app/assets/javascripts/registry/settings/registry_settings_bundle.js
+++ b/app/assets/javascripts/registry/settings/registry_settings_bundle.js
@@ -13,7 +13,13 @@ export default () => {
if (!el) {
return null;
}
- const { projectPath, isAdmin, adminSettingsPath, enableHistoricEntries } = el.dataset;
+ const {
+ isAdmin,
+ enableHistoricEntries,
+ projectPath,
+ adminSettingsPath,
+ tagsRegexHelpPagePath,
+ } = el.dataset;
return new Vue({
el,
apolloProvider,
@@ -21,10 +27,11 @@ export default () => {
RegistrySettingsApp,
},
provide: {
- projectPath,
isAdmin: parseBoolean(isAdmin),
- adminSettingsPath,
enableHistoricEntries: parseBoolean(enableHistoricEntries),
+ projectPath,
+ adminSettingsPath,
+ tagsRegexHelpPagePath,
},
render(createElement) {
return createElement('registry-settings-app', {});
diff --git a/app/assets/javascripts/registry/shared/utils.js b/app/assets/javascripts/registry/settings/utils.js
index bdf1ab9507d..51b4fb6bdb8 100644
--- a/app/assets/javascripts/registry/shared/utils.js
+++ b/app/assets/javascripts/registry/settings/utils.js
@@ -6,27 +6,7 @@ export const findDefaultOption = options => {
return item ? item.key : null;
};
-export const mapComputedToEvent = (list, root) => {
- const result = {};
- list.forEach(e => {
- result[e] = {
- get() {
- return this[root][e];
- },
- set(value) {
- this.$emit('input', { newValue: { ...this[root], [e]: value }, modified: e });
- },
- };
- });
- 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);
diff --git a/app/assets/javascripts/registry/shared/components/expiration_policy_fields.vue b/app/assets/javascripts/registry/shared/components/expiration_policy_fields.vue
deleted file mode 100644
index 2b8e9f6ff64..00000000000
--- a/app/assets/javascripts/registry/shared/components/expiration_policy_fields.vue
+++ /dev/null
@@ -1,258 +0,0 @@
-<script>
-import { uniqueId } from 'lodash';
-import { GlFormGroup, GlToggle, GlFormSelect, GlFormTextarea, GlSprintf } from '@gitlab/ui';
-import {
- NAME_REGEX_LENGTH,
- ENABLED_TEXT,
- DISABLED_TEXT,
- TEXT_AREA_INVALID_FEEDBACK,
- EXPIRATION_INTERVAL_LABEL,
- EXPIRATION_SCHEDULE_LABEL,
- KEEP_N_LABEL,
- NAME_REGEX_LABEL,
- NAME_REGEX_PLACEHOLDER,
- NAME_REGEX_DESCRIPTION,
- NAME_REGEX_KEEP_LABEL,
- NAME_REGEX_KEEP_PLACEHOLDER,
- NAME_REGEX_KEEP_DESCRIPTION,
- ENABLE_TOGGLE_LABEL,
- ENABLE_TOGGLE_DESCRIPTION,
-} from '../constants';
-import { mapComputedToEvent } from '../utils';
-
-export default {
- components: {
- GlFormGroup,
- GlToggle,
- GlFormSelect,
- GlFormTextarea,
- GlSprintf,
- },
- props: {
- formOptions: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- apiErrors: {
- type: Object,
- required: false,
- default: null,
- },
- isLoading: {
- type: Boolean,
- required: false,
- default: false,
- },
- value: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- labelCols: {
- type: [Number, String],
- required: false,
- default: 3,
- },
- labelAlign: {
- type: String,
- required: false,
- default: 'right',
- },
- },
- i18n: {
- ENABLE_TOGGLE_LABEL,
- ENABLE_TOGGLE_DESCRIPTION,
- },
- selectList: [
- {
- name: 'expiration-policy-interval',
- label: EXPIRATION_INTERVAL_LABEL,
- model: 'olderThan',
- },
- {
- name: 'expiration-policy-schedule',
- label: EXPIRATION_SCHEDULE_LABEL,
- model: 'cadence',
- },
- {
- name: 'expiration-policy-latest',
- label: KEEP_N_LABEL,
- model: 'keepN',
- },
- ],
- textAreaList: [
- {
- name: 'expiration-policy-name-matching',
- label: NAME_REGEX_LABEL,
- model: 'nameRegex',
- placeholder: NAME_REGEX_PLACEHOLDER,
- description: NAME_REGEX_DESCRIPTION,
- },
- {
- name: 'expiration-policy-keep-name',
- label: NAME_REGEX_KEEP_LABEL,
- model: 'nameRegexKeep',
- placeholder: NAME_REGEX_KEEP_PLACEHOLDER,
- description: NAME_REGEX_KEEP_DESCRIPTION,
- },
- ],
- data() {
- return {
- uniqueId: uniqueId(),
- };
- },
- computed: {
- ...mapComputedToEvent(
- ['enabled', 'cadence', 'olderThan', 'keepN', 'nameRegex', 'nameRegexKeep'],
- 'value',
- ),
- policyEnabledText() {
- return this.enabled ? ENABLED_TEXT : DISABLED_TEXT;
- },
- textAreaValidation() {
- const nameRegexErrors = this.apiErrors?.nameRegex || this.validateRegexLength(this.nameRegex);
- const nameKeepRegexErrors =
- this.apiErrors?.nameRegexKeep || this.validateRegexLength(this.nameRegexKeep);
-
- return {
- /*
- * The state has this form:
- * null: gray border, no message
- * true: green border, no message ( because none is configured)
- * false: red border, error message
- * So in this function we keep null if the are no message otherwise we 'invert' the error message
- */
- nameRegex: {
- state: nameRegexErrors === null ? null : !nameRegexErrors,
- message: nameRegexErrors,
- },
- nameRegexKeep: {
- state: nameKeepRegexErrors === null ? null : !nameKeepRegexErrors,
- message: nameKeepRegexErrors,
- },
- };
- },
- fieldsValidity() {
- return (
- this.textAreaValidation.nameRegex.state !== false &&
- this.textAreaValidation.nameRegexKeep.state !== false
- );
- },
- isFormElementDisabled() {
- return !this.enabled || this.isLoading;
- },
- },
- watch: {
- fieldsValidity: {
- immediate: true,
- handler(valid) {
- if (valid) {
- this.$emit('validated');
- } else {
- this.$emit('invalidated');
- }
- },
- },
- },
- methods: {
- validateRegexLength(value) {
- if (!value) {
- return null;
- }
- return value.length <= NAME_REGEX_LENGTH ? '' : TEXT_AREA_INVALID_FEEDBACK;
- },
- idGenerator(id) {
- return `${id}_${this.uniqueId}`;
- },
- updateModel(value, key) {
- this[key] = value;
- },
- },
-};
-</script>
-
-<template>
- <div ref="form-elements" class="gl-line-height-20">
- <gl-form-group
- :id="idGenerator('expiration-policy-toggle-group')"
- :label-cols="labelCols"
- :label-align="labelAlign"
- :label-for="idGenerator('expiration-policy-toggle')"
- :label="$options.i18n.ENABLE_TOGGLE_LABEL"
- >
- <div class="gl-display-flex">
- <gl-toggle
- :id="idGenerator('expiration-policy-toggle')"
- v-model="enabled"
- :disabled="isLoading"
- />
- <span class="gl-mb-3 gl-ml-3 gl-line-height-20">
- <gl-sprintf :message="$options.i18n.ENABLE_TOGGLE_DESCRIPTION">
- <template #toggleStatus>
- <strong>{{ policyEnabledText }}</strong>
- </template>
- </gl-sprintf>
- </span>
- </div>
- </gl-form-group>
-
- <gl-form-group
- v-for="select in $options.selectList"
- :id="idGenerator(`${select.name}-group`)"
- :key="select.name"
- :label-cols="labelCols"
- :label-align="labelAlign"
- :label-for="idGenerator(select.name)"
- :label="select.label"
- >
- <gl-form-select
- :id="idGenerator(select.name)"
- :value="value[select.model]"
- :disabled="isFormElementDisabled"
- @input="updateModel($event, select.model)"
- >
- <option v-for="option in formOptions[select.model]" :key="option.key" :value="option.key">
- {{ option.label }}
- </option>
- </gl-form-select>
- </gl-form-group>
-
- <gl-form-group
- v-for="textarea in $options.textAreaList"
- :id="idGenerator(`${textarea.name}-group`)"
- :key="textarea.name"
- :label-cols="labelCols"
- :label-align="labelAlign"
- :label-for="idGenerator(textarea.name)"
- :state="textAreaValidation[textarea.model].state"
- :invalid-feedback="textAreaValidation[textarea.model].message"
- >
- <template #label>
- <gl-sprintf :message="textarea.label">
- <template #italic="{content}">
- <i>{{ content }}</i>
- </template>
- </gl-sprintf>
- </template>
- <gl-form-textarea
- :id="idGenerator(textarea.name)"
- :value="value[textarea.model]"
- :placeholder="textarea.placeholder"
- :state="textAreaValidation[textarea.model].state"
- :disabled="isFormElementDisabled"
- trim
- @input="updateModel($event, textarea.model)"
- />
- <template #description>
- <span ref="regex-description">
- <gl-sprintf :message="textarea.description">
- <template #code="{content}">
- <code>{{ content }}</code>
- </template>
- </gl-sprintf>
- </span>
- </template>
- </gl-form-group>
- </div>
-</template>
diff --git a/app/assets/javascripts/registry/shared/constants.js b/app/assets/javascripts/registry/shared/constants.js
deleted file mode 100644
index d1e3d93938b..00000000000
--- a/app/assets/javascripts/registry/shared/constants.js
+++ /dev/null
@@ -1,69 +0,0 @@
-import { s__, __ } from '~/locale';
-
-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;
-
-export const ENABLED_TEXT = __('Enabled');
-export const DISABLED_TEXT = __('Disabled');
-
-export const ENABLE_TOGGLE_LABEL = s__('ContainerRegistry|Cleanup policy:');
-export const ENABLE_TOGGLE_DESCRIPTION = s__(
- 'ContainerRegistry|%{toggleStatus} - Tags matching the patterns defined below will be scheduled for deletion',
-);
-
-export const TEXT_AREA_INVALID_FEEDBACK = s__(
- 'ContainerRegistry|The value of this input should be less than 256 characters',
-);
-
-export const EXPIRATION_INTERVAL_LABEL = s__('ContainerRegistry|Expiration interval:');
-export const EXPIRATION_SCHEDULE_LABEL = s__('ContainerRegistry|Expiration schedule:');
-export const KEEP_N_LABEL = s__('ContainerRegistry|Number of tags to retain:');
-export const NAME_REGEX_LABEL = s__(
- 'ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}',
-);
-export const NAME_REGEX_PLACEHOLDER = '';
-export const NAME_REGEX_DESCRIPTION = s__(
- 'ContainerRegistry|Wildcards such as %{codeStart}.*-test%{codeEnd} or %{codeStart}dev-.*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}',
-);
-export const NAME_REGEX_KEEP_LABEL = s__(
- 'ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}be preserved:%{italicEnd}',
-);
-export const NAME_REGEX_KEEP_PLACEHOLDER = '';
-export const NAME_REGEX_KEEP_DESCRIPTION = s__(
- 'ContainerRegistry|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 },
-];