diff --git a/src/__tests__/TagPage/TagDetails.test.js b/src/__tests__/TagPage/TagDetails.test.js
index 7ef570d7..754bdf7a 100644
--- a/src/__tests__/TagPage/TagDetails.test.js
+++ b/src/__tests__/TagPage/TagDetails.test.js
@@ -4,11 +4,16 @@ import userEvent from '@testing-library/user-event';
import { api } from 'api';
import TagDetails from 'components/Tag/TagDetails';
import MockThemeProvier from '__mocks__/MockThemeProvider';
+import { BrowserRouter, Routes, Route } from 'react-router-dom';
const TagDetailsThemeWrapper = () => {
return (
-
+
+
+ } />
+
+
);
};
@@ -191,6 +196,48 @@ const mockImageHigh = {
}
};
+const mockDependenciesList = {
+ data: {
+ BaseImageList: {
+ Page: { ItemCount: 4, TotalCount: 4 },
+ Results: [
+ {
+ RepoName: 'project-stacker/c3/static-ubuntu-amd64',
+ Tag: 'tag1',
+ Vulnerabilities: {
+ MaxSeverity: 'HIGH',
+ Count: 5
+ }
+ },
+ {
+ RepoName: 'tag2',
+ Tag: 'tag2',
+ Vulnerabilities: {
+ MaxSeverity: 'CRITICAL',
+ Count: 2
+ }
+ },
+ {
+ RepoName: 'tag3',
+ Tag: 'tag3',
+ Vulnerabilities: {
+ MaxSeverity: 'LOW',
+ Count: 7
+ }
+ },
+ {
+ RepoName: 'tag4',
+ Tag: 'tag4',
+ Vulnerabilities: {
+ MaxSeverity: 'HIGH',
+ Count: 5
+ }
+ }
+ ]
+ }
+ }
+};
+
// mock clipboard copy fn
const mockCopyToClipboard = jest.fn();
Object.assign(navigator, {
@@ -232,7 +279,7 @@ describe('Tags details', () => {
it('should show tabs and allow nagivation between them', async () => {
jest.spyOn(api, 'get').mockResolvedValueOnce({ status: 200, data: { data: mockImage } });
render();
- jest.spyOn(api, 'get').mockResolvedValue({ status: 500, data: { data: { errors: ['test error'] } } });
+ jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: mockDependenciesList });
const dependenciesTab = await screen.findByTestId('dependencies-tab');
fireEvent.click(dependenciesTab);
expect(await screen.findByTestId('depends-on-container')).toBeInTheDocument();
diff --git a/src/__tests__/TagPage/VulnerabilitiesDetails.test.js b/src/__tests__/TagPage/VulnerabilitiesDetails.test.js
index c503aa18..668a89bb 100644
--- a/src/__tests__/TagPage/VulnerabilitiesDetails.test.js
+++ b/src/__tests__/TagPage/VulnerabilitiesDetails.test.js
@@ -433,17 +433,35 @@ const mockCVEList = {
};
const mockCVEFixed = {
- ImageListWithCVEFixed: [
- {
- Tag: '1.0.16'
- },
- {
- Tag: '0.4.33'
- },
- {
- Tag: '1.0.17'
+ pageOne: {
+ ImageListWithCVEFixed: {
+ Page: { TotalCount: 5, ItemCount: 3 },
+ Results: [
+ {
+ Tag: '1.0.16'
+ },
+ {
+ Tag: '0.4.33'
+ },
+ {
+ Tag: '1.0.17'
+ }
+ ]
}
- ]
+ },
+ pageTwo: {
+ ImageListWithCVEFixed: {
+ Page: { TotalCount: 5, ItemCount: 2 },
+ Results: [
+ {
+ Tag: 'slim'
+ },
+ {
+ Tag: 'latest'
+ }
+ ]
+ }
+ }
};
beforeEach(() => {
@@ -484,7 +502,7 @@ describe('Vulnerabilties page', () => {
render();
await waitFor(() => expect(screen.getAllByText(/description/i)).toHaveLength(20));
const openText = screen.getAllByText(/description/i);
- fireEvent.click(openText[0]);
+ await fireEvent.click(openText[0]);
await waitFor(() =>
expect(screen.getAllByText(/CPAN 2.28 allows Signature Verification Bypass./i)).toHaveLength(1)
);
@@ -504,13 +522,30 @@ describe('Vulnerabilties page', () => {
it('should find out which version fixes the CVEs', async () => {
jest
.spyOn(api, 'get')
-
.mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } })
-
- .mockResolvedValue({ status: 200, data: { data: mockCVEFixed } });
+ .mockResolvedValueOnce({ status: 200, data: { data: mockCVEFixed.pageOne } })
+ .mockResolvedValueOnce({ status: 200, data: { data: mockCVEFixed.pageTwo } });
render();
await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1));
- fireEvent.click(screen.getAllByText(/fixed in/i)[0]);
+ await fireEvent.click(screen.getAllByText(/fixed in/i)[0]);
await waitFor(() => expect(screen.getByText('1.0.16')).toBeInTheDocument());
+ const loadMoreBtn = screen.getByText(/load more/i);
+ expect(loadMoreBtn).toBeInTheDocument();
+ await fireEvent.click(loadMoreBtn);
+ await waitFor(() => expect(loadMoreBtn).not.toBeInTheDocument());
+ await expect(await screen.findByText('latest')).toBeInTheDocument();
+ });
+
+ it('should handle fixed CVE query errors', async () => {
+ jest
+ .spyOn(api, 'get')
+ .mockResolvedValueOnce({ status: 200, data: { data: mockCVEList } })
+ .mockRejectedValue({ status: 500, data: {} });
+ render();
+ await waitFor(() => expect(screen.getAllByText('Vulnerabilities')).toHaveLength(1));
+ const error = jest.spyOn(console, 'error').mockImplementation(() => {});
+ await fireEvent.click(screen.getAllByText(/fixed in/i)[0]);
+ await waitFor(() => expect(screen.getByText(/not fixed/i)).toBeInTheDocument());
+ await waitFor(() => expect(error).toBeCalledTimes(1));
});
});
diff --git a/src/api.js b/src/api.js
index 18bc8346..22894688 100644
--- a/src/api.js
+++ b/src/api.js
@@ -86,8 +86,10 @@ const endpoints = {
}}){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) =>
- `/v2/_zot/ext/search?query={ImageListWithCVEFixed(id:"${cveId}", image:"${repoName}") {Tag}}`,
+ imageListWithCVEFixed: (cveId, repoName, { pageNumber = 1, pageSize = 3 }) =>
+ `/v2/_zot/ext/search?query={ImageListWithCVEFixed(id:"${cveId}", image:"${repoName}", requestedPage: {limit:${pageSize} offset:${
+ (pageNumber - 1) * pageSize
+ }}) {Page {TotalCount ItemCount} Results {Tag}}}`,
dependsOnForImage: (name, { pageNumber = 1, pageSize = 15 } = {}) =>
`/v2/_zot/ext/search?query={BaseImageList(image: "${name}", requestedPage: {limit:${pageSize} offset:${
(pageNumber - 1) * pageSize
diff --git a/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx b/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx
index 83204016..e00b08dc 100644
--- a/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx
+++ b/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx
@@ -14,7 +14,7 @@ import Loading from '../../Shared/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';
+import { CVE_FIXEDIN_PAGE_SIZE, EXPLORE_PAGE_SIZE } from 'utilities/paginationConstants';
const useStyles = makeStyles(() => ({
card: {
@@ -91,25 +91,50 @@ function VulnerabilitiyCard(props) {
const [openFixed, setOpenFixed] = useState(false);
const [loadingFixed, setLoadingFixed] = useState(true);
const [fixedInfo, setFixedInfo] = useState([]);
+ const abortController = useMemo(() => new AbortController(), []);
- useEffect(() => {
- if (!openFixed || !isEmpty(fixedInfo)) {
+ // pagination props
+ const [pageNumber, setPageNumber] = useState(1);
+ const [isEndOfList, setIsEndOfList] = useState(false);
+
+ const getPaginatedResults = () => {
+ if (!openFixed || isEndOfList) {
return;
}
setLoadingFixed(true);
api
- .get(`${host()}${endpoints.imageListWithCVEFixed(cve.id, name)}`)
+ .get(
+ `${host()}${endpoints.imageListWithCVEFixed(cve.id, name, { pageNumber, pageSize: CVE_FIXEDIN_PAGE_SIZE })}`,
+ abortController.signal
+ )
.then((response) => {
if (response.data && response.data.data) {
- const fixedTagsList = response.data.data.ImageListWithCVEFixed?.map((e) => e.Tag);
- setFixedInfo(fixedTagsList);
+ const fixedTagsList = response.data.data.ImageListWithCVEFixed?.Results?.map((e) => e.Tag);
+ setFixedInfo((previousState) => [...previousState, ...fixedTagsList]);
+ setIsEndOfList(
+ [...fixedInfo, ...fixedTagsList].length >= response.data.data.ImageListWithCVEFixed?.Page?.TotalCount
+ );
}
setLoadingFixed(false);
})
.catch((e) => {
console.error(e);
+ setIsEndOfList(true);
+ setLoadingFixed(false);
});
- }, [openFixed]);
+ };
+
+ useEffect(() => {
+ getPaginatedResults();
+ return () => {
+ abortController.abort();
+ };
+ }, [openFixed, pageNumber]);
+
+ const loadMore = () => {
+ if (loadingFixed || isEndOfList) return;
+ setPageNumber((pageNumber) => pageNumber + 1);
+ };
const renderFixedVer = () => {
if (!isEmpty(fixedInfo)) {
@@ -152,8 +177,28 @@ function VulnerabilitiyCard(props) {
- {' '}
- {loadingFixed ? 'Loading...' : renderFixedVer()}{' '}
+ {loadingFixed ? (
+ 'Loading...'
+ ) : (
+ <>
+ {renderFixedVer()}
+ {!isEndOfList && (
+
+ Load more
+
+ )}
+ >
+ )}
diff --git a/src/utilities/paginationConstants.js b/src/utilities/paginationConstants.js
index 9ed5f8f9..18a77d07 100644
--- a/src/utilities/paginationConstants.js
+++ b/src/utilities/paginationConstants.js
@@ -1,5 +1,6 @@
const HEADER_SEARCH_PAGE_SIZE = 9;
const EXPLORE_PAGE_SIZE = 10;
const HOME_PAGE_SIZE = 10;
+const CVE_FIXEDIN_PAGE_SIZE = 5;
-export { HEADER_SEARCH_PAGE_SIZE, EXPLORE_PAGE_SIZE, HOME_PAGE_SIZE };
+export { HEADER_SEARCH_PAGE_SIZE, EXPLORE_PAGE_SIZE, HOME_PAGE_SIZE, CVE_FIXEDIN_PAGE_SIZE };