diff options
Diffstat (limited to 'app/assets/javascripts/registry/explorer/components')
14 files changed, 777 insertions, 194 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"> + · + </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> |