feat: add expand/collapse view list buttons for vulnerabilities (#409)
Signed-off-by: Andreea-Lupu <andreealupu1470@yahoo.com>
This commit is contained in:
parent
6cda89c710
commit
df19fa811c
@ -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')
|
||||||
|
@ -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>
|
||||||
|
@ -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}
|
||||||
|
@ -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);
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user