mirror of
https://github.com/Joxit/docker-registry-ui.git
synced 2025-04-29 00:19:54 +03:00
feat(confirm-delete): add dialog for confirmation of multiple image delete
This commit is contained in:
parent
f02c99f12d
commit
e065298eed
4 changed files with 123 additions and 49 deletions
|
@ -27,7 +27,7 @@
|
||||||
"core-js": "^3.9.1",
|
"core-js": "^3.9.1",
|
||||||
"js-beautify": "^1.13.0",
|
"js-beautify": "^1.13.0",
|
||||||
"riot": "^5.3.1",
|
"riot": "^5.3.1",
|
||||||
"riot-mui": "joxit/riot-5-mui#03c37c7",
|
"riot-mui": "joxit/riot-5-mui#ba273d7",
|
||||||
"rollup": "^2.34.2",
|
"rollup": "^2.34.2",
|
||||||
"rollup-plugin-app-utils": "^1.0.6",
|
"rollup-plugin-app-utils": "^1.0.6",
|
||||||
"rollup-plugin-commonjs": "^10.1.0",
|
"rollup-plugin-commonjs": "^10.1.0",
|
||||||
|
|
108
src/components/dialogs/confirm-delete-image.riot
Normal file
108
src/components/dialogs/confirm-delete-image.riot
Normal 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>
|
|
@ -52,46 +52,5 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
this.props.handleCheckboxChange(checked, this.props.image);
|
this.props.handleCheckboxChange(checked, 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>
|
</script>
|
||||||
</remove-image>
|
</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/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
-->
|
-->
|
||||||
<tag-table>
|
<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">
|
<material-card class="taglist">
|
||||||
<table style="border: none;">
|
<table style="border: none;">
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -87,12 +90,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
import ImageContentDigest from './image-content-digest.riot';
|
import ImageContentDigest from './image-content-digest.riot';
|
||||||
import CopyToClipboard from './copy-to-clipboard.riot';
|
import CopyToClipboard from './copy-to-clipboard.riot';
|
||||||
import TagHistoryButton from './tag-history-button.riot';
|
import TagHistoryButton from './tag-history-button.riot';
|
||||||
import RemoveImage, {
|
import RemoveImage from './remove-image.riot';
|
||||||
deleteImage
|
|
||||||
} from './remove-image.riot';
|
|
||||||
import {
|
import {
|
||||||
matchSearch
|
matchSearch
|
||||||
} from '../search-bar.riot';
|
} from '../search-bar.riot';
|
||||||
|
import ConfirmDeleteImage from '../dialogs/confirm-delete-image.riot';
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
ImageDate,
|
ImageDate,
|
||||||
|
@ -102,6 +104,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
CopyToClipboard,
|
CopyToClipboard,
|
||||||
RemoveImage,
|
RemoveImage,
|
||||||
TagHistoryButton,
|
TagHistoryButton,
|
||||||
|
ConfirmDeleteImage,
|
||||||
},
|
},
|
||||||
onBeforeMount(props) {
|
onBeforeMount(props) {
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -117,10 +120,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
state.page = props.page
|
state.page = props.page
|
||||||
},
|
},
|
||||||
bulkDelete() {
|
bulkDelete() {
|
||||||
this.state.toDelete.forEach(image => deleteImage(image, {
|
this.update({
|
||||||
...this.props,
|
confirmDeleteImage: true
|
||||||
ignoreError: true
|
})
|
||||||
}))
|
},
|
||||||
|
onConfirmDeleteImageClick() {
|
||||||
|
this.update({
|
||||||
|
confirmDeleteImage: false
|
||||||
|
})
|
||||||
},
|
},
|
||||||
onRemoveImageHeaderChange(checked, event) {
|
onRemoveImageHeaderChange(checked, event) {
|
||||||
if (event.altKey === true) {
|
if (event.altKey === true) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue