feat: Implemented logo display

Signed-off-by: Raul Kele <raulkeleblk@gmail.com>
This commit is contained in:
Raul Kele 2022-10-18 13:41:27 +03:00
parent ac782c04d0
commit 2dc4a35c87
10 changed files with 176 additions and 199 deletions

View File

@ -102,7 +102,7 @@ describe('Tags details', () => {
jest.spyOn(api, 'get').mockRejectedValue({ status: 500, data: {} });
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
render(<TagDetails />);
await waitFor(() => expect(error).toBeCalledTimes(2));
await waitFor(() => expect(error).toBeCalledTimes(1));
});
it('should show tag details metadata', async () => {
// @ts-ignore

View File

@ -63,11 +63,11 @@ const endpoints = {
repoList: ({ pageNumber = 1, pageSize = 15 } = {}) =>
`/v2/_zot/ext/search?query={RepoListWithNewestImage(requestedPage: {limit:${pageSize} offset:${
(pageNumber - 1) * pageSize
}}){Name LastUpdated Size Platforms {Os Arch} NewestImage { Tag Description Licenses Title Source IsSigned Documentation History {Layer {Size Digest} HistoryDescription {Created CreatedBy Author Comment EmptyLayer}} Vendor Labels} DownloadCount}}`,
}}){Name LastUpdated Size Platforms {Os Arch} NewestImage { Tag Description Licenses Logo Title Source IsSigned Documentation History {Layer {Size Digest} HistoryDescription {Created CreatedBy Author Comment EmptyLayer}} Vendor Labels} DownloadCount}}`,
detailedRepoInfo: (name) =>
`/v2/_zot/ext/search?query={ExpandedRepoInfo(repo:"${name}"){Images {Digest Tag LastUpdated Vendor Size Platform {Os Arch} } Summary {Name LastUpdated Size Platforms {Os Arch} Vendors NewestImage {RepoName Layers {Size Digest} Digest Tag Title Documentation DownloadCount Source Description Licenses History {Layer {Size Digest} HistoryDescription {Created CreatedBy Author Comment EmptyLayer}}}}}}`,
`/v2/_zot/ext/search?query={ExpandedRepoInfo(repo:"${name}"){Images {Digest Tag LastUpdated Vendor Size Platform {Os Arch} } Summary {Name LastUpdated Size Platforms {Os Arch} Vendors NewestImage {RepoName Layers {Size Digest} Digest Tag Logo Title Documentation DownloadCount Source Description Licenses History {Layer {Size Digest} HistoryDescription {Created CreatedBy Author Comment EmptyLayer}}}}}}`,
detailedImageInfo: (name, tag) =>
`/v2/_zot/ext/search?query={Image(image: "${name}:${tag}"){RepoName Tag Digest LastUpdated Size ConfigDigest Platform {Os Arch} Vendor Licenses History {Layer {Size Digest Score} HistoryDescription {Created CreatedBy Author Comment EmptyLayer} }}}`,
`/v2/_zot/ext/search?query={Image(image: "${name}:${tag}"){RepoName Tag Digest LastUpdated Size ConfigDigest Platform {Os Arch} Vendor Licenses Logo}}`,
vulnerabilitiesForRepo: (name) =>
`/v2/_zot/ext/search?query={CVEListForImage(image: "${name}"){Tag, CVEList {Id Title Description Severity PackageList {Name InstalledVersion FixedVersion}}}}`,
layersDetailsForImage: (name) =>
@ -87,7 +87,7 @@ const endpoints = {
if (filter.HasToBeSigned) filterParam += ` HasToBeSigned: ${filter.HasToBeSigned}`;
filterParam += '}';
if (Object.keys(filter).length === 0) filterParam = '';
return `/v2/_zot/ext/search?query={GlobalSearch(${searchParam}, ${paginationParam} ${filterParam}) {Repos {Name LastUpdated Size Platforms { Os Arch } NewestImage { Tag Description IsSigned Licenses Vendor Labels } DownloadCount}}}`;
return `/v2/_zot/ext/search?query={GlobalSearch(${searchParam}, ${paginationParam} ${filterParam}) {Repos {Name LastUpdated Size Platforms { Os Arch } NewestImage { Tag Description IsSigned Logo Licenses Vendor Labels } DownloadCount}}}`;
}
};

View File

@ -114,6 +114,7 @@ function Explore() {
platforms={item.platforms}
key={index}
lastUpdated={item.lastUpdated}
logo={item.logo}
/>
);
})

View File

@ -44,7 +44,7 @@ function FilterCard(props) {
const filterRows = filters;
return filterRows.map((filter, index) => {
return (
<Tooltip key={index} title={filter.tooltip ?? filter.label} placement="right">
<Tooltip key={index} title={filter.tooltip ?? filter.label} placement="top" arrow>
<FormControlLabel
componentsProps={{ typography: { variant: 'body2' } }}
control={<Checkbox />}

View File

@ -96,7 +96,7 @@ function Home() {
homeData.slice(0, 4).map((item, index) => {
return (
<Grid item xs={3} key={index}>
<PreviewCard name={item.name} lastUpdated={item.lastUpdated} isSigned={item.isSigned} />
<PreviewCard name={item.name} lastUpdated={item.lastUpdated} isSigned={item.isSigned} logo={item.logo} />
</Grid>
);
})
@ -141,6 +141,7 @@ function Home() {
platforms={item.platforms}
key={index}
lastUpdated={item.lastUpdated}
logo={item.logo}
/>
);
})

View File

@ -12,6 +12,7 @@ import repocube4 from '../assets/repocube-4.png';
//icons
import GppBadOutlinedIcon from '@mui/icons-material/GppBadOutlined';
import GppGoodOutlinedIcon from '@mui/icons-material/GppGoodOutlined';
import { isEmpty } from 'lodash';
//import GppMaybeOutlinedIcon from '@mui/icons-material/GppMaybeOutlined';
// temporary utility to get image
@ -73,7 +74,7 @@ const useStyles = makeStyles(() => ({
function PreviewCard(props) {
const classes = useStyles();
const navigate = useNavigate();
const { name, isSigned } = props;
const { name, isSigned, logo } = props;
const goToDetails = () => {
navigate(`/image/${encodeURIComponent(name)}`);
@ -139,7 +140,7 @@ function PreviewCard(props) {
img: classes.avatar
}}
component="img"
image={randomImage()}
image={!isEmpty(logo) ? `data:image/png;base64, ${logo}` : randomImage()}
alt="icon"
/>
<Tooltip title={name} placement="top">

View File

@ -18,6 +18,7 @@ import repocube4 from '../assets/repocube-4.png';
import GppBadOutlinedIcon from '@mui/icons-material/GppBadOutlined';
import GppGoodOutlinedIcon from '@mui/icons-material/GppGoodOutlined';
import { Markdown } from 'utilities/MarkdowntojsxWrapper';
import { isEmpty } from 'lodash';
// temporary utility to get image
const randomIntFromInterval = (min, max) => {
@ -90,12 +91,7 @@ const useStyles = makeStyles(() => ({
function RepoCard(props) {
const classes = useStyles();
const navigate = useNavigate();
const { name, vendor, platforms, description, downloads, isSigned, lastUpdated, version } = props;
//function that returns a random element from an array
// function getRandom(list) {
// return list[Math.floor(Math.random() * list.length)];
// }
const { name, vendor, platforms, description, downloads, isSigned, lastUpdated, version, logo } = props;
const goToDetails = () => {
navigate(`/image/${encodeURIComponent(name)}`);
@ -197,7 +193,7 @@ function RepoCard(props) {
img: classes.avatar
}}
component="img"
image={randomImage()}
image={!isEmpty(logo) ? `data:image/png;base64, ${logo}` : randomImage()}
alt="icon"
/>
<Tooltip title={name} placement="top">

View File

@ -19,6 +19,7 @@ import repocube4 from '../assets/repocube-4.png';
import { TabContext, TabList, TabPanel } from '@mui/lab';
import RepoDetailsMetadata from './RepoDetailsMetadata';
import Loading from './Loading';
import { isEmpty } from 'lodash';
// @ts-ignore
const useStyles = makeStyles(() => ({
@ -141,12 +142,13 @@ function RepoDetails() {
platforms: repoInfo.Summary?.Platforms,
vendors: repoInfo.Summary?.Vendors,
newestTag: repoInfo.Summary?.NewestImage,
description: repoInfo.Summary?.NewestImage.Description,
title: repoInfo.Summary?.NewestImage.Title,
source: repoInfo.Summary?.NewestImage.Source,
downloads: repoInfo.Summary?.NewestImage.DownloadCount,
overview: repoInfo.Summary?.NewestImage.Documentation,
license: repoInfo.Summary?.NewestImage.Licenses
description: repoInfo.Summary?.NewestImage?.Description,
title: repoInfo.Summary?.NewestImage?.Title,
source: repoInfo.Summary?.NewestImage?.Source,
downloads: repoInfo.Summary?.NewestImage?.DownloadCount,
overview: repoInfo.Summary?.NewestImage?.Documentation,
license: repoInfo.Summary?.NewestImage?.Licenses,
logo: repoInfo.Summary?.NewestImage?.Logo
};
setRepoDetailData(imageData);
setTags(imageData.images);
@ -163,19 +165,6 @@ function RepoDetails() {
abortController.abort();
};
}, [name]);
//function that returns a random element from an array
// function getRandom(list) {
// return list[Math.floor(Math.random() * list.length)];
// }
// const signatureCheck = () => {
// const unverifiedSignature = <Chip label="Unverified Signature" sx={{backgroundColor: "#FEEBEE",color: "#E53935",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <GppBadOutlinedIcon sx={{ color: "#E53935!important" }} />}/>;
// const untrustedSignature = <Chip label="Untrusted Signature" sx={{backgroundColor: "#ECEFF1",color: "#52637A",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <GppMaybeOutlinedIcon sx={{ color: "#52637A!important" }} />}/>;
// const verifiedSignature = <Chip label="Verified Signature" sx={{backgroundColor: "#E8F5E9",color: "#388E3C",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <GppGoodOutlinedIcon sx={{ color: "#388E3C!important" }} />}/>;
// const arrSignature = [unverifiedSignature, untrustedSignature, verifiedSignature]
// return(getRandom(arrSignature));
// }
const platformChips = () => {
// @ts-ignore
@ -234,22 +223,6 @@ function RepoDetails() {
);
};
// const renderDependencies = () => {
// return (<Card className={classes.card}>
// <CardContent>
// <Typography variant="h4" align="left">Dependecies ({dependencies || '---'})</Typography>
// </CardContent>
// </Card>);
// };
// const renderDependents = () => {
// return (<Card className={classes.card}>
// <CardContent>
// <Typography variant="h4" align="left">Dependents ({dependents || '---'})</Typography>
// </CardContent>
// </Card>);
// };
return (
<>
{isLoading ? (
@ -267,7 +240,9 @@ function RepoDetails() {
img: classes.avatar
}}
component="img"
image={randomImage()}
// @ts-ignore
// eslint-disable-next-line prettier/prettier
image={!isEmpty(repoDetailData?.logo) ? `data:image/png;base64, ${repoDetailData?.logo}` : randomImage()}
alt="icon"
/>
<Typography variant="h3" className={classes.repoName}>
@ -304,12 +279,6 @@ function RepoDetails() {
>
<Tab value="Overview" label="Overview" className={classes.tabContent} />
<Tab value="Tags" label="Tags" className={classes.tabContent} />
{/* <Tab value="Dependencies" label={`${dependencies || 0} Dependencies`} className={classes.tabContent}/>
<Tab value="Dependents" label={`${dependents || 0} Dependents`} className={classes.tabContent}/>
<Tab value="Vulnerabilities" label="Vulnerabilities" className={classes.tabContent}/>
<Tab value="6" label="Tab 6" className={classes.tabContent}/>
<Tab value="7" label="Tab 7" className={classes.tabContent}/>
<Tab value="8" label="Tab 8" className={classes.tabContent}/> */}
</TabList>
<Grid container>
<Grid item xs={12}>
@ -319,15 +288,6 @@ function RepoDetails() {
<TabPanel value="Tags" className={classes.tabPanel}>
<Tags tags={tags} />
</TabPanel>
{/* <TabPanel value="Dependencies" className={classes.tabPanel}>
{renderDependencies()}
</TabPanel>
<TabPanel value="Dependents" className={classes.tabPanel}>
{renderDependents()}
</TabPanel>
<TabPanel value="Vulnerabilities" className={classes.tabPanel}>
{renderVulnerabilities()}
</TabPanel> */}
</Grid>
</Grid>
</Box>

View File

@ -35,6 +35,8 @@ import VulnerabilitiesDetails from './VulnerabilitiesDetails';
import HistoryLayers from './HistoryLayers';
import DependsOn from './DependsOn';
import IsDependentOn from './IsDependentOn';
import { isEmpty } from 'lodash';
import Loading from './Loading';
// @ts-ignore
const useStyles = makeStyles(() => ({
@ -133,8 +135,7 @@ const randomImage = () => {
function TagDetails() {
const [imageDetailData, setImageDetailData] = useState({});
// @ts-ignore
//const [isLoading, setIsLoading] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [selectedTab, setSelectedTab] = useState('Layers');
const abortController = useMemo(() => new AbortController(), []);
@ -148,6 +149,7 @@ function TagDetails() {
// if same-page navigation because of tag update, following 2 lines help ux
setSelectedTab('Layers');
window?.scrollTo(0, 0);
setIsLoading(true);
api
.get(`${host()}${endpoints.detailedImageInfo(name, tag)}`, abortController.signal)
.then((response) => {
@ -162,11 +164,12 @@ function TagDetails() {
platform: imageInfo.Platform,
vendor: imageInfo.Vendor,
history: imageInfo.History,
license: imageInfo.Licenses
license: imageInfo.Licenses,
logo: imageInfo.Logo
};
setImageDetailData(imageData);
setFullName(imageData.name + ':' + imageData.tag);
//setIsLoading(false);
setIsLoading(false);
}
})
.catch((e) => {
@ -177,10 +180,6 @@ function TagDetails() {
abortController.abort();
};
}, [name, tag]);
//function that returns a random element from an array
// function getRandom(list) {
// return list[Math.floor(Math.random() * list.length)];
// }
// const signatureCheck = () => {
// const unverifiedSignature = <Chip label="Unverified Signature" sx={{backgroundColor: "#FEEBEE",color: "#E53935",fontSize: "0.8125rem",}} variant="filled" onDelete={() => { return; }} deleteIcon={ <GppBadOutlinedIcon sx={{ color: "#E53935!important" }} />}/>;
@ -206,134 +205,152 @@ function TagDetails() {
};
return (
<div className={classes.pageWrapper}>
<Card className={classes.cardRoot}>
<CardContent>
<Grid container className={classes.header}>
<Grid item xs={8}>
<Stack alignItems="center" direction="row" spacing={2}>
<CardMedia
classes={{
root: classes.media,
img: classes.avatar
}}
component="img"
image={randomImage()}
alt="icon"
/>
<Typography variant="h3" className={classes.repoName}>
{name}:{tag}
</Typography>
{/* {vulnerabilityCheck()}
<>
{isLoading ? (
<Loading />
) : (
<div className={classes.pageWrapper}>
<Card className={classes.cardRoot}>
<CardContent>
<Grid container className={classes.header}>
<Grid item xs={8}>
<Stack alignItems="center" direction="row" spacing={2}>
<CardMedia
classes={{
root: classes.media,
img: classes.avatar
}}
component="img"
image={
// @ts-ignore
// eslint-disable-next-line prettier/prettier
!isEmpty(imageDetailData?.logo) ? `data:image/ png;base64, ${imageDetailData?.logo}` : randomImage()
}
alt="icon"
/>
<Typography variant="h3" className={classes.repoName}>
{name}:{tag}
</Typography>
{/* {vulnerabilityCheck()}
{signatureCheck()} */}
{/* <BookmarkIcon sx={{color:"#52637A"}}/> */}
</Stack>
<Typography
pt={1}
sx={{ fontSize: 16, lineHeight: '1.5rem', color: 'rgba(0, 0, 0, 0.6)', paddingLeft: '4rem' }}
gutterBottom
align="left"
>
DIGEST:{' '}
{
// @ts-ignore
imageDetailData?.digest
}
</Typography>
</Grid>
<Grid item xs={4}>
<Stack direction="row">
<Grid item xs={10}>
<Typography variant="body1" sx={{ color: '#52637A', fontSize: '1rem', paddingTop: '0.75rem' }}>
Pull this image
{/* <BookmarkIcon sx={{color:"#52637A"}}/> */}
</Stack>
<Typography
pt={1}
sx={{ fontSize: 16, lineHeight: '1.5rem', color: 'rgba(0, 0, 0, 0.6)', paddingLeft: '4rem' }}
gutterBottom
align="left"
>
DIGEST:{' '}
{
// @ts-ignore
imageDetailData?.digest
}
</Typography>
</Grid>
<Grid item xs={2}>
<IconButton
aria-label="copy"
onClick={() => navigator.clipboard.writeText(pullString)}
data-testid="pullcopy-btn"
>
<ContentCopyIcon />
</IconButton>
</Grid>
</Stack>
<FormControl sx={{ m: 1, paddingLeft: '1.5rem' }} variant="outlined">
<Select
className={classes.inputForm}
value={pullString}
onChange={handleSelectionChange}
inputProps={{ 'aria-label': 'Without label' }}
sx={{ m: 1, width: '20.625rem', borderRadius: '0.5rem', color: '#14191F', alignContent: 'left' }}
>
<MenuItem value={`docker pull ${hostRoot()}/${fullName}`}>
docker pull {hostRoot()}/{fullName}
</MenuItem>
<MenuItem value={`podman pull ${hostRoot()}/${fullName}`}>
podman pull {hostRoot()}/{fullName}
</MenuItem>
<MenuItem value={`skopeo copy docker://${hostRoot()}/${fullName}`}>
skopeo copy docker://{hostRoot()}/{fullName}
</MenuItem>
</Select>
</FormControl>
</Grid>
</Grid>
<Grid container>
<Grid item xs={8} className={classes.tabs}>
<TabContext value={selectedTab}>
<Box>
<TabList
onChange={handleTabChange}
TabIndicatorProps={{ className: classes.selectedTab }}
sx={{ '& button.Mui-selected': { color: '#14191F', fontWeight: '600' } }}
>
<Tab value="Layers" label="Layers" className={classes.tabContent} />
<Tab value="DependsOn" label="Uses" className={classes.tabContent} data-testid="dependencies-tab" />
<Tab value="IsDependentOn" label="Used by" className={classes.tabContent} />
<Tab value="Vulnerabilities" label="Vulnerabilities" className={classes.tabContent} />
</TabList>
<Grid container>
<Grid item xs={12}>
<TabPanel value="Layers" className={classes.tabPanel}>
<HistoryLayers
name={fullName}
history={
// @ts-ignore
imageDetailData.history
}
/>
</TabPanel>
<TabPanel value="DependsOn" className={classes.tabPanel}>
<DependsOn name={fullName} />
</TabPanel>
<TabPanel value="IsDependentOn" className={classes.tabPanel}>
<IsDependentOn name={fullName} />
</TabPanel>
<TabPanel value="Vulnerabilities" className={classes.tabPanel}>
<VulnerabilitiesDetails name={name} tag={tag} />
</TabPanel>
<Grid item xs={4} justifyContent="flex-start">
<Stack direction="row">
<Grid item xs={10}>
<Typography
variant="body1"
sx={{ color: '#52637A', fontSize: '1rem', paddingTop: '0.75rem', textAlign: 'left' }}
>
Pull this image
</Typography>
</Grid>
</Grid>
</Box>
</TabContext>
</Grid>
<Grid item xs={4} className={classes.metadata}>
<TagDetailsMetadata
// @ts-ignore
platform={getPlatform()}
// @ts-ignore
size={imageDetailData?.size}
// @ts-ignore
lastUpdated={imageDetailData?.lastUpdated}
// @ts-ignore
license={imageDetailData?.license}
/>
</Grid>
</Grid>
</CardContent>
</Card>
</div>
<Grid item xs={2}>
<IconButton
aria-label="copy"
onClick={() => navigator.clipboard.writeText(pullString)}
data-testid="pullcopy-btn"
>
<ContentCopyIcon />
</IconButton>
</Grid>
</Stack>
<FormControl sx={{ m: 1 }} variant="outlined">
<Select
className={classes.inputForm}
value={pullString}
onChange={handleSelectionChange}
inputProps={{ 'aria-label': 'Without label' }}
sx={{ m: 1, width: '20.625rem', borderRadius: '0.5rem', color: '#14191F', alignContent: 'left' }}
>
<MenuItem value={`docker pull ${hostRoot()}/${fullName}`}>
docker pull {hostRoot()}/{fullName}
</MenuItem>
<MenuItem value={`podman pull ${hostRoot()}/${fullName}`}>
podman pull {hostRoot()}/{fullName}
</MenuItem>
<MenuItem value={`skopeo copy docker://${hostRoot()}/${fullName}`}>
skopeo copy docker://{hostRoot()}/{fullName}
</MenuItem>
</Select>
</FormControl>
</Grid>
</Grid>
<Grid container>
<Grid item xs={8} className={classes.tabs}>
<TabContext value={selectedTab}>
<Box>
<TabList
onChange={handleTabChange}
TabIndicatorProps={{ className: classes.selectedTab }}
sx={{ '& button.Mui-selected': { color: '#14191F', fontWeight: '600' } }}
>
<Tab value="Layers" label="Layers" className={classes.tabContent} />
<Tab
value="DependsOn"
label="Uses"
className={classes.tabContent}
data-testid="dependencies-tab"
/>
<Tab value="IsDependentOn" label="Used by" className={classes.tabContent} />
<Tab value="Vulnerabilities" label="Vulnerabilities" className={classes.tabContent} />
</TabList>
<Grid container>
<Grid item xs={12}>
<TabPanel value="Layers" className={classes.tabPanel}>
<HistoryLayers
name={fullName}
history={
// @ts-ignore
imageDetailData.history
}
/>
</TabPanel>
<TabPanel value="DependsOn" className={classes.tabPanel}>
<DependsOn name={fullName} />
</TabPanel>
<TabPanel value="IsDependentOn" className={classes.tabPanel}>
<IsDependentOn name={fullName} />
</TabPanel>
<TabPanel value="Vulnerabilities" className={classes.tabPanel}>
<VulnerabilitiesDetails name={name} tag={tag} />
</TabPanel>
</Grid>
</Grid>
</Box>
</TabContext>
</Grid>
<Grid item xs={4} className={classes.metadata}>
<TagDetailsMetadata
// @ts-ignore
platform={getPlatform()}
// @ts-ignore
size={imageDetailData?.size}
// @ts-ignore
lastUpdated={imageDetailData?.lastUpdated}
// @ts-ignore
license={imageDetailData?.license}
/>
</Grid>
</Grid>
</CardContent>
</Card>
</div>
)}
</>
);
}

View File

@ -9,6 +9,7 @@ const mapToRepo = (responseRepo) => {
licenses: responseRepo.NewestImage?.Licenses,
size: responseRepo.Size,
vendor: responseRepo.NewestImage?.Vendor,
logo: responseRepo.NewestImage?.Logo,
lastUpdated: responseRepo.LastUpdated,
downloads: responseRepo.DownloadCount
};