patch: integrated pagination in cve tab
Signed-off-by: Raul Kele <raulkeleblk@gmail.com>
This commit is contained in:
parent
cea2fb605e
commit
a4b25adb51
@ -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));
|
||||
|
@ -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) =>
|
||||
|
@ -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]);
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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) => ({
|
||||
|
Loading…
Reference in New Issue
Block a user