diff --git a/src/__tests__/TagPage/VulnerabilitiesDetails.test.js b/src/__tests__/TagPage/VulnerabilitiesDetails.test.js
index 668a89bb..cb9328d9 100644
--- a/src/__tests__/TagPage/VulnerabilitiesDetails.test.js
+++ b/src/__tests__/TagPage/VulnerabilitiesDetails.test.js
@@ -1,4 +1,5 @@
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
import { api } from 'api';
import VulnerabilitiesDetails from 'components/Tag/Tabs/VulnerabilitiesDetails';
import React from 'react';
@@ -432,6 +433,14 @@ const mockCVEList = {
}
};
+const mockCVEListFiltered = {
+ CVEListForImage: {
+ Tag: '',
+ Page: { ItemCount: 20, TotalCount: 20 },
+ CVEList: mockCVEList.CVEListForImage.CVEList.filter((e) => e.Id.includes('2022'))
+ }
+};
+
const mockCVEFixed = {
pageOne: {
ImageListWithCVEFixed: {
@@ -488,6 +497,16 @@ describe('Vulnerabilties page', () => {
await waitFor(() => expect(screen.getAllByText(/fixed in/i)).toHaveLength(20));
});
+ it('sends filtered query if user types in the search bar', async () => {
+ jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEList } });
+ render();
+ const cveSearchInput = screen.getByPlaceholderText(/search for/i);
+ jest.spyOn(api, 'get').mockRejectedValueOnce({ status: 200, data: { data: mockCVEListFiltered } });
+ await userEvent.type(cveSearchInput, '2022');
+ expect((await screen.queryAllByText(/2023/i).length) === 0);
+ expect((await screen.findAllByText(/2022/i)).length === 6);
+ });
+
it('renders no vulnerabilities if there are not any', async () => {
jest.spyOn(api, 'get').mockResolvedValue({
status: 200,
diff --git a/src/api.js b/src/api.js
index 82228420..a6fb426c 100644
--- a/src/api.js
+++ b/src/api.js
@@ -79,10 +79,15 @@ const endpoints = {
`/v2/_zot/ext/search?query={ExpandedRepoInfo(repo:"${name}"){Images {Manifests {Digest Platform {Os Arch} Size} Vulnerabilities {MaxSeverity Count} Tag LastUpdated Vendor } Summary {Name LastUpdated Size Platforms {Os Arch} Vendors NewestImage {RepoName IsSigned Vulnerabilities {MaxSeverity Count} Manifests {Digest} Tag Title Documentation DownloadCount Source Description Licenses}}}}`,
detailedImageInfo: (name, tag) =>
`/v2/_zot/ext/search?query={Image(image: "${name}:${tag}"){RepoName IsSigned Vulnerabilities {MaxSeverity Count} Tag Manifests {History {Layer {Size Digest} HistoryDescription {CreatedBy EmptyLayer}} Digest ConfigDigest LastUpdated Size Platform {Os Arch}} Vendor Licenses }}`,
- vulnerabilitiesForRepo: (name, { pageNumber = 1, pageSize = 15 }) =>
- `/v2/_zot/ext/search?query={CVEListForImage(image: "${name}", requestedPage: {limit:${pageSize} offset:${
+ vulnerabilitiesForRepo: (name, { pageNumber = 1, pageSize = 15 }, searchTerm = '') => {
+ let query = `/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}}}}`,
+ }}`;
+ if (!isEmpty(searchTerm)) {
+ query += `, searchedCVE: "${searchTerm}"`;
+ }
+ return `${query}){Tag Page {TotalCount ItemCount} CVEList {Id Title Description Severity PackageList {Name InstalledVersion FixedVersion}}}}`;
+ },
imageListWithCVEFixed: (cveId, repoName, { pageNumber = 1, pageSize = 3 }) =>
`/v2/_zot/ext/search?query={ImageListWithCVEFixed(id:"${cveId}", image:"${repoName}", requestedPage: {limit:${pageSize} offset:${
(pageNumber - 1) * pageSize
diff --git a/src/components/Explore/Explore.jsx b/src/components/Explore/Explore.jsx
index b6239a84..6a40e0e5 100644
--- a/src/components/Explore/Explore.jsx
+++ b/src/components/Explore/Explore.jsx
@@ -269,7 +269,7 @@ function Explore({ searchInputValue }) {
if (!isLoading && !isEndOfList) {
return
;
}
- return '';
+ return;
};
return (
diff --git a/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx b/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx
index cc7a60ae..eb4ba077 100644
--- a/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx
+++ b/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx
@@ -5,16 +5,17 @@ import { api, endpoints } from '../../../api';
// components
import Collapse from '@mui/material/Collapse';
-import { Box, Card, CardContent, Divider, Stack, Typography } from '@mui/material';
+import { Box, Card, CardContent, Divider, Stack, Typography, InputBase } from '@mui/material';
import makeStyles from '@mui/styles/makeStyles';
import { host } from '../../../host';
-import { isEmpty } from 'lodash';
+import { debounce, isEmpty } from 'lodash';
import { Link } from 'react-router-dom';
import Loading from '../../Shared/Loading';
import { KeyboardArrowDown, KeyboardArrowRight } from '@mui/icons-material';
import { VulnerabilityChipCheck } from 'utilities/vulnerabilityAndSignatureCheck';
import { mapCVEInfo } from 'utilities/objectModels';
import { CVE_FIXEDIN_PAGE_SIZE, EXPLORE_PAGE_SIZE } from 'utilities/paginationConstants';
+import SearchIcon from '@mui/icons-material/Search';
const useStyles = makeStyles(() => ({
card: {
@@ -81,6 +82,25 @@ const useStyles = makeStyles(() => ({
fontWeight: '600',
cursor: 'pointer',
textAlign: 'center'
+ },
+ search: {
+ position: 'relative',
+ minWidth: '100%',
+ flexDirection: 'row',
+ marginBottom: '1.7rem',
+ boxShadow: '0rem 0.3125rem 0.625rem rgba(131, 131, 131, 0.08)',
+ border: '0.125rem solid #E7E7E7',
+ borderRadius: '1rem',
+ zIndex: 1155
+ },
+ searchIcon: {
+ color: '#52637A',
+ paddingRight: '3%'
+ },
+ input: {
+ color: '#464141',
+ marginLeft: 1,
+ width: '90%'
}
}));
@@ -236,27 +256,27 @@ function VulnerabilitiesDetails(props) {
const { name, tag } = props;
// pagination props
+ const [cveFilter, setCveFilter] = useState('');
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}`, { pageNumber, pageSize: EXPLORE_PAGE_SIZE })}`,
+ `${host()}${endpoints.vulnerabilitiesForRepo(
+ `${name}:${tag}`,
+ { pageNumber, pageSize: EXPLORE_PAGE_SIZE },
+ cveFilter
+ )}`,
abortController.signal
)
.then((response) => {
if (response.data && response.data.data) {
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
- );
+ setCveData((previousState) => (pageNumber === 1 ? cveListData : [...previousState, ...cveListData]));
+ setIsEndOfList(response.data.data.CVEListForImage.Page?.ItemCount < EXPLORE_PAGE_SIZE);
} else if (response.data.errors) {
setIsEndOfList(true);
}
@@ -270,11 +290,25 @@ function VulnerabilitiesDetails(props) {
});
};
+ const resetPagination = () => {
+ setIsLoading(true);
+ setIsEndOfList(false);
+ if (pageNumber !== 1) {
+ setPageNumber(1);
+ } else {
+ getPaginatedCVEs();
+ }
+ };
+
+ const handleCveFilterChange = (e) => {
+ const { value } = e.target;
+ setCveFilter(value);
+ };
+
+ const debouncedChangeHandler = useMemo(() => debounce(handleCveFilterChange, 300));
+
useEffect(() => {
getPaginatedCVEs();
- return () => {
- abortController.abort();
- };
}, [pageNumber]);
// setup intersection obeserver for infinite scroll
@@ -302,6 +336,18 @@ function VulnerabilitiesDetails(props) {
};
}, [isLoading, isEndOfList]);
+ useEffect(() => {
+ if (isLoading) return;
+ resetPagination();
+ }, [cveFilter]);
+
+ useEffect(() => {
+ return () => {
+ abortController.abort();
+ debouncedChangeHandler.cancel();
+ };
+ }, []);
+
const renderCVEs = () => {
return !isEmpty(cveData) ? (
cveData.map((cve, index) => {
@@ -347,6 +393,17 @@ function VulnerabilitiesDetails(props) {
width: '100%'
}}
/>
+
+
+
+
+
+
{renderCVEs()}