mirror of
https://github.com/Joxit/docker-registry-ui.git
synced 2025-04-26 23:19:54 +03:00
feat(riot-v5): upgrade remove-image and copy-to-clipboard
This commit is contained in:
parent
9c303d32c7
commit
669c3399d0
9 changed files with 186 additions and 147 deletions
|
@ -20,7 +20,7 @@ const plugins = [
|
|||
nodeResolve(),
|
||||
commonjs(),
|
||||
scss({ output: `./${output}/docker-registry-ui.css`, outputStyle: 'compressed' }),
|
||||
babel({ babelHelpers: 'bundled', presets: [['@babel/env', { useBuiltIns: 'usage' }]] }),
|
||||
babel({ babelHelpers: 'bundled', presets: [['@babel/env', { useBuiltIns: 'usage', corejs: { version: "2" } }]] }),
|
||||
html({ template: () => htmlUseref('./src/index.html') }),
|
||||
copy({
|
||||
targets: [
|
||||
|
|
|
@ -27,7 +27,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
catalog-elements-limit="{ state.catalogElementsLimit }" />
|
||||
</route>
|
||||
<route path="{baseRoute}taglist/(.*)">
|
||||
<tag-list registry-url="{ state.registryUrl }" registry-name="{ state.name }"
|
||||
<tag-list registry-url="{ state.registryUrl }" registry-name="{ state.name }" pull-url="{ state.pullUrl }"
|
||||
image="{ router.getTagListImage() }" show-content-digest="{props.showContentDigest}"
|
||||
is-image-remove-activated="{props.isImageRemoveActivated}"></tag-list>
|
||||
</route>
|
||||
|
@ -73,6 +73,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
''));
|
||||
this.state.name = props.name || stripHttps(props.registryUrl);
|
||||
this.state.catalogElementsLimit = props.catalogElementsLimit || 100000;
|
||||
this.state.pullUrl = this.pullUrl(props);
|
||||
},
|
||||
pullUrl(props) {
|
||||
const url = props.pullUrl ||
|
||||
(props.registryUrl && props.registryUrl.length > 0 && props.registryUrl) ||
|
||||
window.location.host;
|
||||
return stripHttps(url);
|
||||
},
|
||||
baseRoute: '/(\\?[^#]*)?(#!)?(/?)',
|
||||
router,
|
||||
|
|
55
src/components/tag-list/copy-to-clipboard.riot
Normal file
55
src/components/tag-list/copy-to-clipboard.riot
Normal file
|
@ -0,0 +1,55 @@
|
|||
<!--
|
||||
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/>.
|
||||
-->
|
||||
<copy-to-clipboard>
|
||||
<div class="copy-to-clipboard">
|
||||
<input style="display: none; width: 1px; height: 1px;" value="{ state.dockerCmd }">
|
||||
<material-button waves-center="true" rounded="true" waves-color="#ddd" onClick="{ copy }"
|
||||
title="Copy pull command.">
|
||||
<i class="material-icons">content_copy</i>
|
||||
</material-button>
|
||||
</div>
|
||||
<script>
|
||||
export default {
|
||||
onBeforeMount(props, state) {
|
||||
const prefix = 'docker pull ' + props.pullUrl + '/' + props.image.name;
|
||||
if (props.target === 'tag') {
|
||||
state.dockerCmd = prefix + ':' + props.image.tag;
|
||||
} else {
|
||||
props.image.one('content-digest', (digest) => {
|
||||
this.update({
|
||||
dockerCmd: prefix + '@' + digest
|
||||
})
|
||||
});
|
||||
props.image.trigger('get-content-digest');
|
||||
}
|
||||
},
|
||||
copy() {
|
||||
if (!this.state.dockerCmd) {
|
||||
// registryUI.showErrorCanNotReadContentDigest();
|
||||
return;
|
||||
}
|
||||
const copyText = this.$('input');
|
||||
copyText.style.display = 'block';
|
||||
copyText.select();
|
||||
document.execCommand('copy');
|
||||
copyText.style.display = 'none';
|
||||
|
||||
// registryUI.snackbar('`' + this.state.dockerCmd + '` has been copied to clipboard.')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</copy-to-clipboard>
|
78
src/components/tag-list/remove-image.riot
Normal file
78
src/components/tag-list/remove-image.riot
Normal file
|
@ -0,0 +1,78 @@
|
|||
<!--
|
||||
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/>.
|
||||
-->
|
||||
<remove-image>
|
||||
<material-button waves-center="true" rounded="true" waves-color="#ddd" title="This will delete the image."
|
||||
if="{ !props.multiDelete }" disabled="{ !state.digest }" onClick="{ deleteImage }">
|
||||
<i class="material-icons">delete</i>
|
||||
</material-button>
|
||||
<material-checkbox if="{ props.multiDelete }" title="Select this tag to delete it." disabled="{ !state.digest }"
|
||||
onChange="{ handleCheckboxChange }">
|
||||
</material-checkbox>
|
||||
<script>
|
||||
import {
|
||||
Http
|
||||
} from '../../scripts/http';
|
||||
import router from '../../scripts/router'
|
||||
export default {
|
||||
onBeforeMount(props, state) {
|
||||
props.image.one('content-digest', (digest) => {
|
||||
this.update({
|
||||
digest
|
||||
});
|
||||
});
|
||||
props.image.trigger('get-content-digest');
|
||||
},
|
||||
deleteImage(ignoreError) {
|
||||
// registryUI.taglist.go(name);
|
||||
deleteImage(this.props.image, this.props.registryUrl, ignoreError)
|
||||
},
|
||||
handleCheckboxChange(checked) {
|
||||
this.props.handleCheckboxChange(checked, this.props.image);
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteImage(image, registryUrl, ignoreError) {
|
||||
if (!image.digest) {
|
||||
// registryUI.snackbar('Information for ' + name + ':' + tag + ' are not yet loaded.');
|
||||
return;
|
||||
}
|
||||
const name = image.name;
|
||||
const tag = image.tag;
|
||||
const oReq = new Http();
|
||||
oReq.addEventListener('loadend', function () {
|
||||
if (this.status == 200 || this.status == 202) {
|
||||
router.taglist(name);
|
||||
// registryUI.snackbar('Deleting ' + name + ':' + tag +
|
||||
// ' image. Run `registry garbage-collect config.yml` on your registry');
|
||||
} else if (this.status == 404) {
|
||||
// ignoreError || registryUI.errorSnackbar('Digest not found for this image in your registry.');
|
||||
} else {
|
||||
// registryUI.snackbar(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 () {
|
||||
registryUI.errorSnackbar(
|
||||
'An error occurred when deleting image. Check if your server accept DELETE methods Access-Control-Allow-Methods: [\'DELETE\'].'
|
||||
);
|
||||
});
|
||||
oReq.send();
|
||||
}
|
||||
</script>
|
||||
</remove-image>
|
|
@ -1,5 +1,5 @@
|
|||
<!--
|
||||
Copyright (C) 2016-2019 Jones Magloire @Joxit
|
||||
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
|
||||
|
@ -17,7 +17,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
<tag-list>
|
||||
<material-card class="header">
|
||||
<div class="material-card-title-action ">
|
||||
<material-button waves-center="true" rounded="true" waves-color="#ddd" onclick="registryUI.home();">
|
||||
<material-button waves-center="true" rounded="true" waves-color="#ddd" onClick="{ router.home }">
|
||||
<i class="material-icons">arrow_back</i>
|
||||
</material-button>
|
||||
<h2>
|
||||
|
@ -38,7 +38,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
|
||||
<tag-table if="{ state.loadend }" tags="{state.tags}" asc="{state.asc}" page="{ state.page }"
|
||||
show-content-digest="{props.showContentDigest}" is-image-remove-activated="{props.isImageRemoveActivated}"
|
||||
onReverseOrder="{ onReverseOrder }"></tag-table>
|
||||
onReverseOrder="{ onReverseOrder }" registry-url="{ props.registryUrl }" pull-url="{ props.pullUrl }"></tag-table>
|
||||
|
||||
<pagination pages="{ getPageLabels(state.page, getNumPages(state.tags)) }" onPageUpdate="{onPageUpdate}"></pagination>
|
||||
|
||||
|
@ -56,6 +56,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
} from '../../scripts/utils'
|
||||
import Pagination from './pagination.riot'
|
||||
import TagTable from './tag-table.riot'
|
||||
import router from '../../scripts/router'
|
||||
export default {
|
||||
components: {
|
||||
Pagination,
|
||||
|
@ -151,7 +152,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
this.update();
|
||||
},
|
||||
getPageLabels,
|
||||
getNumPages
|
||||
getNumPages,
|
||||
router
|
||||
}
|
||||
</script>
|
||||
</tag-list>
|
|
@ -1,5 +1,5 @@
|
|||
<!--
|
||||
Copyright (C) 2016-2019 Jones Magloire @Joxit
|
||||
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
|
||||
|
@ -28,11 +28,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
onclick="{() => props.onReverseOrder() }">Tag
|
||||
</th>
|
||||
<th class="show-tag-history">History</th>
|
||||
<th class="remove-tag { state.toDelete > 0 ? 'delete' : '' }" if="{ props.isImageRemoveActivated }">
|
||||
<material-checkbox ref="remove-tag-checkbox" class="indeterminate" if="{ state.toDelete === 0}"
|
||||
title="Toggle multi-delete. Alt+Click to select all tags."></material-checkbox>
|
||||
<th class="remove-tag { state.toDelete.size > 0 ? 'delete' : '' }" if="{ props.isImageRemoveActivated }">
|
||||
<material-checkbox ref="remove-tag-checkbox" class="indeterminate" if="{ state.toDelete.size === 0}"
|
||||
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 > 0 }">
|
||||
title="This will delete selected images." onClick="{ bulkDelete }" if="{ state.toDelete.size > 0 }">
|
||||
<i class="material-icons">delete</i>
|
||||
</material-button>
|
||||
</th>
|
||||
|
@ -48,17 +49,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
</td>
|
||||
<td if="{ props.showContentDigest }">
|
||||
<image-content-digest image="{ image }" />
|
||||
<copy-to-clipboard target="digest" image="{ image }" />
|
||||
<copy-to-clipboard target="digest" image="{ image }" pull-url="{ props.pullUrl }" />
|
||||
</td>
|
||||
<td>
|
||||
<image-tag image="{ image }" />
|
||||
<copy-to-clipboard target="tag" image="{ image }" />
|
||||
<copy-to-clipboard target="tag" image="{ image }" pull-url="{ props.pullUrl }" />
|
||||
</td>
|
||||
<td class="show-tag-history">
|
||||
<tag-history-button image="{ image }" />
|
||||
</td>
|
||||
<td if="{ props.isImageRemoveActivated }">
|
||||
<remove-image multi-delete="{ state.multiDelete }" image="{ image }" />
|
||||
<td if="{ props.isImageRemoveActivated }" class="remove-tag">
|
||||
<remove-image multi-delete="{ state.multiDelete }" image="{ image }" registry-url="{ props.registryUrl }"
|
||||
handleCheckboxChange="{ onRemoveImageChange }" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
@ -75,21 +77,44 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||
import ImageSize from './image-size.riot';
|
||||
import ImageTag from './image-tag.riot';
|
||||
import ImageContentDigest from './image-content-digest.riot';
|
||||
import CopyToClipboard from './copy-to-clipboard.riot';
|
||||
import RemoveImage, {
|
||||
deleteImage
|
||||
} from './remove-image.riot';
|
||||
export default {
|
||||
components: {
|
||||
ImageDate,
|
||||
ImageSize,
|
||||
ImageTag,
|
||||
ImageContentDigest,
|
||||
CopyToClipboard,
|
||||
RemoveImage,
|
||||
},
|
||||
onBeforeMount(props) {
|
||||
this.state = {
|
||||
toDelete: 0,
|
||||
multiDelete: false
|
||||
toDelete: new Set(),
|
||||
multiDelete: false,
|
||||
}
|
||||
},
|
||||
onMounted(props, state) {},
|
||||
bulkDelete() {},
|
||||
bulkDelete() {
|
||||
this.state.toDelete.forEach(image => deleteImage(image, this.props.registryUrl, true))
|
||||
},
|
||||
onRemoveImageHeaderChange(checked) {
|
||||
this.update({
|
||||
multiDelete: checked
|
||||
})
|
||||
},
|
||||
onRemoveImageChange(checked, image) {
|
||||
if (checked) {
|
||||
this.state.toDelete.add(image)
|
||||
} else {
|
||||
this.state.toDelete.delete(image)
|
||||
}
|
||||
this.update({
|
||||
toDelete: this.state.toDelete
|
||||
})
|
||||
},
|
||||
getPage
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -6,7 +6,7 @@ function baseUrl() {
|
|||
|
||||
export default {
|
||||
home() {
|
||||
router.pus(baseUrl());
|
||||
router.push(baseUrl());
|
||||
},
|
||||
taglist(image) {
|
||||
router.push(`${baseUrl()}#!/taglist/${image}`);
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
<!--
|
||||
Copyright (C) 2016-2019 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/>.
|
||||
-->
|
||||
<copy-to-clipboard>
|
||||
<div class="copy-to-clipboard">
|
||||
<input ref="input" style="display: none; width: 1px; height: 1px;" value="{ this.dockerCmd }">
|
||||
<material-button waves-center="true" rounded="true" waves-color="#ddd" onclick="{ this.copy }" title="Copy pull command.">
|
||||
<i class="material-icons">content_copy</i>
|
||||
</material-button>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
this.prefix = 'docker pull ' + registryUI.cleanName() + '/' + opts.image.name;
|
||||
const self = this;
|
||||
if (opts.target === 'tag') {
|
||||
self.dockerCmd = self.prefix + ':' + opts.image.tag;
|
||||
} else {
|
||||
opts.image.one('content-digest', function (digest) {
|
||||
self.dockerCmd = self.prefix + '@' + digest;
|
||||
});
|
||||
opts.image.trigger('get-content-digest');
|
||||
}
|
||||
|
||||
this.copy = function () {
|
||||
if (!self.dockerCmd) {
|
||||
registryUI.showErrorCanNotReadContentDigest();
|
||||
return;
|
||||
}
|
||||
const copyText = this.refs['input'];
|
||||
copyText.style.display = 'block';
|
||||
copyText.select();
|
||||
document.execCommand('copy');
|
||||
copyText.style.display = 'none';
|
||||
|
||||
registryUI.snackbar('`' + this.dockerCmd + '` has been copied to clipboard.')
|
||||
};
|
||||
</script>
|
||||
</copy-to-clipboard>
|
|
@ -1,78 +0,0 @@
|
|||
<!--
|
||||
Copyright (C) 2016-2019 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/>.
|
||||
-->
|
||||
<remove-image>
|
||||
<material-button waves-center="true" rounded="true" waves-color="#ddd" title="This will delete the image." if="{ !opts.multiDelete }" disabled="{ !this.digest }">
|
||||
<i class="material-icons">delete</i>
|
||||
</material-button>
|
||||
<material-checkbox if="{ opts.multiDelete }" title="Select this tag to delete it." disabled="{ !this.digest }"></material-checkbox>
|
||||
<script type="text/javascript">
|
||||
const self = this;
|
||||
|
||||
this.on('updated', function() {
|
||||
if (self.multiDelete == self.opts.multiDelete) {
|
||||
return;
|
||||
}
|
||||
if (self.tags['material-button']) {
|
||||
self.delete = function(ignoreError) {
|
||||
const name = self.opts.image.name;
|
||||
const tag = self.opts.image.tag;
|
||||
registryUI.taglist.go(name);
|
||||
if (!self.digest) {
|
||||
registryUI.snackbar('Information for ' + name + ':' + tag + ' are not yet loaded.');
|
||||
return;
|
||||
}
|
||||
const oReq = new Http();
|
||||
oReq.addEventListener('loadend', function() {
|
||||
if (this.status == 200 || this.status == 202) {
|
||||
registryUI.taglist.display()
|
||||
registryUI.snackbar('Deleting ' + name + ':' + tag + ' image. Run `registry garbage-collect config.yml` on your registry');
|
||||
} else if (this.status == 404) {
|
||||
ignoreError || registryUI.errorSnackbar('Digest not found for this image in your registry.');
|
||||
} else {
|
||||
registryUI.snackbar(this.responseText);
|
||||
}
|
||||
});
|
||||
oReq.open('DELETE', registryUI.url() + '/v2/' + name + '/manifests/' + self.digest);
|
||||
oReq.setRequestHeader('Accept', 'application/vnd.docker.distribution.manifest.v2+json, application/vnd.oci.image.manifest.v1+json');
|
||||
oReq.addEventListener('error', function() {
|
||||
registryUI.errorSnackbar('An error occurred when deleting image. Check if your server accept DELETE methods Access-Control-Allow-Methods: [\'DELETE\'].');
|
||||
});
|
||||
oReq.send();
|
||||
};
|
||||
self.tags['material-button'].root.onclick = function() {
|
||||
self.delete();
|
||||
}
|
||||
}
|
||||
|
||||
if (self.tags['material-checkbox']) {
|
||||
if (!self.opts.multiDelete && self.tags['material-checkbox'].checked) {
|
||||
self.tags['material-checkbox'].toggle();
|
||||
}
|
||||
self.tags['material-checkbox'].on('toggle', function() {
|
||||
registryUI.taglist.instance.trigger('toggle-remove-image', this.checked);
|
||||
});
|
||||
}
|
||||
self.multiDelete = self.opts.multiDelete;
|
||||
});
|
||||
|
||||
opts.image.one('content-digest', function(digest) {
|
||||
self.digest = digest;
|
||||
self.update();
|
||||
});
|
||||
opts.image.trigger('get-content-digest');
|
||||
</script>
|
||||
</remove-image>
|
Loading…
Add table
Add a link
Reference in a new issue