feat: show confirm dialog before deleting images (#182)
This commit is contained in:
commit
3dc035dac8
|
@ -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",
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
Loading…
Reference in New Issue