feat: add expand/collapse view list buttons for vulnerabilities (#409)

Signed-off-by: Andreea-Lupu <andreealupu1470@yahoo.com>
This commit is contained in:
Andreea Lupu 2024-01-17 15:38:39 +02:00 committed by GitHub
parent 6cda89c710
commit df19fa811c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 117 additions and 49 deletions

View File

@ -586,6 +586,22 @@ describe('Vulnerabilties page', () => {
expect(await screen.findByTestId('export-excel-menuItem')).not.toBeInTheDocument(); 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 () => { it('should handle fixed CVE query errors', async () => {
jest jest
.spyOn(api, 'get') .spyOn(api, 'get')

View File

@ -66,13 +66,18 @@ const useStyles = makeStyles((theme) => ({
cursor: 'pointer', cursor: 'pointer',
textAlign: 'center' textAlign: 'center'
}, },
dropdownCVE: {
color: '#1479FF',
cursor: 'pointer'
},
vulnerabilityCardDivider: { vulnerabilityCardDivider: {
margin: '1rem 0' margin: '1rem 0'
} }
})); }));
function VulnerabilitiyCard(props) { function VulnerabilitiyCard(props) {
const classes = useStyles(); 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 [openDesc, setOpenDesc] = useState(false);
const [openFixed, setOpenFixed] = useState(false); const [openFixed, setOpenFixed] = useState(false);
const [loadingFixed, setLoadingFixed] = useState(true); const [loadingFixed, setLoadingFixed] = useState(true);
@ -122,6 +127,10 @@ function VulnerabilitiyCard(props) {
}; };
}, [openFixed, pageNumber]); }, [openFixed, pageNumber]);
useEffect(() => {
setOpenCVE(expand);
}, [expand]);
const loadMore = () => { const loadMore = () => {
if (loadingFixed || isEndOfList) return; if (loadingFixed || isEndOfList) return;
setPageNumber((pageNumber) => pageNumber + 1); setPageNumber((pageNumber) => pageNumber + 1);
@ -166,49 +175,56 @@ function VulnerabilitiyCard(props) {
<Card className={classes.card} raised> <Card className={classes.card} raised>
<CardContent className={classes.content}> <CardContent className={classes.content}>
<Stack direction="row" spacing="1.25rem"> <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}> <Typography variant="body1" align="left" className={classes.cveId}>
{cve.id} {cve.id}
</Typography> </Typography>
<VulnerabilityChipCheck vulnerabilitySeverity={cve.severity} /> <VulnerabilityChipCheck vulnerabilitySeverity={cve.severity} />
</Stack> </Stack>
<Typography variant="body1" align="left" className={classes.cveSummary}> <Collapse in={openCVE} timeout="auto" unmountOnExit>
{cve.title} <Typography variant="body1" align="left" className={classes.cveSummary}>
</Typography> {cve.title}
<Divider className={classes.vulnerabilityCardDivider} /> </Typography>
<Stack className={classes.dropdown} onClick={() => setOpenFixed(!openFixed)}> <Divider className={classes.vulnerabilityCardDivider} />
{!openFixed ? ( <Stack className={classes.dropdown} onClick={() => setOpenFixed(!openFixed)}>
<KeyboardArrowRight className={classes.dropdownText} /> {!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...'
) : ( ) : (
<Stack direction="row" sx={{ flexWrap: 'wrap' }}> <KeyboardArrowDown className={classes.dropdownText} />
{renderFixedVer()}
{renderLoadMore()}
</Stack>
)} )}
</Box> <Typography className={classes.dropdownText}>Fixed in</Typography>
</Collapse> </Stack>
<Stack className={classes.dropdown} onClick={() => setOpenDesc(!openDesc)}> <Collapse in={openFixed} timeout="auto" unmountOnExit>
{!openDesc ? ( <Box sx={{ width: '100%', padding: '0.5rem 0' }}>
<KeyboardArrowRight className={classes.dropdownText} /> {loadingFixed ? (
) : ( 'Loading...'
<KeyboardArrowDown className={classes.dropdownText} /> ) : (
)} <Stack direction="row" sx={{ flexWrap: 'wrap' }}>
<Typography className={classes.dropdownText}>Description</Typography> {renderFixedVer()}
</Stack> {renderLoadMore()}
<Collapse in={openDesc} timeout="auto" unmountOnExit> </Stack>
<Box sx={{ padding: '0.5rem 0' }}> )}
<Typography variant="body2" align="left" sx={{ color: '#0F2139', fontSize: '1rem' }}> </Box>
{cve.description} </Collapse>
</Typography> <Stack className={classes.dropdown} onClick={() => setOpenDesc(!openDesc)}>
</Box> {!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> </Collapse>
</CardContent> </CardContent>
</Card> </Card>

View File

@ -9,6 +9,7 @@ import {
Stack, Stack,
Typography, Typography,
InputBase, InputBase,
ToggleButton,
Menu, Menu,
MenuItem, MenuItem,
Divider, Divider,
@ -26,6 +27,8 @@ import DownloadIcon from '@mui/icons-material/Download';
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
import exportFromJSON from 'export-from-json'; 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 VulnerabilitiyCard from '../../Shared/VulnerabilityCard';
@ -71,6 +74,17 @@ const useStyles = makeStyles((theme) => ({
border: '0.063rem solid #E7E7E7', border: '0.063rem solid #E7E7E7',
borderRadius: '0.625rem' borderRadius: '0.625rem'
}, },
view: {
alignContent: 'right',
variant: 'outlined'
},
viewModes: {
position: 'relative',
maxWidth: '100%',
flexDirection: 'row',
alignItems: 'right',
justifyContent: 'right'
},
searchIcon: { searchIcon: {
color: '#52637A', color: '#52637A',
paddingRight: '3%' paddingRight: '3%'
@ -87,9 +101,6 @@ const useStyles = makeStyles((theme) => ({
opacity: '1' opacity: '1'
} }
}, },
export: {
alignContent: 'right'
},
popper: { popper: {
width: '100%', width: '100%',
overflow: 'hidden', overflow: 'hidden',
@ -117,6 +128,8 @@ function VulnerabilitiesDetails(props) {
const [anchorExport, setAnchorExport] = useState(null); const [anchorExport, setAnchorExport] = useState(null);
const openExport = Boolean(anchorExport); const openExport = Boolean(anchorExport);
const [selectedViewMore, setSelectedViewMore] = useState(true);
const getCVERequestName = () => { const getCVERequestName = () => {
return digest !== '' ? `${name}@${digest}` : `${name}:${tag}`; return digest !== '' ? `${name}@${digest}` : `${name}:${tag}`;
}; };
@ -263,7 +276,7 @@ function VulnerabilitiesDetails(props) {
const renderCVEs = () => { const renderCVEs = () => {
return !isEmpty(cveData) ? ( return !isEmpty(cveData) ? (
cveData.map((cve, index) => { 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> <div>{!isLoading && <Typography className={classes.none}> No Vulnerabilities </Typography>}</div>
@ -286,14 +299,36 @@ function VulnerabilitiesDetails(props) {
<Typography variant="h4" gutterBottom component="div" align="left" className={classes.title}> <Typography variant="h4" gutterBottom component="div" align="left" className={classes.title}>
Vulnerabilities Vulnerabilities
</Typography> </Typography>
<IconButton disableRipple onClick={handleClickExport} className={classes.export}> <Stack direction="row" spacing="1rem" className={classes.viewModes}>
<DownloadIcon /> <IconButton disableRipple onClick={handleClickExport}>
</IconButton> <DownloadIcon />
<Snackbar </IconButton>
open={openExport && isLoadingAllCve} <Snackbar
message="Getting your data ready for export" open={openExport && isLoadingAllCve}
action={<CircularProgress size="2rem" sx={{ color: '#FFFFFF' }} />} 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 <Menu
anchorEl={anchorExport} anchorEl={anchorExport}
open={openExport} open={openExport}

View File

@ -37,6 +37,7 @@ test.describe('Tag page test', () => {
await page.goto(`${hosts.ui}/image/${tagWithVulnerabilities.title}/tag/${tagWithVulnerabilities.tag}`); await page.goto(`${hosts.ui}/image/${tagWithVulnerabilities.title}/tag/${tagWithVulnerabilities.tag}`);
await page.getByRole('tab', { name: 'Vulnerabilities' }).click(); await page.getByRole('tab', { name: 'Vulnerabilities' }).click();
await expect(page.getByTestId('vulnerability-container').locator('div').nth(1)).toBeVisible({ timeout: 100000 }); 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()).toBeGreaterThan(0);
await expect(await page.getByText('CVE-').count()).toBeLessThanOrEqual(pageSizes.EXPLORE); await expect(await page.getByText('CVE-').count()).toBeLessThanOrEqual(pageSizes.EXPLORE);
}); });