debian-mirror-gitlab/app/assets/javascripts/registry/explorer/pages/details.vue
2020-03-13 15:44:24 +05:30

333 lines
9.7 KiB
Vue

<script>
import { mapState, mapActions } from 'vuex';
import {
GlTable,
GlFormCheckbox,
GlButton,
GlIcon,
GlTooltipDirective,
GlPagination,
GlModal,
GlLoadingIcon,
GlSprintf,
GlEmptyState,
GlResizeObserverDirective,
} from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import { n__, s__ } 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 { 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,
} from '../constants';
export default {
components: {
GlTable,
GlFormCheckbox,
GlButton,
GlIcon,
ClipboardButton,
GlPagination,
GlModal,
GlLoadingIcon,
GlSprintf,
GlEmptyState,
},
directives: {
GlTooltip: GlTooltipDirective,
GlResizeObserver: GlResizeObserverDirective,
},
mixins: [timeagoMixin, Tracking.mixin()],
data() {
return {
selectedItems: [],
itemsToBeDeleted: [],
selectAllChecked: false,
modalDescription: null,
isDesktop: true,
};
},
computed: {
...mapState(['tags', 'tagsPagination', 'isLoading', 'config']),
imageName() {
const { name } = decodeAndParse(this.$route.params.id);
return name;
},
fields() {
return [
{ key: LIST_KEY_CHECKBOX, label: '' },
{ key: LIST_KEY_TAG, label: LIST_LABEL_TAG },
{ 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',
};
},
modalAction() {
return n__(
'ContainerRegistry|Remove tag',
'ContainerRegistry|Remove tags',
this.isMultiDelete ? this.itemsToBeDeleted.length : 1,
);
},
currentPage: {
get() {
return this.tagsPagination.page;
},
set(page) {
this.requestTagsList({ pagination: { page }, id: this.$route.params.id });
},
},
},
methods: {
...mapActions(['requestTagsList', 'requestDeleteTag', 'requestDeleteTags']),
setModalDescription(itemIndex = -1) {
if (itemIndex === -1) {
this.modalDescription = {
message: s__(`ContainerRegistry|You are about to remove %{item} tags. Are you sure?`),
item: this.itemsToBeDeleted.length,
};
} else {
const { path } = this.tags[itemIndex];
this.modalDescription = {
message: s__(`ContainerRegistry|You are about to remove %{item}. Are you sure?`),
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];
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(itemToDelete) {
this.itemsToBeDeleted = [];
this.requestDeleteTag({ tag: itemToDelete, params: this.$route.params.id });
},
handleMultipleDelete() {
const { itemsToBeDeleted } = this;
this.itemsToBeDeleted = [];
this.selectedItems = [];
this.requestDeleteTags({
ids: itemsToBeDeleted.map(x => this.tags[x].name),
params: this.$route.params.id,
});
},
onDeletionConfirmed() {
this.track('confirm_delete');
if (this.isMultiDelete) {
this.handleMultipleDelete();
} else {
const index = this.itemsToBeDeleted[0];
this.handleSingleDelete(this.tags[index]);
}
},
handleResize() {
this.isDesktop = GlBreakpointInstance.isDesktop();
},
},
};
</script>
<template>
<div
v-gl-resize-observer="handleResize"
class="my-3 position-absolute w-100 slide-enter-to-element"
>
<div class="d-flex my-3 align-items-center">
<h4>
<gl-sprintf :message="s__('ContainerRegistry|%{imageName} tags')">
<template #imageName>
{{ imageName }}
</template>
</gl-sprintf>
</h4>
</div>
<gl-loading-icon v-if="isLoading" />
<template v-else-if="tags.length > 0">
<gl-table :items="tags" :fields="fields" :stacked="!isDesktop">
<template v-if="isDesktop" #head(checkbox)>
<gl-form-checkbox
ref="mainCheckbox"
:checked="selectAllChecked"
@change="onSelectAllChange"
/>
</template>
<template #head(actions)>
<gl-button
ref="bulkDeleteButton"
v-gl-tooltip
:disabled="!selectedItems || selectedItems.length === 0"
class="float-right"
variant="danger"
:title="s__('ContainerRegistry|Remove selected tags')"
:aria-label="s__('ContainerRegistry|Remove selected tags')"
@click="deleteMultipleItems()"
>
<gl-icon name="remove" />
</gl-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}">
<span ref="rowName">
{{ 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"
/>
</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">
{{ timeFormatted(value) }}
</span>
</template>
<template #cell(actions)="{index, item}">
<gl-button
ref="singleDeleteButton"
:title="s__('ContainerRegistry|Remove tag')"
:aria-label="s__('ContainerRegistry|Remove tag')"
: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-button>
</template>
</gl-table>
<gl-pagination
ref="pagination"
v-model="currentPage"
:per-page="tagsPagination.perPage"
:total-items="tagsPagination.total"
align="center"
class="w-100"
/>
<gl-modal
ref="deleteModal"
modal-id="delete-tag-modal"
ok-variant="danger"
@ok="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>
</template>
<gl-empty-state
v-else
:title="s__('ContainerRegistry|This image has no active tags')"
:svg-path="config.noContainersImage"
:description="
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.`,
)
"
class="mx-auto my-0"
/>
</div>
</template>