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:
Diffstat (limited to 'app/assets/javascripts/registry')
-rw-r--r--app/assets/javascripts/registry/explorer/components/delete_image.vue76
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/delete_modal.vue37
-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/empty_state.vue44
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/empty_tags_state.vue33
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/status_alert.vue50
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue13
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue10
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue5
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue6
-rw-r--r--app/assets/javascripts/registry/explorer/constants/details.js64
-rw-r--r--app/assets/javascripts/registry/explorer/constants/list.js10
-rw-r--r--app/assets/javascripts/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql19
-rw-r--r--app/assets/javascripts/registry/explorer/index.js18
-rw-r--r--app/assets/javascripts/registry/explorer/pages/details.vue157
-rw-r--r--app/assets/javascripts/registry/explorer/pages/list.vue129
-rw-r--r--app/assets/javascripts/registry/explorer/router.js4
-rw-r--r--app/assets/javascripts/registry/settings/components/registry_settings_app.vue2
-rw-r--r--app/assets/javascripts/registry/settings/components/settings_form.vue6
-rw-r--r--app/assets/javascripts/registry/settings/registry_settings_bundle.js4
20 files changed, 531 insertions, 192 deletions
diff --git a/app/assets/javascripts/registry/explorer/components/delete_image.vue b/app/assets/javascripts/registry/explorer/components/delete_image.vue
new file mode 100644
index 00000000000..22fe9fc1da6
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/components/delete_image.vue
@@ -0,0 +1,76 @@
+<script>
+import { produce } from 'immer';
+import { GRAPHQL_PAGE_SIZE } from '../constants/index';
+import deleteContainerRepositoryMutation from '../graphql/mutations/delete_container_repository.mutation.graphql';
+import getContainerRepositoryDetailsQuery from '../graphql/queries/get_container_repository_details.query.graphql';
+
+export default {
+ props: {
+ id: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ useUpdateFn: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ methods: {
+ updateImageStatus(store, { data: { destroyContainerRepository } }) {
+ const variables = {
+ id: this.id,
+ first: GRAPHQL_PAGE_SIZE,
+ };
+ const sourceData = store.readQuery({
+ query: getContainerRepositoryDetailsQuery,
+ variables,
+ });
+
+ const data = produce(sourceData, (draftState) => {
+ // eslint-disable-next-line no-param-reassign
+ draftState.containerRepository.status =
+ destroyContainerRepository.containerRepository.status;
+ });
+
+ store.writeQuery({
+ query: getContainerRepositoryDetailsQuery,
+ variables,
+ data,
+ });
+ },
+ doDelete() {
+ this.$emit('start');
+ return this.$apollo
+ .mutate({
+ mutation: deleteContainerRepositoryMutation,
+ variables: {
+ id: this.id,
+ },
+ update: this.useUpdateFn ? this.updateImageStatus : undefined,
+ })
+ .then(({ data }) => {
+ if (data?.destroyContainerRepository?.errors[0]) {
+ this.$emit('error', data?.destroyContainerRepository?.errors);
+ return;
+ }
+ this.$emit('success');
+ })
+ .catch((e) => {
+ // note: we are adding an array to follow the same format of the error raised above
+ this.$emit('error', [e]);
+ })
+ .finally(() => {
+ this.$emit('end');
+ });
+ },
+ },
+ render() {
+ if (this.$scopedSlots?.default) {
+ return this.$scopedSlots.default({ doDelete: this.doDelete });
+ }
+ return null;
+ },
+};
+</script>
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/delete_modal.vue b/app/assets/javascripts/registry/explorer/components/details_page/delete_modal.vue
index 96f221bf71d..0432cf1123c 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/delete_modal.vue
+++ b/app/assets/javascripts/registry/explorer/components/details_page/delete_modal.vue
@@ -1,7 +1,12 @@
<script>
import { GlModal, GlSprintf } from '@gitlab/ui';
import { n__ } from '~/locale';
-import { REMOVE_TAG_CONFIRMATION_TEXT, REMOVE_TAGS_CONFIRMATION_TEXT } from '../../constants/index';
+import {
+ REMOVE_TAG_CONFIRMATION_TEXT,
+ REMOVE_TAGS_CONFIRMATION_TEXT,
+ DELETE_IMAGE_CONFIRMATION_TITLE,
+ DELETE_IMAGE_CONFIRMATION_TEXT,
+} from '../../constants';
export default {
components: {
@@ -14,9 +19,17 @@ export default {
required: false,
default: () => [],
},
+ deleteImage: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
},
computed: {
- modalAction() {
+ modalTitle() {
+ if (this.deleteImage) {
+ return DELETE_IMAGE_CONFIRMATION_TITLE;
+ }
return n__(
'ContainerRegistry|Remove tag',
'ContainerRegistry|Remove tags',
@@ -24,14 +37,19 @@ export default {
);
},
modalDescription() {
+ if (this.deleteImage) {
+ return {
+ message: DELETE_IMAGE_CONFIRMATION_TEXT,
+ };
+ }
if (this.itemsToBeDeleted.length > 1) {
return {
message: REMOVE_TAGS_CONFIRMATION_TEXT,
item: this.itemsToBeDeleted.length,
};
}
- const [first] = this.itemsToBeDeleted;
+ const [first] = this.itemsToBeDeleted;
return {
message: REMOVE_TAG_CONFIRMATION_TEXT,
item: first?.path,
@@ -51,16 +69,17 @@ export default {
ref="deleteModal"
modal-id="delete-tag-modal"
ok-variant="danger"
- @ok="$emit('confirmDelete')"
+ :action-primary="{ text: __('Confirm'), attributes: { variant: 'danger' } }"
+ :action-cancel="{ text: __('Cancel') }"
+ @primary="$emit('confirmDelete')"
@cancel="$emit('cancelDelete')"
>
- <template #modal-title>{{ modalAction }}</template>
- <template #modal-ok>{{ modalAction }}</template>
+ <template #modal-title>{{ modalTitle }}</template>
<p v-if="modalDescription" data-testid="description">
<gl-sprintf :message="modalDescription.message">
- <template #item
- ><b>{{ modalDescription.item }}</b></template
- >
+ <template #item>
+ <b>{{ modalDescription.item }}</b>
+ </template>
</gl-sprintf>
</p>
</gl-modal>
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 ed02aa264ed..a4b4c08bc34 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,8 +1,8 @@
<script>
-import { GlSprintf } from '@gitlab/ui';
+import { GlSprintf, GlButton } from '@gitlab/ui';
import { sprintf, n__ } from '~/locale';
-import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import {
DETAILS_PAGE_TITLE,
@@ -24,7 +24,7 @@ import {
export default {
name: 'DetailsHeader',
- components: { GlSprintf, TitleArea, MetadataItem },
+ components: { GlSprintf, GlButton, TitleArea, MetadataItem },
mixins: [timeagoMixin],
props: {
image: {
@@ -36,6 +36,11 @@ export default {
required: false,
default: false,
},
+ disabled: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
},
computed: {
visibilityIcon() {
@@ -65,6 +70,9 @@ export default {
[UNFINISHED_STATUS]: { text: CLEANUP_UNFINISHED_TEXT, tooltip: CLEANUP_UNFINISHED_TOOLTIP },
}[this.image?.expirationPolicyCleanupStatus];
},
+ deleteButtonDisabled() {
+ return this.disabled || !this.image.canDelete;
+ },
},
i18n: {
DETAILS_PAGE_TITLE,
@@ -75,11 +83,13 @@ export default {
<template>
<title-area :metadata-loading="metadataLoading">
<template #title>
- <gl-sprintf :message="$options.i18n.DETAILS_PAGE_TITLE">
- <template #imageName>
- {{ image.name }}
- </template>
- </gl-sprintf>
+ <span data-testid="title">
+ <gl-sprintf :message="$options.i18n.DETAILS_PAGE_TITLE">
+ <template #imageName>
+ {{ image.name }}
+ </template>
+ </gl-sprintf>
+ </span>
</template>
<template #metadata-tags-count>
<metadata-item icon="tag" :text="tagCountText" data-testid="tags-count" />
@@ -103,5 +113,15 @@ export default {
data-testid="updated-and-visibility"
/>
</template>
+ <template #right-actions>
+ <gl-button
+ v-if="!metadataLoading"
+ variant="danger"
+ :disabled="deleteButtonDisabled"
+ @click="$emit('delete')"
+ >
+ {{ __('Delete') }}
+ </gl-button>
+ </template>
</title-area>
</template>
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/empty_state.vue b/app/assets/javascripts/registry/explorer/components/details_page/empty_state.vue
new file mode 100644
index 00000000000..a16d95a6b30
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/components/details_page/empty_state.vue
@@ -0,0 +1,44 @@
+<script>
+import { GlEmptyState } from '@gitlab/ui';
+import {
+ NO_TAGS_TITLE,
+ NO_TAGS_MESSAGE,
+ MISSING_OR_DELETED_IMAGE_TITLE,
+ MISSING_OR_DELETED_IMAGE_MESSAGE,
+} from '../../constants/index';
+
+export default {
+ components: {
+ GlEmptyState,
+ },
+ props: {
+ noContainersImage: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ isEmptyImage: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ },
+ computed: {
+ title() {
+ return this.isEmptyImage ? MISSING_OR_DELETED_IMAGE_TITLE : NO_TAGS_TITLE;
+ },
+ description() {
+ return this.isEmptyImage ? MISSING_OR_DELETED_IMAGE_MESSAGE : NO_TAGS_MESSAGE;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-empty-state
+ :title="title"
+ :svg-path="noContainersImage"
+ :description="description"
+ class="gl-mx-auto gl-my-0"
+ />
+</template>
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/empty_tags_state.vue b/app/assets/javascripts/registry/explorer/components/details_page/empty_tags_state.vue
deleted file mode 100644
index 0c684d124d5..00000000000
--- a/app/assets/javascripts/registry/explorer/components/details_page/empty_tags_state.vue
+++ /dev/null
@@ -1,33 +0,0 @@
-<script>
-import { GlEmptyState } from '@gitlab/ui';
-import {
- EMPTY_IMAGE_REPOSITORY_TITLE,
- EMPTY_IMAGE_REPOSITORY_MESSAGE,
-} from '../../constants/index';
-
-export default {
- components: {
- GlEmptyState,
- },
- props: {
- noContainersImage: {
- type: String,
- required: false,
- default: '',
- },
- },
- i18n: {
- EMPTY_IMAGE_REPOSITORY_TITLE,
- EMPTY_IMAGE_REPOSITORY_MESSAGE,
- },
-};
-</script>
-
-<template>
- <gl-empty-state
- :title="$options.i18n.EMPTY_IMAGE_REPOSITORY_TITLE"
- :svg-path="noContainersImage"
- :description="$options.i18n.EMPTY_IMAGE_REPOSITORY_MESSAGE"
- class="gl-mx-auto gl-my-0"
- />
-</template>
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/status_alert.vue b/app/assets/javascripts/registry/explorer/components/details_page/status_alert.vue
new file mode 100644
index 00000000000..fc1504f6c31
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/components/details_page/status_alert.vue
@@ -0,0 +1,50 @@
+<script>
+import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
+import {
+ IMAGE_STATUS_MESSAGES,
+ IMAGE_STATUS_TITLES,
+ IMAGE_STATUS_ALERT_TYPE,
+ PACKAGE_DELETE_HELP_PAGE_PATH,
+} from '../../constants';
+
+export default {
+ components: {
+ GlAlert,
+ GlSprintf,
+ GlLink,
+ },
+ props: {
+ status: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ message() {
+ return IMAGE_STATUS_MESSAGES[this.status];
+ },
+ title() {
+ return IMAGE_STATUS_TITLES[this.status];
+ },
+ variant() {
+ return IMAGE_STATUS_ALERT_TYPE[this.status];
+ },
+ },
+ links: {
+ PACKAGE_DELETE_HELP_PAGE_PATH,
+ },
+};
+</script>
+<template>
+ <gl-alert :title="title" :variant="variant">
+ <span data-testid="message">
+ <gl-sprintf :message="message">
+ <template #link="{ content }">
+ <gl-link :href="$options.links.PACKAGE_DELETE_HELP_PAGE_PATH" target="_blank">{{
+ content
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </span>
+ </gl-alert>
+</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 1e0736c4a53..bc10246614a 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
@@ -1,7 +1,7 @@
<script>
import { GlButton } from '@gitlab/ui';
-import TagsListRow from './tags_list_row.vue';
import { REMOVE_TAGS_BUTTON_TITLE, TAGS_LIST_TITLE } from '../../constants/index';
+import TagsListRow from './tags_list_row.vue';
export default {
name: 'TagsList',
@@ -20,6 +20,11 @@ export default {
default: true,
required: false,
},
+ disabled: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
},
i18n: {
REMOVE_TAGS_BUTTON_TITLE,
@@ -37,6 +42,9 @@ export default {
showMultiDeleteButton() {
return this.tags.some((tag) => tag.canDelete) && !this.isMobile;
},
+ multiDeleteButtonIsDisabled() {
+ return !this.hasSelectedItems || this.disabled;
+ },
},
methods: {
updateSelectedItems(name) {
@@ -55,7 +63,7 @@ export default {
<gl-button
v-if="showMultiDeleteButton"
- :disabled="!hasSelectedItems"
+ :disabled="multiDeleteButtonIsDisabled"
category="secondary"
variant="danger"
@click="$emit('delete', selectedItems)"
@@ -70,6 +78,7 @@ export default {
:first="index === 0"
:selected="selectedItems[tag.name]"
:is-mobile="isMobile"
+ :disabled="disabled"
@select="updateSelectedItems(tag.name)"
@delete="$emit('delete', { [tag.name]: true })"
/>
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 2e4a489f2cb..c66f92bdd67 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
@@ -1,13 +1,12 @@
<script>
import { GlFormCheckbox, GlTooltipDirective, GlSprintf, GlIcon } from '@gitlab/ui';
+import { formatDate } from '~/lib/utils/datetime_utility';
+import { numberToHumanSize } from '~/lib/utils/number_utils';
import { n__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import { numberToHumanSize } from '~/lib/utils/number_utils';
-import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import { formatDate } from '~/lib/utils/datetime_utility';
-import ListItem from '~/vue_shared/components/registry/list_item.vue';
-import DeleteButton from '../delete_button.vue';
import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
+import ListItem from '~/vue_shared/components/registry/list_item.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import {
REMOVE_TAG_BUTTON_TITLE,
DIGEST_LABEL,
@@ -20,6 +19,7 @@ import {
NOT_AVAILABLE_TEXT,
NOT_AVAILABLE_SIZE,
} from '../../constants/index';
+import DeleteButton from '../delete_button.vue';
export default {
components: {
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 264a3c27cde..9ae5b0f9eb1 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,12 +1,10 @@
<script>
import { GlTooltipDirective, GlIcon, GlSprintf, GlSkeletonLoader } from '@gitlab/ui';
-import { n__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { n__ } from '~/locale';
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';
-
import {
ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
LIST_DELETE_BUTTON_DISABLED,
@@ -16,6 +14,7 @@ import {
IMAGE_DELETE_SCHEDULED_STATUS,
IMAGE_FAILED_DELETED_STATUS,
} from '../../constants/index';
+import DeleteButton from '../delete_button.vue';
export default {
name: 'ImageListRow',
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 f01e3c9d24a..8d7505dfbae 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,8 +1,8 @@
<script>
-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';
import { approximateDuration, calculateRemainingMilliseconds } from '~/lib/utils/datetime_utility';
+import { n__, sprintf } from '~/locale';
+import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import {
CONTAINER_REGISTRY_TITLE,
diff --git a/app/assets/javascripts/registry/explorer/constants/details.js b/app/assets/javascripts/registry/explorer/constants/details.js
index b5627352857..3f04538a18b 100644
--- a/app/assets/javascripts/registry/explorer/constants/details.js
+++ b/app/assets/javascripts/registry/explorer/constants/details.js
@@ -1,3 +1,4 @@
+import { helpPagePath } from '~/helpers/help_page_helper';
import { s__, __ } from '~/locale';
// Translations strings
@@ -32,18 +33,30 @@ export const CONFIGURATION_DETAILS_ROW_TEST = s__(
export const REMOVE_TAG_BUTTON_TITLE = s__('ContainerRegistry|Remove tag');
export const REMOVE_TAGS_BUTTON_TITLE = s__('ContainerRegistry|Delete selected');
+
export const REMOVE_TAG_CONFIRMATION_TEXT = s__(
`ContainerRegistry|You are about to remove %{item}. Are you sure?`,
);
export const REMOVE_TAGS_CONFIRMATION_TEXT = s__(
`ContainerRegistry|You are about to remove %{item} tags. Are you sure?`,
);
-export const EMPTY_IMAGE_REPOSITORY_TITLE = s__('ContainerRegistry|This image has no active tags');
-export const EMPTY_IMAGE_REPOSITORY_MESSAGE = s__(
+export const NO_TAGS_TITLE = s__('ContainerRegistry|This image has no active tags');
+export const NO_TAGS_MESSAGE = s__(
`ContainerRegistry|The last tag related to this image was recently removed.
This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process.
If you have any questions, contact your administrator.`,
);
+
+export const MISSING_OR_DELETED_IMAGE_TITLE = s__(
+ 'ContainerRegistry|The image repository could not be found.',
+);
+export const MISSING_OR_DELETED_IMAGE_MESSAGE = s__(
+ 'ContainerRegistry|The requested image repository does not exist or has been deleted. If you think this is an error, try refreshing the page.',
+);
+export const MISSING_OR_DELETE_IMAGE_BREADCRUMB = s__(
+ 'ContainerRegistry|Image repository not found',
+);
+
export const ADMIN_GARBAGE_COLLECTION_TIP = s__(
'ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage.',
);
@@ -76,6 +89,29 @@ export const CLEANUP_DISABLED_TOOLTIP = s__(
'ContainerRegistry|Cleanup is disabled for this project',
);
+export const DETAILS_DELETE_IMAGE_ERROR_MESSAGE = s__(
+ 'ContainerRegistry|Something went wrong while scheduling the image for deletion.',
+);
+
+export const DELETE_IMAGE_CONFIRMATION_TITLE = s__('ContainerRegistry|Delete image repository?');
+export const DELETE_IMAGE_CONFIRMATION_TEXT = s__(
+ 'ContainerRegistry|Deleting the image repository will delete all images and tags inside. This action cannot be undone.',
+);
+
+export const SCHEDULED_FOR_DELETION_STATUS_TITLE = s__(
+ 'ContainerRegistry|Image repository will be deleted',
+);
+export const SCHEDULED_FOR_DELETION_STATUS_MESSAGE = s__(
+ 'ContainerRegistry|This image repository will be deleted. %{linkStart}Learn more.%{linkEnd}',
+);
+
+export const FAILED_DELETION_STATUS_TITLE = s__(
+ 'ContainerRegistry|Image repository deletion failed',
+);
+export const FAILED_DELETION_STATUS_MESSAGE = s__(
+ 'ContainerRegistry|This image repository has failed to be deleted',
+);
+
// Parameters
export const DEFAULT_PAGE = 1;
@@ -85,15 +121,39 @@ export const ALERT_SUCCESS_TAG = 'success_tag';
export const ALERT_DANGER_TAG = 'danger_tag';
export const ALERT_SUCCESS_TAGS = 'success_tags';
export const ALERT_DANGER_TAGS = 'danger_tags';
+export const ALERT_DANGER_IMAGE = 'danger_image';
+
+export const DELETE_SCHEDULED = 'DELETE_SCHEDULED';
+export const DELETE_FAILED = 'DELETE_FAILED';
export const ALERT_MESSAGES = {
[ALERT_SUCCESS_TAG]: DELETE_TAG_SUCCESS_MESSAGE,
[ALERT_DANGER_TAG]: DELETE_TAG_ERROR_MESSAGE,
[ALERT_SUCCESS_TAGS]: DELETE_TAGS_SUCCESS_MESSAGE,
[ALERT_DANGER_TAGS]: DELETE_TAGS_ERROR_MESSAGE,
+ [ALERT_DANGER_IMAGE]: DETAILS_DELETE_IMAGE_ERROR_MESSAGE,
};
export const UNFINISHED_STATUS = 'UNFINISHED';
export const UNSCHEDULED_STATUS = 'UNSCHEDULED';
export const SCHEDULED_STATUS = 'SCHEDULED';
export const ONGOING_STATUS = 'ONGOING';
+
+export const IMAGE_STATUS_TITLES = {
+ [DELETE_SCHEDULED]: SCHEDULED_FOR_DELETION_STATUS_TITLE,
+ [DELETE_FAILED]: FAILED_DELETION_STATUS_TITLE,
+};
+
+export const IMAGE_STATUS_MESSAGES = {
+ [DELETE_SCHEDULED]: SCHEDULED_FOR_DELETION_STATUS_MESSAGE,
+ [DELETE_FAILED]: FAILED_DELETION_STATUS_MESSAGE,
+};
+
+export const IMAGE_STATUS_ALERT_TYPE = {
+ [DELETE_SCHEDULED]: 'info',
+ [DELETE_FAILED]: 'warning',
+};
+
+export const PACKAGE_DELETE_HELP_PAGE_PATH = helpPagePath('user/packages/container_registry', {
+ anchor: 'delete-images',
+});
diff --git a/app/assets/javascripts/registry/explorer/constants/list.js b/app/assets/javascripts/registry/explorer/constants/list.js
index 37ced72861e..f59b9d7a9f5 100644
--- a/app/assets/javascripts/registry/explorer/constants/list.js
+++ b/app/assets/javascripts/registry/explorer/constants/list.js
@@ -1,4 +1,4 @@
-import { s__ } from '~/locale';
+import { s__, __ } from '~/locale';
// Translations strings
@@ -35,8 +35,6 @@ export const ASYNC_DELETE_IMAGE_ERROR_MESSAGE = s__(
export const DELETE_IMAGE_SUCCESS_MESSAGE = s__(
'ContainerRegistry|%{title} was successfully scheduled for deletion',
);
-export const IMAGE_REPOSITORY_LIST_LABEL = s__('ContainerRegistry|Image Repositories');
-export const SEARCH_PLACEHOLDER_TEXT = s__('ContainerRegistry|Filter by name');
export const EMPTY_RESULT_TITLE = s__('ContainerRegistry|Sorry, your filter produced no results.');
export const EMPTY_RESULT_MESSAGE = s__(
'ContainerRegistry|To widen your search, change or remove the filters above.',
@@ -47,3 +45,9 @@ export const EMPTY_RESULT_MESSAGE = s__(
export const IMAGE_DELETE_SCHEDULED_STATUS = 'DELETE_SCHEDULED';
export const IMAGE_FAILED_DELETED_STATUS = 'DELETE_FAILED';
export const GRAPHQL_PAGE_SIZE = 10;
+
+export const SORT_FIELDS = [
+ { orderBy: 'UPDATED', label: __('Updated') },
+ { orderBy: 'CREATED', label: __('Created') },
+ { orderBy: 'NAME', label: __('Name') },
+];
diff --git a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql b/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql
index 8b6d778c655..01cb7fa1cab 100644
--- a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql
+++ b/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql
@@ -6,9 +6,17 @@ query getContainerRepositoriesDetails(
$after: String
$before: String
$isGroupPage: Boolean!
+ $sort: ContainerRepositorySort
) {
project(fullPath: $fullPath) @skip(if: $isGroupPage) {
- containerRepositories(name: $name, after: $after, before: $before, first: $first, last: $last) {
+ containerRepositories(
+ name: $name
+ after: $after
+ before: $before
+ first: $first
+ last: $last
+ sort: $sort
+ ) {
nodes {
id
tagsCount
@@ -16,7 +24,14 @@ query getContainerRepositoriesDetails(
}
}
group(fullPath: $fullPath) @include(if: $isGroupPage) {
- containerRepositories(name: $name, after: $after, before: $before, first: $first, last: $last) {
+ containerRepositories(
+ name: $name
+ after: $after
+ before: $before
+ first: $first
+ last: $last
+ sort: $sort
+ ) {
nodes {
id
tagsCount
diff --git a/app/assets/javascripts/registry/explorer/index.js b/app/assets/javascripts/registry/explorer/index.js
index a3890ab5c42..f66839a74bf 100644
--- a/app/assets/javascripts/registry/explorer/index.js
+++ b/app/assets/javascripts/registry/explorer/index.js
@@ -1,12 +1,12 @@
-import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
-import Translate from '~/vue_shared/translate';
+import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import PerformancePlugin from '~/performance/vue_performance_plugin';
-import RegistryExplorer from './pages/index.vue';
+import Translate from '~/vue_shared/translate';
import RegistryBreadcrumb from './components/registry_breadcrumb.vue';
-import createRouter from './router';
import { apolloProvider } from './graphql/index';
+import RegistryExplorer from './pages/index.vue';
+import createRouter from './router';
Vue.use(Translate);
Vue.use(GlToast);
@@ -29,7 +29,14 @@ export default () => {
return null;
}
- const { endpoint, expirationPolicy, isGroupPage, isAdmin, ...config } = el.dataset;
+ const {
+ endpoint,
+ expirationPolicy,
+ isGroupPage,
+ isAdmin,
+ showUnfinishedTagCleanupCallout,
+ ...config
+ } = el.dataset;
// This is a mini state to help the breadcrumb have the correct name in the details page
const breadCrumbState = Vue.observable({
@@ -57,6 +64,7 @@ export default () => {
expirationPolicy: expirationPolicy ? JSON.parse(expirationPolicy) : undefined,
isGroupPage: parseBoolean(isGroupPage),
isAdmin: parseBoolean(isAdmin),
+ showUnfinishedTagCleanupCallout: parseBoolean(showUnfinishedTagCleanupCallout),
},
/* eslint-disable @gitlab/require-i18n-strings */
dockerBuildCommand: `docker build -t ${config.repositoryUrl} .`,
diff --git a/app/assets/javascripts/registry/explorer/pages/details.vue b/app/assets/javascripts/registry/explorer/pages/details.vue
index 0894fd6fcfa..0403467468a 100644
--- a/app/assets/javascripts/registry/explorer/pages/details.vue
+++ b/app/assets/javascripts/registry/explorer/pages/details.vue
@@ -2,28 +2,32 @@
import { GlKeysetPagination, GlResizeObserverDirective } from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import createFlash from '~/flash';
-import Tracking from '~/tracking';
+import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
+import Tracking from '~/tracking';
+import DeleteImage from '../components/delete_image.vue';
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 EmptyState from '../components/details_page/empty_state.vue';
+import PartialCleanupAlert from '../components/details_page/partial_cleanup_alert.vue';
+import StatusAlert from '../components/details_page/status_alert.vue';
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,
+ ALERT_DANGER_IMAGE,
GRAPHQL_PAGE_SIZE,
FETCH_IMAGES_LIST_ERROR_MESSAGE,
UNFINISHED_STATUS,
+ MISSING_OR_DELETE_IMAGE_BREADCRUMB,
} from '../constants/index';
+import deleteContainerRepositoryTagsMutation from '../graphql/mutations/delete_container_repository_tags.mutation.graphql';
+import getContainerRepositoryDetailsQuery from '../graphql/queries/get_container_repository_details.query.graphql';
export default {
name: 'RegistryDetailsPage',
@@ -35,7 +39,9 @@ export default {
DeleteModal,
TagsList,
TagsLoader,
- EmptyTagsState,
+ EmptyState,
+ StatusAlert,
+ DeleteImage,
},
directives: {
GlResizeObserver: GlResizeObserverDirective,
@@ -53,7 +59,7 @@ export default {
},
result({ data }) {
this.tagsPageInfo = data.containerRepository?.tags?.pageInfo;
- this.breadCrumbState.updateName(data.containerRepository?.name);
+ this.updateBreadcrumb();
},
error() {
createFlash({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE });
@@ -68,7 +74,8 @@ export default {
isMobile: false,
mutationLoading: false,
deleteAlertType: null,
- dismissPartialCleanupWarning: false,
+ hidePartialCleanupWarning: false,
+ deleteImageAlert: false,
};
},
computed: {
@@ -86,8 +93,9 @@ export default {
},
showPartialCleanupWarning() {
return (
+ this.config.showUnfinishedTagCleanupCallout &&
this.image?.expirationPolicyCleanupStatus === UNFINISHED_STATUS &&
- !this.dismissPartialCleanupWarning
+ !this.hidePartialCleanupWarning
);
},
tracking() {
@@ -99,14 +107,32 @@ export default {
showPagination() {
return this.tagsPageInfo.hasPreviousPage || this.tagsPageInfo.hasNextPage;
},
+ hasNoTags() {
+ return this.tags.length === 0;
+ },
+ pageActionsAreDisabled() {
+ return Boolean(this.image?.status);
+ },
},
methods: {
+ updateBreadcrumb() {
+ const name = this.image?.name || MISSING_OR_DELETE_IMAGE_BREADCRUMB;
+ this.breadCrumbState.updateName(name);
+ },
deleteTags(toBeDeleted) {
+ this.deleteImageAlert = false;
this.itemsToBeDeleted = this.tags.filter((tag) => toBeDeleted[tag.name]);
this.track('click_button');
this.$refs.deleteModal.show();
},
- async handleDelete() {
+ confirmDelete() {
+ if (this.deleteImageAlert) {
+ this.$refs.deleteImage.doDelete();
+ } else {
+ this.handleDeleteTag();
+ }
+ },
+ async handleDeleteTag() {
this.track('confirm_delete');
const { itemsToBeDeleted } = this;
this.itemsToBeDeleted = [];
@@ -168,51 +194,94 @@ export default {
});
}
},
+ dismissPartialCleanupWarning() {
+ this.hidePartialCleanupWarning = true;
+ axios.post(this.config.userCalloutsPath, {
+ feature_name: this.config.userCalloutId,
+ });
+ },
+ deleteImage() {
+ this.deleteImageAlert = true;
+ this.itemsToBeDeleted = [{ path: this.image.path }];
+ this.$refs.deleteModal.show();
+ },
+ deleteImageError() {
+ this.deleteAlertType = ALERT_DANGER_IMAGE;
+ },
+ deleteImageIniit() {
+ this.itemsToBeDeleted = [];
+ this.mutationLoading = true;
+ },
},
};
</script>
<template>
<div v-gl-resize-observer="handleResize" class="gl-my-3">
- <delete-alert
- v-model="deleteAlertType"
- :garbage-collection-help-page-path="config.garbageCollectionHelpPagePath"
- :is-admin="config.isAdmin"
- class="gl-my-2"
- />
+ <template v-if="image">
+ <delete-alert
+ v-model="deleteAlertType"
+ :garbage-collection-help-page-path="config.garbageCollectionHelpPagePath"
+ :is-admin="config.isAdmin"
+ class="gl-my-2"
+ />
+
+ <partial-cleanup-alert
+ v-if="showPartialCleanupWarning"
+ :run-cleanup-policies-help-page-path="config.runCleanupPoliciesHelpPagePath"
+ :cleanup-policies-help-page-path="config.cleanupPoliciesHelpPagePath"
+ @dismiss="dismissPartialCleanupWarning"
+ />
- <partial-cleanup-alert
- v-if="showPartialCleanupWarning"
- :run-cleanup-policies-help-page-path="config.runCleanupPoliciesHelpPagePath"
- :cleanup-policies-help-page-path="config.cleanupPoliciesHelpPagePath"
- @dismiss="dismissPartialCleanupWarning = true"
- />
+ <status-alert v-if="image.status" :status="image.status" />
- <details-header :image="image" :metadata-loading="isLoading" />
+ <details-header
+ :image="image"
+ :metadata-loading="isLoading"
+ :disabled="pageActionsAreDisabled"
+ @delete="deleteImage"
+ />
- <tags-loader v-if="isLoading" />
- <template v-else>
- <empty-tags-state v-if="tags.length === 0" :no-containers-image="config.noContainersImage" />
+ <tags-loader v-if="isLoading" />
<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"
+ <empty-state v-if="hasNoTags" :no-containers-image="config.noContainersImage" />
+ <template v-else>
+ <tags-list
+ :tags="tags"
+ :is-mobile="isMobile"
+ :disabled="pageActionsAreDisabled"
+ @delete="deleteTags"
/>
- </div>
+ <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>
- </template>
- <delete-modal
- ref="deleteModal"
- :items-to-be-deleted="itemsToBeDeleted"
- @confirmDelete="handleDelete"
- @cancel="track('cancel_delete')"
- />
+ <delete-image
+ :id="image.id"
+ ref="deleteImage"
+ use-update-fn
+ @start="deleteImageIniit"
+ @error="deleteImageError"
+ @end="mutationLoading = false"
+ />
+
+ <delete-modal
+ ref="deleteModal"
+ :items-to-be-deleted="itemsToBeDeleted"
+ :delete-image="deleteImageAlert"
+ @confirmDelete="confirmDelete"
+ @cancel="track('cancel_delete')"
+ />
+ </template>
+ <empty-state v-else is-empty-image :no-containers-image="config.noContainersImage" />
</div>
</template>
diff --git a/app/assets/javascripts/registry/explorer/pages/list.vue b/app/assets/javascripts/registry/explorer/pages/list.vue
index 336a997d629..8cad9b4ecfc 100644
--- a/app/assets/javascripts/registry/explorer/pages/list.vue
+++ b/app/assets/javascripts/registry/explorer/pages/list.vue
@@ -7,17 +7,15 @@ import {
GlLink,
GlAlert,
GlSkeletonLoader,
- GlSearchBoxByClick,
} from '@gitlab/ui';
import { get } from 'lodash';
import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql';
-import Tracking from '~/tracking';
import createFlash from '~/flash';
+import Tracking from '~/tracking';
+import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
+import DeleteImage from '../components/delete_image.vue';
import RegistryHeader from '../components/list_page/registry_header.vue';
-import getContainerRepositoriesDetails from '../graphql/queries/get_container_repositories_details.query.graphql';
-import deleteContainerRepositoryMutation from '../graphql/mutations/delete_container_repository.mutation.graphql';
-
import {
DELETE_IMAGE_SUCCESS_MESSAGE,
DELETE_IMAGE_ERROR_MESSAGE,
@@ -25,13 +23,13 @@ import {
CONNECTION_ERROR_MESSAGE,
REMOVE_REPOSITORY_MODAL_TEXT,
REMOVE_REPOSITORY_LABEL,
- SEARCH_PLACEHOLDER_TEXT,
- IMAGE_REPOSITORY_LIST_LABEL,
EMPTY_RESULT_TITLE,
EMPTY_RESULT_MESSAGE,
GRAPHQL_PAGE_SIZE,
FETCH_IMAGES_LIST_ERROR_MESSAGE,
+ SORT_FIELDS,
} from '../constants/index';
+import getContainerRepositoriesDetails from '../graphql/queries/get_container_repositories_details.query.graphql';
export default {
name: 'RegistryListPage',
@@ -58,8 +56,9 @@ export default {
GlLink,
GlAlert,
GlSkeletonLoader,
- GlSearchBoxByClick,
RegistryHeader,
+ DeleteImage,
+ RegistrySearch,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -76,11 +75,10 @@ export default {
CONNECTION_ERROR_MESSAGE,
REMOVE_REPOSITORY_MODAL_TEXT,
REMOVE_REPOSITORY_LABEL,
- SEARCH_PLACEHOLDER_TEXT,
- IMAGE_REPOSITORY_LIST_LABEL,
EMPTY_RESULT_TITLE,
EMPTY_RESULT_MESSAGE,
},
+ searchConfig: SORT_FIELDS,
apollo: {
baseImages: {
query: getContainerRepositoriesQuery,
@@ -122,7 +120,8 @@ export default {
containerRepositoriesCount: 0,
itemToDelete: {},
deleteAlertType: null,
- searchValue: null,
+ filter: [],
+ sorting: { orderBy: 'UPDATED', sort: 'desc' },
name: null,
mutationLoading: false,
fetchAdditionalDetails: false,
@@ -141,6 +140,7 @@ export default {
queryVariables() {
return {
name: this.name,
+ sort: this.sortBy,
fullPath: this.config.isGroupPage ? this.config.groupPath : this.config.projectPath,
isGroupPage: this.config.isGroupPage,
first: GRAPHQL_PAGE_SIZE,
@@ -165,6 +165,10 @@ export default {
? DELETE_IMAGE_SUCCESS_MESSAGE
: DELETE_IMAGE_ERROR_MESSAGE;
},
+ sortBy() {
+ const { orderBy, sort } = this.sorting;
+ return `${orderBy}_${sort}`.toUpperCase();
+ },
},
mounted() {
// If the two graphql calls - which are not batched - resolve togheter we will have a race
@@ -179,30 +183,6 @@ export default {
this.itemToDelete = item;
this.$refs.deleteModal.show();
},
- handleDeleteImage() {
- this.track('confirm_delete');
- 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 = {};
@@ -250,6 +230,20 @@ export default {
});
}
},
+ startDelete() {
+ this.track('confirm_delete');
+ this.mutationLoading = true;
+ },
+ updateSorting(value) {
+ this.sorting = {
+ ...this.sorting,
+ ...value,
+ };
+ },
+ doFilter() {
+ const search = this.filter.find((i) => i.type === 'filtered-search-term');
+ this.name = search?.value?.data;
+ },
},
};
</script>
@@ -302,6 +296,16 @@ export default {
</template>
</registry-header>
+ <registry-search
+ :filter="filter"
+ :sorting="sorting"
+ :tokens="[]"
+ :sortable-fields="$options.searchConfig"
+ @sorting:changed="updateSorting"
+ @filter:changed="filter = $event"
+ @filter:submit="doFilter"
+ />
+
<div v-if="isLoading" class="gl-mt-5">
<gl-skeleton-loader
v-for="index in $options.loader.repeat"
@@ -317,20 +321,6 @@ export default {
</div>
<template v-else>
<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="searchValue"
- :placeholder="$options.i18n.SEARCH_PLACEHOLDER_TEXT"
- @clear="name = null"
- @submit="name = $event"
- />
- </div>
- </div>
-
<image-list
v-if="images.length"
:images="images"
@@ -358,23 +348,32 @@ export default {
</template>
</template>
- <gl-modal
- ref="deleteModal"
- modal-id="delete-image-modal"
- ok-variant="danger"
- @ok="handleDeleteImage"
- @cancel="track('cancel_delete')"
+ <delete-image
+ :id="itemToDelete.id"
+ @start="startDelete"
+ @error="deleteAlertType = 'danger'"
+ @success="deleteAlertType = 'success'"
+ @end="mutationLoading = false"
>
- <template #modal-title>{{ $options.i18n.REMOVE_REPOSITORY_LABEL }}</template>
- <p>
- <gl-sprintf :message="$options.i18n.REMOVE_REPOSITORY_MODAL_TEXT">
- <template #title>
- <b>{{ itemToDelete.path }}</b>
- </template>
- </gl-sprintf>
- </p>
- <template #modal-ok>{{ __('Remove') }}</template>
- </gl-modal>
+ <template #default="{ doDelete }">
+ <gl-modal
+ ref="deleteModal"
+ modal-id="delete-image-modal"
+ :action-primary="{ text: __('Remove'), attributes: { variant: 'danger' } }"
+ @primary="doDelete"
+ @cancel="track('cancel_delete')"
+ >
+ <template #modal-title>{{ $options.i18n.REMOVE_REPOSITORY_LABEL }}</template>
+ <p>
+ <gl-sprintf :message="$options.i18n.REMOVE_REPOSITORY_MODAL_TEXT">
+ <template #title>
+ <b>{{ itemToDelete.path }}</b>
+ </template>
+ </gl-sprintf>
+ </p>
+ </gl-modal>
+ </template>
+ </delete-image>
</template>
</div>
</template>
diff --git a/app/assets/javascripts/registry/explorer/router.js b/app/assets/javascripts/registry/explorer/router.js
index d8903cf0931..a0c4417d549 100644
--- a/app/assets/javascripts/registry/explorer/router.js
+++ b/app/assets/javascripts/registry/explorer/router.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
import VueRouter from 'vue-router';
-import List from './pages/list.vue';
-import Details from './pages/details.vue';
import { CONTAINER_REGISTRY_TITLE } from './constants/index';
+import Details from './pages/details.vue';
+import List from './pages/list.vue';
Vue.use(VueRouter);
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 66eb681784e..480590ec71e 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,6 @@
<script>
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
import { isEqual, get, isEmpty } from 'lodash';
-import expirationPolicyQuery from '../graphql/queries/get_expiration_policy.query.graphql';
import {
FETCH_SETTINGS_ERROR_MESSAGE,
UNAVAILABLE_FEATURE_TITLE,
@@ -9,6 +8,7 @@ import {
UNAVAILABLE_USER_FEATURE_TEXT,
UNAVAILABLE_ADMIN_FEATURE_TEXT,
} from '../constants';
+import expirationPolicyQuery from '../graphql/queries/get_expiration_policy.query.graphql';
import SettingsForm from './settings_form.vue';
diff --git a/app/assets/javascripts/registry/settings/components/settings_form.vue b/app/assets/javascripts/registry/settings/components/settings_form.vue
index 7043cea49ba..eb731c382e1 100644
--- a/app/assets/javascripts/registry/settings/components/settings_form.vue
+++ b/app/assets/javascripts/registry/settings/components/settings_form.vue
@@ -1,6 +1,5 @@
<script>
import { GlCard, GlButton, GlSprintf } from '@gitlab/ui';
-import Tracking from '~/tracking';
import {
UPDATE_SETTINGS_ERROR_MESSAGE,
UPDATE_SETTINGS_SUCCESS_MESSAGE,
@@ -19,13 +18,14 @@ import {
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 { formOptionsGenerator } from '~/registry/settings/utils';
+import Tracking from '~/tracking';
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';
+import ExpirationToggle from './expiration_toggle.vue';
export default {
components: {
diff --git a/app/assets/javascripts/registry/settings/registry_settings_bundle.js b/app/assets/javascripts/registry/settings/registry_settings_bundle.js
index 6a4584b1b28..65af6f846aa 100644
--- a/app/assets/javascripts/registry/settings/registry_settings_bundle.js
+++ b/app/assets/javascripts/registry/settings/registry_settings_bundle.js
@@ -1,7 +1,7 @@
-import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
-import Translate from '~/vue_shared/translate';
+import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
+import Translate from '~/vue_shared/translate';
import RegistrySettingsApp from './components/registry_settings_app.vue';
import { apolloProvider } from './graphql/index';