Skip to content

Commit

Permalink
patch: integrated pagination in cve tab
Browse files Browse the repository at this point in the history
Signed-off-by: Raul Kele <raulkeleblk@gmail.com>
  • Loading branch information
raulkele committed Jan 26, 2023
1 parent cea2fb6 commit 470e698
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 33 deletions.
14 changes: 13 additions & 1 deletion src/__tests__/TagPage/VulnerabilitiesDetails.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const StateVulnerabilitiesWrapper = () => {
const mockCVEList = {
CVEListForImage: {
Tag: '',
Page: { ItemCount: 4, TotalCount: 4 },
CVEList: [
{
Id: 'CVE-2020-16156',
Expand Down Expand Up @@ -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();
Expand All @@ -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));
Expand Down
6 changes: 4 additions & 2 deletions src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down
9 changes: 7 additions & 2 deletions src/components/TagDetails.jsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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();
Expand All @@ -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);
Expand All @@ -246,6 +248,7 @@ function TagDetails() {
});
return () => {
abortController.abort();
mounted.current = false;
};
}, [reponame, tag]);

Expand All @@ -265,7 +268,9 @@ function TagDetails() {
useEffect(() => {
if (isCopied) {
setTimeout(() => {
setIsCopied(false);
if (mounted.current) {
setIsCopied(false);
}
}, 3000);
}
}, [isCopied]);
Expand Down
102 changes: 78 additions & 24 deletions src/components/VulnerabilitiesDetails.jsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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: {
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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>
</>
);
}
Expand Down
6 changes: 2 additions & 4 deletions src/utilities/objectModels.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,15 @@ const mapToImage = (responseImage) => {
};

const mapCVEInfo = (cveInfo) => {
const cveList = cveInfo.CVEList?.map((cve) => {
const cveList = cveInfo.map((cve) => {
return {
id: cve.Id,
severity: cve.Severity,
title: cve.Title,
description: cve.Description
};
});
return {
cveList
};
return cveList;
};

const mapReferrer = (referrer) => ({
Expand Down

0 comments on commit 470e698

Please sign in to comment.