summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/registry/explorer
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/registry/explorer')
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/delete_alert.vue70
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/delete_modal.vue67
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/details_header.vue30
-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/tags_loader.vue34
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/tags_table.vue210
-rw-r--r--app/assets/javascripts/registry/explorer/components/image_list.vue124
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue (renamed from app/assets/javascripts/registry/explorer/components/quickstart_dropdown.vue)2
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/group_empty_state.vue (renamed from app/assets/javascripts/registry/explorer/components/group_empty_state.vue)0
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/image_list.vue52
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue136
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/project_empty_state.vue (renamed from app/assets/javascripts/registry/explorer/components/project_empty_state.vue)7
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue138
-rw-r--r--app/assets/javascripts/registry/explorer/components/project_policy_alert.vue68
-rw-r--r--app/assets/javascripts/registry/explorer/constants.js130
-rw-r--r--app/assets/javascripts/registry/explorer/constants/details.js60
-rw-r--r--app/assets/javascripts/registry/explorer/constants/expiration_policies.js11
-rw-r--r--app/assets/javascripts/registry/explorer/constants/index.js4
-rw-r--r--app/assets/javascripts/registry/explorer/constants/list.js48
-rw-r--r--app/assets/javascripts/registry/explorer/constants/quick_start.js9
-rw-r--r--app/assets/javascripts/registry/explorer/pages/details.vue401
-rw-r--r--app/assets/javascripts/registry/explorer/pages/list.vue52
-rw-r--r--app/assets/javascripts/registry/explorer/router.js4
-rw-r--r--app/assets/javascripts/registry/explorer/stores/actions.js2
-rw-r--r--app/assets/javascripts/registry/explorer/stores/getters.js6
-rw-r--r--app/assets/javascripts/registry/explorer/stores/index.js4
-rw-r--r--app/assets/javascripts/registry/explorer/stores/mutations.js8
27 files changed, 992 insertions, 718 deletions
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/delete_alert.vue b/app/assets/javascripts/registry/explorer/components/details_page/delete_alert.vue
new file mode 100644
index 00000000000..8bdf043a106
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/components/details_page/delete_alert.vue
@@ -0,0 +1,70 @@
+<script>
+import { GlSprintf, GlAlert, GlLink } from '@gitlab/ui';
+
+import { ALERT_MESSAGES, ADMIN_GARBAGE_COLLECTION_TIP } from '../../constants/index';
+
+export default {
+ components: {
+ GlSprintf,
+ GlAlert,
+ GlLink,
+ },
+ model: {
+ prop: 'deleteAlertType',
+ event: 'change',
+ },
+ props: {
+ deleteAlertType: {
+ type: String,
+ default: null,
+ required: false,
+ validator(value) {
+ return !value || ALERT_MESSAGES[value] !== undefined;
+ },
+ },
+ garbageCollectionHelpPagePath: { type: String, required: false, default: '' },
+ isAdmin: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ },
+ computed: {
+ deleteAlertConfig() {
+ const config = {
+ title: '',
+ message: '',
+ type: 'success',
+ };
+ if (this.deleteAlertType) {
+ [config.type] = this.deleteAlertType.split('_');
+
+ config.message = ALERT_MESSAGES[this.deleteAlertType];
+
+ if (this.isAdmin && config.type === 'success') {
+ config.title = config.message;
+ config.message = ADMIN_GARBAGE_COLLECTION_TIP;
+ }
+ }
+ return config;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-alert
+ v-if="deleteAlertType"
+ :variant="deleteAlertConfig.type"
+ :title="deleteAlertConfig.title"
+ @dismiss="$emit('change', null)"
+ >
+ <gl-sprintf :message="deleteAlertConfig.message">
+ <template #docLink="{content}">
+ <gl-link :href="garbageCollectionHelpPagePath" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </gl-alert>
+</template>
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
new file mode 100644
index 00000000000..96f221bf71d
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/components/details_page/delete_modal.vue
@@ -0,0 +1,67 @@
+<script>
+import { GlModal, GlSprintf } from '@gitlab/ui';
+import { n__ } from '~/locale';
+import { REMOVE_TAG_CONFIRMATION_TEXT, REMOVE_TAGS_CONFIRMATION_TEXT } from '../../constants/index';
+
+export default {
+ components: {
+ GlModal,
+ GlSprintf,
+ },
+ props: {
+ itemsToBeDeleted: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ computed: {
+ modalAction() {
+ return n__(
+ 'ContainerRegistry|Remove tag',
+ 'ContainerRegistry|Remove tags',
+ this.itemsToBeDeleted.length,
+ );
+ },
+ modalDescription() {
+ if (this.itemsToBeDeleted.length > 1) {
+ return {
+ message: REMOVE_TAGS_CONFIRMATION_TEXT,
+ item: this.itemsToBeDeleted.length,
+ };
+ }
+ const [first] = this.itemsToBeDeleted;
+
+ return {
+ message: REMOVE_TAG_CONFIRMATION_TEXT,
+ item: first?.path,
+ };
+ },
+ },
+ methods: {
+ show() {
+ this.$refs.deleteModal.show();
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-modal
+ ref="deleteModal"
+ modal-id="delete-tag-modal"
+ ok-variant="danger"
+ @ok="$emit('confirmDelete')"
+ @cancel="$emit('cancelDelete')"
+ >
+ <template #modal-title>{{ modalAction }}</template>
+ <template #modal-ok>{{ modalAction }}</template>
+ <p v-if="modalDescription" data-testid="description">
+ <gl-sprintf :message="modalDescription.message">
+ <template #item
+ ><b>{{ modalDescription.item }}</b></template
+ >
+ </gl-sprintf>
+ </p>
+ </gl-modal>
+</template>
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
new file mode 100644
index 00000000000..c254dd05aa4
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue
@@ -0,0 +1,30 @@
+<script>
+import { GlSprintf } from '@gitlab/ui';
+import { DETAILS_PAGE_TITLE } from '../../constants/index';
+
+export default {
+ components: { GlSprintf },
+ props: {
+ imageName: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ i18n: {
+ DETAILS_PAGE_TITLE,
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-my-2 gl-align-items-center">
+ <h4>
+ <gl-sprintf :message="$options.i18n.DETAILS_PAGE_TITLE">
+ <template #imageName>
+ {{ imageName }}
+ </template>
+ </gl-sprintf>
+ </h4>
+ </div>
+</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
new file mode 100644
index 00000000000..0c684d124d5
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/components/details_page/empty_tags_state.vue
@@ -0,0 +1,33 @@
+<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/tags_loader.vue b/app/assets/javascripts/registry/explorer/components/details_page/tags_loader.vue
new file mode 100644
index 00000000000..b7afa5fba33
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/components/details_page/tags_loader.vue
@@ -0,0 +1,34 @@
+<script>
+import { GlSkeletonLoader } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlSkeletonLoader,
+ },
+ loader: {
+ repeat: 10,
+ width: 1000,
+ height: 40,
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-skeleton-loader
+ v-for="index in $options.loader.repeat"
+ :key="index"
+ :width="$options.loader.width"
+ :height="$options.loader.height"
+ preserve-aspect-ratio="xMinYMax meet"
+ >
+ <rect width="15" x="0" y="12.5" height="15" rx="4" />
+ <rect width="250" x="25" y="10" height="20" rx="4" />
+ <circle cx="290" cy="20" r="10" />
+ <rect width="100" x="315" y="10" height="20" rx="4" />
+ <rect width="100" x="500" y="10" height="20" rx="4" />
+ <rect width="100" x="630" y="10" height="20" rx="4" />
+ <rect x="960" y="0" width="40" height="40" rx="4" />
+ </gl-skeleton-loader>
+ </div>
+</template>
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/tags_table.vue b/app/assets/javascripts/registry/explorer/components/details_page/tags_table.vue
new file mode 100644
index 00000000000..81be778e1e5
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/components/details_page/tags_table.vue
@@ -0,0 +1,210 @@
+<script>
+import { GlTable, GlFormCheckbox, GlButton, GlTooltipDirective } from '@gitlab/ui';
+import { n__ } from '~/locale';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import { numberToHumanSize } from '~/lib/utils/number_utils';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
+import {
+ LIST_KEY_TAG,
+ LIST_KEY_IMAGE_ID,
+ LIST_KEY_SIZE,
+ LIST_KEY_LAST_UPDATED,
+ LIST_KEY_ACTIONS,
+ LIST_KEY_CHECKBOX,
+ LIST_LABEL_TAG,
+ LIST_LABEL_IMAGE_ID,
+ LIST_LABEL_SIZE,
+ LIST_LABEL_LAST_UPDATED,
+ REMOVE_TAGS_BUTTON_TITLE,
+ REMOVE_TAG_BUTTON_TITLE,
+} from '../../constants/index';
+
+export default {
+ components: {
+ GlTable,
+ GlFormCheckbox,
+ GlButton,
+ ClipboardButton,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ mixins: [timeagoMixin],
+ props: {
+ tags: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ isLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isDesktop: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ i18n: {
+ REMOVE_TAGS_BUTTON_TITLE,
+ REMOVE_TAG_BUTTON_TITLE,
+ },
+ data() {
+ return {
+ selectedItems: [],
+ };
+ },
+ computed: {
+ fields() {
+ const tagClass = this.isDesktop ? 'w-25' : '';
+ const tagInnerClass = this.isDesktop ? 'mw-m' : 'gl-justify-content-end';
+ return [
+ { key: LIST_KEY_CHECKBOX, label: '', class: 'gl-w-16' },
+ {
+ key: LIST_KEY_TAG,
+ label: LIST_LABEL_TAG,
+ class: `${tagClass} js-tag-column`,
+ innerClass: tagInnerClass,
+ },
+ { key: LIST_KEY_IMAGE_ID, label: LIST_LABEL_IMAGE_ID },
+ { key: LIST_KEY_SIZE, label: LIST_LABEL_SIZE },
+ { key: LIST_KEY_LAST_UPDATED, label: LIST_LABEL_LAST_UPDATED },
+ { key: LIST_KEY_ACTIONS, label: '' },
+ ].filter(f => f.key !== LIST_KEY_CHECKBOX || this.isDesktop);
+ },
+ tagsNames() {
+ return this.tags.map(t => t.name);
+ },
+ selectAllChecked() {
+ return this.selectedItems.length === this.tags.length && this.tags.length > 0;
+ },
+ },
+ watch: {
+ tagsNames: {
+ immediate: false,
+ handler(tagsNames) {
+ this.selectedItems = this.selectedItems.filter(t => tagsNames.includes(t));
+ },
+ },
+ },
+ methods: {
+ formatSize(size) {
+ return numberToHumanSize(size);
+ },
+ layers(layers) {
+ return layers ? n__('%d layer', '%d layers', layers) : '';
+ },
+ onSelectAllChange() {
+ if (this.selectAllChecked) {
+ this.selectedItems = [];
+ } else {
+ this.selectedItems = this.tags.map(x => x.name);
+ }
+ },
+ updateSelectedItems(name) {
+ const delIndex = this.selectedItems.findIndex(x => x === name);
+
+ if (delIndex > -1) {
+ this.selectedItems.splice(delIndex, 1);
+ } else {
+ this.selectedItems.push(name);
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-table :items="tags" :fields="fields" :stacked="!isDesktop" show-empty :busy="isLoading">
+ <template v-if="isDesktop" #head(checkbox)>
+ <gl-form-checkbox
+ data-testid="mainCheckbox"
+ :checked="selectAllChecked"
+ @change="onSelectAllChange"
+ />
+ </template>
+ <template #head(actions)>
+ <span class="gl-display-flex gl-justify-content-end">
+ <gl-button
+ v-gl-tooltip
+ data-testid="bulkDeleteButton"
+ :disabled="!selectedItems || selectedItems.length === 0"
+ icon="remove"
+ variant="danger"
+ :title="$options.i18n.REMOVE_TAGS_BUTTON_TITLE"
+ :aria-label="$options.i18n.REMOVE_TAGS_BUTTON_TITLE"
+ @click="$emit('delete', selectedItems)"
+ />
+ </span>
+ </template>
+
+ <template #cell(checkbox)="{item}">
+ <gl-form-checkbox
+ data-testid="rowCheckbox"
+ :checked="selectedItems.includes(item.name)"
+ @change="updateSelectedItems(item.name)"
+ />
+ </template>
+ <template #cell(name)="{item, field}">
+ <div data-testid="rowName" :class="[field.innerClass, 'gl-display-flex']">
+ <span
+ v-gl-tooltip
+ data-testid="rowNameText"
+ :title="item.name"
+ class="gl-text-overflow-ellipsis gl-overflow-hidden gl-white-space-nowrap"
+ >
+ {{ item.name }}
+ </span>
+ <clipboard-button
+ v-if="item.location"
+ data-testid="rowClipboardButton"
+ :title="item.location"
+ :text="item.location"
+ css-class="btn-default btn-transparent btn-clipboard"
+ />
+ </div>
+ </template>
+ <template #cell(short_revision)="{value}">
+ <span data-testid="rowShortRevision">
+ {{ value }}
+ </span>
+ </template>
+ <template #cell(total_size)="{item}">
+ <span data-testid="rowSize">
+ {{ formatSize(item.total_size) }}
+ <template v-if="item.total_size && item.layers">
+ &middot;
+ </template>
+ {{ layers(item.layers) }}
+ </span>
+ </template>
+ <template #cell(created_at)="{value}">
+ <span v-gl-tooltip data-testid="rowTime" :title="tooltipTitle(value)">
+ {{ timeFormatted(value) }}
+ </span>
+ </template>
+ <template #cell(actions)="{item}">
+ <span class="gl-display-flex gl-justify-content-end">
+ <gl-button
+ data-testid="singleDeleteButton"
+ :title="$options.i18n.REMOVE_TAG_BUTTON_TITLE"
+ :aria-label="$options.i18n.REMOVE_TAG_BUTTON_TITLE"
+ :disabled="!item.destroy_path"
+ variant="danger"
+ icon="remove"
+ category="secondary"
+ @click="$emit('delete', [item.name])"
+ />
+ </span>
+ </template>
+
+ <template #empty>
+ <slot name="empty"></slot>
+ </template>
+ <template #table-busy>
+ <slot name="loader"></slot>
+ </template>
+ </gl-table>
+</template>
diff --git a/app/assets/javascripts/registry/explorer/components/image_list.vue b/app/assets/javascripts/registry/explorer/components/image_list.vue
deleted file mode 100644
index bc209b12738..00000000000
--- a/app/assets/javascripts/registry/explorer/components/image_list.vue
+++ /dev/null
@@ -1,124 +0,0 @@
-<script>
-import { GlPagination, GlTooltipDirective, GlDeprecatedButton, GlIcon } from '@gitlab/ui';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-
-import {
- ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
- LIST_DELETE_BUTTON_DISABLED,
- REMOVE_REPOSITORY_LABEL,
- ROW_SCHEDULED_FOR_DELETION,
-} from '../constants';
-
-export default {
- name: 'ImageList',
- components: {
- GlPagination,
- ClipboardButton,
- GlDeprecatedButton,
- GlIcon,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- props: {
- images: {
- type: Array,
- required: true,
- },
- pagination: {
- type: Object,
- required: true,
- },
- },
- i18n: {
- LIST_DELETE_BUTTON_DISABLED,
- REMOVE_REPOSITORY_LABEL,
- ROW_SCHEDULED_FOR_DELETION,
- ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
- },
- computed: {
- currentPage: {
- get() {
- return this.pagination.page;
- },
- set(page) {
- this.$emit('pageChange', page);
- },
- },
- },
- methods: {
- encodeListItem(item) {
- const params = JSON.stringify({ name: item.path, tags_path: item.tags_path, id: item.id });
- return window.btoa(params);
- },
- },
-};
-</script>
-
-<template>
- <div class="gl-display-flex gl-flex-direction-column">
- <div
- v-for="(listItem, index) in images"
- :key="index"
- v-gl-tooltip="{
- placement: 'left',
- disabled: !listItem.deleting,
- title: $options.i18n.ROW_SCHEDULED_FOR_DELETION,
- }"
- data-testid="rowItem"
- >
- <div
- class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-py-2 gl-px-1 border-bottom"
- :class="{ 'border-top': index === 0, 'disabled-content': listItem.deleting }"
- >
- <div class="gl-display-flex gl-align-items-center">
- <router-link
- data-testid="detailsLink"
- :to="{ name: 'details', params: { id: encodeListItem(listItem) } }"
- >
- {{ listItem.path }}
- </router-link>
- <clipboard-button
- v-if="listItem.location"
- :disabled="listItem.deleting"
- :text="listItem.location"
- :title="listItem.location"
- css-class="btn-default btn-transparent btn-clipboard"
- />
- <gl-icon
- v-if="listItem.failedDelete"
- v-gl-tooltip
- :title="$options.i18n.ASYNC_DELETE_IMAGE_ERROR_MESSAGE"
- name="warning"
- class="text-warning align-middle"
- />
- </div>
- <div
- v-gl-tooltip="{ disabled: listItem.destroy_path }"
- class="d-none d-sm-block"
- :title="$options.i18n.LIST_DELETE_BUTTON_DISABLED"
- >
- <gl-deprecated-button
- v-gl-tooltip
- data-testid="deleteImageButton"
- :disabled="!listItem.destroy_path || listItem.deleting"
- :title="$options.i18n.REMOVE_REPOSITORY_LABEL"
- :aria-label="$options.i18n.REMOVE_REPOSITORY_LABEL"
- class="btn-inverted"
- variant="danger"
- @click="$emit('delete', listItem)"
- >
- <gl-icon name="remove" />
- </gl-deprecated-button>
- </div>
- </div>
- </div>
- <gl-pagination
- v-model="currentPage"
- :per-page="pagination.perPage"
- :total-items="pagination.total"
- align="center"
- class="w-100 gl-mt-2"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/registry/explorer/components/quickstart_dropdown.vue b/app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue
index 96455496239..8b06797c0ae 100644
--- a/app/assets/javascripts/registry/explorer/components/quickstart_dropdown.vue
+++ b/app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue
@@ -11,7 +11,7 @@ import {
COPY_BUILD_TITLE,
PUSH_COMMAND_LABEL,
COPY_PUSH_TITLE,
-} from '../constants';
+} from '../../constants/index';
export default {
components: {
diff --git a/app/assets/javascripts/registry/explorer/components/group_empty_state.vue b/app/assets/javascripts/registry/explorer/components/list_page/group_empty_state.vue
index a29a9bd23c3..a29a9bd23c3 100644
--- a/app/assets/javascripts/registry/explorer/components/group_empty_state.vue
+++ b/app/assets/javascripts/registry/explorer/components/list_page/group_empty_state.vue
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
new file mode 100644
index 00000000000..9d48769cbad
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/components/list_page/image_list.vue
@@ -0,0 +1,52 @@
+<script>
+import { GlPagination } from '@gitlab/ui';
+import ImageListRow from './image_list_row.vue';
+
+export default {
+ name: 'ImageList',
+ components: {
+ GlPagination,
+ ImageListRow,
+ },
+ props: {
+ images: {
+ type: Array,
+ required: true,
+ },
+ pagination: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ currentPage: {
+ get() {
+ return this.pagination.page;
+ },
+ set(page) {
+ this.$emit('pageChange', page);
+ },
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-flex-direction-column">
+ <image-list-row
+ v-for="(listItem, index) in images"
+ :key="index"
+ :item="listItem"
+ :show-top-border="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>
+</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
new file mode 100644
index 00000000000..cd878c38081
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue
@@ -0,0 +1,136 @@
+<script>
+import { GlTooltipDirective, GlButton, GlIcon, GlSprintf } from '@gitlab/ui';
+import { n__ } from '~/locale';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+
+import {
+ ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
+ LIST_DELETE_BUTTON_DISABLED,
+ REMOVE_REPOSITORY_LABEL,
+ ROW_SCHEDULED_FOR_DELETION,
+} from '../../constants/index';
+
+export default {
+ name: 'ImageListrow',
+ components: {
+ ClipboardButton,
+ GlButton,
+ GlSprintf,
+ GlIcon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ item: {
+ type: Object,
+ required: true,
+ },
+ showTopBorder: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ },
+ i18n: {
+ LIST_DELETE_BUTTON_DISABLED,
+ REMOVE_REPOSITORY_LABEL,
+ ROW_SCHEDULED_FOR_DELETION,
+ ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
+ },
+ computed: {
+ encodedItem() {
+ const params = JSON.stringify({
+ name: this.item.path,
+ tags_path: this.item.tags_path,
+ id: this.item.id,
+ });
+ return window.btoa(params);
+ },
+ disabledDelete() {
+ return !this.item.destroy_path || this.item.deleting;
+ },
+ tagsCountText() {
+ return n__(
+ 'ContainerRegistry|%{count} Tag',
+ 'ContainerRegistry|%{count} Tags',
+ this.item.tags_count,
+ );
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ v-gl-tooltip="{
+ placement: 'left',
+ disabled: !item.deleting,
+ title: $options.i18n.ROW_SCHEDULED_FOR_DELETION,
+ }"
+ >
+ <div
+ class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-py-2 gl-px-1 gl-border-gray-200 gl-border-b-solid gl-border-b-1 gl-py-4 "
+ :class="{
+ 'gl-border-t-solid gl-border-t-1': showTopBorder,
+ 'disabled-content': item.deleting,
+ }"
+ >
+ <div class="gl-display-flex gl-flex-direction-column">
+ <div class="gl-display-flex gl-align-items-center">
+ <router-link
+ class="gl-text-black-normal gl-font-weight-bold"
+ data-testid="detailsLink"
+ :to="{ name: 'details', params: { id: encodedItem } }"
+ >
+ {{ item.path }}
+ </router-link>
+ <clipboard-button
+ v-if="item.location"
+ :disabled="item.deleting"
+ :text="item.location"
+ :title="item.location"
+ css-class="btn-default btn-transparent btn-clipboard gl-text-gray-500"
+ />
+ <gl-icon
+ v-if="item.failedDelete"
+ v-gl-tooltip
+ :title="$options.i18n.ASYNC_DELETE_IMAGE_ERROR_MESSAGE"
+ name="warning"
+ class="text-warning"
+ />
+ </div>
+ <div class="gl-font-sm gl-text-gray-500">
+ <span class="gl-display-flex gl-align-items-center" data-testid="tagsCount">
+ <gl-icon name="tag" class="gl-mr-2" />
+ <gl-sprintf :message="tagsCountText">
+ <template #count>
+ {{ item.tags_count }}
+ </template>
+ </gl-sprintf>
+ </span>
+ </div>
+ </div>
+ <div
+ v-gl-tooltip="{
+ disabled: item.destroy_path,
+ title: $options.i18n.LIST_DELETE_BUTTON_DISABLED,
+ }"
+ class="d-none d-sm-block"
+ data-testid="deleteButtonWrapper"
+ >
+ <gl-button
+ v-gl-tooltip
+ data-testid="deleteImageButton"
+ :disabled="disabledDelete"
+ :title="$options.i18n.REMOVE_REPOSITORY_LABEL"
+ :aria-label="$options.i18n.REMOVE_REPOSITORY_LABEL"
+ category="secondary"
+ variant="danger"
+ icon="remove"
+ @click="$emit('delete', item)"
+ />
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/registry/explorer/components/project_empty_state.vue b/app/assets/javascripts/registry/explorer/components/list_page/project_empty_state.vue
index 0ce38c4a9ec..c27d53f4351 100644
--- a/app/assets/javascripts/registry/explorer/components/project_empty_state.vue
+++ b/app/assets/javascripts/registry/explorer/components/list_page/project_empty_state.vue
@@ -3,7 +3,12 @@ import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
import { mapState, mapGetters } from 'vuex';
import { s__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import { COPY_LOGIN_TITLE, COPY_BUILD_TITLE, COPY_PUSH_TITLE, QUICK_START } from '../constants';
+import {
+ COPY_LOGIN_TITLE,
+ COPY_BUILD_TITLE,
+ COPY_PUSH_TITLE,
+ QUICK_START,
+} from '../../constants/index';
export default {
name: 'ProjectEmptyState',
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
new file mode 100644
index 00000000000..d4ff84447bb
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue
@@ -0,0 +1,138 @@
+<script>
+import { GlSprintf, GlLink, GlIcon } from '@gitlab/ui';
+import { n__ } from '~/locale';
+import { approximateDuration, calculateRemainingMilliseconds } from '~/lib/utils/datetime_utility';
+
+import {
+ CONTAINER_REGISTRY_TITLE,
+ LIST_INTRO_TEXT,
+ EXPIRATION_POLICY_WILL_RUN_IN,
+ EXPIRATION_POLICY_DISABLED_TEXT,
+ EXPIRATION_POLICY_DISABLED_MESSAGE,
+} from '../../constants/index';
+
+export default {
+ components: {
+ GlIcon,
+ GlSprintf,
+ GlLink,
+ },
+ props: {
+ expirationPolicy: {
+ type: Object,
+ default: () => ({}),
+ required: false,
+ },
+ imagesCount: {
+ type: Number,
+ default: 0,
+ required: false,
+ },
+ helpPagePath: {
+ type: String,
+ default: '',
+ required: false,
+ },
+ expirationPolicyHelpPagePath: {
+ type: String,
+ default: '',
+ required: false,
+ },
+ hideExpirationPolicyData: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ loader: {
+ repeat: 10,
+ width: 1000,
+ height: 40,
+ },
+ i18n: {
+ CONTAINER_REGISTRY_TITLE,
+ LIST_INTRO_TEXT,
+ EXPIRATION_POLICY_DISABLED_MESSAGE,
+ },
+ computed: {
+ imagesCountText() {
+ return n__(
+ 'ContainerRegistry|%{count} Image repository',
+ 'ContainerRegistry|%{count} Image repositories',
+ this.imagesCount,
+ );
+ },
+ timeTillRun() {
+ const difference = calculateRemainingMilliseconds(this.expirationPolicy?.next_run_at);
+ return approximateDuration(difference / 1000);
+ },
+ expirationPolicyEnabled() {
+ return this.expirationPolicy?.enabled;
+ },
+ expirationPolicyText() {
+ return this.expirationPolicyEnabled
+ ? EXPIRATION_POLICY_WILL_RUN_IN
+ : EXPIRATION_POLICY_DISABLED_TEXT;
+ },
+ showExpirationPolicyTip() {
+ return (
+ !this.expirationPolicyEnabled && this.imagesCount > 0 && !this.hideExpirationPolicyData
+ );
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div
+ class="gl-display-flex gl-justify-content-space-between gl-align-items-center"
+ data-testid="header"
+ >
+ <h4 data-testid="title">{{ $options.i18n.CONTAINER_REGISTRY_TITLE }}</h4>
+ <div class="gl-display-none d-sm-block" data-testid="commands-slot">
+ <slot name="commands"></slot>
+ </div>
+ </div>
+ <div
+ v-if="imagesCount"
+ class="gl-display-flex gl-align-items-center gl-mt-1 gl-mb-3 gl-text-gray-700"
+ data-testid="subheader"
+ >
+ <span class="gl-mr-3" data-testid="images-count">
+ <gl-icon class="gl-mr-1" name="container-image" />
+ <gl-sprintf :message="imagesCountText">
+ <template #count>
+ {{ imagesCount }}
+ </template>
+ </gl-sprintf>
+ </span>
+ <span v-if="!hideExpirationPolicyData" data-testid="expiration-policy">
+ <gl-icon class="gl-mr-1" name="expire" />
+ <gl-sprintf :message="expirationPolicyText">
+ <template #time>
+ {{ timeTillRun }}
+ </template>
+ </gl-sprintf>
+ </span>
+ </div>
+ <div data-testid="info-area">
+ <p>
+ <span data-testid="default-intro">
+ <gl-sprintf :message="$options.i18n.LIST_INTRO_TEXT">
+ <template #docLink="{content}">
+ <gl-link :href="helpPagePath" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </span>
+ <span v-if="showExpirationPolicyTip" data-testid="expiration-disabled-message">
+ <gl-sprintf :message="$options.i18n.EXPIRATION_POLICY_DISABLED_MESSAGE">
+ <template #docLink="{content}">
+ <gl-link :href="expirationPolicyHelpPagePath" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </span>
+ </p>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/registry/explorer/components/project_policy_alert.vue b/app/assets/javascripts/registry/explorer/components/project_policy_alert.vue
deleted file mode 100644
index 88a0710574f..00000000000
--- a/app/assets/javascripts/registry/explorer/components/project_policy_alert.vue
+++ /dev/null
@@ -1,68 +0,0 @@
-<script>
-import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
-import { mapState } from 'vuex';
-import { approximateDuration, calculateRemainingMilliseconds } from '~/lib/utils/datetime_utility';
-import {
- EXPIRATION_POLICY_ALERT_TITLE,
- EXPIRATION_POLICY_ALERT_PRIMARY_BUTTON,
- EXPIRATION_POLICY_ALERT_FULL_MESSAGE,
- EXPIRATION_POLICY_ALERT_SHORT_MESSAGE,
-} from '../constants';
-
-export default {
- components: {
- GlAlert,
- GlSprintf,
- GlLink,
- },
-
- computed: {
- ...mapState(['config', 'images', 'isLoading']),
- isEmpty() {
- return !this.images || this.images.length === 0;
- },
- showAlert() {
- return this.config.expirationPolicy?.enabled;
- },
- timeTillRun() {
- const difference = calculateRemainingMilliseconds(this.config.expirationPolicy?.next_run_at);
- return approximateDuration(difference / 1000);
- },
- alertConfiguration() {
- if (this.isEmpty || this.isLoading) {
- return {
- title: null,
- primaryButton: null,
- message: EXPIRATION_POLICY_ALERT_SHORT_MESSAGE,
- };
- }
- return {
- title: EXPIRATION_POLICY_ALERT_TITLE,
- primaryButton: EXPIRATION_POLICY_ALERT_PRIMARY_BUTTON,
- message: EXPIRATION_POLICY_ALERT_FULL_MESSAGE,
- };
- },
- },
-};
-</script>
-
-<template>
- <gl-alert
- v-if="showAlert"
- :dismissible="false"
- :primary-button-text="alertConfiguration.primaryButton"
- :primary-button-link="config.settingsPath"
- :title="alertConfiguration.title"
- >
- <gl-sprintf :message="alertConfiguration.message">
- <template #days>
- <strong>{{ timeTillRun }}</strong>
- </template>
- <template #link="{content}">
- <gl-link :href="config.expirationPolicyHelpPagePath" target="_blank">
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </gl-alert>
-</template>
diff --git a/app/assets/javascripts/registry/explorer/constants.js b/app/assets/javascripts/registry/explorer/constants.js
deleted file mode 100644
index 7cbe657bfc0..00000000000
--- a/app/assets/javascripts/registry/explorer/constants.js
+++ /dev/null
@@ -1,130 +0,0 @@
-import { s__ } from '~/locale';
-
-// List page
-
-export const CONTAINER_REGISTRY_TITLE = s__('ContainerRegistry|Container Registry');
-export const CONNECTION_ERROR_TITLE = s__('ContainerRegistry|Docker connection error');
-export const CONNECTION_ERROR_MESSAGE = s__(
- `ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}`,
-);
-export const LIST_INTRO_TEXT = s__(
- `ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}`,
-);
-
-export const LIST_DELETE_BUTTON_DISABLED = s__(
- 'ContainerRegistry|Missing or insufficient permission, delete button disabled',
-);
-export const REMOVE_REPOSITORY_LABEL = s__('ContainerRegistry|Remove repository');
-export const REMOVE_REPOSITORY_MODAL_TEXT = s__(
- 'ContainerRegistry|You are about to remove repository %{title}. Once you confirm, this repository will be permanently deleted.',
-);
-export const ROW_SCHEDULED_FOR_DELETION = s__(
- `ContainerRegistry|This image repository is scheduled for deletion`,
-);
-export const FETCH_IMAGES_LIST_ERROR_MESSAGE = s__(
- 'ContainerRegistry|Something went wrong while fetching the repository list.',
-);
-export const FETCH_TAGS_LIST_ERROR_MESSAGE = s__(
- 'ContainerRegistry|Something went wrong while fetching the tags list.',
-);
-export const DELETE_IMAGE_ERROR_MESSAGE = s__(
- 'ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again.',
-);
-export const ASYNC_DELETE_IMAGE_ERROR_MESSAGE = s__(
- `ContainerRegistry|There was an error during the deletion of this image repository, please try again.`,
-);
-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.',
-);
-
-// Image details page
-
-export const DETAILS_PAGE_TITLE = s__('ContainerRegistry|%{imageName} tags');
-
-export const DELETE_TAG_ERROR_MESSAGE = s__(
- 'ContainerRegistry|Something went wrong while marking the tag for deletion.',
-);
-export const DELETE_TAG_SUCCESS_MESSAGE = s__(
- 'ContainerRegistry|Tag successfully marked for deletion.',
-);
-export const DELETE_TAGS_ERROR_MESSAGE = s__(
- 'ContainerRegistry|Something went wrong while marking the tags for deletion.',
-);
-export const DELETE_TAGS_SUCCESS_MESSAGE = s__(
- 'ContainerRegistry|Tags successfully marked for deletion.',
-);
-
-export const DEFAULT_PAGE = 1;
-export const DEFAULT_PAGE_SIZE = 10;
-
-export const GROUP_PAGE_TYPE = 'groups';
-
-export const LIST_KEY_TAG = 'name';
-export const LIST_KEY_IMAGE_ID = 'short_revision';
-export const LIST_KEY_SIZE = 'total_size';
-export const LIST_KEY_LAST_UPDATED = 'created_at';
-export const LIST_KEY_ACTIONS = 'actions';
-export const LIST_KEY_CHECKBOX = 'checkbox';
-
-export const LIST_LABEL_TAG = s__('ContainerRegistry|Tag');
-export const LIST_LABEL_IMAGE_ID = s__('ContainerRegistry|Image ID');
-export const LIST_LABEL_SIZE = s__('ContainerRegistry|Compressed Size');
-export const LIST_LABEL_LAST_UPDATED = s__('ContainerRegistry|Last Updated');
-
-export const REMOVE_TAG_BUTTON_TITLE = s__('ContainerRegistry|Remove tag');
-export const REMOVE_TAGS_BUTTON_TITLE = s__('ContainerRegistry|Remove selected tags');
-
-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__(
- `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 ADMIN_GARBAGE_COLLECTION_TIP = s__(
- 'ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage.',
-);
-
-// Expiration policies
-
-export const EXPIRATION_POLICY_ALERT_TITLE = s__(
- 'ContainerRegistry|Retention policy has been Enabled',
-);
-export const EXPIRATION_POLICY_ALERT_PRIMARY_BUTTON = s__('ContainerRegistry|Edit Settings');
-export const EXPIRATION_POLICY_ALERT_FULL_MESSAGE = s__(
- 'ContainerRegistry|The retention and expiration policy for this Container Registry has been enabled and will run in %{days}. For more information visit the %{linkStart}documentation%{linkEnd}',
-);
-export const EXPIRATION_POLICY_ALERT_SHORT_MESSAGE = s__(
- 'ContainerRegistry|The retention and expiration policy for this Container Registry has been enabled. For more information visit the %{linkStart}documentation%{linkEnd}',
-);
-
-// Quick Start
-
-export const QUICK_START = s__('ContainerRegistry|Quick Start');
-export const LOGIN_COMMAND_LABEL = s__('ContainerRegistry|Login');
-export const COPY_LOGIN_TITLE = s__('ContainerRegistry|Copy login command');
-export const BUILD_COMMAND_LABEL = s__('ContainerRegistry|Build an image');
-export const COPY_BUILD_TITLE = s__('ContainerRegistry|Copy build command');
-export const PUSH_COMMAND_LABEL = s__('ContainerRegistry|Push an image');
-export const COPY_PUSH_TITLE = s__('ContainerRegistry|Copy push command');
-
-// Image state
-
-export const IMAGE_DELETE_SCHEDULED_STATUS = 'delete_scheduled';
-export const IMAGE_FAILED_DELETED_STATUS = 'delete_failed';
diff --git a/app/assets/javascripts/registry/explorer/constants/details.js b/app/assets/javascripts/registry/explorer/constants/details.js
new file mode 100644
index 00000000000..a1fa995c17f
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/constants/details.js
@@ -0,0 +1,60 @@
+import { s__ } from '~/locale';
+
+// Translations strings
+export const DETAILS_PAGE_TITLE = s__('ContainerRegistry|%{imageName} tags');
+export const DELETE_TAG_ERROR_MESSAGE = s__(
+ 'ContainerRegistry|Something went wrong while marking the tag for deletion.',
+);
+export const DELETE_TAG_SUCCESS_MESSAGE = s__(
+ 'ContainerRegistry|Tag successfully marked for deletion.',
+);
+export const DELETE_TAGS_ERROR_MESSAGE = s__(
+ 'ContainerRegistry|Something went wrong while marking the tags for deletion.',
+);
+export const DELETE_TAGS_SUCCESS_MESSAGE = s__(
+ 'ContainerRegistry|Tags successfully marked for deletion.',
+);
+export const LIST_LABEL_TAG = s__('ContainerRegistry|Tag');
+export const LIST_LABEL_IMAGE_ID = s__('ContainerRegistry|Image ID');
+export const LIST_LABEL_SIZE = s__('ContainerRegistry|Compressed Size');
+export const LIST_LABEL_LAST_UPDATED = s__('ContainerRegistry|Last Updated');
+export const REMOVE_TAG_BUTTON_TITLE = s__('ContainerRegistry|Remove tag');
+export const REMOVE_TAGS_BUTTON_TITLE = s__('ContainerRegistry|Remove selected tags');
+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__(
+ `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 ADMIN_GARBAGE_COLLECTION_TIP = s__(
+ 'ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage.',
+);
+
+// Parameters
+
+export const DEFAULT_PAGE = 1;
+export const DEFAULT_PAGE_SIZE = 10;
+export const GROUP_PAGE_TYPE = 'groups';
+export const LIST_KEY_TAG = 'name';
+export const LIST_KEY_IMAGE_ID = 'short_revision';
+export const LIST_KEY_SIZE = 'total_size';
+export const LIST_KEY_LAST_UPDATED = 'created_at';
+export const LIST_KEY_ACTIONS = 'actions';
+export const LIST_KEY_CHECKBOX = 'checkbox';
+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_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,
+};
diff --git a/app/assets/javascripts/registry/explorer/constants/expiration_policies.js b/app/assets/javascripts/registry/explorer/constants/expiration_policies.js
new file mode 100644
index 00000000000..8af25ca6ecc
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/constants/expiration_policies.js
@@ -0,0 +1,11 @@
+import { s__ } from '~/locale';
+
+export const EXPIRATION_POLICY_WILL_RUN_IN = s__(
+ 'ContainerRegistry|Expiration policy will run in %{time}',
+);
+export const EXPIRATION_POLICY_DISABLED_TEXT = s__(
+ 'ContainerRegistry|Expiration policy is disabled',
+);
+export const EXPIRATION_POLICY_DISABLED_MESSAGE = s__(
+ 'ContainerRegistry|Expiration policies help manage the storage space used by the Container Registry, but the expiration policies for this registry are disabled. Contact your administrator to enable. %{docLinkStart}More information%{docLinkEnd}',
+);
diff --git a/app/assets/javascripts/registry/explorer/constants/index.js b/app/assets/javascripts/registry/explorer/constants/index.js
new file mode 100644
index 00000000000..10816e12ead
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/constants/index.js
@@ -0,0 +1,4 @@
+export * from './expiration_policies';
+export * from './quick_start';
+export * from './list';
+export * from './details';
diff --git a/app/assets/javascripts/registry/explorer/constants/list.js b/app/assets/javascripts/registry/explorer/constants/list.js
new file mode 100644
index 00000000000..39f63d2a153
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/constants/list.js
@@ -0,0 +1,48 @@
+import { s__ } from '~/locale';
+
+// Translations strings
+
+export const CONTAINER_REGISTRY_TITLE = s__('ContainerRegistry|Container Registry');
+export const CONNECTION_ERROR_TITLE = s__('ContainerRegistry|Docker connection error');
+export const CONNECTION_ERROR_MESSAGE = s__(
+ `ContainerRegistry|We are having trouble connecting to the Registry, which could be due to an issue with your project name or path. %{docLinkStart}More information%{docLinkEnd}`,
+);
+export const LIST_INTRO_TEXT = s__(
+ `ContainerRegistry|With the GitLab Container Registry, every project can have its own space to store images. %{docLinkStart}More information%{docLinkEnd}`,
+);
+export const LIST_DELETE_BUTTON_DISABLED = s__(
+ 'ContainerRegistry|Missing or insufficient permission, delete button disabled',
+);
+export const REMOVE_REPOSITORY_LABEL = s__('ContainerRegistry|Remove repository');
+export const REMOVE_REPOSITORY_MODAL_TEXT = s__(
+ 'ContainerRegistry|You are about to remove repository %{title}. Once you confirm, this repository will be permanently deleted.',
+);
+export const ROW_SCHEDULED_FOR_DELETION = s__(
+ `ContainerRegistry|This image repository is scheduled for deletion`,
+);
+export const FETCH_IMAGES_LIST_ERROR_MESSAGE = s__(
+ 'ContainerRegistry|Something went wrong while fetching the repository list.',
+);
+export const FETCH_TAGS_LIST_ERROR_MESSAGE = s__(
+ 'ContainerRegistry|Something went wrong while fetching the tags list.',
+);
+export const DELETE_IMAGE_ERROR_MESSAGE = s__(
+ 'ContainerRegistry|Something went wrong while scheduling %{title} for deletion. Please try again.',
+);
+export const ASYNC_DELETE_IMAGE_ERROR_MESSAGE = s__(
+ `ContainerRegistry|There was an error during the deletion of this image repository, please try again.`,
+);
+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.',
+);
+
+// Parameters
+
+export const IMAGE_DELETE_SCHEDULED_STATUS = 'delete_scheduled';
+export const IMAGE_FAILED_DELETED_STATUS = 'delete_failed';
diff --git a/app/assets/javascripts/registry/explorer/constants/quick_start.js b/app/assets/javascripts/registry/explorer/constants/quick_start.js
new file mode 100644
index 00000000000..6a39c07eba2
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/constants/quick_start.js
@@ -0,0 +1,9 @@
+import { s__ } from '~/locale';
+
+export const QUICK_START = s__('ContainerRegistry|CLI Commands');
+export const LOGIN_COMMAND_LABEL = s__('ContainerRegistry|Login');
+export const COPY_LOGIN_TITLE = s__('ContainerRegistry|Copy login command');
+export const BUILD_COMMAND_LABEL = s__('ContainerRegistry|Build an image');
+export const COPY_BUILD_TITLE = s__('ContainerRegistry|Copy build command');
+export const PUSH_COMMAND_LABEL = s__('ContainerRegistry|Push an image');
+export const COPY_PUSH_TITLE = s__('ContainerRegistry|Copy push command');
diff --git a/app/assets/javascripts/registry/explorer/pages/details.vue b/app/assets/javascripts/registry/explorer/pages/details.vue
index cc2dc531dc8..598e643ce1a 100644
--- a/app/assets/javascripts/registry/explorer/pages/details.vue
+++ b/app/assets/javascripts/registry/explorer/pages/details.vue
@@ -1,139 +1,56 @@
<script>
-import { mapState, mapActions, mapGetters } from 'vuex';
-import {
- GlTable,
- GlFormCheckbox,
- GlDeprecatedButton,
- GlIcon,
- GlTooltipDirective,
- GlPagination,
- GlModal,
- GlSprintf,
- GlAlert,
- GlLink,
- GlEmptyState,
- GlResizeObserverDirective,
- GlSkeletonLoader,
-} from '@gitlab/ui';
+import { mapState, mapActions } from 'vuex';
+import { GlPagination, GlResizeObserverDirective } from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
-import { n__ } from '~/locale';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import { numberToHumanSize } from '~/lib/utils/number_utils';
-import timeagoMixin from '~/vue_shared/mixins/timeago';
import Tracking from '~/tracking';
+import DeleteAlert from '../components/details_page/delete_alert.vue';
+import DeleteModal from '../components/details_page/delete_modal.vue';
+import DetailsHeader from '../components/details_page/details_header.vue';
+import TagsTable from '../components/details_page/tags_table.vue';
+import TagsLoader from '../components/details_page/tags_loader.vue';
+import EmptyTagsState from '../components/details_page/empty_tags_state.vue';
+
import { decodeAndParse } from '../utils';
import {
- LIST_KEY_TAG,
- LIST_KEY_IMAGE_ID,
- LIST_KEY_SIZE,
- LIST_KEY_LAST_UPDATED,
- LIST_KEY_ACTIONS,
- LIST_KEY_CHECKBOX,
- LIST_LABEL_TAG,
- LIST_LABEL_IMAGE_ID,
- LIST_LABEL_SIZE,
- LIST_LABEL_LAST_UPDATED,
- DELETE_TAG_SUCCESS_MESSAGE,
- DELETE_TAG_ERROR_MESSAGE,
- DELETE_TAGS_SUCCESS_MESSAGE,
- DELETE_TAGS_ERROR_MESSAGE,
- REMOVE_TAG_CONFIRMATION_TEXT,
- REMOVE_TAGS_CONFIRMATION_TEXT,
- DETAILS_PAGE_TITLE,
- REMOVE_TAGS_BUTTON_TITLE,
- REMOVE_TAG_BUTTON_TITLE,
- EMPTY_IMAGE_REPOSITORY_TITLE,
- EMPTY_IMAGE_REPOSITORY_MESSAGE,
- ADMIN_GARBAGE_COLLECTION_TIP,
-} from '../constants';
+ ALERT_SUCCESS_TAG,
+ ALERT_DANGER_TAG,
+ ALERT_SUCCESS_TAGS,
+ ALERT_DANGER_TAGS,
+} from '../constants/index';
export default {
components: {
- GlTable,
- GlFormCheckbox,
- GlDeprecatedButton,
- GlIcon,
- ClipboardButton,
+ DeleteAlert,
+ DetailsHeader,
GlPagination,
- GlModal,
- GlSkeletonLoader,
- GlSprintf,
- GlEmptyState,
- GlAlert,
- GlLink,
+ DeleteModal,
+ TagsTable,
+ TagsLoader,
+ EmptyTagsState,
},
directives: {
- GlTooltip: GlTooltipDirective,
GlResizeObserver: GlResizeObserverDirective,
},
- mixins: [timeagoMixin, Tracking.mixin()],
- loader: {
- repeat: 10,
- width: 1000,
- height: 40,
- },
- i18n: {
- DETAILS_PAGE_TITLE,
- REMOVE_TAGS_BUTTON_TITLE,
- REMOVE_TAG_BUTTON_TITLE,
- EMPTY_IMAGE_REPOSITORY_TITLE,
- EMPTY_IMAGE_REPOSITORY_MESSAGE,
- },
- alertMessages: {
- success_tag: DELETE_TAG_SUCCESS_MESSAGE,
- danger_tag: DELETE_TAG_ERROR_MESSAGE,
- success_tags: DELETE_TAGS_SUCCESS_MESSAGE,
- danger_tags: DELETE_TAGS_ERROR_MESSAGE,
- },
+ mixins: [Tracking.mixin()],
data() {
return {
- selectedItems: [],
itemsToBeDeleted: [],
- selectAllChecked: false,
- modalDescription: null,
isDesktop: true,
- deleteAlertType: false,
+ deleteAlertType: null,
};
},
computed: {
- ...mapGetters(['tags']),
- ...mapState(['tagsPagination', 'isLoading', 'config']),
+ ...mapState(['tagsPagination', 'isLoading', 'config', 'tags']),
imageName() {
const { name } = decodeAndParse(this.$route.params.id);
return name;
},
- fields() {
- const tagClass = this.isDesktop ? 'w-25' : '';
- const tagInnerClass = this.isDesktop ? 'mw-m' : 'gl-justify-content-end';
- return [
- { key: LIST_KEY_CHECKBOX, label: '', class: 'gl-w-16' },
- {
- key: LIST_KEY_TAG,
- label: LIST_LABEL_TAG,
- class: `${tagClass} js-tag-column`,
- innerClass: tagInnerClass,
- },
- { key: LIST_KEY_IMAGE_ID, label: LIST_LABEL_IMAGE_ID },
- { key: LIST_KEY_SIZE, label: LIST_LABEL_SIZE },
- { key: LIST_KEY_LAST_UPDATED, label: LIST_LABEL_LAST_UPDATED },
- { key: LIST_KEY_ACTIONS, label: '' },
- ].filter(f => f.key !== LIST_KEY_CHECKBOX || this.isDesktop);
- },
- isMultiDelete() {
- return this.itemsToBeDeleted.length > 1;
- },
tracking() {
return {
- label: this.isMultiDelete ? 'bulk_registry_tag_delete' : 'registry_tag_delete',
+ label:
+ this.itemsToBeDeleted?.length > 1 ? 'bulk_registry_tag_delete' : 'registry_tag_delete',
};
},
- modalAction() {
- return n__(
- 'ContainerRegistry|Remove tag',
- 'ContainerRegistry|Remove tags',
- this.isMultiDelete ? this.itemsToBeDeleted.length : 1,
- );
- },
currentPage: {
get() {
return this.tagsPagination.page;
@@ -142,132 +59,51 @@ export default {
this.requestTagsList({ pagination: { page }, params: this.$route.params.id });
},
},
- deleteAlertConfig() {
- const config = {
- title: '',
- message: '',
- type: 'success',
- };
- if (this.deleteAlertType) {
- [config.type] = this.deleteAlertType.split('_');
-
- const defaultMessage = this.$options.alertMessages[this.deleteAlertType];
-
- if (this.config.isAdmin && config.type === 'success') {
- config.title = defaultMessage;
- config.message = ADMIN_GARBAGE_COLLECTION_TIP;
- } else {
- config.message = defaultMessage;
- }
- }
- return config;
- },
},
mounted() {
this.requestTagsList({ params: this.$route.params.id });
},
methods: {
...mapActions(['requestTagsList', 'requestDeleteTag', 'requestDeleteTags']),
- setModalDescription(itemIndex = -1) {
- if (itemIndex === -1) {
- this.modalDescription = {
- message: REMOVE_TAGS_CONFIRMATION_TEXT,
- item: this.itemsToBeDeleted.length,
- };
- } else {
- const { path } = this.tags[itemIndex];
-
- this.modalDescription = {
- message: REMOVE_TAG_CONFIRMATION_TEXT,
- item: path,
- };
- }
- },
- formatSize(size) {
- return numberToHumanSize(size);
- },
- layers(layers) {
- return layers ? n__('%d layer', '%d layers', layers) : '';
- },
- onSelectAllChange() {
- if (this.selectAllChecked) {
- this.deselectAll();
- } else {
- this.selectAll();
- }
- },
- selectAll() {
- this.selectedItems = this.tags.map((x, index) => index);
- this.selectAllChecked = true;
- },
- deselectAll() {
- this.selectedItems = [];
- this.selectAllChecked = false;
- },
- updateSelectedItems(index) {
- const delIndex = this.selectedItems.findIndex(x => x === index);
-
- if (delIndex > -1) {
- this.selectedItems.splice(delIndex, 1);
- this.selectAllChecked = false;
- } else {
- this.selectedItems.push(index);
-
- if (this.selectedItems.length === this.tags.length) {
- this.selectAllChecked = true;
- }
- }
- },
- deleteSingleItem(index) {
- this.setModalDescription(index);
- this.itemsToBeDeleted = [index];
+ deleteTags(toBeDeletedList) {
+ this.itemsToBeDeleted = toBeDeletedList.map(name => ({
+ ...this.tags.find(t => t.name === name),
+ }));
this.track('click_button');
this.$refs.deleteModal.show();
},
- deleteMultipleItems() {
- this.itemsToBeDeleted = [...this.selectedItems];
- if (this.selectedItems.length === 1) {
- this.setModalDescription(this.itemsToBeDeleted[0]);
- } else if (this.selectedItems.length > 1) {
- this.setModalDescription();
- }
- this.track('click_button');
- this.$refs.deleteModal.show();
- },
- handleSingleDelete(index) {
- const itemToDelete = this.tags[index];
+ handleSingleDelete() {
+ const [itemToDelete] = this.itemsToBeDeleted;
this.itemsToBeDeleted = [];
- this.selectedItems = this.selectedItems.filter(i => i !== index);
return this.requestDeleteTag({ tag: itemToDelete, params: this.$route.params.id })
.then(() => {
- this.deleteAlertType = 'success_tag';
+ this.deleteAlertType = ALERT_SUCCESS_TAG;
})
.catch(() => {
- this.deleteAlertType = 'danger_tag';
+ this.deleteAlertType = ALERT_DANGER_TAG;
});
},
handleMultipleDelete() {
const { itemsToBeDeleted } = this;
this.itemsToBeDeleted = [];
- this.selectedItems = [];
return this.requestDeleteTags({
- ids: itemsToBeDeleted.map(x => this.tags[x].name),
+ ids: itemsToBeDeleted.map(x => x.name),
params: this.$route.params.id,
})
.then(() => {
- this.deleteAlertType = 'success_tags';
+ this.deleteAlertType = ALERT_SUCCESS_TAGS;
})
.catch(() => {
- this.deleteAlertType = 'danger_tags';
+ this.deleteAlertType = ALERT_DANGER_TAGS;
});
},
onDeletionConfirmed() {
this.track('confirm_delete');
- if (this.isMultiDelete) {
+ if (this.itemsToBeDeleted.length > 1) {
this.handleMultipleDelete();
} else {
- this.handleSingleDelete(this.itemsToBeDeleted[0]);
+ this.handleSingleDelete();
}
},
handleResize() {
@@ -279,141 +115,23 @@ export default {
<template>
<div v-gl-resize-observer="handleResize" class="my-3 w-100 slide-enter-to-element">
- <gl-alert
- v-if="deleteAlertType"
- :variant="deleteAlertConfig.type"
- :title="deleteAlertConfig.title"
+ <delete-alert
+ v-model="deleteAlertType"
+ :garbage-collection-help-page-path="config.garbageCollectionHelpPagePath"
+ :is-admin="config.isAdmin"
class="my-2"
- @dismiss="deleteAlertType = null"
- >
- <gl-sprintf :message="deleteAlertConfig.message">
- <template #docLink="{content}">
- <gl-link :href="config.garbageCollectionHelpPagePath" target="_blank">
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </gl-alert>
- <div class="d-flex my-3 align-items-center">
- <h4>
- <gl-sprintf :message="$options.i18n.DETAILS_PAGE_TITLE">
- <template #imageName>
- {{ imageName }}
- </template>
- </gl-sprintf>
- </h4>
- </div>
-
- <gl-table :items="tags" :fields="fields" :stacked="!isDesktop" show-empty>
- <template v-if="isDesktop" #head(checkbox)>
- <gl-form-checkbox
- ref="mainCheckbox"
- :checked="selectAllChecked"
- @change="onSelectAllChange"
- />
- </template>
- <template #head(actions)>
- <gl-deprecated-button
- ref="bulkDeleteButton"
- v-gl-tooltip
- :disabled="!selectedItems || selectedItems.length === 0"
- class="float-right"
- variant="danger"
- :title="$options.i18n.REMOVE_TAGS_BUTTON_TITLE"
- :aria-label="$options.i18n.REMOVE_TAGS_BUTTON_TITLE"
- @click="deleteMultipleItems()"
- >
- <gl-icon name="remove" />
- </gl-deprecated-button>
- </template>
+ />
- <template #cell(checkbox)="{index}">
- <gl-form-checkbox
- ref="rowCheckbox"
- class="js-row-checkbox"
- :checked="selectedItems.includes(index)"
- @change="updateSelectedItems(index)"
- />
- </template>
- <template #cell(name)="{item, field}">
- <div ref="rowName" :class="[field.innerClass, 'gl-display-flex']">
- <span
- v-gl-tooltip
- data-testid="rowNameText"
- :title="item.name"
- class="gl-text-overflow-ellipsis gl-overflow-hidden gl-white-space-nowrap"
- >
- {{ item.name }}
- </span>
- <clipboard-button
- v-if="item.location"
- ref="rowClipboardButton"
- :title="item.location"
- :text="item.location"
- css-class="btn-default btn-transparent btn-clipboard"
- />
- </div>
- </template>
- <template #cell(short_revision)="{value}">
- <span ref="rowShortRevision">
- {{ value }}
- </span>
- </template>
- <template #cell(total_size)="{item}">
- <span ref="rowSize">
- {{ formatSize(item.total_size) }}
- <template v-if="item.total_size && item.layers">
- &middot;
- </template>
- {{ layers(item.layers) }}
- </span>
- </template>
- <template #cell(created_at)="{value}">
- <span ref="rowTime" v-gl-tooltip :title="tooltipTitle(value)">
- {{ timeFormatted(value) }}
- </span>
- </template>
- <template #cell(actions)="{index, item}">
- <gl-deprecated-button
- ref="singleDeleteButton"
- :title="$options.i18n.REMOVE_TAG_BUTTON_TITLE"
- :aria-label="$options.i18n.REMOVE_TAG_BUTTON_TITLE"
- :disabled="!item.destroy_path"
- variant="danger"
- class="js-delete-registry float-right btn-inverted btn-border-color btn-icon"
- @click="deleteSingleItem(index)"
- >
- <gl-icon name="remove" />
- </gl-deprecated-button>
- </template>
+ <details-header :image-name="imageName" />
+ <tags-table :tags="tags" :is-loading="isLoading" :is-desktop="isDesktop" @delete="deleteTags">
<template #empty>
- <template v-if="isLoading">
- <gl-skeleton-loader
- v-for="index in $options.loader.repeat"
- :key="index"
- :width="$options.loader.width"
- :height="$options.loader.height"
- preserve-aspect-ratio="xMinYMax meet"
- >
- <rect width="15" x="0" y="12.5" height="15" rx="4" />
- <rect width="250" x="25" y="10" height="20" rx="4" />
- <circle cx="290" cy="20" r="10" />
- <rect width="100" x="315" y="10" height="20" rx="4" />
- <rect width="100" x="500" y="10" height="20" rx="4" />
- <rect width="100" x="630" y="10" height="20" rx="4" />
- <rect x="960" y="0" width="40" height="40" rx="4" />
- </gl-skeleton-loader>
- </template>
- <gl-empty-state
- v-else
- :title="$options.i18n.EMPTY_IMAGE_REPOSITORY_TITLE"
- :svg-path="config.noContainersImage"
- :description="$options.i18n.EMPTY_IMAGE_REPOSITORY_MESSAGE"
- class="mx-auto my-0"
- />
+ <empty-tags-state :no-containers-image="config.noContainersImage" />
</template>
- </gl-table>
+ <template #loader>
+ <tags-loader v-once />
+ </template>
+ </tags-table>
<gl-pagination
v-if="!isLoading"
@@ -425,22 +143,11 @@ export default {
class="w-100"
/>
- <gl-modal
+ <delete-modal
ref="deleteModal"
- modal-id="delete-tag-modal"
- ok-variant="danger"
- @ok="onDeletionConfirmed"
+ :items-to-be-deleted="itemsToBeDeleted"
+ @confirmDelete="onDeletionConfirmed"
@cancel="track('cancel_delete')"
- >
- <template #modal-title>{{ modalAction }}</template>
- <template #modal-ok>{{ modalAction }}</template>
- <p v-if="modalDescription">
- <gl-sprintf :message="modalDescription.message">
- <template #item>
- <b>{{ modalDescription.item }}</b>
- </template>
- </gl-sprintf>
- </p>
- </gl-modal>
+ />
</div>
</template>
diff --git a/app/assets/javascripts/registry/explorer/pages/list.vue b/app/assets/javascripts/registry/explorer/pages/list.vue
index 4efa6f08d84..e8a26dc58f2 100644
--- a/app/assets/javascripts/registry/explorer/pages/list.vue
+++ b/app/assets/javascripts/registry/explorer/pages/list.vue
@@ -12,26 +12,24 @@ import {
} from '@gitlab/ui';
import Tracking from '~/tracking';
-import ProjectEmptyState from '../components/project_empty_state.vue';
-import GroupEmptyState from '../components/group_empty_state.vue';
-import ProjectPolicyAlert from '../components/project_policy_alert.vue';
-import QuickstartDropdown from '../components/quickstart_dropdown.vue';
-import ImageList from '../components/image_list.vue';
+import ProjectEmptyState from '../components/list_page/project_empty_state.vue';
+import GroupEmptyState from '../components/list_page/group_empty_state.vue';
+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 {
DELETE_IMAGE_SUCCESS_MESSAGE,
DELETE_IMAGE_ERROR_MESSAGE,
- CONTAINER_REGISTRY_TITLE,
CONNECTION_ERROR_TITLE,
CONNECTION_ERROR_MESSAGE,
- LIST_INTRO_TEXT,
REMOVE_REPOSITORY_MODAL_TEXT,
REMOVE_REPOSITORY_LABEL,
SEARCH_PLACEHOLDER_TEXT,
IMAGE_REPOSITORY_LIST_LABEL,
EMPTY_RESULT_TITLE,
EMPTY_RESULT_MESSAGE,
-} from '../constants';
+} from '../constants/index';
export default {
name: 'RegistryListApp',
@@ -39,8 +37,6 @@ export default {
GlEmptyState,
ProjectEmptyState,
GroupEmptyState,
- ProjectPolicyAlert,
- QuickstartDropdown,
ImageList,
GlModal,
GlSprintf,
@@ -48,6 +44,8 @@ export default {
GlAlert,
GlSkeletonLoader,
GlSearchBoxByClick,
+ RegistryHeader,
+ CliCommands,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -59,10 +57,8 @@ export default {
height: 40,
},
i18n: {
- CONTAINER_REGISTRY_TITLE,
CONNECTION_ERROR_TITLE,
CONNECTION_ERROR_MESSAGE,
- LIST_INTRO_TEXT,
REMOVE_REPOSITORY_MODAL_TEXT,
REMOVE_REPOSITORY_LABEL,
SEARCH_PLACEHOLDER_TEXT,
@@ -85,7 +81,7 @@ export default {
label: 'registry_repository_delete',
};
},
- showQuickStartDropdown() {
+ showCommands() {
return Boolean(!this.isLoading && !this.config?.isGroupPage && this.images?.length);
},
showDeleteAlert() {
@@ -149,8 +145,6 @@ export default {
</gl-sprintf>
</gl-alert>
- <project-policy-alert v-if="!config.isGroupPage" class="mt-2" />
-
<gl-empty-state
v-if="config.characterError"
:title="$options.i18n.CONNECTION_ERROR_TITLE"
@@ -170,21 +164,17 @@ export default {
</gl-empty-state>
<template v-else>
- <div>
- <div class="d-flex justify-content-between align-items-center">
- <h4>{{ $options.i18n.CONTAINER_REGISTRY_TITLE }}</h4>
- <quickstart-dropdown v-if="showQuickStartDropdown" class="d-none d-sm-block" />
- </div>
- <p>
- <gl-sprintf :message="$options.i18n.LIST_INTRO_TEXT">
- <template #docLink="{content}">
- <gl-link :href="config.helpPagePath" target="_blank">
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </p>
- </div>
+ <registry-header
+ :images-count="pagination.total"
+ :expiration-policy="config.expirationPolicy"
+ :help-page-path="config.helpPagePath"
+ :expiration-policy-help-page-path="config.expirationPolicyHelpPagePath"
+ :hide-expiration-policy-data="config.isGroupPage"
+ >
+ <template #commands>
+ <cli-commands v-if="showCommands" />
+ </template>
+ </registry-header>
<div v-if="isLoading" class="mt-2">
<gl-skeleton-loader
@@ -201,7 +191,7 @@ export default {
</div>
<template v-else>
<template v-if="!isEmpty">
- <div class="gl-display-flex gl-p-1" data-testid="listHeader">
+ <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>
diff --git a/app/assets/javascripts/registry/explorer/router.js b/app/assets/javascripts/registry/explorer/router.js
index 478eaca1a68..f570987023b 100644
--- a/app/assets/javascripts/registry/explorer/router.js
+++ b/app/assets/javascripts/registry/explorer/router.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
import VueRouter from 'vue-router';
-import { s__ } from '~/locale';
import List from './pages/list.vue';
import Details from './pages/details.vue';
import { decodeAndParse } from './utils';
+import { CONTAINER_REGISTRY_TITLE } from './constants/index';
Vue.use(VueRouter);
@@ -17,7 +17,7 @@ export default function createRouter(base) {
path: '/',
component: List,
meta: {
- nameGenerator: () => s__('ContainerRegistry|Container Registry'),
+ nameGenerator: () => CONTAINER_REGISTRY_TITLE,
root: true,
},
},
diff --git a/app/assets/javascripts/registry/explorer/stores/actions.js b/app/assets/javascripts/registry/explorer/stores/actions.js
index 7f80bc21d6e..3d73ffbd23f 100644
--- a/app/assets/javascripts/registry/explorer/stores/actions.js
+++ b/app/assets/javascripts/registry/explorer/stores/actions.js
@@ -6,7 +6,7 @@ import {
DEFAULT_PAGE,
DEFAULT_PAGE_SIZE,
FETCH_TAGS_LIST_ERROR_MESSAGE,
-} from '../constants';
+} from '../constants/index';
import { decodeAndParse } from '../utils';
export const setInitialState = ({ commit }, data) => commit(types.SET_INITIAL_STATE, data);
diff --git a/app/assets/javascripts/registry/explorer/stores/getters.js b/app/assets/javascripts/registry/explorer/stores/getters.js
index a371d0e6356..7b5d1bd6da3 100644
--- a/app/assets/javascripts/registry/explorer/stores/getters.js
+++ b/app/assets/javascripts/registry/explorer/stores/getters.js
@@ -1,9 +1,3 @@
-export const tags = state => {
- // to show the loader inside the table we need to pass an empty array to gl-table whenever the table is loading
- // this is to take in account isLoading = true and state.tags =[1,2,3] during pagination and delete
- return state.isLoading ? [] : state.tags;
-};
-
export const dockerBuildCommand = state => {
/* eslint-disable @gitlab/require-i18n-strings */
return `docker build -t ${state.config.repositoryUrl} .`;
diff --git a/app/assets/javascripts/registry/explorer/stores/index.js b/app/assets/javascripts/registry/explorer/stores/index.js
index 153032e37d3..54a8e0e1c1c 100644
--- a/app/assets/javascripts/registry/explorer/stores/index.js
+++ b/app/assets/javascripts/registry/explorer/stores/index.js
@@ -7,6 +7,7 @@ import state from './state';
Vue.use(Vuex);
+// eslint-disable-next-line import/prefer-default-export
export const createStore = () =>
new Vuex.Store({
state,
@@ -14,6 +15,3 @@ export const createStore = () =>
actions,
mutations,
});
-
-// Deprecated and to be removed
-export default createStore();
diff --git a/app/assets/javascripts/registry/explorer/stores/mutations.js b/app/assets/javascripts/registry/explorer/stores/mutations.js
index b25a0221dc1..706f6489287 100644
--- a/app/assets/javascripts/registry/explorer/stores/mutations.js
+++ b/app/assets/javascripts/registry/explorer/stores/mutations.js
@@ -1,14 +1,14 @@
import * as types from './mutation_types';
-import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
-import { IMAGE_DELETE_SCHEDULED_STATUS, IMAGE_FAILED_DELETED_STATUS } from '../constants';
+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: config.isGroupPage !== undefined,
- isAdmin: config.isAdmin !== undefined,
+ isGroupPage: parseBoolean(config.isGroupPage),
+ isAdmin: parseBoolean(config.isAdmin),
};
},