diff --git a/src/__tests__/TagPage/VulnerabilitiesDetails.test.js b/src/__tests__/TagPage/VulnerabilitiesDetails.test.js index 816a93e..6bdb050 100644 --- a/src/__tests__/TagPage/VulnerabilitiesDetails.test.js +++ b/src/__tests__/TagPage/VulnerabilitiesDetails.test.js @@ -521,6 +521,20 @@ describe('Vulnerabilties page', () => { expect((await screen.findAllByText(/2022/i)).length === 6); }); + it('should have a collapsable search bar', async () => { + jest.spyOn(api, 'get').mockResolvedValue({ status: 200, data: { data: mockCVEList } }); + render(); + const cveSearchInput = screen.getByPlaceholderText(/search/i); + const expandSearch = cveSearchInput.parentElement.parentElement.parentElement.parentElement.childNodes[0]; + await fireEvent.click(expandSearch); + await waitFor(() => + expect(screen.getAllByPlaceholderText("Exclude")).toHaveLength(1) + ); + const excludeInput = screen.getByPlaceholderText("Exclude"); + userEvent.type(excludeInput, '2022'); + expect((await screen.findAllByText(/2022/i)).length === 0); + }) + 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 9ea5687..a8d9435 100644 --- a/src/api.js +++ b/src/api.js @@ -90,13 +90,16 @@ const endpoints = { `/v2/_zot/ext/search?query={ExpandedRepoInfo(repo:"${name}"){Images {Manifests {Digest Platform {Os Arch} Size} Vulnerabilities {MaxSeverity Count} Tag LastUpdated Vendor IsDeletable } Summary {Name LastUpdated Size Platforms {Os Arch} Vendors IsStarred IsBookmarked NewestImage {RepoName IsSigned SignatureInfo { Tool IsTrusted Author } Vulnerabilities {MaxSeverity Count} Manifests {Digest} Tag Vendor Title Documentation DownloadCount Source Description Licenses}}}}`, detailedImageInfo: (name, tag) => `/v2/_zot/ext/search?query={Image(image: "${name}:${tag}"){RepoName IsSigned SignatureInfo { Tool IsTrusted Author } Vulnerabilities {MaxSeverity Count} Referrers {MediaType ArtifactType Size Digest Annotations{Key Value}} Tag Manifests {History {Layer {Size Digest} HistoryDescription {CreatedBy EmptyLayer}} Digest ConfigDigest LastUpdated Size Platform {Os Arch}} Vendor Licenses }}`, - vulnerabilitiesForRepo: (name, { pageNumber = 1, pageSize = 15 }, searchTerm = '') => { + vulnerabilitiesForRepo: (name, { pageNumber = 1, pageSize = 15 }, searchTerm = '', excludedTerm = '') => { let query = `/v2/_zot/ext/search?query={CVEListForImage(image: "${name}", requestedPage: {limit:${pageSize} offset:${ (pageNumber - 1) * pageSize }}`; if (!isEmpty(searchTerm)) { query += `, searchedCVE: "${searchTerm}"`; } + if (!isEmpty(excludedTerm)) { + query += `, excludedCVE: "${excludedTerm}"`; + } return `${query}){Tag Page {TotalCount ItemCount} CVEList {Id Title Description Severity PackageList {Name InstalledVersion FixedVersion}} Summary {Count UnknownCount LowCount MediumCount HighCount CriticalCount}}}`; }, allVulnerabilitiesForRepo: (name) => diff --git a/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx b/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx index bd217de..0edc14d 100644 --- a/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx +++ b/src/components/Tag/Tabs/VulnerabilitiesDetails.jsx @@ -30,6 +30,9 @@ import exportFromJSON from 'export-from-json'; import ViewHeadlineIcon from '@mui/icons-material/ViewHeadline'; import ViewAgendaIcon from '@mui/icons-material/ViewAgenda'; +import { KeyboardArrowDown, KeyboardArrowRight } from '@mui/icons-material'; +import Collapse from '@mui/material/Collapse'; + import VulnerabilitiyCard from '../../Shared/VulnerabilityCard'; import VulnerabilityCountCard from '../../Shared/VulnerabilityCountCard'; @@ -122,6 +125,21 @@ const useStyles = makeStyles((theme) => ({ padding: '0.3rem', display: 'flex', justifyContent: 'left' + }, + dropdownArrowBox: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center' + }, + dropdownText: { + color: '#1479FF', + fontSize: '1.5rem', + fontWeight: '600', + cursor: 'pointer', + textAlign: 'center' + }, + test: { + width: '95%' } })); @@ -135,8 +153,11 @@ function VulnerabilitiesDetails(props) { const abortController = useMemo(() => new AbortController(), []); const { name, tag, digest, platform } = props; + const [openExcludeSearch, setOpenExcludeSearch] = useState(false); + // pagination props const [cveFilter, setCveFilter] = useState(''); + const [cveExcludeFilter, setCveExcludeFilter] = useState(''); const [pageNumber, setPageNumber] = useState(1); const [isEndOfList, setIsEndOfList] = useState(false); const listBottom = useRef(null); @@ -156,7 +177,8 @@ function VulnerabilitiesDetails(props) { `${host()}${endpoints.vulnerabilitiesForRepo( getCVERequestName(), { pageNumber, pageSize: EXPLORE_PAGE_SIZE }, - cveFilter + cveFilter, + cveExcludeFilter )}`, abortController.signal ) @@ -255,7 +277,17 @@ function VulnerabilitiesDetails(props) { setAnchorExport(null); }; + const handleExpandCVESearch = () => { + setOpenExcludeSearch((openExcludeSearch) => !openExcludeSearch); + }; + + const handleCveExcludeFilterChange = (e) => { + const { value } = e.target; + setCveExcludeFilter(value); + }; + const debouncedChangeHandler = useMemo(() => debounce(handleCveFilterChange, 300)); + const debouncedExcludeFilterChangeHandler = useMemo(() => debounce(handleCveExcludeFilterChange, 300)); useEffect(() => { getPaginatedCVEs(); @@ -289,12 +321,13 @@ function VulnerabilitiesDetails(props) { useEffect(() => { if (isLoading) return; resetPagination(); - }, [cveFilter]); + }, [cveFilter, cveExcludeFilter]); useEffect(() => { return () => { abortController.abort(); debouncedChangeHandler.cancel(); + debouncedExcludeFilterChangeHandler.cancel(); }; }, []); @@ -417,15 +450,36 @@ function VulnerabilitiesDetails(props) { {renderCVESummary()} - - -
- + +
+ {!openExcludeSearch ? ( + + ) : ( + + )}
+ + + +
+ +
+
+ + + + + + +
{renderCVEs()} {renderListBottom()}