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 = { const mockCVEList = {
CVEListForImage: { CVEListForImage: {
Tag: '', Tag: '',
Page: { ItemCount: 20, TotalCount: 20 },
CVEList: [ CVEList: [
{ {
Id: 'CVE-2020-16156', 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(() => { afterEach(() => {
// restore the spy created with spyOn // restore the spy created with spyOn
jest.restoreAllMocks(); jest.restoreAllMocks();
@ -461,7 +473,7 @@ describe('Vulnerabilties page', () => {
it('renders no vulnerabilities if there are not any', async () => { it('renders no vulnerabilities if there are not any', async () => {
jest.spyOn(api, 'get').mockResolvedValue({ jest.spyOn(api, 'get').mockResolvedValue({
status: 200, status: 200,
data: { data: { CVEListForImage: { Tag: '', CVEList: [] } } } data: { data: { CVEListForImage: { Tag: '', Page: {}, CVEList: [] } } }
}); });
render(<StateVulnerabilitiesWrapper />); render(<StateVulnerabilitiesWrapper />);
await waitFor(() => expect(screen.getAllByText('No Vulnerabilities')).toHaveLength(1)); 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}}}}}}`, `/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) => 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}}`, `/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) => vulnerabilitiesForRepo: (name, { pageNumber = 1, pageSize = 15 }) =>
`/v2/_zot/ext/search?query={CVEListForImage(image: "${name}"){Tag, CVEList {Id Title Description Severity PackageList {Name InstalledVersion FixedVersion}}}}`, `/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) => layersDetailsForImage: (name) =>
`/v2/_zot/ext/search?query={Image(image: "${name}"){History {Layer {Size Digest Score} HistoryDescription {Created CreatedBy Author Comment EmptyLayer} }}}`, `/v2/_zot/ext/search?query={Image(image: "${name}"){History {Layer {Size Digest Score} HistoryDescription {Created CreatedBy Author Comment EmptyLayer} }}}`,
imageListWithCVEFixed: (cveId, repoName) => imageListWithCVEFixed: (cveId, repoName) =>

View File

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

View File

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