diff options
Diffstat (limited to 'app/assets/javascripts/registry/explorer/components/details_page')
6 files changed, 444 insertions, 0 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> |