diff options
Diffstat (limited to 'app/assets/javascripts/registry/explorer/components/details_page')
4 files changed, 323 insertions, 210 deletions
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/details_row.vue b/app/assets/javascripts/registry/explorer/components/details_page/details_row.vue new file mode 100644 index 00000000000..c4358b83e23 --- /dev/null +++ b/app/assets/javascripts/registry/explorer/components/details_page/details_row.vue @@ -0,0 +1,26 @@ +<script> +import { GlIcon } from '@gitlab/ui'; + +export default { + components: { + GlIcon, + }, + props: { + icon: { + type: String, + required: true, + }, + }, +}; +</script> + +<template> + <div + class="gl-display-flex gl-align-items-center gl-font-monospace gl-font-sm gl-py-2 gl-word-break-all" + > + <gl-icon :name="icon" class="gl-mr-4" /> + <span> + <slot></slot> + </span> + </div> +</template> diff --git a/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue b/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue new file mode 100644 index 00000000000..8494967ab57 --- /dev/null +++ b/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue @@ -0,0 +1,77 @@ +<script> +import { GlButton } from '@gitlab/ui'; +import TagsListRow from './tags_list_row.vue'; +import { REMOVE_TAGS_BUTTON_TITLE, TAGS_LIST_TITLE } from '../../constants/index'; + +export default { + components: { + GlButton, + TagsListRow, + }, + props: { + tags: { + type: Array, + required: false, + default: () => [], + }, + isDesktop: { + type: Boolean, + default: false, + required: false, + }, + }, + i18n: { + REMOVE_TAGS_BUTTON_TITLE, + TAGS_LIST_TITLE, + }, + data() { + return { + selectedItems: {}, + }; + }, + computed: { + hasSelectedItems() { + return this.tags.some(tag => this.selectedItems[tag.name]); + }, + showMultiDeleteButton() { + return this.tags.some(tag => tag.destroy_path) && this.isDesktop; + }, + }, + methods: { + updateSelectedItems(name) { + this.$set(this.selectedItems, name, !this.selectedItems[name]); + }, + }, +}; +</script> + +<template> + <div> + <div class="gl-display-flex gl-justify-content-space-between gl-mb-3"> + <h5 data-testid="list-title"> + {{ $options.i18n.TAGS_LIST_TITLE }} + </h5> + + <gl-button + v-if="showMultiDeleteButton" + :disabled="!hasSelectedItems" + category="secondary" + variant="danger" + @click="$emit('delete', selectedItems)" + > + {{ $options.i18n.REMOVE_TAGS_BUTTON_TITLE }} + </gl-button> + </div> + <tags-list-row + v-for="(tag, index) in tags" + :key="tag.path" + :tag="tag" + :first="index === 0" + :last="index === tags.length - 1" + :selected="selectedItems[tag.name]" + :is-desktop="isDesktop" + @select="updateSelectedItems(tag.name)" + @delete="$emit('delete', { [tag.name]: true })" + /> + </div> +</template> diff --git a/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue b/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue new file mode 100644 index 00000000000..51ba2337db6 --- /dev/null +++ b/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue @@ -0,0 +1,220 @@ +<script> +import { GlFormCheckbox, GlTooltipDirective, GlSprintf, GlIcon } from '@gitlab/ui'; +import { n__ } from '~/locale'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import { numberToHumanSize } from '~/lib/utils/number_utils'; +import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import { formatDate } from '~/lib/utils/datetime_utility'; +import DeleteButton from '../delete_button.vue'; +import ListItem from '../list_item.vue'; +import DetailsRow from './details_row.vue'; +import { + REMOVE_TAG_BUTTON_TITLE, + DIGEST_LABEL, + CREATED_AT_LABEL, + REMOVE_TAG_BUTTON_DISABLE_TOOLTIP, + PUBLISHED_DETAILS_ROW_TEXT, + MANIFEST_DETAILS_ROW_TEST, + CONFIGURATION_DETAILS_ROW_TEST, + MISSING_MANIFEST_WARNING_TOOLTIP, + NOT_AVAILABLE_TEXT, + NOT_AVAILABLE_SIZE, +} from '../../constants/index'; + +export default { + components: { + GlSprintf, + GlFormCheckbox, + GlIcon, + DeleteButton, + ListItem, + ClipboardButton, + TimeAgoTooltip, + DetailsRow, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + tag: { + type: Object, + required: true, + }, + isDesktop: { + type: Boolean, + default: false, + required: false, + }, + selected: { + type: Boolean, + default: false, + required: false, + }, + }, + i18n: { + REMOVE_TAG_BUTTON_TITLE, + DIGEST_LABEL, + CREATED_AT_LABEL, + REMOVE_TAG_BUTTON_DISABLE_TOOLTIP, + PUBLISHED_DETAILS_ROW_TEXT, + MANIFEST_DETAILS_ROW_TEST, + CONFIGURATION_DETAILS_ROW_TEST, + MISSING_MANIFEST_WARNING_TOOLTIP, + }, + computed: { + formattedSize() { + return this.tag.total_size ? numberToHumanSize(this.tag.total_size) : NOT_AVAILABLE_SIZE; + }, + layers() { + return this.tag.layers ? n__('%d layer', '%d layers', this.tag.layers) : ''; + }, + mobileClasses() { + return this.isDesktop ? '' : 'mw-s'; + }, + shortDigest() { + // remove sha256: from the string, and show only the first 7 char + return this.tag.digest?.substring(7, 14) ?? NOT_AVAILABLE_TEXT; + }, + publishedDate() { + return formatDate(this.tag.created_at, 'isoDate'); + }, + publishedTime() { + return formatDate(this.tag.created_at, 'hh:MM Z'); + }, + formattedRevision() { + // to be removed when API response is adjusted + // see https://gitlab.com/gitlab-org/gitlab/-/issues/225324 + // eslint-disable-next-line @gitlab/require-i18n-strings + return `sha256:${this.tag.revision}`; + }, + tagLocation() { + return this.tag.path?.replace(`:${this.tag.name}`, ''); + }, + invalidTag() { + return !this.tag.digest; + }, + }, +}; +</script> + +<template> + <list-item v-bind="$attrs" :selected="selected"> + <template #left-action> + <gl-form-checkbox + v-if="Boolean(tag.destroy_path)" + :disabled="invalidTag" + class="gl-m-0" + :checked="selected" + @change="$emit('select')" + /> + </template> + <template #left-primary> + <div class="gl-display-flex gl-align-items-center"> + <div + v-gl-tooltip="{ title: tag.name }" + data-testid="name" + class="gl-text-overflow-ellipsis gl-overflow-hidden gl-white-space-nowrap" + :class="mobileClasses" + > + {{ tag.name }} + </div> + + <clipboard-button + v-if="tag.location" + :title="tag.location" + :text="tag.location" + css-class="btn-default btn-transparent btn-clipboard" + /> + + <gl-icon + v-if="invalidTag" + v-gl-tooltip="{ title: $options.i18n.MISSING_MANIFEST_WARNING_TOOLTIP }" + name="warning" + class="gl-text-orange-500 gl-mb-2 gl-ml-2" + /> + </div> + </template> + + <template #left-secondary> + <span data-testid="size"> + {{ formattedSize }} + <template v-if="formattedSize && layers" + >·</template + > + {{ layers }} + </span> + </template> + <template #right-primary> + <span data-testid="time"> + <gl-sprintf :message="$options.i18n.CREATED_AT_LABEL"> + <template #timeInfo> + <time-ago-tooltip :time="tag.created_at" /> + </template> + </gl-sprintf> + </span> + </template> + <template #right-secondary> + <span data-testid="digest"> + <gl-sprintf :message="$options.i18n.DIGEST_LABEL"> + <template #imageId>{{ shortDigest }}</template> + </gl-sprintf> + </span> + </template> + <template #right-action> + <delete-button + :disabled="!tag.destroy_path || invalidTag" + :title="$options.i18n.REMOVE_TAG_BUTTON_TITLE" + :tooltip-title="$options.i18n.REMOVE_TAG_BUTTON_DISABLE_TOOLTIP" + :tooltip-disabled="Boolean(tag.destroy_path)" + data-testid="single-delete-button" + @delete="$emit('delete')" + /> + </template> + + <template v-if="!invalidTag" #details_published> + <details-row icon="clock" data-testid="published-date-detail"> + <gl-sprintf :message="$options.i18n.PUBLISHED_DETAILS_ROW_TEXT"> + <template #repositoryPath> + <i>{{ tagLocation }}</i> + </template> + <template #time> + {{ publishedTime }} + </template> + <template #date> + {{ publishedDate }} + </template> + </gl-sprintf> + </details-row> + </template> + <template v-if="!invalidTag" #details_manifest_digest> + <details-row icon="log" data-testid="manifest-detail"> + <gl-sprintf :message="$options.i18n.MANIFEST_DETAILS_ROW_TEST"> + <template #digest> + {{ tag.digest }} + </template> + </gl-sprintf> + <clipboard-button + v-if="tag.digest" + :title="tag.digest" + :text="tag.digest" + css-class="btn-default btn-transparent btn-clipboard gl-p-0" + /> + </details-row> + </template> + <template v-if="!invalidTag" #details_configuration_digest> + <details-row icon="cloud-gear" data-testid="configuration-detail"> + <gl-sprintf :message="$options.i18n.CONFIGURATION_DETAILS_ROW_TEST"> + <template #digest> + {{ formattedRevision }} + </template> + </gl-sprintf> + <clipboard-button + v-if="formattedRevision" + :title="formattedRevision" + :text="formattedRevision" + css-class="btn-default btn-transparent btn-clipboard gl-p-0" + /> + </details-row> + </template> + </list-item> +</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 deleted file mode 100644 index 81be778e1e5..00000000000 --- a/app/assets/javascripts/registry/explorer/components/details_page/tags_table.vue +++ /dev/null @@ -1,210 +0,0 @@ -<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> |