diff --git a/playwright.config.js b/playwright.config.js
index 294d54e..c88309c 100644
--- a/playwright.config.js
+++ b/playwright.config.js
@@ -42,7 +42,8 @@ const config = {
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
- ignoreHTTPSErrors: true
+ ignoreHTTPSErrors: true,
+ screenshot: 'only-on-failure'
},
/* Configure projects for major browsers */
@@ -101,7 +102,7 @@ const config = {
],
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
- // outputDir: 'test-results/',
+ outputDir: 'test-results/',
/* Run your local dev server before starting the tests */
// webServer: {
diff --git a/src/__tests__/TagPage/VulnerabilitiesDetails.test.js b/src/__tests__/TagPage/VulnerabilitiesDetails.test.js
index 9b01527..9b9df38 100644
--- a/src/__tests__/TagPage/VulnerabilitiesDetails.test.js
+++ b/src/__tests__/TagPage/VulnerabilitiesDetails.test.js
@@ -515,7 +515,7 @@ describe('Vulnerabilties page', () => {
it('renders no vulnerabilities if there are not any', async () => {
jest.spyOn(api, 'get').mockResolvedValue({
status: 200,
- data: { data: { CVEListForImage: { Tag: '', Page: {}, CVEList: [] } } }
+ data: { data: { CVEListForImage: { Tag: '', Page: {}, CVEList: [], Summary: {} } } }
});
render();
await waitFor(() => expect(screen.getAllByText('No Vulnerabilities')).toHaveLength(1));
diff --git a/src/api.js b/src/api.js
index 0596edf..9ea5687 100644
--- a/src/api.js
+++ b/src/api.js
@@ -97,7 +97,7 @@ const endpoints = {
if (!isEmpty(searchTerm)) {
query += `, searchedCVE: "${searchTerm}"`;
}
- return `${query}){Tag Page {TotalCount ItemCount} CVEList {Id Title Description Severity PackageList {Name InstalledVersion FixedVersion}}}}`;
+ return `${query}){Tag Page {TotalCount ItemCount} CVEList {Id Title Description Severity PackageList {Name InstalledVersion FixedVersion}} Summary {Count UnknownCount LowCount MediumCount HighCount CriticalCount}}}`;
},
allVulnerabilitiesForRepo: (name) =>
`/v2/_zot/ext/search?query={CVEListForImage(image: "${name}"){Tag Page {TotalCount ItemCount} CVEList {Id Title Description Severity Reference PackageList {Name InstalledVersion FixedVersion}}}}`,
diff --git a/src/components/Shared/VulnerabilityCountCard.jsx b/src/components/Shared/VulnerabilityCountCard.jsx
new file mode 100644
index 0000000..c478136
--- /dev/null
+++ b/src/components/Shared/VulnerabilityCountCard.jsx
@@ -0,0 +1,92 @@
+import React from 'react';
+
+import makeStyles from '@mui/styles/makeStyles';
+import { Stack, Tooltip } from '@mui/material';
+
+const criticalColor = '#ff5c74';
+const criticalBorderColor = '#f9546d';
+
+const highColor = '#ff6840';
+const highBorderColor = '#ee6b49';
+
+const mediumColor = '#ffa052';
+const mediumBorderColor = '#f19d5b';
+
+const lowColor = '#f9f486';
+const lowBorderColor = '#f0ed94';
+
+const unknownColor = '#f2ffdd';
+const unknownBorderColor = '#e9f4d7';
+
+const fontSize = '0.75rem';
+
+const useStyles = makeStyles((theme) => ({
+ cveCountCard: {
+ display: 'flex',
+ alignItems: 'center',
+ paddingLeft: '0.5rem',
+ paddingRight: '0.5rem',
+ color: theme.palette.primary.main,
+ fontSize: fontSize,
+ fontWeight: '600',
+ borderRadius: '3px',
+ marginBottom: '0'
+ },
+ severityList: {
+ fontSize: fontSize,
+ display: 'flex',
+ flexWrap: 'wrap',
+ alignItems: 'center',
+ gap: '0.5em'
+ },
+ criticalSeverity: {
+ backgroundColor: criticalColor,
+ border: '1px solid ' + criticalBorderColor
+ },
+ highSeverity: {
+ backgroundColor: highColor,
+ border: '1px solid ' + highBorderColor
+ },
+ mediumSeverity: {
+ backgroundColor: mediumColor,
+ border: '1px solid ' + mediumBorderColor
+ },
+ lowSeverity: {
+ backgroundColor: lowColor,
+ border: '1px solid ' + lowBorderColor
+ },
+ unknownSeverity: {
+ backgroundColor: unknownColor,
+ border: '1px solid ' + unknownBorderColor
+ }
+}));
+
+function VulnerabilitiyCountCard(props) {
+ const classes = useStyles();
+ const { total, critical, high, medium, low, unknown } = props;
+
+ return (
+
+ Total {total}
+
+
+ C {critical}
+
+
+ H {high}
+
+
+ M {medium}
+
+
+ L {low}
+
+
+ U {unknown}
+
+
+
+ );
+}
+
+export default VulnerabilitiyCountCard;
diff --git a/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx b/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx
index f80612e..a1a7e50 100644
--- a/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx
+++ b/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx
@@ -31,14 +31,25 @@ import ViewHeadlineIcon from '@mui/icons-material/ViewHeadline';
import ViewAgendaIcon from '@mui/icons-material/ViewAgenda';
import VulnerabilitiyCard from '../../Shared/VulnerabilityCard';
+import VulnerabilityCountCard from '../../Shared/VulnerabilityCountCard';
const useStyles = makeStyles((theme) => ({
+ searchAndDisplayBar: {
+ display: 'flex',
+ justifyContent: 'space-between'
+ },
title: {
color: theme.palette.primary.main,
fontSize: '1.5rem',
fontWeight: '600',
marginBottom: '0'
},
+ cveCountSummary: {
+ color: theme.palette.primary.main,
+ fontSize: '1.5rem',
+ fontWeight: '600',
+ marginBottom: '0'
+ },
cveId: {
color: theme.palette.primary.main,
fontSize: '1rem',
@@ -67,6 +78,7 @@ const useStyles = makeStyles((theme) => ({
search: {
position: 'relative',
maxWidth: '100%',
+ flex: 0.95,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
@@ -74,15 +86,18 @@ const useStyles = makeStyles((theme) => ({
border: '0.063rem solid #E7E7E7',
borderRadius: '0.625rem'
},
+ expandableSearchInput: {
+ flexGrow: 0.95
+ },
view: {
alignContent: 'right',
variant: 'outlined'
},
viewModes: {
position: 'relative',
+ alignItems: 'baseline',
maxWidth: '100%',
flexDirection: 'row',
- alignItems: 'right',
justifyContent: 'right'
},
searchIcon: {
@@ -114,6 +129,7 @@ function VulnerabilitiesDetails(props) {
const classes = useStyles();
const [cveData, setCveData] = useState([]);
const [allCveData, setAllCveData] = useState([]);
+ const [cveSummary, setCVESummary] = useState({});
const [isLoading, setIsLoading] = useState(true);
const [isLoadingAllCve, setIsLoadingAllCve] = useState(true);
const abortController = useMemo(() => new AbortController(), []);
@@ -147,9 +163,23 @@ function VulnerabilitiesDetails(props) {
.then((response) => {
if (response.data && response.data.data) {
let cveInfo = response.data.data.CVEListForImage?.CVEList;
+ let summary = response.data.data.CVEListForImage?.Summary;
let cveListData = mapCVEInfo(cveInfo);
setCveData((previousState) => (pageNumber === 1 ? cveListData : [...previousState, ...cveListData]));
setIsEndOfList(response.data.data.CVEListForImage.Page?.ItemCount < EXPLORE_PAGE_SIZE);
+ setCVESummary((previousState) => {
+ if (isEmpty(summary)) {
+ return previousState;
+ }
+ return {
+ Count: summary.Count,
+ UnknownCount: summary.UnknownCount,
+ LowCount: summary.LowCount,
+ MediumCount: summary.MediumCount,
+ HighCount: summary.HighCount,
+ CriticalCount: summary.CriticalCount
+ };
+ });
} else if (response.data.errors) {
setIsEndOfList(true);
}
@@ -159,6 +189,7 @@ function VulnerabilitiesDetails(props) {
console.error(e);
setIsLoading(false);
setCveData([]);
+ setCVESummary(() => {});
setIsEndOfList(true);
});
};
@@ -283,6 +314,25 @@ function VulnerabilitiesDetails(props) {
);
};
+ const renderCVESummary = () => {
+ if (cveSummary === undefined) {
+ return;
+ }
+
+ console.log('Test');
+
+ return !isEmpty(cveSummary) ? (
+
+ ) : (<>>);
+ };
+
const renderListBottom = () => {
if (isLoading) {
return ;
@@ -364,6 +414,7 @@ function VulnerabilitiesDetails(props) {
+ {renderCVESummary()}