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:
Amelia-Maria Breda 2022-09-30 16:41:33 +03:00 committed by GitHub
parent 4873d84efc
commit 9cbf029786
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 313 additions and 43 deletions

View File

@ -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 () => {

View File

@ -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 };

View 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;

View 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;

View File

@ -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}>

View File

@ -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>

View File

@ -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}>

View File

@ -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'];