Layers tab features (#87)
* Added API data that was ready on 29th of September * Added the layers tab in tag page. Signed-off-by: Amelia-Maria Breda <ameliamaria.breda@dxc.com>
This commit is contained in:
parent
4873d84efc
commit
9cbf029786
@ -83,7 +83,7 @@ describe('Tags details', () => {
|
||||
// @ts-ignore
|
||||
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockImage } });
|
||||
render(<TagDetails />);
|
||||
await waitFor(() => expect(screen.getAllByRole('tab')).toHaveLength(1));
|
||||
await waitFor(() => expect(screen.getAllByRole('tab')).toHaveLength(4));
|
||||
});
|
||||
|
||||
it("should log an error when data can't be fetched", async () => {
|
||||
|
@ -64,7 +64,10 @@ const endpoints = {
|
||||
detailedRepoInfo: (name) =>
|
||||
`/v2/_zot/ext/search?query={ExpandedRepoInfo(repo:"${name}"){Images {Digest Tag Layers {Size Digest}} Summary {Name LastUpdated Size Platforms {Os Arch} Vendors NewestImage {RepoName Layers {Size Digest} Digest Tag Title Documentation DownloadCount Source Description History {Layer {Size Digest} HistoryDescription {Created CreatedBy Author Comment EmptyLayer}}}}}}`,
|
||||
vulnerabilitiesForRepo: (name) =>
|
||||
`/v2/_zot/ext/search?query={CVEListForImage(image: "${name}"){Tag, CVEList {Id Title Description Severity PackageList {Name InstalledVersion FixedVersion}}}}`
|
||||
`/v2/_zot/ext/search?query={CVEListForImage(image: "${name}"){Tag, CVEList {Id Title Description Severity PackageList {Name InstalledVersion FixedVersion}}}}`,
|
||||
layersDetailsForImage: (name) =>
|
||||
`/v2/_zot/ext/search?query={Image(image: "${name}"){History {Layer {Size Digest Score} HistoryDescription {Created CreatedBy Author Comment EmptyLayer} }}}`,
|
||||
dependsOnForImage: (name) => `/v2/_zot/ext/search?query={BaseImageList(image: "${name}"){RepoName}}`
|
||||
};
|
||||
|
||||
export { api, endpoints };
|
||||
|
93
src/components/DependsOn.jsx
Normal file
93
src/components/DependsOn.jsx
Normal file
@ -0,0 +1,93 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
// utility
|
||||
import { api, endpoints } from '../api';
|
||||
|
||||
// components
|
||||
import { Divider, Typography } from '@mui/material';
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
import { host } from '../host';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
card: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
background: '#FFFFFF',
|
||||
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
|
||||
borderRadius: '1.875rem',
|
||||
flex: 'none',
|
||||
alignSelf: 'stretch',
|
||||
flexGrow: 0,
|
||||
order: 0,
|
||||
width: '100%',
|
||||
marginTop: '2rem',
|
||||
marginBottom: '2rem'
|
||||
},
|
||||
content: {
|
||||
textAlign: 'left',
|
||||
color: '#606060',
|
||||
padding: '2% 3% 2% 3%',
|
||||
width: '100%'
|
||||
},
|
||||
title: {
|
||||
color: '#828282',
|
||||
fontSize: '1rem',
|
||||
paddingRight: '0.5rem',
|
||||
paddingBottom: '0.5rem',
|
||||
paddingTop: '0.5rem'
|
||||
},
|
||||
values: {
|
||||
color: '#000000',
|
||||
fontSize: '1rem',
|
||||
fontWeight: '600',
|
||||
paddingBottom: '0.5rem',
|
||||
paddingTop: '0.5rem'
|
||||
}
|
||||
}));
|
||||
|
||||
function DependsOn(props) {
|
||||
const [images, setImages] = useState([]);
|
||||
const { name } = props;
|
||||
const classes = useStyles();
|
||||
|
||||
useEffect(() => {
|
||||
api
|
||||
.get(`${host()}${endpoints.dependsOnForImage(name)}`)
|
||||
.then((response) => {
|
||||
if (response.data && response.data.data) {
|
||||
let images = response.data.data.BaseImageList;
|
||||
// let cveListData = {
|
||||
// cveList: cveInfo?.CVEList
|
||||
// }
|
||||
setImages(images);
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
setImages([]);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Typography
|
||||
variant="h4"
|
||||
gutterBottom
|
||||
component="div"
|
||||
align="left"
|
||||
className={classes.title}
|
||||
style={{ color: 'rgba(0, 0, 0, 0.87)', fontSize: '1.5rem', fontWeight: '600', paddingTop: '0.5rem' }}
|
||||
>
|
||||
Depends On
|
||||
</Typography>
|
||||
<Divider
|
||||
variant="fullWidth"
|
||||
sx={{ margin: '5% 0% 5% 0%', background: 'rgba(0, 0, 0, 0.38)', height: '0.00625rem', width: '100%' }}
|
||||
/>
|
||||
{console.log(JSON.stringify(images))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DependsOn;
|
186
src/components/HistoryLayers.jsx
Normal file
186
src/components/HistoryLayers.jsx
Normal file
@ -0,0 +1,186 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import transform from 'utilities/transform';
|
||||
|
||||
// utility
|
||||
import { api, endpoints } from '../api';
|
||||
|
||||
// components
|
||||
import { Card, CardContent, Divider, Grid, Stack, Typography } from '@mui/material';
|
||||
import makeStyles from '@mui/styles/makeStyles';
|
||||
import { host } from '../host';
|
||||
|
||||
const useStyles = makeStyles(() => ({
|
||||
card: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
background: '#FFFFFF',
|
||||
boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
|
||||
borderRadius: '1.875rem',
|
||||
flex: 'none',
|
||||
alignSelf: 'stretch',
|
||||
flexGrow: 0,
|
||||
order: 0,
|
||||
width: '100%',
|
||||
marginTop: '0rem',
|
||||
marginBottom: '0rem',
|
||||
padding: '1rem 1.5rem '
|
||||
},
|
||||
content: {
|
||||
textAlign: 'left',
|
||||
color: '#606060',
|
||||
width: '100%',
|
||||
flexDirection: 'column'
|
||||
},
|
||||
title: {
|
||||
color: '#14191F',
|
||||
fontSize: '1rem',
|
||||
fontWeight: '400',
|
||||
paddingRight: '0.5rem',
|
||||
paddingBottom: '0.5rem',
|
||||
paddingTop: '0.5rem'
|
||||
},
|
||||
layer: {
|
||||
color: '#14191F',
|
||||
fontSize: '1rem',
|
||||
fontWeight: '400',
|
||||
paddingRight: '0.5rem',
|
||||
paddingBottom: '0.5rem',
|
||||
paddingTop: '0.5rem',
|
||||
width: '100%',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
cursor: 'pointer'
|
||||
},
|
||||
values: {
|
||||
color: '#52637A',
|
||||
fontSize: '1rem',
|
||||
fontWeight: '400',
|
||||
paddingBottom: '0.5rem',
|
||||
paddingTop: '0.5rem'
|
||||
}
|
||||
}));
|
||||
|
||||
function LayerCard(props) {
|
||||
const classes = useStyles();
|
||||
const [size, setSize] = useState(0);
|
||||
const { index, layer, historyDescription, isSelected } = props;
|
||||
|
||||
useEffect(() => {
|
||||
if (historyDescription.EmptyLayer) {
|
||||
let s = 0;
|
||||
setSize(s);
|
||||
} else {
|
||||
setSize(layer.Size);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Grid sx={isSelected ? { backgroundColor: '#F7F7F7' } : null} container>
|
||||
<Grid item xs={10} container>
|
||||
<Grid item xs={1}>
|
||||
<Typography variant="body1" align="left" className={classes.title}>
|
||||
{index}:{' '}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<Typography variant="body1" align="left" className={classes.layer}>
|
||||
{historyDescription.CreatedBy}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={2}>
|
||||
<Typography variant="body1" align="left" className={classes.values}>
|
||||
{' '}
|
||||
{transform.formatBytes(size)}{' '}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
function HistoryLayers(props) {
|
||||
const classes = useStyles();
|
||||
const [historyData, setHistoryData] = useState([]);
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
const { name } = props;
|
||||
|
||||
useEffect(() => {
|
||||
api
|
||||
.get(`${host()}${endpoints.layersDetailsForImage(name)}`)
|
||||
.then((response) => {
|
||||
if (response.data && response.data.data) {
|
||||
let layersHistory = response.data.data.Image;
|
||||
setHistoryData(layersHistory?.History);
|
||||
setIsLoaded(true);
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
setHistoryData([]);
|
||||
setIsLoaded(false);
|
||||
});
|
||||
}, [name]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Typography
|
||||
variant="h4"
|
||||
gutterBottom
|
||||
component="div"
|
||||
align="left"
|
||||
style={{ color: 'rgba(0, 0, 0, 0.87)', fontSize: '1.5rem', fontWeight: '600', paddingTop: '0.5rem' }}
|
||||
>
|
||||
Layers
|
||||
</Typography>
|
||||
<Divider
|
||||
variant="fullWidth"
|
||||
sx={{ margin: '5% 0% 0% 0%', background: 'rgba(0, 0, 0, 0.38)', height: '0.00625rem', width: '100%' }}
|
||||
/>
|
||||
<Card className={classes.card} raised>
|
||||
<CardContent className={classes.content}>
|
||||
{historyData &&
|
||||
historyData.map((layer, index) => {
|
||||
return (
|
||||
<div key={`${layer?.Layer?.Size}${index}`} onClick={() => setSelectedIndex(index)}>
|
||||
<LayerCard
|
||||
key={`${layer?.Layer?.Size}${index}`}
|
||||
index={index + 1}
|
||||
isSelected={selectedIndex === index}
|
||||
layer={layer?.Layer}
|
||||
historyDescription={layer?.HistoryDescription}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</CardContent>
|
||||
</Card>
|
||||
{isLoaded && historyData && (
|
||||
<Card className={classes.card} raised>
|
||||
<CardContent className={classes.content}>
|
||||
<Grid item xs={11}>
|
||||
<Stack sx={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between' }}>
|
||||
<Typography variant="body1" align="left" className={classes.title}>
|
||||
{' '}
|
||||
Command{' '}
|
||||
</Typography>
|
||||
<Typography variant="body1" align="left" className={classes.values}>
|
||||
{' '}
|
||||
{transform.formatBytes(historyData[selectedIndex].Layer?.Size)}{' '}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Typography variant="body1" align="left" className={classes.title} sx={{ backgroundColor: '#F7F7F7' }}>
|
||||
{' '}
|
||||
{historyData[selectedIndex].HistoryDescription?.CreatedBy}{' '}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default HistoryLayers;
|
@ -272,14 +272,6 @@ function RepoDetails(props) {
|
||||
// </Card>);
|
||||
// };
|
||||
|
||||
// const renderVulnerabilities = () => {
|
||||
// return (<Card className={classes.card}>
|
||||
// <CardContent>
|
||||
// <Typography variant="h4" align="left">Vulnerabilities</Typography>
|
||||
// </CardContent>
|
||||
// </Card>);
|
||||
// };
|
||||
|
||||
return (
|
||||
<div className={classes.pageWrapper}>
|
||||
<Card className={classes.cardRoot}>
|
||||
|
@ -18,6 +18,8 @@ import repocube4 from '../assets/repocube-4.png';
|
||||
import { TabContext, TabList, TabPanel } from '@mui/lab';
|
||||
import TagDetailsMetadata from './TagDetailsMetadata';
|
||||
import VulnerabilitiesDetails from './VulnerabilitiesDetails';
|
||||
import HistoryLayers from './HistoryLayers';
|
||||
import DependsOn from './DependsOn';
|
||||
|
||||
// @ts-ignore
|
||||
const useStyles = makeStyles(() => ({
|
||||
@ -117,8 +119,9 @@ const randomImage = () => {
|
||||
function TagDetails() {
|
||||
const [repoDetailData, setRepoDetailData] = useState({});
|
||||
// @ts-ignore
|
||||
// const [isLoading, setIsLoading] = useState(false);
|
||||
const [selectedTab, setSelectedTab] = useState('Vulnerabilities');
|
||||
//const [isLoading, setIsLoading] = useState(false);
|
||||
const [selectedTab, setSelectedTab] = useState('Layers');
|
||||
const [tagName, setTagName] = useState('');
|
||||
|
||||
// get url param from <Route here (i.e. image name)
|
||||
const { name } = useParams();
|
||||
@ -140,10 +143,11 @@ function TagDetails() {
|
||||
layers: repoInfo.Images[0].Layers,
|
||||
platforms: repoInfo.Summary?.Platforms,
|
||||
vendors: repoInfo.Summary?.Vendors,
|
||||
newestTag: repoInfo.Summary?.NewestImage
|
||||
newestTag: repoInfo.Summary?.NewestImage.Tag
|
||||
};
|
||||
setRepoDetailData(imageData);
|
||||
// setIsLoading(false);
|
||||
setTagName(imageData.name + ':' + imageData.newestTag);
|
||||
//setIsLoading(false);
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
@ -232,7 +236,7 @@ function TagDetails() {
|
||||
{name}:
|
||||
{
|
||||
// @ts-ignore
|
||||
repoDetailData?.tags
|
||||
repoDetailData?.newestTag
|
||||
}
|
||||
</Typography>
|
||||
{/* {vulnerabilityCheck()}
|
||||
@ -241,12 +245,7 @@ function TagDetails() {
|
||||
</Stack>
|
||||
<Typography
|
||||
pt={1}
|
||||
sx={{
|
||||
fontSize: 16,
|
||||
lineHeight: '1.5rem',
|
||||
color: 'rgba(0, 0, 0, 0.6)',
|
||||
paddingLeft: '4rem'
|
||||
}}
|
||||
sx={{ fontSize: 16, lineHeight: '1.5rem', color: 'rgba(0, 0, 0, 0.6)', paddingLeft: '4rem' }}
|
||||
gutterBottom
|
||||
align="left"
|
||||
>
|
||||
@ -267,22 +266,22 @@ function TagDetails() {
|
||||
TabIndicatorProps={{ className: classes.selectedTab }}
|
||||
sx={{ '& button.Mui-selected': { color: '#14191F', fontWeight: '600' } }}
|
||||
>
|
||||
{/* <Tab value="Layers" label="Layers" className={classes.tabContent}/>
|
||||
<Tab value="DependsOn" label="Depends on" className={classes.tabContent}/>
|
||||
<Tab value="IsDependentOn" label="Is Dependent On" className={classes.tabContent}/> */}
|
||||
<Tab value="Layers" label="Layers" className={classes.tabContent} />
|
||||
<Tab value="DependsOn" label="Depends on" className={classes.tabContent} />
|
||||
<Tab value="IsDependentOn" label="Is dependent on" className={classes.tabContent} />
|
||||
<Tab value="Vulnerabilities" label="Vulnerabilities" className={classes.tabContent} />
|
||||
</TabList>
|
||||
<Grid container>
|
||||
<Grid item xs={12}>
|
||||
{/* <TabPanel value="Layers" className={classes.tabPanel}>
|
||||
<Typography> Layers </Typography>
|
||||
</TabPanel>
|
||||
<TabPanel value="DependsOn" className={classes.tabPanel}>
|
||||
<Typography> Depends On </Typography>
|
||||
</TabPanel>
|
||||
<TabPanel value="IsDependentOn" className={classes.tabPanel}>
|
||||
<Typography> Is Dependent On </Typography>
|
||||
</TabPanel> */}
|
||||
<TabPanel value="Layers" className={classes.tabPanel}>
|
||||
<HistoryLayers name={tagName} />
|
||||
</TabPanel>
|
||||
<TabPanel value="DependsOn" className={classes.tabPanel}>
|
||||
<DependsOn name={tagName} />
|
||||
</TabPanel>
|
||||
<TabPanel value="IsDependentOn" className={classes.tabPanel}>
|
||||
<Typography> Is Dependent On </Typography>
|
||||
</TabPanel>
|
||||
<TabPanel value="Vulnerabilities" className={classes.tabPanel}>
|
||||
<VulnerabilitiesDetails name={name} />
|
||||
</TabPanel>
|
||||
|
@ -85,15 +85,10 @@ function TagCard(props) {
|
||||
<Typography
|
||||
variant="body1"
|
||||
align="left"
|
||||
sx={{
|
||||
color: '#1479FF',
|
||||
fontSize: '1rem',
|
||||
textDecorationLine: 'underline',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onClick={() => goToTags(tagRow?.Tag)}
|
||||
sx={{ color: '#1479FF', fontSize: '1rem', textDecorationLine: 'underline', cursor: 'pointer' }}
|
||||
onClick={() => goToTags(tagRow.Tag)}
|
||||
>
|
||||
{tagRow.Tag}
|
||||
{tagRow?.Tag}
|
||||
</Typography>
|
||||
|
||||
<Stack sx={{ display: 'inline' }} direction="row" spacing={0.5}>
|
||||
|
@ -2,8 +2,10 @@ const transform = {
|
||||
// takes raw # of bytes and decimal value to be returned;
|
||||
// returns bytes with nearest human-readable unit
|
||||
formatBytes: (bytes) => {
|
||||
if (isNaN(bytes) || bytes === 0) {
|
||||
return 0;
|
||||
if (isNaN(bytes)) {
|
||||
return '0 Bytes';
|
||||
} else if (bytes === 0) {
|
||||
return '0 Bytes';
|
||||
}
|
||||
|
||||
const DATA_UNITS = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
Loading…
x
Reference in New Issue
Block a user