docker-registry-ui/src/components/tag-list/tag-table.riot

294 lines
10 KiB
Plaintext

<!--
Copyright (C) 2016-2023 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/>.
-->
<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 }"
is-registry-secured="{ props.isRegistrySecured }"
on-image-deleted="{ props.onImageDeleted }"
></confirm-delete-image>
<material-card class="taglist">
<table style="border: none">
<thead>
<tr>
<th
class="creation-date { (state.desc && state.orderType === 'date') ? 'material-card-th-sorted-descending' : 'material-card-th-sorted-ascending' }"
onclick="{() => onPageReorder('date') }"
>
Creation date
</th>
<th
class="image-size { (state.desc && state.orderType === 'size') ? 'material-card-th-sorted-descending' : 'material-card-th-sorted-ascending' }"
onclick="{() => onPageReorder('size') }"
>
Size
</th>
<th id="image-content-digest-header" if="{ props.showContentDigest }">Content Digest</th>
<th
id="image-tag-header"
class="{ props.asc ? 'material-card-th-sorted-ascending' : 'material-card-th-sorted-descending' }"
onclick="{ onReverseOrder }"
>
Tag
</th>
<th class="architectures">Arch</th>
<th class="show-tag-history" if="{ props.showTagHistory }">History</th>
<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
if="{ state.toDelete.size > 0 && !state.singleDeleteAction }"
waves-center="true"
color="inherit"
text-color="var(--neutral-background)"
waves-color="var(--hover-background)"
title="This will delete selected images."
onClick="{ deleteImages }"
icon
>
<i class="material-icons">delete</i>
</material-button>
</th>
</tr>
</thead>
<tbody>
<tr each="{ image in getPage(props.tags, props.page) }" if="{ matchSearch(props.filterResults, image.tag) }">
<td class="creation-date">
<image-date image="{ image }"></image-date>
</td>
<td class="image-size">
<image-size image="{ image }"></image-size>
</td>
<td if="{ props.showContentDigest }">
<image-content-digest image="{ image }"></image-content-digest>
<copy-to-clipboard
target="digest"
image="{ image }"
pull-url="{ props.pullUrl }"
on-notify="{ props.onNotify }"
></copy-to-clipboard>
</td>
<td>
<image-tag image="{ image }"></image-tag>
<copy-to-clipboard
target="tag"
image="{ image }"
pull-url="{ props.pullUrl }"
on-notify="{ props.onNotify }"
></copy-to-clipboard>
</td>
<td class="architectures">
<architectures image="{ image }"></architectures>
</td>
<td class="show-tag-history" if="{ props.showTagHistory }">
<tag-history-button image="{ image }"></tag-history-button>
</td>
<td if="{ props.isImageRemoveActivated }" class="remove-tag">
<remove-image
multi-delete="{ state.multiDelete }"
image="{ image }"
registry-url="{ props.registryUrl }"
handleCheckboxChange="{ onRemoveImageChange }"
checked="{ state.toDelete.has(image) }"
on-notify="{ props.onNotify }"
on-authentication="{ props.onAuthentication }"
></remove-image>
</td>
</tr>
</tbody>
</table>
</material-card>
<script>
import { getPage } from '../../scripts/utils';
import ImageDate from './image-date.riot';
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 TagHistoryButton from './tag-history-button.riot';
import RemoveImage from './remove-image.riot';
import Architectures from './architectures.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,
ImageSize,
ImageTag,
ImageContentDigest,
CopyToClipboard,
RemoveImage,
TagHistoryButton,
ConfirmDeleteImage,
Architectures,
},
onBeforeMount(props) {
this.state = {
toDelete: new Set(),
multiDelete: false,
page: props.page,
};
},
onBeforeUpdate(props, state) {
if (state.page !== props.page) {
state.toDelete.clear();
}
state.page = props.page;
},
deleteImages() {
this.update({
confirmDeleteImage: true,
});
},
onConfirmDeleteImageClick() {
if (this.state.singleDeleteAction) {
this.state.toDelete.clear();
}
this.update({
singleDeleteAction: false,
confirmDeleteImage: false,
});
},
onRemoveImageHeaderChange(event) {
if (event.altKey === true) {
const tags = getPage(this.props.tags, this.props.page);
tags
.filter((image) => matchSearch(this.props.filterResults, image.tag))
.forEach((tag) => this.state.toDelete.add(tag));
this.update({
multiDelete: true,
toDelete: this.state.toDelete,
selectedImage: undefined,
});
} else {
this.update({
multiDelete: event.target.checked,
selectedImage: undefined,
});
}
},
onRemoveImageChange(action, image, shiftKey) {
let confirmDeleteImage = false;
let singleDeleteAction = false;
let selectedImage = undefined;
switch (action) {
case ACTION_CHECK_TO_DELETE: {
this.state.toDelete.add(image);
if (shiftKey) {
selectedImage = this.supportShiftKey(image, true);
}
break;
}
case ACTION_UNCHECK_TO_DELETE: {
this.state.toDelete.delete(image);
if (shiftKey) {
selectedImage = this.supportShiftKey(image, false);
}
break;
}
case ACTION_DELETE_IMAGE: {
this.state.toDelete.clear();
this.state.toDelete.add(image);
confirmDeleteImage = true;
singleDeleteAction = true;
}
}
this.update({
toDelete: this.state.toDelete,
confirmDeleteImage,
singleDeleteAction,
selectedImage,
});
},
supportShiftKey(selectedImage, addOrRemove) {
if (!this.state.selectedImage) {
return selectedImage;
} else {
let shouldChange = false;
const tags = this.getPage(this.props.tags, this.props.page);
tags
.filter((image) => {
if (image == this.state.selectedImage || image == selectedImage) {
shouldChange = !shouldChange;
return true;
}
return shouldChange;
})
.forEach((image) => {
if (addOrRemove) {
this.state.toDelete.add(image);
} else {
this.state.toDelete.delete(image);
}
});
return undefined;
}
},
onReverseOrder() {
this.state.orderType = null;
this.state.desc = false;
this.props.onReverseOrder();
},
onPageReorder(type) {
this.update({
orderType: type,
desc: (this.state.orderType && this.state.orderType !== type) || !this.state.desc,
});
},
getPage(tags, page) {
const sortedTags = getPage(tags, page, this.props.tagsPerPage);
if (this.state.orderType === 'date') {
sortedTags.sort((e1, e2) =>
!this.state.desc
? (e2.creationDate?.getTime() || 0) - (e1.creationDate?.getTime() || 0)
: (e1.creationDate?.getTime() || 0) - (e2.creationDate?.getTime() || 0)
);
} else if (this.state.orderType === 'size') {
sortedTags.sort((e1, e2) => (!this.state.desc ? e2.size - e1.size : e1.size - e2.size));
}
return sortedTags;
},
matchSearch,
};
export { ACTION_CHECK_TO_DELETE, ACTION_UNCHECK_TO_DELETE, ACTION_DELETE_IMAGE };
</script>
<style>
tag-table table th.architectures {
text-align: center;
}
</style>
</tag-table>