diff options
Diffstat (limited to 'app/assets/javascripts/registry/components/table_registry.vue')
-rw-r--r-- | app/assets/javascripts/registry/components/table_registry.vue | 185 |
1 files changed, 151 insertions, 34 deletions
diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue index 0ec5e2c7a87..e9067bc2b56 100644 --- a/app/assets/javascripts/registry/components/table_registry.vue +++ b/app/assets/javascripts/registry/components/table_registry.vue @@ -1,7 +1,13 @@ <script> import { mapActions } from 'vuex'; -import { GlButton, GlTooltipDirective, GlModal, GlModalDirective } from '@gitlab/ui'; -import { n__ } from '../../locale'; +import { + GlButton, + GlFormCheckbox, + GlTooltipDirective, + GlModal, + GlModalDirective, +} from '@gitlab/ui'; +import { n__, s__, sprintf } from '../../locale'; import createFlash from '../../flash'; import ClipboardButton from '../../vue_shared/components/clipboard_button.vue'; import TablePagination from '../../vue_shared/components/pagination/table_pagination.vue'; @@ -14,6 +20,7 @@ export default { components: { ClipboardButton, TablePagination, + GlFormCheckbox, GlButton, Icon, GlModal, @@ -31,32 +38,98 @@ export default { }, data() { return { - itemToBeDeleted: null, + itemsToBeDeleted: [], + modalId: `confirm-image-deletion-modal-${this.repo.id}`, + selectAllChecked: false, + modalDescription: '', }; }, computed: { + bulkDeletePath() { + return this.repo.tagsPath ? this.repo.tagsPath.replace('?format=json', '/bulk_destroy') : ''; + }, shouldRenderPagination() { return this.repo.pagination.total > this.repo.pagination.perPage; }, + modalTitle() { + return n__( + 'ContainerRegistry|Remove image', + 'ContainerRegistry|Remove images', + this.itemsToBeDeleted.length === 0 ? 1 : this.itemsToBeDeleted.length, + ); + }, + }, + mounted() { + this.$refs.deleteModal.$refs.modal.$on('hide', this.removeModalEvents); }, methods: { - ...mapActions(['fetchList', 'deleteItem']), + ...mapActions(['fetchList', 'deleteItem', 'multiDeleteItems']), + setModalDescription(itemIndex = -1) { + if (itemIndex === -1) { + this.modalDescription = sprintf( + s__(`ContainerRegistry|You are about to delete <b>%{count}</b> images. This will + delete the images and all tags pointing to them.`), + { count: this.itemsToBeDeleted.length }, + ); + } else { + const { tag } = this.repo.list[itemIndex]; + + this.modalDescription = sprintf( + s__(`ContainerRegistry|You are about to delete the image <b>%{title}</b>. This will + delete the image and all tags pointing to this image.`), + { title: `${this.repo.name}:${tag}` }, + ); + } + }, layers(item) { return item.layers ? n__('%d layer', '%d layers', item.layers) : ''; }, formatSize(size) { return numberToHumanSize(size); }, - setItemToBeDeleted(item) { - this.itemToBeDeleted = item; + removeModalEvents() { + this.$refs.deleteModal.$refs.modal.$off('ok'); }, - handleDeleteRegistry() { - const { itemToBeDeleted } = this; - this.itemToBeDeleted = null; - this.deleteItem(itemToBeDeleted) + deleteSingleItem(index) { + this.setModalDescription(index); + + this.$refs.deleteModal.$refs.modal.$once('ok', () => { + this.removeModalEvents(); + this.handleSingleDelete(this.repo.list[index]); + }); + }, + deleteMultipleItems() { + if (this.itemsToBeDeleted.length === 1) { + this.setModalDescription(this.itemsToBeDeleted[0]); + } else if (this.itemsToBeDeleted.length > 1) { + this.setModalDescription(); + } + + this.$refs.deleteModal.$refs.modal.$once('ok', () => { + this.removeModalEvents(); + this.handleMultipleDelete(); + }); + }, + handleSingleDelete(itemToDelete) { + this.deleteItem(itemToDelete) .then(() => this.fetchList({ repo: this.repo })) .catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY)); }, + handleMultipleDelete() { + const { itemsToBeDeleted } = this; + this.itemsToBeDeleted = []; + + if (this.bulkDeletePath) { + this.multiDeleteItems({ + path: this.bulkDeletePath, + items: itemsToBeDeleted.map(x => this.repo.list[x].tag), + }) + .then(() => this.fetchList({ repo: this.repo })) + .catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY)); + } else { + this.showError(errorMessagesTypes.DELETE_REGISTRY); + } + }, onPageChange(pageNumber) { this.fetchList({ repo: this.repo, page: pageNumber }).catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY), @@ -65,6 +138,35 @@ export default { showError(message) { createFlash(errorMessages[message]); }, + onSelectAllChange() { + if (this.selectAllChecked) { + this.deselectAll(); + } else { + this.selectAll(); + } + }, + selectAll() { + this.itemsToBeDeleted = this.repo.list.map((x, index) => index); + this.selectAllChecked = true; + }, + deselectAll() { + this.itemsToBeDeleted = []; + this.selectAllChecked = false; + }, + updateItemsToBeDeleted(index) { + const delIndex = this.itemsToBeDeleted.findIndex(x => x === index); + + if (delIndex > -1) { + this.itemsToBeDeleted.splice(delIndex, 1); + this.selectAllChecked = false; + } else { + this.itemsToBeDeleted.push(index); + + if (this.itemsToBeDeleted.length === this.repo.list.length) { + this.selectAllChecked = true; + } + } + }, }, }; </script> @@ -73,15 +175,44 @@ export default { <table class="table tags"> <thead> <tr> + <th> + <gl-form-checkbox + v-if="repo.canDelete" + class="js-select-all-checkbox" + :checked="selectAllChecked" + @change="onSelectAllChange" + /> + </th> <th>{{ s__('ContainerRegistry|Tag') }}</th> <th>{{ s__('ContainerRegistry|Tag ID') }}</th> <th>{{ s__('ContainerRegistry|Size') }}</th> <th>{{ s__('ContainerRegistry|Last Updated') }}</th> - <th></th> + <th> + <gl-button + v-if="repo.canDelete" + v-gl-tooltip + v-gl-modal="modalId" + :disabled="!itemsToBeDeleted || itemsToBeDeleted.length === 0" + class="js-delete-registry float-right" + variant="danger" + :title="s__('ContainerRegistry|Remove selected images')" + :aria-label="s__('ContainerRegistry|Remove selected images')" + @click="deleteMultipleItems()" + ><icon name="remove" + /></gl-button> + </th> </tr> </thead> <tbody> - <tr v-for="item in repo.list" :key="item.tag"> + <tr v-for="(item, index) in repo.list" :key="item.tag" class="registry-image-row"> + <td class="check"> + <gl-form-checkbox + v-if="item.canDelete" + class="js-select-checkbox" + :checked="itemsToBeDeleted && itemsToBeDeleted.includes(index)" + @change="updateItemsToBeDeleted(index)" + /> + </td> <td class="monospace"> {{ item.tag }} <clipboard-button @@ -110,16 +241,15 @@ export default { </span> </td> - <td class="content"> + <td class="content action-buttons"> <gl-button v-if="item.canDelete" - v-gl-tooltip - v-gl-modal="'confirm-image-deletion-modal'" + v-gl-modal="modalId" :title="s__('ContainerRegistry|Remove image')" :aria-label="s__('ContainerRegistry|Remove image')" variant="danger" - class="js-delete-registry d-none d-sm-block float-right" - @click="setItemToBeDeleted(item)" + class="js-delete-registry-row float-right btn-inverted btn-border-color btn-icon" + @click="deleteSingleItem(index)" > <icon name="remove" /> </gl-button> @@ -134,23 +264,10 @@ export default { :page-info="repo.pagination" /> - <gl-modal - modal-id="confirm-image-deletion-modal" - ok-variant="danger" - @ok="handleDeleteRegistry" - > - <template v-slot:modal-title>{{ s__('ContainerRegistry|Remove image') }}</template> - <template v-slot:modal-ok>{{ s__('ContainerRegistry|Remove image and tags') }}</template> - <p - v-html=" - sprintf( - s__( - 'ContainerRegistry|You are about to delete the image <b>%{title}</b>. This will delete the image and all tags pointing to this image.', - ), - { title: repo.name }, - ) - " - ></p> + <gl-modal ref="deleteModal" :modal-id="modalId" ok-variant="danger"> + <template v-slot:modal-title>{{ modalTitle }}</template> + <template v-slot:modal-ok>{{ s__('ContainerRegistry|Remove image(s) and tags') }}</template> + <p v-html="modalDescription"></p> </gl-modal> </div> </template> |