feat: show confirm dialog before deleting images (#182)

This commit is contained in:
Jones Magloire 2021-04-27 09:52:34 +02:00 committed by GitHub
commit 3dc035dac8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 173 additions and 65 deletions

View File

@ -27,7 +27,7 @@
"core-js": "^3.9.1",
"js-beautify": "^1.13.0",
"riot": "^5.3.1",
"riot-mui": "joxit/riot-5-mui#03c37c7",
"riot-mui": "joxit/riot-5-mui#ba273d7",
"rollup": "^2.34.2",
"rollup-plugin-app-utils": "^1.0.6",
"rollup-plugin-commonjs": "^10.1.0",

View File

@ -0,0 +1,108 @@
<!--
Copyright (C) 2016-2021 Jones Magloire @Joxit
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<confirm-delete-image>
<material-popup opened="{ props.opened }" onClick="{ props.onClick }">
<div slot="title">These images will be deleted</div>
<div slot="content">
<ul>
<li each="{ image in displayImagesToDelete(props.toDelete, props.tags) }">{ image.name }:{ image.tag }</li>
</ul>
</div>
<div slot="action">
<material-button class="dialog-button" waves-color="rgba(158,158,158,.4)" onClick="{ deleteImages }">
Delete
</material-button>
<material-button class="dialog-button" waves-color="rgba(158,158,158,.4)" onClick="{ props.onClick }">
Cancel
</material-button>
</div>
</material-popup>
<script>
import {
Http
} from '../../scripts/http';
import router from '../../scripts/router';
export default {
displayImagesToDelete(toDelete, tags) {
const digests = new Set();
toDelete.forEach(image => {
if (image.digest) {
digests.add(image.digest);
}
})
return tags.filter(image => digests.has(image.digest))
},
deleteImages() {
this.props.toDelete.forEach(image => this.deleteImage(image, this.props));
},
deleteImage(image, opts) {
const {
registryUrl,
ignoreError,
onNotify,
onAuthentication,
onClick
} = opts;
if (!image.digest) {
onNotify(`Information for ${name}:${tag} are not yet loaded.`);
return;
}
const name = image.name;
const tag = image.tag;
const oReq = new Http({
onAuthentication: onAuthentication
});
oReq.addEventListener('loadend', function () {
if (this.status == 200 || this.status == 202) {
router.taglist(name);
onNotify(`Deleting ${name}:${tag} image. Run \`registry garbage-collect config.yml\` on your registry`);
} else if (this.status == 404) {
ignoreError || onNotify({
message: 'Digest not found for this image in your registry.',
isError: true
});
} else {
onNotify(this.responseText);
}
onClick();
});
oReq.open('DELETE', `${registryUrl}/v2/${name}/manifests/${image.digest}`);
oReq.setRequestHeader('Accept',
'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json');
oReq.addEventListener('error', function () {
onNotify({
message: 'An error occurred when deleting image. Check if your server accept DELETE methods Access-Control-Allow-Methods: [\'DELETE\'].',
isError: true
});
});
oReq.send();
}
}
</script>
<style>
:host {
color: #000;
list-style-type: disc;
margin-block-start: 0.7em;
}
:host material-popup .content .material-popup-content {
overflow-y: auto;
max-height: 250px;
}
</style>
</confirm-delete-image>

View File

@ -27,6 +27,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
Http
} from '../../scripts/http';
import router from '../../scripts/router'
import {
ACTION_CHECK_TO_DELETE,
ACTION_UNCHECK_TO_DELETE,
ACTION_DELETE_IMAGE
} from './tag-table.riot';
export default {
onBeforeMount(props, state) {
state.checked = props.checked;
@ -42,56 +47,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
onBeforeUpdate(props, state) {
state.checked = props.checked;
},
deleteImage(ignoreError) {
deleteImage(this.props.image, {
...this.props,
ignoreError
})
deleteImage() {
this.props.handleCheckboxChange(ACTION_DELETE_IMAGE, this.props.image);
},
handleCheckboxChange(checked) {
this.props.handleCheckboxChange(checked, this.props.image);
const action = checked ? ACTION_CHECK_TO_DELETE : ACTION_UNCHECK_TO_DELETE;
this.props.handleCheckboxChange(action, this.props.image);
}
}
export function deleteImage(image, opts) {
const {
registryUrl,
ignoreError,
onNotify,
onAuthentication
} = opts;
if (!image.digest) {
onNotify(`Information for ${name}:${tag} are not yet loaded.`);
return;
}
const name = image.name;
const tag = image.tag;
const oReq = new Http({
onAuthentication: onAuthentication
});
oReq.addEventListener('loadend', function () {
if (this.status == 200 || this.status == 202) {
router.taglist(name);
onNotify(`Deleting ${name}:${tag} image. Run \`registry garbage-collect config.yml\` on your registry`);
} else if (this.status == 404) {
ignoreError || onNotify({
message: 'Digest not found for this image in your registry.',
isError: true
});
} else {
onNotify(this.responseText);
}
});
oReq.open('DELETE', `${registryUrl}/v2/${name}/manifests/${image.digest}`);
oReq.setRequestHeader('Accept',
'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json');
oReq.addEventListener('error', function () {
onNotify({
message: 'An error occurred when deleting image. Check if your server accept DELETE methods Access-Control-Allow-Methods: [\'DELETE\'].',
isError: true
});
});
oReq.send();
}
</script>
</remove-image>

View File

@ -15,6 +15,9 @@ You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<tag-table>
<confirm-delete-image opened="{ state.confirmDeleteImage }" on-click="{ onConfirmDeleteImageClick }"
registry-url="{ props.registryUrl }" on-notify="{ props.onNotify }" on-authentication="{ props.onAuthentication }"
tags="{ props.tags }" to-delete="{ state.toDelete }"></confirm-delete-image>
<material-card class="taglist">
<table style="border: none;">
<thead>
@ -36,12 +39,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
onclick="{ onReverseOrder }">Tag
</th>
<th class="show-tag-history">History</th>
<th class="remove-tag { state.toDelete.size > 0 ? 'delete' : '' }" if="{ props.isImageRemoveActivated }">
<material-checkbox class="indeterminate" checked="{ state.multiDelete }" if="{ state.toDelete.size === 0}"
<th class="remove-tag { state.toDelete.size > 0 && !state.singleDeleteAction ? 'delete' : '' }"
if="{ props.isImageRemoveActivated }">
<material-checkbox class="indeterminate" checked="{ state.multiDelete }"
if="{ state.toDelete.size === 0 || state.singleDeleteAction }"
title="Toggle multi-delete. Alt+Click to select all tags." onChange="{ onRemoveImageHeaderChange }">
</material-checkbox>
<material-button waves-center="true" rounded="true" waves-color="#ddd"
title="This will delete selected images." onClick="{ bulkDelete }" if="{ state.toDelete.size > 0 }">
title="This will delete selected images." onClick="{ deleteImages }"
if="{ state.toDelete.size > 0 && !state.singleDeleteAction }">
<i class="material-icons">delete</i>
</material-button>
</th>
@ -87,12 +93,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import ImageContentDigest from './image-content-digest.riot';
import CopyToClipboard from './copy-to-clipboard.riot';
import TagHistoryButton from './tag-history-button.riot';
import RemoveImage, {
deleteImage
} from './remove-image.riot';
import RemoveImage from './remove-image.riot';
import {
matchSearch
} from '../search-bar.riot';
import ConfirmDeleteImage from '../dialogs/confirm-delete-image.riot';
const ACTION_CHECK_TO_DELETE = 'CHECK';
const ACTION_UNCHECK_TO_DELETE = 'UNCHECK';
const ACTION_DELETE_IMAGE = 'DELETE';
export default {
components: {
ImageDate,
@ -102,6 +111,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
CopyToClipboard,
RemoveImage,
TagHistoryButton,
ConfirmDeleteImage,
},
onBeforeMount(props) {
this.state = {
@ -116,11 +126,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
}
state.page = props.page
},
bulkDelete() {
this.state.toDelete.forEach(image => deleteImage(image, {
...this.props,
ignoreError: true
}))
deleteImages() {
this.update({
confirmDeleteImage: true
})
},
onConfirmDeleteImageClick() {
if (this.state.singleDeleteAction) {
this.state.toDelete.clear();
}
this.update({
singleDeleteAction: false,
confirmDeleteImage: false
})
},
onRemoveImageHeaderChange(checked, event) {
if (event.altKey === true) {
@ -136,14 +154,29 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
})
}
},
onRemoveImageChange(checked, image) {
if (checked) {
this.state.toDelete.add(image)
} else {
this.state.toDelete.delete(image)
onRemoveImageChange(action, image) {
let confirmDeleteImage = false;
let singleDeleteAction = false;
switch (action) {
case ACTION_CHECK_TO_DELETE: {
this.state.toDelete.add(image);
break;
}
case ACTION_UNCHECK_TO_DELETE: {
this.state.toDelete.delete(image);
break;
}
case ACTION_DELETE_IMAGE: {
this.state.toDelete.clear();
this.state.toDelete.add(image);
confirmDeleteImage = true;
singleDeleteAction = true;
}
}
this.update({
toDelete: this.state.toDelete
toDelete: this.state.toDelete,
confirmDeleteImage,
singleDeleteAction
})
},
onReverseOrder() {
@ -174,5 +207,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
},
matchSearch
}
export {
ACTION_CHECK_TO_DELETE,
ACTION_UNCHECK_TO_DELETE,
ACTION_DELETE_IMAGE
}
</script>
</tag-table>