summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorNick Kipling <nkipling@gitlab.com>2019-07-12 10:27:23 +0100
committerNathan Friend <nathan@gitlab.com>2019-07-30 13:49:47 -0300
commit51b04b5f2273284c674a8813a4c5da13825b431e (patch)
tree7c8f1a42acb779fd2dbfed409aff727add2bda96 /app
parent9ba87676c89c68d5d3901710e798b15987ed0b58 (diff)
downloadgitlab-ce-51b04b5f2273284c674a8813a4c5da13825b431e.tar.gz
Implement multi select deletion for container registry
Added checkboxes to each image row Added delete selected images button Changed row delete button to appear on row hover Changed confirmation modal message Changed delete logic to support multi Added tests for multi select Updated pot file Updated rspec test for new functionality
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/registry/components/collapsible_container.vue2
-rw-r--r--app/assets/javascripts/registry/components/table_registry.vue156
-rw-r--r--app/assets/stylesheets/pages/container_registry.scss23
3 files changed, 153 insertions, 28 deletions
diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue
index e157036871b..bfb2305c48c 100644
--- a/app/assets/javascripts/registry/components/collapsible_container.vue
+++ b/app/assets/javascripts/registry/components/collapsible_container.vue
@@ -84,7 +84,7 @@ export default {
v-gl-modal="modalId"
:title="s__('ContainerRegistry|Remove repository')"
:aria-label="s__('ContainerRegistry|Remove repository')"
- class="js-remove-repo"
+ class="js-remove-repo btn-inverted"
variant="danger"
>
<icon name="remove" />
diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue
index a498a553908..a241db13e5a 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,14 +38,44 @@ export default {
},
data() {
return {
- itemToBeDeleted: null,
+ singleItemToBeDeleted: null,
+ itemsToBeDeleted: [],
modalId: `confirm-image-deletion-modal-${this.repo.id}`,
+ selectAllChecked: false,
};
},
computed: {
shouldRenderPagination() {
return this.repo.pagination.total > this.repo.pagination.perPage;
},
+ modalTitle() {
+ if (this.singleItemToBeDeleted !== null || this.itemsToBeDeleted.length === 1) {
+ return s__('ContainerRegistry|Remove image');
+ }
+ return s__('ContainerRegistry|Remove images');
+ },
+ modalDescription() {
+ const selectedCount = this.itemsToBeDeleted.length;
+
+ if (this.singleItemToBeDeleted !== null || selectedCount === 1) {
+ const { tag } =
+ this.singleItemToBeDeleted !== null
+ ? this.repo.list[this.singleItemToBeDeleted]
+ : this.repo.list[this.itemsToBeDeleted[0]];
+
+ return 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}` },
+ );
+ }
+
+ return sprintf(
+ s__(`ContainerRegistry|You are about to delete <b>%{count}</b> images. This will
+ delete the images and all tags pointing to them.`),
+ { count: selectedCount },
+ );
+ },
},
methods: {
...mapActions(['fetchList', 'deleteItem']),
@@ -48,13 +85,32 @@ export default {
formatSize(size) {
return numberToHumanSize(size);
},
- setItemToBeDeleted(item) {
- this.itemToBeDeleted = item;
+ setSingleItemToBeDeleted(idx) {
+ this.singleItemToBeDeleted = idx;
+ },
+ resetSingleItemToBeDeleted() {
+ this.singleItemToBeDeleted = null;
},
handleDeleteRegistry() {
- const { itemToBeDeleted } = this;
- this.itemToBeDeleted = null;
- this.deleteItem(itemToBeDeleted)
+ let { itemsToBeDeleted } = this;
+ this.itemsToBeDeleted = [];
+
+ if (this.singleItemToBeDeleted !== null) {
+ const { singleItemToBeDeleted } = this;
+ this.singleItemToBeDeleted = null;
+ itemsToBeDeleted = [singleItemToBeDeleted];
+ }
+
+ const deleteActions = itemsToBeDeleted.map(
+ x =>
+ new Promise((resolve, reject) => {
+ this.deleteItem(this.repo.list[x])
+ .then(resolve)
+ .catch(reject);
+ }),
+ );
+
+ Promise.all(deleteActions)
.then(() => this.fetchList({ repo: this.repo }))
.catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY));
},
@@ -66,6 +122,29 @@ export default {
showError(message) {
createFlash(errorMessages[message]);
},
+ selectAll() {
+ if (!this.selectAllChecked) {
+ this.itemsToBeDeleted = this.repo.list.map((x, idx) => idx);
+ this.selectAllChecked = true;
+ } else {
+ this.itemsToBeDeleted = [];
+ this.selectAllChecked = false;
+ }
+ },
+ updateItemsToBeDeleted(idx) {
+ const delIdx = this.itemsToBeDeleted.findIndex(x => x === idx);
+
+ if (delIdx > -1) {
+ this.itemsToBeDeleted.splice(delIdx, 1);
+ this.selectAllChecked = false;
+ } else {
+ this.itemsToBeDeleted.push(idx);
+
+ if (this.itemsToBeDeleted.length === this.repo.list.length) {
+ this.selectAllChecked = true;
+ }
+ }
+ },
},
};
</script>
@@ -74,15 +153,43 @@ export default {
<table class="table tags">
<thead>
<tr>
+ <th>
+ <gl-form-checkbox
+ v-if="repo.canDelete"
+ class="js-select-all-checkbox"
+ :checked="selectAllChecked"
+ @change="selectAll"
+ />
+ </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')"
+ ><icon name="remove"
+ /></gl-button>
+ </th>
</tr>
</thead>
<tbody>
- <tr v-for="item in repo.list" :key="item.tag">
+ <tr v-for="(item, idx) in repo.list" :key="item.tag">
+ <td class="check">
+ <gl-form-checkbox
+ v-if="item.canDelete"
+ class="js-select-checkbox"
+ :checked="itemsToBeDeleted && itemsToBeDeleted.includes(idx)"
+ @change="updateItemsToBeDeleted(idx)"
+ />
+ </td>
<td class="monospace">
{{ item.tag }}
<clipboard-button
@@ -111,16 +218,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="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="setSingleItemToBeDeleted(idx)"
>
<icon name="remove" />
</gl-button>
@@ -135,19 +241,15 @@ export default {
:page-info="repo.pagination"
/>
- <gl-modal :modal-id="modalId" 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
+ :modal-id="modalId"
+ ok-variant="danger"
+ @ok="handleDeleteRegistry"
+ @cancel="resetSingleItemToBeDeleted"
+ >
+ <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>
diff --git a/app/assets/stylesheets/pages/container_registry.scss b/app/assets/stylesheets/pages/container_registry.scss
index a21fa29f34a..ed9de6f7e30 100644
--- a/app/assets/stylesheets/pages/container_registry.scss
+++ b/app/assets/stylesheets/pages/container_registry.scss
@@ -31,4 +31,27 @@
.table.tags {
margin-bottom: 0;
+
+ th {
+ height: 55px;
+ }
+
+ tr {
+ &:hover {
+ td {
+ &.action-buttons {
+ opacity: 1;
+ }
+ }
+ }
+
+ td.check {
+ padding-right: $gl-padding;
+ width: 5%;
+ }
+
+ td.action-buttons {
+ opacity: 0;
+ }
+ }
}