patch: integrated pagination in cve tab

Signed-off-by: Raul Kele <raulkeleblk@gmail.com>
This commit is contained in:
Raul Kele 2023-01-26 13:46:51 +02:00
parent cea2fb605e
commit a4b25adb51
5 changed files with 104 additions and 33 deletions

View File

@ -15,6 +15,7 @@ const StateVulnerabilitiesWrapper = () => {
const mockCVEList = {
CVEListForImage: {
Tag: '',
Page: { ItemCount: 20, TotalCount: 20 },
CVEList: [
{
Id: 'CVE-2020-16156',
@ -445,6 +446,17 @@ const mockCVEFixed = {
]
};
beforeEach(() => {
// IntersectionObserver isn't available in test environment
const mockIntersectionObserver = jest.fn();
mockIntersectionObserver.mockReturnValue({
observe: () => null,
unobserve: () => null,
disconnect: () => null
});
window.IntersectionObserver = mockIntersectionObserver;
});
afterEach(() => {
// restore the spy created with spyOn
jest.restoreAllMocks();
@ -461,7 +473,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: '', CVEList: [] } } }
data: { data: { CVEListForImage: { Tag: '', Page: {}, CVEList: [] } } }
});
render(<StateVulnerabilitiesWrapper />);
await waitFor(() => expect(screen.getAllByText('No Vulnerabilities')).toHaveLength(1));

View File

@ -68,8 +68,10 @@ const endpoints = {
`/v2/_zot/ext/search?query={ExpandedRepoInfo(repo:"${name}"){Images {Digest Vulnerabilities {MaxSeverity Count} Tag LastUpdated Vendor Size Platform {Os Arch}} Summary {Name LastUpdated Size Platforms {Os Arch} Vendors NewestImage {RepoName IsSigned Vulnerabilities {MaxSeverity Count} Layers {Size Digest} Digest Tag Logo 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 IsSigned Vulnerabilities {MaxSeverity Count} Tag Digest LastUpdated Size ConfigDigest Platform {Os Arch} Vendor Licenses Logo}}`,
vulnerabilitiesForRepo: (name) =>
`/v2/_zot/ext/search?query={CVEListForImage(image: "${name}"){Tag, CVEList {Id Title Description Severity PackageList {Name InstalledVersion FixedVersion}}}}`,
vulnerabilitiesForRepo: (name, { pageNumber = 1, pageSize = 15 }) =>
`/v2/_zot/ext/search?query={CVEListForImage(image: "${name}", requestedPage: {limit:${pageSize} offset:${
(pageNumber - 1) * pageSize
}}){Tag Page {TotalCount ItemCount} CVEList {Id Title Description Severity PackageList {Name InstalledVersion FixedVersion}}}}`,
layersDetailsForImage: (name) =>
`/v2/_zot/ext/search?query={Image(image: "${name}"){History {Layer {Size Digest Score} HistoryDescription {Created CreatedBy Author Comment EmptyLayer} }}}`,
imageListWithCVEFixed: (cveId, repoName) =>

View File

@ -1,5 +1,5 @@
import { useParams } from 'react-router-dom';
import React, { useEffect, useMemo, useState } from 'react';
import React, { useEffect, useMemo, useState, useRef } from 'react';
// utility
import { api, endpoints } from '../api';
@ -214,6 +214,7 @@ function TagDetails() {
const [selectedTab, setSelectedTab] = useState('Layers');
const [selectedPullTab, setSelectedPullTab] = useState('');
const abortController = useMemo(() => new AbortController(), []);
const mounted = useRef(false);
// get url param from <Route here (i.e. image name)
const { reponame, tag } = useParams();
@ -223,6 +224,7 @@ function TagDetails() {
const classes = useStyles();
useEffect(() => {
mounted.current = true;
// if same-page navigation because of tag update, following 2 lines help ux
setSelectedTab('Layers');
window?.scrollTo(0, 0);
@ -246,6 +248,7 @@ function TagDetails() {
});
return () => {
abortController.abort();
mounted.current = false;
};
}, [reponame, tag]);
@ -265,7 +268,9 @@ function TagDetails() {
useEffect(() => {
if (isCopied) {
setTimeout(() => {
setIsCopied(false);
if (mounted.current) {
setIsCopied(false);
}
}, 3000);
}
}, [isCopied]);

View File

@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useState } from 'react';
import React, { useEffect, useMemo, useState, useRef } from 'react';
// utility
import { api, endpoints } from '../api';
@ -14,6 +14,7 @@ import Loading from './Loading';
import { KeyboardArrowDown, KeyboardArrowRight } from '@mui/icons-material';
import { VulnerabilityChipCheck } from 'utilities/vulnerabilityAndSignatureCheck';
import { mapCVEInfo } from 'utilities/objectModels';
import { EXPLORE_PAGE_SIZE } from 'utilities/paginationConstants';
const useStyles = makeStyles(() => ({
card: {
@ -179,48 +180,96 @@ function VulnerabilitiyCard(props) {
function VulnerabilitiesDetails(props) {
const classes = useStyles();
const [cveData, setCveData] = useState({});
const [cveData, setCveData] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const abortController = useMemo(() => new AbortController(), []);
const { name, tag } = props;
useEffect(() => {
// pagination props
const [pageNumber, setPageNumber] = useState(1);
const [isEndOfList, setIsEndOfList] = useState(false);
const listBottom = useRef(null);
const getPaginatedCVEs = () => {
setIsLoading(true);
api
.get(`${host()}${endpoints.vulnerabilitiesForRepo(`${name}:${tag}`)}`, abortController.signal)
.get(
`${host()}${endpoints.vulnerabilitiesForRepo(`${name}:${tag}`, { pageNumber, pageSize: EXPLORE_PAGE_SIZE })}`,
abortController.signal
)
.then((response) => {
if (response.data && response.data.data) {
let cveInfo = response.data.data.CVEListForImage;
let cveListData = mapCVEInfo(cveInfo);
setCveData(cveListData);
if (!isEmpty(response.data.data.CVEListForImage?.CVEList)) {
let cveInfo = response.data.data.CVEListForImage.CVEList;
let cveListData = mapCVEInfo(cveInfo);
const newCVEList = [...cveData, ...cveListData];
setCveData(newCVEList);
setIsEndOfList(
response.data.data.CVEListForImage.Page?.ItemCount < EXPLORE_PAGE_SIZE ||
newCVEList.length >= response.data.data.CVEListForImage?.Page?.TotalCount
);
}
}
setIsLoading(false);
})
.catch((e) => {
console.error(e);
setIsLoading(false);
setCveData({});
setCveData([]);
setIsEndOfList(true);
});
};
useEffect(() => {
getPaginatedCVEs();
return () => {
abortController.abort();
};
}, []);
}, [pageNumber]);
const renderCVEs = (cves) => {
if (cves?.length !== 0) {
return (
cves &&
cves.map((cve, index) => {
return <VulnerabilitiyCard key={index} cve={cve} name={name} />;
})
);
} else {
return (
<div>
<Typography className={classes.none}> No Vulnerabilities </Typography>{' '}
</div>
);
// setup intersection obeserver for infinite scroll
useEffect(() => {
if (isLoading || isEndOfList) return;
const handleIntersection = (entries) => {
if (isLoading || isEndOfList) return;
const [target] = entries;
if (target?.isIntersecting) {
setPageNumber((pageNumber) => pageNumber + 1);
}
};
const intersectionObserver = new IntersectionObserver(handleIntersection, {
root: null,
rootMargin: '0px',
threshold: 0
});
if (listBottom.current) {
intersectionObserver.observe(listBottom.current);
}
return () => {
intersectionObserver.disconnect();
};
}, [isLoading, isEndOfList]);
const renderCVEs = () => {
return !isEmpty(cveData) ? (
cveData.map((cve, index) => {
return <VulnerabilitiyCard key={index} cve={cve} name={name} />;
})
) : (
<div>{!isLoading && <Typography className={classes.none}> No Vulnerabilities </Typography>}</div>
);
};
const renderListBottom = () => {
if (isLoading) {
return <Loading />;
}
if (!isLoading && !isEndOfList) {
return <div ref={listBottom} />;
}
return '';
};
return (
@ -248,7 +297,12 @@ function VulnerabilitiesDetails(props) {
width: '100%'
}}
/>
{isLoading ? <Loading /> : renderCVEs(cveData?.cveList)}
<Stack direction="column" spacing={2}>
<Stack direction="column" spacing={2}>
{renderCVEs()}
{renderListBottom()}
</Stack>
</Stack>
</>
);
}

View File

@ -60,7 +60,7 @@ const mapToImage = (responseImage) => {
};
const mapCVEInfo = (cveInfo) => {
const cveList = cveInfo.CVEList?.map((cve) => {
const cveList = cveInfo.map((cve) => {
return {
id: cve.Id,
severity: cve.Severity,
@ -68,9 +68,7 @@ const mapCVEInfo = (cveInfo) => {
description: cve.Description
};
});
return {
cveList
};
return cveList;
};
const mapReferrer = (referrer) => ({