feat: added button to delete tag
Signed-off-by: Petu Eusebiu <peusebiu@cisco.com>
This commit is contained in:
parent
2e1e2e92b7
commit
c375c0697a
@ -22,6 +22,7 @@ const mockedTagsData = [
|
|||||||
{
|
{
|
||||||
tag: 'latest',
|
tag: 'latest',
|
||||||
vendor: 'test1',
|
vendor: 'test1',
|
||||||
|
isDeletable: true,
|
||||||
manifests: [
|
manifests: [
|
||||||
{
|
{
|
||||||
lastUpdated: '2022-07-19T18:06:18.818788283Z',
|
lastUpdated: '2022-07-19T18:06:18.818788283Z',
|
||||||
@ -37,6 +38,7 @@ const mockedTagsData = [
|
|||||||
{
|
{
|
||||||
tag: 'bullseye',
|
tag: 'bullseye',
|
||||||
vendor: 'test1',
|
vendor: 'test1',
|
||||||
|
isDeletable: true,
|
||||||
manifests: [
|
manifests: [
|
||||||
{
|
{
|
||||||
digest: 'sha256:adca4815c494becc1bf053af0c4640b2d81ab1a779e6d649e1b8b92a75f1d559',
|
digest: 'sha256:adca4815c494becc1bf053af0c4640b2d81ab1a779e6d649e1b8b92a75f1d559',
|
||||||
@ -52,6 +54,7 @@ const mockedTagsData = [
|
|||||||
{
|
{
|
||||||
tag: '1.5.2',
|
tag: '1.5.2',
|
||||||
vendor: 'test1',
|
vendor: 'test1',
|
||||||
|
isDeletable: true,
|
||||||
manifests: [
|
manifests: [
|
||||||
{
|
{
|
||||||
lastUpdated: '2022-07-19T18:06:18.818788283Z',
|
lastUpdated: '2022-07-19T18:06:18.818788283Z',
|
||||||
@ -76,6 +79,18 @@ describe('Tags component', () => {
|
|||||||
await waitFor(() => expect(screen.queryByText(/OS\/ARCH/i)).not.toBeInTheDocument());
|
await waitFor(() => expect(screen.queryByText(/OS\/ARCH/i)).not.toBeInTheDocument());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should see delete tag button and its dialog', async () => {
|
||||||
|
render(<TagsThemeWrapper />);
|
||||||
|
const deleteBtn = await screen.findAllByTestId('DeleteIcon');
|
||||||
|
fireEvent.click(deleteBtn[0]);
|
||||||
|
expect(await screen.findByTestId('delete-dialog')).toBeInTheDocument();
|
||||||
|
const confirmBtn = await screen.findByTestId('confirm-delete');
|
||||||
|
expect(confirmBtn).toBeInTheDocument();
|
||||||
|
fireEvent.click(confirmBtn);
|
||||||
|
expect(await screen.findByTestId('confirm-delete')).toBeInTheDocument();
|
||||||
|
expect(await screen.findByTestId('cancel-delete')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it('should navigate to tag page details when tag is clicked', async () => {
|
it('should navigate to tag page details when tag is clicked', async () => {
|
||||||
render(<TagsThemeWrapper />);
|
render(<TagsThemeWrapper />);
|
||||||
const tagLink = await screen.findByText('latest');
|
const tagLink = await screen.findByText('latest');
|
||||||
|
@ -81,12 +81,13 @@ const endpoints = {
|
|||||||
authConfig: `/v2/_zot/ext/mgmt`,
|
authConfig: `/v2/_zot/ext/mgmt`,
|
||||||
openidAuth: `/zot/auth/login`,
|
openidAuth: `/zot/auth/login`,
|
||||||
logout: `/zot/auth/logout`,
|
logout: `/zot/auth/logout`,
|
||||||
|
deleteImage: (name, tag) => `/v2/${name}/manifests/${tag}`,
|
||||||
repoList: ({ pageNumber = 1, pageSize = 15 } = {}) =>
|
repoList: ({ pageNumber = 1, pageSize = 15 } = {}) =>
|
||||||
`/v2/_zot/ext/search?query={RepoListWithNewestImage(requestedPage: {limit:${pageSize} offset:${
|
`/v2/_zot/ext/search?query={RepoListWithNewestImage(requestedPage: {limit:${pageSize} offset:${
|
||||||
(pageNumber - 1) * pageSize
|
(pageNumber - 1) * pageSize
|
||||||
}}){Results {Name LastUpdated Size Platforms {Os Arch} NewestImage { Tag Vulnerabilities {MaxSeverity Count} Description Licenses Title Source IsSigned SignatureInfo { Tool IsTrusted Author } Documentation Vendor Labels} IsStarred IsBookmarked StarCount DownloadCount}}}`,
|
}}){Results {Name LastUpdated Size Platforms {Os Arch} NewestImage { Tag Vulnerabilities {MaxSeverity Count} Description Licenses Title Source IsSigned SignatureInfo { Tool IsTrusted Author } Documentation Vendor Labels} IsStarred IsBookmarked StarCount DownloadCount}}}`,
|
||||||
detailedRepoInfo: (name) =>
|
detailedRepoInfo: (name) =>
|
||||||
`/v2/_zot/ext/search?query={ExpandedRepoInfo(repo:"${name}"){Images {Manifests {Digest Platform {Os Arch} Size} Vulnerabilities {MaxSeverity Count} Tag LastUpdated Vendor } Summary {Name LastUpdated Size Platforms {Os Arch} Vendors IsStarred IsBookmarked NewestImage {RepoName IsSigned SignatureInfo { Tool IsTrusted Author } Vulnerabilities {MaxSeverity Count} Manifests {Digest} Tag Vendor Title Documentation DownloadCount Source Description Licenses}}}}`,
|
`/v2/_zot/ext/search?query={ExpandedRepoInfo(repo:"${name}"){Images {Manifests {Digest Platform {Os Arch} Size} Vulnerabilities {MaxSeverity Count} Tag LastUpdated Vendor IsDeletable } Summary {Name LastUpdated Size Platforms {Os Arch} Vendors IsStarred IsBookmarked NewestImage {RepoName IsSigned SignatureInfo { Tool IsTrusted Author } Vulnerabilities {MaxSeverity Count} Manifests {Digest} Tag Vendor Title Documentation DownloadCount Source Description Licenses}}}}`,
|
||||||
detailedImageInfo: (name, tag) =>
|
detailedImageInfo: (name, tag) =>
|
||||||
`/v2/_zot/ext/search?query={Image(image: "${name}:${tag}"){RepoName IsSigned SignatureInfo { Tool IsTrusted Author } Vulnerabilities {MaxSeverity Count} Referrers {MediaType ArtifactType Size Digest Annotations{Key Value}} Tag Manifests {History {Layer {Size Digest} HistoryDescription {CreatedBy EmptyLayer}} Digest ConfigDigest LastUpdated Size Platform {Os Arch}} Vendor Licenses }}`,
|
`/v2/_zot/ext/search?query={Image(image: "${name}:${tag}"){RepoName IsSigned SignatureInfo { Tool IsTrusted Author } Vulnerabilities {MaxSeverity Count} Referrers {MediaType ArtifactType Size Digest Annotations{Key Value}} Tag Manifests {History {Layer {Size Digest} HistoryDescription {CreatedBy EmptyLayer}} Digest ConfigDigest LastUpdated Size Platform {Os Arch}} Vendor Licenses }}`,
|
||||||
vulnerabilitiesForRepo: (name, { pageNumber = 1, pageSize = 15 }, searchTerm = '') => {
|
vulnerabilitiesForRepo: (name, { pageNumber = 1, pageSize = 15 }, searchTerm = '') => {
|
||||||
|
@ -197,6 +197,10 @@ function RepoDetails() {
|
|||||||
};
|
};
|
||||||
}, [name]);
|
}, [name]);
|
||||||
|
|
||||||
|
const handleDeleteTag = (removed) => {
|
||||||
|
setTags((prevState) => prevState.filter((tag) => tag.tag !== removed));
|
||||||
|
};
|
||||||
|
|
||||||
const handlePlatformChipClick = (event) => {
|
const handlePlatformChipClick = (event) => {
|
||||||
const { textContent } = event.target;
|
const { textContent } = event.target;
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@ -223,7 +227,7 @@ function RepoDetails() {
|
|||||||
|
|
||||||
const handleBookmarkClick = () => {
|
const handleBookmarkClick = () => {
|
||||||
api.put(`${host()}${endpoints.bookmarkToggle(name)}`, abortController.signal).then((response) => {
|
api.put(`${host()}${endpoints.bookmarkToggle(name)}`, abortController.signal).then((response) => {
|
||||||
if (response.status === 200) {
|
if (response && response.status === 200) {
|
||||||
setRepoDetailData((prevState) => ({
|
setRepoDetailData((prevState) => ({
|
||||||
...prevState,
|
...prevState,
|
||||||
isBookmarked: !prevState.isBookmarked
|
isBookmarked: !prevState.isBookmarked
|
||||||
@ -341,7 +345,7 @@ function RepoDetails() {
|
|||||||
<Grid item xs={12} md={8} className={classes.tags}>
|
<Grid item xs={12} md={8} className={classes.tags}>
|
||||||
<Card className={classes.cardRoot}>
|
<Card className={classes.cardRoot}>
|
||||||
<CardContent className={classes.tagsContent}>
|
<CardContent className={classes.tagsContent}>
|
||||||
<Tags tags={tags} />
|
<Tags tags={tags} repoName={name} onTagDelete={handleDeleteTag} />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -43,7 +43,7 @@ const useStyles = makeStyles(() => ({
|
|||||||
|
|
||||||
export default function Tags(props) {
|
export default function Tags(props) {
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const { tags } = props;
|
const { tags, repoName, onTagDelete } = props;
|
||||||
const [tagsFilter, setTagsFilter] = useState('');
|
const [tagsFilter, setTagsFilter] = useState('');
|
||||||
const [sortFilter, setSortFilter] = useState(tagsSortByCriteria.updateTimeDesc.value);
|
const [sortFilter, setSortFilter] = useState(tagsSortByCriteria.updateTimeDesc.value);
|
||||||
|
|
||||||
@ -63,6 +63,9 @@ export default function Tags(props) {
|
|||||||
lastUpdated={tag.lastUpdated}
|
lastUpdated={tag.lastUpdated}
|
||||||
vendor={tag.vendor}
|
vendor={tag.vendor}
|
||||||
manifests={tag.manifests}
|
manifests={tag.manifests}
|
||||||
|
repo={repoName}
|
||||||
|
onTagDelete={onTagDelete}
|
||||||
|
isDeletable={tag.isDeletable}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
54
src/components/Shared/DeleteTag.jsx
Normal file
54
src/components/Shared/DeleteTag.jsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { IconButton } from '@mui/material';
|
||||||
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
|
|
||||||
|
// utility
|
||||||
|
import { api, endpoints } from '../../api';
|
||||||
|
|
||||||
|
// components
|
||||||
|
import DeleteTagConfirmDialog from 'components/Shared/DeleteTagConfirmDialog';
|
||||||
|
import { host } from '../../host';
|
||||||
|
|
||||||
|
export default function DeleteTag(props) {
|
||||||
|
const { repo, tag, onTagDelete } = props;
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleClickOpen = () => {
|
||||||
|
setOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteTag = (repo, tag) => {
|
||||||
|
api
|
||||||
|
.delete(`${host()}${endpoints.deleteImage(repo, tag)}`)
|
||||||
|
.then((response) => {
|
||||||
|
if (response && response.status == 202) {
|
||||||
|
onTagDelete(tag);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onConfirm = () => {
|
||||||
|
deleteTag(repo, tag);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<IconButton onClick={handleClickOpen}>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
<DeleteTagConfirmDialog
|
||||||
|
onClose={handleClose}
|
||||||
|
open={open}
|
||||||
|
title={`Permanently delete image ${repo}:${tag}?`}
|
||||||
|
onConfirm={onConfirm}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
30
src/components/Shared/DeleteTagConfirmDialog.jsx
Normal file
30
src/components/Shared/DeleteTagConfirmDialog.jsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { Button, Dialog, DialogTitle, DialogActions } from '@mui/material';
|
||||||
|
|
||||||
|
export default function DeleteTagConfirmDialog(props) {
|
||||||
|
const { onClose, open, title, onConfirm } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog data-testid="delete-dialog" onClose={onClose} open={open} color="primary">
|
||||||
|
<DialogTitle> {title} </DialogTitle>
|
||||||
|
<DialogActions style={{ justifyContent: 'center' }}>
|
||||||
|
<Button data-testid="cancel-delete" variant="contained" onClick={onClose} color="primary">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
data-testid="confirm-delete"
|
||||||
|
color="error"
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => {
|
||||||
|
onConfirm();
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
@ -6,6 +6,7 @@ import { Markdown } from 'utilities/MarkdowntojsxWrapper';
|
|||||||
import transform from 'utilities/transform';
|
import transform from 'utilities/transform';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { KeyboardArrowDown, KeyboardArrowRight } from '@mui/icons-material';
|
import { KeyboardArrowDown, KeyboardArrowRight } from '@mui/icons-material';
|
||||||
|
import DeleteTag from 'components/Shared/DeleteTag';
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
card: {
|
card: {
|
||||||
@ -78,9 +79,9 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
export default function TagCard(props) {
|
export default function TagCard(props) {
|
||||||
const { repoName, tag, lastUpdated, vendor, manifests } = props;
|
const { repoName, tag, lastUpdated, vendor, manifests, repo, onTagDelete, isDeletable } = props;
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
|
|
||||||
const lastDate = lastUpdated
|
const lastDate = lastUpdated
|
||||||
@ -99,9 +100,12 @@ export default function TagCard(props) {
|
|||||||
return (
|
return (
|
||||||
<Card className={classes.card} raised>
|
<Card className={classes.card} raised>
|
||||||
<CardContent className={classes.content}>
|
<CardContent className={classes.content}>
|
||||||
<Typography variant="body1" align="left" className={classes.tagHeading}>
|
<Stack direction="row" spacing={2} justifyContent="space-between">
|
||||||
Tag
|
<Typography variant="body1" align="left" className={classes.tagHeading}>
|
||||||
</Typography>
|
Tag
|
||||||
|
</Typography>
|
||||||
|
{isDeletable && <DeleteTag repo={repo} tag={tag} onTagDelete={onTagDelete} />}
|
||||||
|
</Stack>
|
||||||
<Typography variant="body1" align="left" className={classes.tagName} onClick={() => goToTags()}>
|
<Typography variant="body1" align="left" className={classes.tagName} onClick={() => goToTags()}>
|
||||||
{repoName && `${repoName}:`}
|
{repoName && `${repoName}:`}
|
||||||
{tag}
|
{tag}
|
||||||
|
@ -69,6 +69,7 @@ const mapToImage = (responseImage) => {
|
|||||||
authors: responseImage.Authors,
|
authors: responseImage.Authors,
|
||||||
vulnerabiltySeverity: responseImage.Vulnerabilities?.MaxSeverity,
|
vulnerabiltySeverity: responseImage.Vulnerabilities?.MaxSeverity,
|
||||||
vulnerabilityCount: responseImage.Vulnerabilities?.Count,
|
vulnerabilityCount: responseImage.Vulnerabilities?.Count,
|
||||||
|
isDeletable: responseImage.IsDeletable,
|
||||||
// frontend only prop to increase interop with Repo objects and code reusability
|
// frontend only prop to increase interop with Repo objects and code reusability
|
||||||
name: `${responseImage.RepoName}:${responseImage.Tag}`
|
name: `${responseImage.RepoName}:${responseImage.Tag}`
|
||||||
};
|
};
|
||||||
|
@ -19,7 +19,7 @@ const pageSizes = {
|
|||||||
const endpoints = {
|
const endpoints = {
|
||||||
repoList: `/v2/_zot/ext/search?query={RepoListWithNewestImage(requestedPage:%20{limit:15%20offset:0}){Results%20{Name%20LastUpdated%20Size%20Platforms%20{Os%20Arch}%20%20NewestImage%20{%20Tag%20Vulnerabilities%20{MaxSeverity%20Count}%20Description%20%20Licenses%20Logo%20Title%20Source%20IsSigned%20Documentation%20Vendor%20Labels}%20StarCount%20DownloadCount}}}`,
|
repoList: `/v2/_zot/ext/search?query={RepoListWithNewestImage(requestedPage:%20{limit:15%20offset:0}){Results%20{Name%20LastUpdated%20Size%20Platforms%20{Os%20Arch}%20%20NewestImage%20{%20Tag%20Vulnerabilities%20{MaxSeverity%20Count}%20Description%20%20Licenses%20Logo%20Title%20Source%20IsSigned%20Documentation%20Vendor%20Labels}%20StarCount%20DownloadCount}}}`,
|
||||||
detailedRepoInfo: (name) =>
|
detailedRepoInfo: (name) =>
|
||||||
`/v2/_zot/ext/search?query={ExpandedRepoInfo(repo:%22${name}%22){Images%20{Manifests%20{Digest%20Platform%20{Os%20Arch}%20Size}%20Vulnerabilities%20{MaxSeverity%20Count}%20Tag%20LastUpdated%20Vendor%20}%20Summary%20{Name%20LastUpdated%20Size%20Platforms%20{Os%20Arch}%20Vendors%20NewestImage%20{RepoName%20IsSigned%20Vulnerabilities%20{MaxSeverity%20Count}%20Manifests%20{Digest}%20Tag%20Title%20Documentation%20DownloadCount%20Source%20Description%20Licenses}}}}`,
|
`/v2/_zot/ext/search?query={ExpandedRepoInfo(repo:%22${name}%22){Images%20{Manifests%20{Digest%20Platform%20{Os%20Arch}%20Size}%20Vulnerabilities%20{MaxSeverity%20Count}%20Tag%20LastUpdated%20Vendor%20IsDeletable%20}%20Summary%20{Name%20LastUpdated%20Size%20Platforms%20{Os%20Arch}%20Vendors%20NewestImage%20{RepoName%20IsSigned%20Vulnerabilities%20{MaxSeverity%20Count}%20Manifests%20{Digest}%20Tag%20Title%20Documentation%20DownloadCount%20Source%20Description%20Licenses}}}}`,
|
||||||
globalSearch: (searchTerm, sortCriteria, pageNumber = 1, pageSize = 10) =>
|
globalSearch: (searchTerm, sortCriteria, pageNumber = 1, pageSize = 10) =>
|
||||||
`/v2/_zot/ext/search?query={GlobalSearch(query:%22${searchTerm}%22,%20requestedPage:%20{limit:${pageSize}%20offset:${
|
`/v2/_zot/ext/search?query={GlobalSearch(query:%22${searchTerm}%22,%20requestedPage:%20{limit:${pageSize}%20offset:${
|
||||||
10 * (pageNumber - 1)
|
10 * (pageNumber - 1)
|
||||||
|
Loading…
Reference in New Issue
Block a user