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 = {
|
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));
|
||||||
|
@ -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) =>
|
||||||
|
@ -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]);
|
||||||
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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) => ({
|
||||||
|
Loading…
Reference in New Issue
Block a user