patch: Implement pagination on ImageListWithCVEFixed query

Signed-off-by: Raul Kele <raulkeleblk@gmail.com>
This commit is contained in:
Raul Kele 2023-02-16 15:10:41 +02:00
parent c9c058a350
commit 6db80e4700
5 changed files with 159 additions and 29 deletions

View File

@ -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 (
<MockThemeProvier>
<TagDetails />
<BrowserRouter>
<Routes>
<Route path="*" element={<TagDetails />} />
</Routes>
</BrowserRouter>
</MockThemeProvier>
);
};
@ -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(<TagDetailsThemeWrapper />);
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();

View File

@ -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(<StateVulnerabilitiesWrapper />);
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(<StateVulnerabilitiesWrapper />);
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(<StateVulnerabilitiesWrapper />);
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));
});
});

View File

@ -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

View File

@ -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) {
<Collapse in={openFixed} timeout="auto" unmountOnExit>
<Box>
<Typography variant="body2" align="left" sx={{ color: '#0F2139', fontSize: '1rem', width: '100%' }}>
{' '}
{loadingFixed ? 'Loading...' : renderFixedVer()}{' '}
{loadingFixed ? (
'Loading...'
) : (
<>
{renderFixedVer()}
{!isEndOfList && (
<Typography
sx={{
color: '#3366CC',
fontSize: '1rem',
display: 'inline',
textDecorationLine: 'underline',
cursor: 'pointer'
}}
onClick={loadMore}
component="span"
>
Load more
</Typography>
)}
</>
)}
</Typography>
</Box>
</Collapse>

View File

@ -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 };