Compare commits
	
		
			4 Commits
		
	
	
		
			commit-12b
			...
			commit-5bf
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 5bf7d5652c | ||
|  | 12f9229320 | ||
|  | df19fa811c | ||
|  | 6cda89c710 | 
| @@ -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: { | ||||
|   | ||||
| @@ -22,6 +22,14 @@ const mockCVEList = { | ||||
|   CVEListForImage: { | ||||
|     Tag: '', | ||||
|     Page: { ItemCount: 20, TotalCount: 20 }, | ||||
|     Summary: { | ||||
|       Count: 5, | ||||
|       UnknownCount: 1, | ||||
|       LowCount: 1, | ||||
|       MediumCount: 1, | ||||
|       HighCount: 1, | ||||
|       CriticalCount: 1, | ||||
|     }, | ||||
|     CVEList: [ | ||||
|       { | ||||
|         Id: 'CVE-2020-16156', | ||||
| @@ -499,6 +507,7 @@ describe('Vulnerabilties page', () => { | ||||
|     jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEList } }); | ||||
|     render(<StateVulnerabilitiesWrapper />); | ||||
|     await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1)); | ||||
|     await waitFor(() => expect(screen.getAllByText('Total 5')).toHaveLength(1)); | ||||
|     await waitFor(() => expect(screen.getAllByText(/fixed in/i)).toHaveLength(20)); | ||||
|   }); | ||||
|  | ||||
| @@ -515,7 +524,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(<StateVulnerabilitiesWrapper />); | ||||
|     await waitFor(() => expect(screen.getAllByText('No Vulnerabilities')).toHaveLength(1)); | ||||
| @@ -580,12 +589,28 @@ describe('Vulnerabilties page', () => { | ||||
|     await fireEvent.click(exportAsCSVBtn); | ||||
|     expect(await screen.findByTestId('export-csv-menuItem')).not.toBeInTheDocument(); | ||||
|     fireEvent.click(downloadBtn[0]); | ||||
|     const exportAsExcelBtn = screen.getByText(/MS Excel/i); | ||||
|     const exportAsExcelBtn = screen.getByText(/xlsx/i); | ||||
|     expect(exportAsExcelBtn).toBeInTheDocument(); | ||||
|     await fireEvent.click(exportAsExcelBtn); | ||||
|     expect(await screen.findByTestId('export-excel-menuItem')).not.toBeInTheDocument(); | ||||
|   }); | ||||
|  | ||||
|   it('should expand/collapse the list of CVEs', async () => { | ||||
|     jest | ||||
|       .spyOn(api, 'get') | ||||
|       .mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }) | ||||
|       .mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } }); | ||||
|     render(<StateVulnerabilitiesWrapper />); | ||||
|     await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1)); | ||||
|     await waitFor(() => expect(screen.getAllByText('Fixed in')).toHaveLength(20)); | ||||
|     const collapseListBtn = await screen.findAllByTestId('ViewHeadlineIcon'); | ||||
|     fireEvent.click(collapseListBtn[0]); | ||||
|     expect(await screen.findByText('Fixed in')).not.toBeVisible(); | ||||
|     const expandListBtn = await screen.findAllByTestId('ViewAgendaIcon'); | ||||
|     fireEvent.click(expandListBtn[0]); | ||||
|     await waitFor(() => expect(screen.getAllByText('Fixed in')).toHaveLength(20)); | ||||
|   }); | ||||
|  | ||||
|   it('should handle fixed CVE query errors', async () => { | ||||
|     jest | ||||
|       .spyOn(api, 'get') | ||||
|   | ||||
| @@ -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}}}}`, | ||||
|   | ||||
| @@ -66,13 +66,18 @@ const useStyles = makeStyles((theme) => ({ | ||||
|     cursor: 'pointer', | ||||
|     textAlign: 'center' | ||||
|   }, | ||||
|   dropdownCVE: { | ||||
|     color: '#1479FF', | ||||
|     cursor: 'pointer' | ||||
|   }, | ||||
|   vulnerabilityCardDivider: { | ||||
|     margin: '1rem 0' | ||||
|   } | ||||
| })); | ||||
| function VulnerabilitiyCard(props) { | ||||
|   const classes = useStyles(); | ||||
|   const { cve, name, platform } = props; | ||||
|   const { cve, name, platform, expand } = props; | ||||
|   const [openCVE, setOpenCVE] = useState(expand); | ||||
|   const [openDesc, setOpenDesc] = useState(false); | ||||
|   const [openFixed, setOpenFixed] = useState(false); | ||||
|   const [loadingFixed, setLoadingFixed] = useState(true); | ||||
| @@ -122,6 +127,10 @@ function VulnerabilitiyCard(props) { | ||||
|     }; | ||||
|   }, [openFixed, pageNumber]); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     setOpenCVE(expand); | ||||
|   }, [expand]); | ||||
|  | ||||
|   const loadMore = () => { | ||||
|     if (loadingFixed || isEndOfList) return; | ||||
|     setPageNumber((pageNumber) => pageNumber + 1); | ||||
| @@ -166,49 +175,56 @@ function VulnerabilitiyCard(props) { | ||||
|     <Card className={classes.card} raised> | ||||
|       <CardContent className={classes.content}> | ||||
|         <Stack direction="row" spacing="1.25rem"> | ||||
|           {!openCVE ? ( | ||||
|             <KeyboardArrowRight className={classes.dropdownCVE} onClick={() => setOpenCVE(!openCVE)} /> | ||||
|           ) : ( | ||||
|             <KeyboardArrowDown className={classes.dropdownCVE} onClick={() => setOpenCVE(!openCVE)} /> | ||||
|           )} | ||||
|           <Typography variant="body1" align="left" className={classes.cveId}> | ||||
|             {cve.id} | ||||
|           </Typography> | ||||
|           <VulnerabilityChipCheck vulnerabilitySeverity={cve.severity} /> | ||||
|         </Stack> | ||||
|         <Typography variant="body1" align="left" className={classes.cveSummary}> | ||||
|           {cve.title} | ||||
|         </Typography> | ||||
|         <Divider className={classes.vulnerabilityCardDivider} /> | ||||
|         <Stack className={classes.dropdown} onClick={() => setOpenFixed(!openFixed)}> | ||||
|           {!openFixed ? ( | ||||
|             <KeyboardArrowRight className={classes.dropdownText} /> | ||||
|           ) : ( | ||||
|             <KeyboardArrowDown className={classes.dropdownText} /> | ||||
|           )} | ||||
|           <Typography className={classes.dropdownText}>Fixed in</Typography> | ||||
|         </Stack> | ||||
|         <Collapse in={openFixed} timeout="auto" unmountOnExit> | ||||
|           <Box sx={{ width: '100%', padding: '0.5rem 0' }}> | ||||
|             {loadingFixed ? ( | ||||
|               'Loading...' | ||||
|         <Collapse in={openCVE} timeout="auto" unmountOnExit> | ||||
|           <Typography variant="body1" align="left" className={classes.cveSummary}> | ||||
|             {cve.title} | ||||
|           </Typography> | ||||
|           <Divider className={classes.vulnerabilityCardDivider} /> | ||||
|           <Stack className={classes.dropdown} onClick={() => setOpenFixed(!openFixed)}> | ||||
|             {!openFixed ? ( | ||||
|               <KeyboardArrowRight className={classes.dropdownText} /> | ||||
|             ) : ( | ||||
|               <Stack direction="row" sx={{ flexWrap: 'wrap' }}> | ||||
|                 {renderFixedVer()} | ||||
|                 {renderLoadMore()} | ||||
|               </Stack> | ||||
|               <KeyboardArrowDown className={classes.dropdownText} /> | ||||
|             )} | ||||
|           </Box> | ||||
|         </Collapse> | ||||
|         <Stack className={classes.dropdown} onClick={() => setOpenDesc(!openDesc)}> | ||||
|           {!openDesc ? ( | ||||
|             <KeyboardArrowRight className={classes.dropdownText} /> | ||||
|           ) : ( | ||||
|             <KeyboardArrowDown className={classes.dropdownText} /> | ||||
|           )} | ||||
|           <Typography className={classes.dropdownText}>Description</Typography> | ||||
|         </Stack> | ||||
|         <Collapse in={openDesc} timeout="auto" unmountOnExit> | ||||
|           <Box sx={{ padding: '0.5rem 0' }}> | ||||
|             <Typography variant="body2" align="left" sx={{ color: '#0F2139', fontSize: '1rem' }}> | ||||
|               {cve.description} | ||||
|             </Typography> | ||||
|           </Box> | ||||
|             <Typography className={classes.dropdownText}>Fixed in</Typography> | ||||
|           </Stack> | ||||
|           <Collapse in={openFixed} timeout="auto" unmountOnExit> | ||||
|             <Box sx={{ width: '100%', padding: '0.5rem 0' }}> | ||||
|               {loadingFixed ? ( | ||||
|                 'Loading...' | ||||
|               ) : ( | ||||
|                 <Stack direction="row" sx={{ flexWrap: 'wrap' }}> | ||||
|                   {renderFixedVer()} | ||||
|                   {renderLoadMore()} | ||||
|                 </Stack> | ||||
|               )} | ||||
|             </Box> | ||||
|           </Collapse> | ||||
|           <Stack className={classes.dropdown} onClick={() => setOpenDesc(!openDesc)}> | ||||
|             {!openDesc ? ( | ||||
|               <KeyboardArrowRight className={classes.dropdownText} /> | ||||
|             ) : ( | ||||
|               <KeyboardArrowDown className={classes.dropdownText} /> | ||||
|             )} | ||||
|             <Typography className={classes.dropdownText}>Description</Typography> | ||||
|           </Stack> | ||||
|           <Collapse in={openDesc} timeout="auto" unmountOnExit> | ||||
|             <Box sx={{ padding: '0.5rem 0' }}> | ||||
|               <Typography variant="body2" align="left" sx={{ color: '#0F2139', fontSize: '1rem' }}> | ||||
|                 {cve.description} | ||||
|               </Typography> | ||||
|             </Box> | ||||
|           </Collapse> | ||||
|         </Collapse> | ||||
|       </CardContent> | ||||
|     </Card> | ||||
|   | ||||
							
								
								
									
										92
									
								
								src/components/Shared/VulnerabilityCountCard.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/components/Shared/VulnerabilityCountCard.jsx
									
									
									
									
									
										Normal file
									
								
							| @@ -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 ( | ||||
|     <Stack direction="row" spacing="0.5em"> | ||||
|       <div className={[classes.cveCountCard].join(' ')}>Total {total}</div> | ||||
|       <div className={classes.severityList}> | ||||
|         <Tooltip title="Critical"> | ||||
|           <div className={[classes.cveCountCard, classes.criticalSeverity].join(' ')}>C {critical}</div> | ||||
|         </Tooltip> | ||||
|         <Tooltip title="High"> | ||||
|           <div className={[classes.cveCountCard, classes.highSeverity].join(' ')}>H {high}</div> | ||||
|         </Tooltip> | ||||
|         <Tooltip title="Medium"> | ||||
|           <div className={[classes.cveCountCard, classes.mediumSeverity].join(' ')}>M {medium}</div> | ||||
|         </Tooltip> | ||||
|         <Tooltip title="Low"> | ||||
|           <div className={[classes.cveCountCard, classes.lowSeverity].join(' ')}>L {low}</div> | ||||
|         </Tooltip> | ||||
|         <Tooltip title="Unknown"> | ||||
|           <div className={[classes.cveCountCard, classes.unknownSeverity].join(' ')}>U {unknown}</div> | ||||
|         </Tooltip> | ||||
|       </div> | ||||
|     </Stack> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export default VulnerabilitiyCountCard; | ||||
| @@ -9,6 +9,7 @@ import { | ||||
|   Stack, | ||||
|   Typography, | ||||
|   InputBase, | ||||
|   ToggleButton, | ||||
|   Menu, | ||||
|   MenuItem, | ||||
|   Divider, | ||||
| @@ -26,16 +27,29 @@ import DownloadIcon from '@mui/icons-material/Download'; | ||||
|  | ||||
| import * as XLSX from 'xlsx'; | ||||
| import exportFromJSON from 'export-from-json'; | ||||
| 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', | ||||
| @@ -64,6 +78,7 @@ const useStyles = makeStyles((theme) => ({ | ||||
|   search: { | ||||
|     position: 'relative', | ||||
|     maxWidth: '100%', | ||||
|     flex: 0.95, | ||||
|     flexDirection: 'row', | ||||
|     alignItems: 'center', | ||||
|     justifyContent: 'space-between', | ||||
| @@ -71,6 +86,20 @@ 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', | ||||
|     justifyContent: 'right' | ||||
|   }, | ||||
|   searchIcon: { | ||||
|     color: '#52637A', | ||||
|     paddingRight: '3%' | ||||
| @@ -87,15 +116,12 @@ const useStyles = makeStyles((theme) => ({ | ||||
|       opacity: '1' | ||||
|     } | ||||
|   }, | ||||
|   export: { | ||||
|     alignContent: 'right' | ||||
|   }, | ||||
|   popper: { | ||||
|     width: '100%', | ||||
|     overflow: 'hidden', | ||||
|     padding: '0.3rem', | ||||
|     display: 'flex', | ||||
|     justifyContent: 'center' | ||||
|     justifyContent: 'left' | ||||
|   } | ||||
| })); | ||||
|  | ||||
| @@ -103,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(), []); | ||||
| @@ -117,6 +144,8 @@ function VulnerabilitiesDetails(props) { | ||||
|   const [anchorExport, setAnchorExport] = useState(null); | ||||
|   const openExport = Boolean(anchorExport); | ||||
|  | ||||
|   const [selectedViewMore, setSelectedViewMore] = useState(true); | ||||
|  | ||||
|   const getCVERequestName = () => { | ||||
|     return digest !== '' ? `${name}@${digest}` : `${name}:${tag}`; | ||||
|   }; | ||||
| @@ -134,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); | ||||
|         } | ||||
| @@ -146,6 +189,7 @@ function VulnerabilitiesDetails(props) { | ||||
|         console.error(e); | ||||
|         setIsLoading(false); | ||||
|         setCveData([]); | ||||
|         setCVESummary(() => {}); | ||||
|         setIsEndOfList(true); | ||||
|       }); | ||||
|   }; | ||||
| @@ -182,7 +226,7 @@ function VulnerabilitiesDetails(props) { | ||||
|     const wb = XLSX.utils.book_new(), | ||||
|       ws = XLSX.utils.json_to_sheet(allCveData); | ||||
|  | ||||
|     XLSX.utils.book_append_sheet(wb, ws, name + '_' + tag); | ||||
|     XLSX.utils.book_append_sheet(wb, ws, name.replaceAll('/', '_') + '_' + tag); | ||||
|  | ||||
|     XLSX.writeFile(wb, `${name}:${tag}-vulnerabilities.xlsx`); | ||||
|  | ||||
| @@ -263,13 +307,34 @@ function VulnerabilitiesDetails(props) { | ||||
|   const renderCVEs = () => { | ||||
|     return !isEmpty(cveData) ? ( | ||||
|       cveData.map((cve, index) => { | ||||
|         return <VulnerabilitiyCard key={index} cve={cve} name={name} platform={platform} />; | ||||
|         return <VulnerabilitiyCard key={index} cve={cve} name={name} platform={platform} expand={selectedViewMore} />; | ||||
|       }) | ||||
|     ) : ( | ||||
|       <div>{!isLoading && <Typography className={classes.none}> No Vulnerabilities </Typography>}</div> | ||||
|     ); | ||||
|   }; | ||||
|  | ||||
|   const renderCVESummary = () => { | ||||
|     if (cveSummary === undefined) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     console.log('Test'); | ||||
|  | ||||
|     return !isEmpty(cveSummary) ? ( | ||||
|       <VulnerabilityCountCard | ||||
|         total={cveSummary.Count} | ||||
|         critical={cveSummary.CriticalCount} | ||||
|         high={cveSummary.HighCount} | ||||
|         medium={cveSummary.MediumCount} | ||||
|         low={cveSummary.LowCount} | ||||
|         unknown={cveSummary.UnknownCount} | ||||
|       /> | ||||
|     ) : ( | ||||
|       <></> | ||||
|     ); | ||||
|   }; | ||||
|  | ||||
|   const renderListBottom = () => { | ||||
|     if (isLoading) { | ||||
|       return <Loading />; | ||||
| @@ -286,14 +351,36 @@ function VulnerabilitiesDetails(props) { | ||||
|         <Typography variant="h4" gutterBottom component="div" align="left" className={classes.title}> | ||||
|           Vulnerabilities | ||||
|         </Typography> | ||||
|         <IconButton disableRipple onClick={handleClickExport} className={classes.export}> | ||||
|           <DownloadIcon /> | ||||
|         </IconButton> | ||||
|         <Snackbar | ||||
|           open={openExport && isLoadingAllCve} | ||||
|           message="Getting your data ready for export" | ||||
|           action={<CircularProgress size="2rem" sx={{ color: '#FFFFFF' }} />} | ||||
|         /> | ||||
|         <Stack direction="row" spacing="1rem" className={classes.viewModes}> | ||||
|           <IconButton disableRipple onClick={handleClickExport}> | ||||
|             <DownloadIcon /> | ||||
|           </IconButton> | ||||
|           <Snackbar | ||||
|             open={openExport && isLoadingAllCve} | ||||
|             message="Getting your data ready for export" | ||||
|             action={<CircularProgress size="2rem" sx={{ color: '#FFFFFF' }} />} | ||||
|           /> | ||||
|           <ToggleButton | ||||
|             value="viewLess" | ||||
|             title="Collapse list view" | ||||
|             size="small" | ||||
|             className={classes.view} | ||||
|             selected={!selectedViewMore} | ||||
|             onChange={() => setSelectedViewMore(false)} | ||||
|           > | ||||
|             <ViewHeadlineIcon /> | ||||
|           </ToggleButton> | ||||
|           <ToggleButton | ||||
|             value="viewMore" | ||||
|             title="Expand list view" | ||||
|             size="small" | ||||
|             className={classes.view} | ||||
|             selected={selectedViewMore} | ||||
|             onChange={() => setSelectedViewMore(true)} | ||||
|           > | ||||
|             <ViewAgendaIcon /> | ||||
|           </ToggleButton> | ||||
|         </Stack> | ||||
|         <Menu | ||||
|           anchorEl={anchorExport} | ||||
|           open={openExport} | ||||
| @@ -325,10 +412,11 @@ function VulnerabilitiesDetails(props) { | ||||
|             className={classes.popper} | ||||
|             data-testid="export-excel-menuItem" | ||||
|           > | ||||
|             MS Excel | ||||
|             xlsx | ||||
|           </MenuItem> | ||||
|         </Menu> | ||||
|       </Stack> | ||||
|       {renderCVESummary()} | ||||
|       <Stack className={classes.search}> | ||||
|         <InputBase | ||||
|           placeholder={'Search'} | ||||
|   | ||||
| @@ -37,6 +37,7 @@ test.describe('Tag page test', () => { | ||||
|     await page.goto(`${hosts.ui}/image/${tagWithVulnerabilities.title}/tag/${tagWithVulnerabilities.tag}`); | ||||
|     await page.getByRole('tab', { name: 'Vulnerabilities' }).click(); | ||||
|     await expect(page.getByTestId('vulnerability-container').locator('div').nth(1)).toBeVisible({ timeout: 100000 }); | ||||
|     await expect(page.getByText('CVE-').nth(0)).toBeVisible({ timeout: 100000 }); | ||||
|     await expect(await page.getByText('CVE-').count()).toBeGreaterThan(0); | ||||
|     await expect(await page.getByText('CVE-').count()).toBeLessThanOrEqual(pageSizes.EXPLORE); | ||||
|   }); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user