Cleaned up uses/used by tabs

Added license info to repodetails and tagdetails
Added md parsing for relevant fields

Signed-off-by: Raul Kele <raulkeleblk@gmail.com>
This commit is contained in:
Raul Kele 2022-10-17 16:59:09 +03:00
parent c2f2cab6c0
commit 77162d1b95
14 changed files with 286 additions and 174 deletions

18
package-lock.json generated
View File

@ -22,6 +22,7 @@
"downshift": "^6.1.12",
"lodash": "^4.17.21",
"luxon": "^2.4.0",
"markdown-to-jsx": "^7.1.7",
"npm": "^8.11.0",
"nth-check": "^2.0.1",
"react": "^17.0.2",
@ -12974,6 +12975,17 @@
"tmpl": "1.0.5"
}
},
"node_modules/markdown-to-jsx": {
"version": "7.1.7",
"resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.1.7.tgz",
"integrity": "sha512-VI3TyyHlGkO8uFle0IOibzpO1c1iJDcXcS/zBrQrXQQvJ2tpdwVzVZ7XdKsyRz1NdRmre4dqQkMZzUHaKIG/1w==",
"engines": {
"node": ">= 10"
},
"peerDependencies": {
"react": ">= 0.14.0"
}
},
"node_modules/mdn-data": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz",
@ -30397,6 +30409,12 @@
"tmpl": "1.0.5"
}
},
"markdown-to-jsx": {
"version": "7.1.7",
"resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.1.7.tgz",
"integrity": "sha512-VI3TyyHlGkO8uFle0IOibzpO1c1iJDcXcS/zBrQrXQQvJ2tpdwVzVZ7XdKsyRz1NdRmre4dqQkMZzUHaKIG/1w==",
"requires": {}
},
"mdn-data": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz",

View File

@ -17,6 +17,7 @@
"downshift": "^6.1.12",
"lodash": "^4.17.21",
"luxon": "^2.4.0",
"markdown-to-jsx": "^7.1.7",
"npm": "^8.11.0",
"nth-check": "^2.0.1",
"react": "^17.0.2",

View File

@ -8,16 +8,20 @@ const mockDependenciesList = {
data: {
BaseImageList: [
{
RepoName: 'project-stacker/c3/static-ubuntu-amd64'
RepoName: 'project-stacker/c3/static-ubuntu-amd64',
Tag: 'tag1'
},
{
RepoName: 'tag2'
RepoName: 'tag2',
Tag: 'tag2'
},
{
RepoName: 'tag3'
RepoName: 'tag3',
Tag: 'tag3'
},
{
RepoName: 'tag4'
RepoName: 'tag4',
Tag: 'tag4'
}
]
}
@ -51,7 +55,7 @@ describe('Dependencies tab', () => {
// @ts-ignore
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: mockDependenciesList });
render(<RouterDependsWrapper />);
expect(await screen.findAllByText(/published/i)).toHaveLength(4);
expect(await screen.findAllByText(/Tag/i)).toHaveLength(8);
});
it('renders no dependencies if there are not any', async () => {

View File

@ -8,16 +8,20 @@ const mockDependentsList = {
data: {
DerivedImageList: [
{
RepoName: 'project-stacker/c3/static-ubuntu-amd64'
RepoName: 'project-stacker/c3/static-ubuntu-amd64',
Tag: 'tag1'
},
{
RepoName: 'tag2'
RepoName: 'tag2',
Tag: 'tag2'
},
{
RepoName: 'tag3'
RepoName: 'tag3',
Tag: 'tag3'
},
{
RepoName: 'tag4'
RepoName: 'tag4',
Tag: 'tag4'
}
]
}
@ -51,7 +55,7 @@ describe('Dependents tab', () => {
// @ts-ignore
jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: mockDependentsList });
render(<RouterDependsWrapper />);
expect(await screen.findAllByText(/published/i)).toHaveLength(4);
expect(await screen.findAllByText(/tag/i)).toHaveLength(8);
});
it('renders no dependents if there are not any', async () => {

View File

@ -65,9 +65,9 @@ const endpoints = {
(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}}`,
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 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 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 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 History {Layer {Size Digest Score} 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}}}}`,
layersDetailsForImage: (name) =>
@ -75,9 +75,9 @@ const endpoints = {
imageListWithCVEFixed: (cveId, repoName) =>
`/v2/_zot/ext/search?query={ImageListWithCVEFixed(id:"${cveId}", image:"${repoName}") {Tag}}`,
dependsOnForImage: (name) =>
`/v2/_zot/ext/search?query={BaseImageList(image: "${name}"){RepoName Tag Description Vendor DownloadCount LastUpdated Platform {Os Arch} IsSigned}}`,
`/v2/_zot/ext/search?query={BaseImageList(image: "${name}"){RepoName Tag Description Digest Vendor DownloadCount LastUpdated Size Platform {Os Arch} IsSigned}}`,
isDependentOnForImage: (name) =>
`/v2/_zot/ext/search?query={DerivedImageList(image: "${name}"){RepoName Tag Description Vendor DownloadCount LastUpdated Platform {Os Arch} IsSigned}}`,
`/v2/_zot/ext/search?query={DerivedImageList(image: "${name}"){RepoName Tag Description Digest Vendor DownloadCount LastUpdated Size Platform {Os Arch} IsSigned}}`,
globalSearch: ({ searchQuery = '""', pageNumber = 1, pageSize = 15, filter = {} }) => {
const searchParam = searchQuery !== '' ? `query:"${searchQuery}"` : `query:""`;
const paginationParam = `requestedPage: {limit:${pageSize} offset:${(pageNumber - 1) * pageSize}}`;

View File

@ -8,7 +8,7 @@ import { Divider, Typography, Stack } from '@mui/material';
import makeStyles from '@mui/styles/makeStyles';
import { host } from '../host';
import Loading from './Loading';
import RepoCard from './RepoCard';
import TagCard from './TagCard';
const useStyles = makeStyles(() => ({
card: {
@ -96,14 +96,14 @@ function DependsOn(props) {
return images?.length ? (
images.map((dependence, index) => {
return (
<RepoCard
name={dependence.RepoName}
version={dependence.Tag}
description={dependence.Description}
<TagCard
repoName={dependence.RepoName}
tag={dependence.Tag}
vendor={dependence.Vendor}
downloads={dependence.DownloadCount}
platforms={[dependence.Platform]}
platform={dependence.Platform}
isSigned={dependence.IsSigned}
size={dependence.Size}
digest={dependence.Digest}
key={index}
lastUpdated={dependence.LastUpdated}
/>

View File

@ -8,7 +8,7 @@ import { Divider, Typography, Stack } from '@mui/material';
import makeStyles from '@mui/styles/makeStyles';
import { host } from '../host';
import Loading from './Loading';
import RepoCard from './RepoCard';
import TagCard from './TagCard';
const useStyles = makeStyles(() => ({
card: {
@ -96,14 +96,14 @@ function IsDependentOn(props) {
return images?.length ? (
images.map((dependence, index) => {
return (
<RepoCard
name={dependence.RepoName}
version={dependence.Tag}
description={dependence.Description}
<TagCard
repoName={dependence.RepoName}
tag={dependence.Tag}
vendor={dependence.Vendor}
downloads={dependence.DownloadCount}
platform={dependence.Platform}
isSigned={dependence.IsSigned}
platforms={[dependence.Platform]}
size={dependence.Size}
digest={dependence.Digest}
key={index}
lastUpdated={dependence.LastUpdated}
/>

View File

@ -17,6 +17,7 @@ import repocube4 from '../assets/repocube-4.png';
//icons
import GppBadOutlinedIcon from '@mui/icons-material/GppBadOutlined';
import GppGoodOutlinedIcon from '@mui/icons-material/GppGoodOutlined';
import Markdown from 'markdown-to-jsx';
// temporary utility to get image
const randomIntFromInterval = (min, max) => {
@ -219,7 +220,7 @@ function RepoCard(props) {
<Stack alignItems="center" direction="row" spacing={1} pt={2}>
<Tooltip title={getVendor()} placement="top">
<Typography className={classes.vendor} variant="body2" noWrap>
{getVendor()}
{<Markdown options={{ forceInline: true }}>{getVendor()}</Markdown>}
</Typography>
</Tooltip>
<Tooltip title={getVersion()} placement="top">

View File

@ -145,7 +145,8 @@ function RepoDetails() {
title: repoInfo.Summary?.NewestImage.Title,
source: repoInfo.Summary?.NewestImage.Source,
downloads: repoInfo.Summary?.NewestImage.DownloadCount,
overview: repoInfo.Summary?.NewestImage.Documentation
overview: repoInfo.Summary?.NewestImage.Documentation,
license: repoInfo.Summary?.NewestImage.Licenses
};
setRepoDetailData(imageData);
setTags(imageData.images);
@ -344,6 +345,8 @@ function RepoDetails() {
size={repoDetailData?.size}
// @ts-ignore
latestTag={repoDetailData?.newestTag}
// @ts-ignore
license={repoDetailData?.license}
/>
</Grid>
</Grid>

View File

@ -1,6 +1,7 @@
import { Card, CardContent, Grid, Typography, Tooltip } from '@mui/material';
import makeStyles from '@mui/styles/makeStyles';
import { DateTime } from 'luxon';
import Markdown from 'markdown-to-jsx';
import React from 'react';
import transform from '../utilities/transform';
@ -35,7 +36,7 @@ const useStyles = makeStyles(() => ({
function RepoDetailsMetadata(props) {
const classes = useStyles();
const { repoURL, totalDownloads, lastUpdated, size } = props;
const { repoURL, totalDownloads, lastUpdated, size, license } = props;
// @ts-ignore
const lastDate = (lastUpdated ? DateTime.fromISO(lastUpdated) : DateTime.now().minus({ days: 1 })).toRelative({
unit: ['weeks', 'days', 'hours', 'minutes']
@ -94,6 +95,22 @@ function RepoDetailsMetadata(props) {
</Card>
</Grid>
</Grid>
<Grid container item xs={12} spacing={2}>
<Grid item xs={12}>
<Card variant="outlined" className={classes.card}>
<CardContent>
<Typography variant="body2" align="left" className={classes.metadataHeader}>
License
</Typography>
<Tooltip title={license || ' '} placement="top">
<Typography variant="body1" align="left" className={classes.metadataBody}>
{license ? <Markdown>{license}</Markdown> : `License info not available`}
</Typography>
</Tooltip>
</CardContent>
</Card>
</Grid>
</Grid>
{/* <Grid container item xs={12} spacing={2}>
<Grid item xs={12}>
<Card variant="outlined" className={classes.card}>

164
src/components/TagCard.jsx Normal file
View File

@ -0,0 +1,164 @@
import React, { useState } from 'react';
import { makeStyles } from '@mui/styles';
import { useNavigate } from 'react-router-dom';
import {
Box,
Card,
CardContent,
Collapse,
Stack,
Table,
TableBody,
TableCell,
tableCellClasses,
TableHead,
TableRow,
Tooltip,
Typography
} from '@mui/material';
import Markdown from 'markdown-to-jsx';
import transform from 'utilities/transform';
import { DateTime } from 'luxon';
const useStyles = makeStyles(() => ({
tagCard: {
marginBottom: 2,
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
background: '#FFFFFF',
boxShadow: 'none!important',
borderRadius: '1.875rem',
flex: 'none',
alignSelf: 'stretch',
flexGrow: 0,
order: 0,
width: '100%'
},
card: {
marginBottom: '2rem',
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%'
},
content: {
textAlign: 'left',
color: '#606060',
padding: '2% 3% 2% 3%',
width: '100%'
},
clickCursor: {
cursor: 'pointer'
}
}));
export default function TagCard(props) {
const { repoName, tag, lastUpdated, vendor, digest, size, platform } = props;
//const tags = data && data.tags;
const [open, setOpen] = useState(false);
const classes = useStyles();
// @ts-ignore
const lastDate = (lastUpdated ? DateTime.fromISO(lastUpdated) : DateTime.now().minus({ days: 1 })).toRelative({
unit: ['weeks', 'days', 'hours', 'minutes']
});
const navigate = useNavigate();
const goToTags = (tag) => {
navigate(`tag/${tag}`);
};
return (
<Card className={classes.card} raised>
<CardContent className={classes.content}>
<Typography variant="body1" align="left" sx={{ color: '#828282', fontSize: '1rem', paddingBottom: '0.5rem' }}>
Tag
</Typography>
<Typography
variant="body1"
align="left"
sx={{ color: '#1479FF', fontSize: '1rem', textDecorationLine: 'underline', cursor: 'pointer' }}
onClick={() => goToTags(tag)}
>
{repoName && `${repoName}:`}
{tag}
</Typography>
<Stack sx={{ display: 'inline' }} direction="row" spacing={0.5}>
<Typography variant="caption" sx={{ fontWeight: '400', fontSize: '0.8125rem' }}>
Pushed
</Typography>
<Tooltip title={lastUpdated?.slice(0, 16) || ' '} placement="top">
<Typography variant="caption" sx={{ fontWeight: '600', fontSize: '0.8125rem' }}>
{lastDate || 'Date not available'} by{' '}
<Markdown options={{ forceInline: true }}>{vendor || 'Vendor not available'}</Markdown>
</Typography>
</Tooltip>
</Stack>
<Typography
sx={{
color: '#1479FF',
paddingTop: '1rem',
fontSize: '0.8125rem',
fontWeight: '600',
cursor: 'pointer'
}}
onClick={() => setOpen(!open)}
>
{!open ? 'See digest' : 'Hide digest'}
</Typography>
<Collapse in={open} timeout="auto" unmountOnExit>
<Box>
<Table size="small" padding="none" sx={{ [`& .${tableCellClasses.root}`]: { borderBottom: 'none' } }}>
<TableHead>
<TableRow>
<TableCell style={{ color: '#696969' }}>
<Typography variant="body1">Digest</Typography>
</TableCell>
<TableCell style={{ color: '#696969' }}>
<Typography variant="body1">OS/ARCH</Typography>
</TableCell>
<TableCell style={{ color: '#696969' }}>
<Typography variant="body1">Size</Typography>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow
key={digest}
onClick={() => {
navigator.clipboard.writeText(digest);
}}
className={classes.clickCursor}
>
<TableCell style={{ color: '#696969' }}>
<Tooltip title={digest || ''} placement="right">
<Typography variant="body1">{digest?.substr(0, 12)}</Typography>
</Tooltip>
</TableCell>
<TableCell style={{ color: '#696969' }}>
<Typography variant="body1">
{platform?.Os}/{platform?.Arch}
</Typography>
</TableCell>
<TableCell component="th" scope="row" style={{ color: '#696969' }}>
<Typography variant="body1">{transform.formatBytes(size)}</Typography>
</TableCell>
</TableRow>
</TableBody>
</Table>
</Box>
</Collapse>
</CardContent>
</Card>
);
}

View File

@ -161,7 +161,8 @@ function TagDetails() {
digest: imageInfo.ConfigDigest,
platform: imageInfo.Platform,
vendor: imageInfo.Vendor,
history: imageInfo.History
history: imageInfo.History,
license: imageInfo.Licenses
};
setImageDetailData(imageData);
setFullName(imageData.name + ':' + imageData.tag);
@ -325,6 +326,8 @@ function TagDetails() {
size={imageDetailData?.size}
// @ts-ignore
lastUpdated={imageDetailData?.lastUpdated}
// @ts-ignore
license={imageDetailData?.license}
/>
</Grid>
</Grid>

View File

@ -1,6 +1,7 @@
import { Card, CardContent, Grid, Typography, Tooltip } from '@mui/material';
import makeStyles from '@mui/styles/makeStyles';
import { DateTime } from 'luxon';
import Markdown from 'markdown-to-jsx';
import React from 'react';
import transform from '../utilities/transform';
@ -35,7 +36,7 @@ const useStyles = makeStyles(() => ({
function TagDetailsMetadata(props) {
const classes = useStyles();
const { platform, lastUpdated, size } = props;
const { platform, lastUpdated, size, license } = props;
const lastDate = (lastUpdated ? DateTime.fromISO(lastUpdated) : DateTime.now().minus({ days: 1 })).toRelative({
unit: ['weeks', 'days', 'hours', 'minutes']
});
@ -81,6 +82,22 @@ function TagDetailsMetadata(props) {
</Card>
</Grid>
</Grid>
<Grid container item xs={12} spacing={2}>
<Grid item xs={12}>
<Card variant="outlined" className={classes.card}>
<CardContent>
<Typography variant="body2" align="left" className={classes.metadataHeader}>
License
</Typography>
<Tooltip title={license || ' '} placement="top">
<Typography variant="body1" align="left" className={classes.metadataBody}>
{license ? <Markdown>{license}</Markdown> : `License info not available`}
</Typography>
</Tooltip>
</CardContent>
</Card>
</Grid>
</Grid>
</Grid>
);
}

View File

@ -1,20 +1,11 @@
// react global
import * as React from 'react';
import { useNavigate } from 'react-router-dom';
import { DateTime } from 'luxon';
// components
import Box from '@mui/material/Box';
import Collapse from '@mui/material/Collapse';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell, { tableCellClasses } from '@mui/material/TableCell';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Typography from '@mui/material/Typography';
import transform from 'utilities/transform';
import { Card, CardContent, Divider, Stack, Tooltip } from '@mui/material';
import { Card, CardContent, Divider } from '@mui/material';
import { makeStyles } from '@mui/styles';
import TagCard from './TagCard';
const useStyles = makeStyles(() => ({
tagCard: {
@ -56,139 +47,28 @@ const useStyles = makeStyles(() => ({
}
}));
function TagCard(props) {
const { tag, lastUpdated, vendors, digest, size, platform } = props;
//const tags = data && data.tags;
const [open, setOpen] = React.useState(false);
const classes = useStyles();
// @ts-ignore
const lastDate = (lastUpdated ? DateTime.fromISO(lastUpdated) : DateTime.now().minus({ days: 1 })).toRelative({
unit: ['weeks', 'days', 'hours', 'minutes']
});
const navigate = useNavigate();
const goToTags = (tag) => {
navigate(`tag/${tag}`);
};
return (
<Card className={classes.card} raised>
<CardContent className={classes.content}>
<Typography variant="body1" align="left" sx={{ color: '#828282', fontSize: '1rem', paddingBottom: '0.5rem' }}>
Tag
</Typography>
<Typography
variant="body1"
align="left"
sx={{ color: '#1479FF', fontSize: '1rem', textDecorationLine: 'underline', cursor: 'pointer' }}
onClick={() => goToTags(tag)}
>
{tag}
</Typography>
<Stack sx={{ display: 'inline' }} direction="row" spacing={0.5}>
<Typography variant="caption" sx={{ fontWeight: '400', fontSize: '0.8125rem' }}>
Last pushed
</Typography>
<Tooltip title={lastUpdated?.slice(0, 16) || ' '} placement="top">
<Typography variant="caption" sx={{ fontWeight: '600', fontSize: '0.8125rem' }}>
{lastDate || 'Date not available'} by {vendors || 'Vendor not available'}
</Typography>
</Tooltip>
</Stack>
<Typography
sx={{
color: '#1479FF',
paddingTop: '1rem',
fontSize: '0.8125rem',
fontWeight: '600',
cursor: 'pointer'
}}
onClick={() => setOpen(!open)}
>
{!open ? 'See digest' : 'Hide digest'}
</Typography>
<Collapse in={open} timeout="auto" unmountOnExit>
<Box>
<Table size="small" padding="none" sx={{ [`& .${tableCellClasses.root}`]: { borderBottom: 'none' } }}>
<TableHead>
<TableRow>
<TableCell style={{ color: '#696969' }}>
<Typography variant="body1">Digest</Typography>
</TableCell>
<TableCell style={{ color: '#696969' }}>
<Typography variant="body1">OS/ARCH</Typography>
</TableCell>
<TableCell style={{ color: '#696969' }}>
<Typography variant="body1">Size</Typography>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow
key={digest}
onClick={() => {
navigator.clipboard.writeText(digest);
}}
className={classes.clickCursor}
>
<TableCell style={{ color: '#696969' }}>
<Typography variant="body1">{digest?.substr(0, 12)}</Typography>
</TableCell>
<TableCell style={{ color: '#696969' }}>
<Typography variant="body1">
{platform.Os}/{platform.Arch}
</Typography>
</TableCell>
<TableCell component="th" scope="row" style={{ color: '#696969' }}>
<Typography variant="body1">{transform.formatBytes(size)}</Typography>
</TableCell>
</TableRow>
</TableBody>
</Table>
</Box>
</Collapse>
</CardContent>
</Card>
);
}
// TagCard.propTypes = {
// row: PropTypes.shape({
// Layers: PropTypes.arrayOf(
// PropTypes.shape({
// Digest: PropTypes.string.isRequired,
// Size: PropTypes.string.isRequired,
// }),
// ).isRequired,
// Tag: PropTypes.string.isRequired,
// }).isRequired,
// };
const renderTags = (tags) => {
const cmp =
tags &&
tags.map((tag) => {
return (
<TagCard
key={tag.Tag}
tag={tag.Tag}
lastUpdated={tag.LastUpdated}
digest={tag.Digest}
vendors={tag.Vendor}
size={tag.Size}
platform={tag.Platform}
/>
);
});
return cmp;
};
export default function Tags(props) {
const classes = useStyles();
const { tags } = props;
const renderTags = (tags) => {
const cmp =
tags &&
tags.map((tag) => {
return (
<TagCard
key={tag.Tag}
tag={tag.Tag}
lastUpdated={tag.LastUpdated}
digest={tag.Digest}
vendor={tag.Vendor}
size={tag.Size}
platform={tag.Platform}
/>
);
});
return cmp;
};
return (
<Card className={classes.tagCard} data-testid="tags-container">
<CardContent className={classes.content}>