diff --git a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/ImageSingleVulnerabilities.tsx b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/ImageSingleVulnerabilities.tsx index 3a146b79e2433..224903955acd8 100644 --- a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/ImageSingleVulnerabilities.tsx +++ b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/ImageSingleVulnerabilities.tsx @@ -21,6 +21,7 @@ import { TabsComponent, Text, Title, + Pagination, } from '@patternfly/react-core'; import { ExclamationCircleIcon, InfoCircleIcon } from '@patternfly/react-icons'; @@ -29,50 +30,22 @@ import { getAxiosErrorMessage } from 'utils/responseErrorUtils'; import useURLStringUnion from 'hooks/useURLStringUnion'; import useURLSearch from 'hooks/useURLSearch'; import { getHasSearchApplied } from 'utils/searchUtils'; +import useURLPagination from 'hooks/useURLPagination'; +import useURLSort, { UseURLSortProps } from 'hooks/useURLSort'; import { cveStatusTabValues, FixableStatus } from './types'; import WorkloadTableToolbar from './WorkloadTableToolbar'; import BySeveritySummaryCard from './SummaryCards/BySeveritySummaryCard'; import CvesByStatusSummaryCard from './SummaryCards/CvesByStatusSummaryCard'; import SingleEntityVulnerabilitiesTable from './Tables/SingleEntityVulnerabilitiesTable'; -import useImageVulnerabilities, { - ImageVulnerabilitiesResponse, -} from './hooks/useImageVulnerabilities'; +import useImageVulnerabilities from './hooks/useImageVulnerabilities'; -function severityCountsFromImageVulnerabilities( - imageVulnerabilities: ImageVulnerabilitiesResponse['image']['imageVulnerabilities'] -): Record { - const severityCounts = { - LOW_VULNERABILITY_SEVERITY: 0, - MODERATE_VULNERABILITY_SEVERITY: 0, - IMPORTANT_VULNERABILITY_SEVERITY: 0, - CRITICAL_VULNERABILITY_SEVERITY: 0, - }; - - imageVulnerabilities.forEach(({ severity }) => { - severityCounts[severity] += 1; - }); - - return severityCounts; -} - -function statusCountsFromImageVulnerabilities( - imageVulnerabilities: ImageVulnerabilitiesResponse['image']['imageVulnerabilities'] -): Record { - const statusCounts = { - Fixable: 0, - 'Not fixable': 0, - }; - - imageVulnerabilities.forEach(({ isFixable }) => { - if (isFixable) { - statusCounts.Fixable += 1; - } else { - statusCounts['Not fixable'] += 1; - } - }); - - return statusCounts; -} +const defaultSortOptions: UseURLSortProps = { + sortFields: ['CVE', 'Severity', 'Fixable'], + defaultSortOption: { + field: 'Severity', + direction: 'desc', + }, +}; export type ImageSingleVulnerabilitiesProps = { imageId: string; @@ -80,13 +53,23 @@ export type ImageSingleVulnerabilitiesProps = { function ImageSingleVulnerabilities({ imageId }: ImageSingleVulnerabilitiesProps) { const { searchFilter } = useURLSearch(); + const { page, perPage, setPage, setPerPage } = useURLPagination(50); + // TODO Need to reset current page on sort + const { sortOption, getSortParams } = useURLSort(defaultSortOptions); // TODO Still need to properly integrate search filter with query - const { data, loading, error } = useImageVulnerabilities(imageId, {}); + const pagination = { + offset: (page - 1) * perPage, + limit: perPage, + sortOption, + }; + const { data, previousData, loading, error } = useImageVulnerabilities(imageId, {}, pagination); const [activeTabKey, setActiveTabKey] = useURLStringUnion('cveStatus', cveStatusTabValues); let mainContent: ReactNode | null = null; + const vulnerabilityData = data || previousData; + if (error) { mainContent = ( @@ -100,20 +83,21 @@ function ImageSingleVulnerabilities({ imageId }: ImageSingleVulnerabilitiesProps ); - } else if (loading && !data) { + } else if (loading && !vulnerabilityData) { mainContent = ( ); - } else if (data) { - const vulnerabilities = data.image.imageVulnerabilities; - const severityCounts = severityCountsFromImageVulnerabilities(vulnerabilities); - const cveStatusCounts = statusCountsFromImageVulnerabilities(vulnerabilities); + } else if (vulnerabilityData) { + const vulnerabilities = vulnerabilityData.image.imageVulnerabilities; + // TODO Integrate these with page search filters const hiddenSeverities = new Set([]); const hiddenStatuses = new Set([]); + const totalVulnerabilityCount = vulnerabilityData.image.imageVulnerabilityCounter.all.total; + mainContent = ( <>
@@ -121,13 +105,13 @@ function ImageSingleVulnerabilities({ imageId }: ImageSingleVulnerabilitiesProps @@ -139,12 +123,7 @@ function ImageSingleVulnerabilities({ imageId }: ImageSingleVulnerabilitiesProps - {pluralize( - data.image.imageVulnerabilities.length, - 'result', - 'results' - )}{' '} - found + {pluralize(totalVulnerabilityCount, 'result', 'results')} found {getHasSearchApplied(searchFilter) && ( - TODO Pagination + + setPage(newPage)} + onPerPageSelect={(_, newPerPage) => { + if (totalVulnerabilityCount < (page - 1) * newPerPage) { + setPage(1); + } + setPerPage(newPerPage); + }} + /> +
diff --git a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/SummaryCards/BySeveritySummaryCard.tsx b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/SummaryCards/BySeveritySummaryCard.tsx index 60c7074b4955f..78eef02e6ce98 100644 --- a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/SummaryCards/BySeveritySummaryCard.tsx +++ b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/SummaryCards/BySeveritySummaryCard.tsx @@ -5,19 +5,23 @@ import SeverityIcons from 'Components/PatternFly/SeverityIcons'; import { VulnerabilitySeverity } from 'types/cve.proto'; import { vulnerabilitySeverityLabels } from 'messages/common'; +import { SVGIconProps } from '@patternfly/react-icons/dist/esm/createIcon'; +import { ImageVulnerabilityCounter } from '../hooks/useImageVulnerabilities'; export type BySeveritySummaryCardProps = { title: string; - severityCounts: Record; + severityCounts: ImageVulnerabilityCounter; hiddenSeverities: Set; }; -const severitiesCriticalToLow = [ - 'CRITICAL_VULNERABILITY_SEVERITY', - 'IMPORTANT_VULNERABILITY_SEVERITY', - 'MODERATE_VULNERABILITY_SEVERITY', - 'LOW_VULNERABILITY_SEVERITY', -] as const; +const vulnCounterToSeverity = { + low: 'LOW_VULNERABILITY_SEVERITY', + moderate: 'MODERATE_VULNERABILITY_SEVERITY', + important: 'IMPORTANT_VULNERABILITY_SEVERITY', + critical: 'CRITICAL_VULNERABILITY_SEVERITY', +} as const; + +const severitiesCriticalToLow = ['critical', 'important', 'moderate', 'low'] as const; const disabledColor100 = 'var(--pf-global--disabled-color--100)'; const disabledColor200 = 'var(--pf-global--disabled-color--200)'; @@ -32,13 +36,14 @@ function BySeveritySummaryCard({ {title} - {severitiesCriticalToLow.map((severity) => { - const count = severityCounts[severity]; - const hasNoResults = count === 0; - const isHidden = hiddenSeverities.has(severity); + {severitiesCriticalToLow.map((counterSeverity) => { + const count = severityCounts[counterSeverity]; + const hasNoResults = count.total === 0; + const vulnSeverity = vulnCounterToSeverity[counterSeverity]; + const isHidden = hiddenSeverities.has(vulnSeverity); let textColor = ''; - let text = `${count} ${vulnerabilitySeverityLabels[severity]}`; + let text = `${count.total} ${vulnerabilitySeverityLabels[vulnSeverity]}`; if (isHidden) { textColor = disabledColor100; @@ -48,16 +53,17 @@ function BySeveritySummaryCard({ text = 'No results'; } - const Icon = SeverityIcons[severity]; + const Icon: React.FC | undefined = + SeverityIcons[vulnSeverity]; return ( - + - + {Icon && } {text} diff --git a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/SummaryCards/CvesByStatusSummaryCard.tsx b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/SummaryCards/CvesByStatusSummaryCard.tsx index f9d1f85a906e8..7e8277a906a96 100644 --- a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/SummaryCards/CvesByStatusSummaryCard.tsx +++ b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/SummaryCards/CvesByStatusSummaryCard.tsx @@ -3,9 +3,13 @@ import { Card, CardTitle, CardBody, Flex, Text, Grid, GridItem } from '@patternf import { CheckCircleIcon, ExclamationCircleIcon } from '@patternfly/react-icons'; import { FixableStatus } from '../types'; +import { + ImageVulnerabilityCounter, + imageVulnerabilityCounterKeys, +} from '../hooks/useImageVulnerabilities'; export type CvesByStatusSummaryCardProps = { - cveStatusCounts: Record; + cveStatusCounts: ImageVulnerabilityCounter; hiddenStatuses: Set; }; @@ -14,15 +18,25 @@ const statusDisplays = [ status: 'Fixable', Icon: CheckCircleIcon, iconColor: 'var(--pf-global--success-color--100)', - text: (counts: CvesByStatusSummaryCardProps['cveStatusCounts']) => - `${counts.Fixable} vulnerabilities with available fixes`, + text: (counts: ImageVulnerabilityCounter) => { + let count = 0; + imageVulnerabilityCounterKeys.forEach((key) => { + count += counts[key].fixable; + }); + return `${count} vulnerabilities with available fixes`; + }, }, { status: 'Not fixable', Icon: ExclamationCircleIcon, iconColor: 'var(--pf-global--danger-color--100)', - text: (counts: CvesByStatusSummaryCardProps['cveStatusCounts']) => - `${counts['Not fixable']} vulnerabilities without fixes`, + text: (counts: ImageVulnerabilityCounter) => { + let count = 0; + imageVulnerabilityCounterKeys.forEach((key) => { + count += counts[key].total - counts[key].fixable; + }); + return `${count} vulnerabilities without fixes`; + }, }, ] as const; diff --git a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Tables/SingleEntityVulnerabilitiesTable.tsx b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Tables/SingleEntityVulnerabilitiesTable.tsx index 33a203c9e2272..ec8cdf326a5e4 100644 --- a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Tables/SingleEntityVulnerabilitiesTable.tsx +++ b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Tables/SingleEntityVulnerabilitiesTable.tsx @@ -8,23 +8,27 @@ import LinkShim from 'Components/PatternFly/LinkShim'; import SeverityIcons from 'Components/PatternFly/SeverityIcons'; import { vulnerabilitySeverityLabels } from 'messages/common'; import { getDistanceStrictAsPhrase } from 'utils/dateUtils'; +import { UseURLSortResult } from 'hooks/useURLSort'; import { ImageVulnerabilitiesResponse } from '../hooks/useImageVulnerabilities'; import { getEntityPagePath } from '../searchUtils'; export type SingleEntityVulnerabilitiesTableProps = { imageVulnerabilities: ImageVulnerabilitiesResponse['image']['imageVulnerabilities']; + getSortParams: UseURLSortResult['getSortParams']; }; function SingleEntityVulnerabilitiesTable({ imageVulnerabilities, + getSortParams, }: SingleEntityVulnerabilitiesTableProps) { return ( - CVE - Severity - CVE Status + CVE + Severity + CVE Status + {/* TODO Add sorting once aggregate sorting is available in BE */} Affected components First discovered diff --git a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/hooks/useImageVulnerabilities.ts b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/hooks/useImageVulnerabilities.ts index 947d931b0b56c..7bf74bfae841b 100644 --- a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/hooks/useImageVulnerabilities.ts +++ b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/hooks/useImageVulnerabilities.ts @@ -1,15 +1,25 @@ import { gql, useQuery } from '@apollo/client'; +import { Pagination } from 'services/types'; import { SearchFilter } from 'types/search'; import { getRequestQueryStringForSearchFilter } from 'utils/searchUtils'; export type ImageVulnerabilitiesVariables = { id: string; vulnQuery: string; + pagination: Pagination; }; +export const imageVulnerabilityCounterKeys = ['low', 'moderate', 'important', 'critical'] as const; + +export type ImageVulnerabilityCounter = Record< + typeof imageVulnerabilityCounterKeys[number] | 'all', + { total: number; fixable: number } +>; + export type ImageVulnerabilitiesResponse = { image: { id: string; + imageVulnerabilityCounter: ImageVulnerabilityCounter; imageVulnerabilities: { severity: string; isFixable: boolean; @@ -29,10 +39,32 @@ export type ImageVulnerabilitiesResponse = { }; export const imageVulnerabilitiesQuery = gql` - query getImageVulnerabilities($id: ID!, $vulnQuery: String!) { + query getImageVulnerabilities($id: ID!, $vulnQuery: String!, $pagination: Pagination!) { image(id: $id) { id - imageVulnerabilities(query: $vulnQuery) { + imageVulnerabilityCounter(query: $vulnQuery) { + all { + total + fixable + } + low { + total + fixable + } + moderate { + total + fixable + } + important { + total + fixable + } + critical { + total + fixable + } + } + imageVulnerabilities(query: $vulnQuery, pagination: $pagination) { severity isFixable cve @@ -51,13 +83,18 @@ export const imageVulnerabilitiesQuery = gql` } `; -export default function useImageVulnerabilities(imageId: string, searchFilter: SearchFilter) { +export default function useImageVulnerabilities( + imageId: string, + searchFilter: SearchFilter, + pagination: Pagination +) { return useQuery( imageVulnerabilitiesQuery, { variables: { id: imageId, vulnQuery: getRequestQueryStringForSearchFilter(searchFilter), + pagination, }, } ); diff --git a/ui/apps/platform/src/hooks/useURLSort.ts b/ui/apps/platform/src/hooks/useURLSort.ts index 741de90d52384..3a38ce11a70ca 100644 --- a/ui/apps/platform/src/hooks/useURLSort.ts +++ b/ui/apps/platform/src/hooks/useURLSort.ts @@ -6,12 +6,12 @@ import { isParsedQs } from 'utils/queryStringUtils'; export type GetSortParams = (field: string) => ThProps['sort'] | undefined; -type UseTableSortProps = { +export type UseURLSortProps = { sortFields: string[]; defaultSortOption: SortOption; }; -type UseTableSortResult = { +export type UseURLSortResult = { sortOption: ApiSortOption; getSortParams: GetSortParams; }; @@ -27,7 +27,7 @@ function isDirection(val: unknown): val is 'asc' | 'desc' { return val === 'asc' || val === 'desc'; } -function useURLSort({ sortFields, defaultSortOption }: UseTableSortProps): UseTableSortResult { +function useURLSort({ sortFields, defaultSortOption }: UseURLSortProps): UseURLSortResult { const [sortOption, setSortOption] = useURLParameter('sortOption', defaultSortOption); // get the parsed sort option values from the URL, if available diff --git a/ui/apps/platform/src/messages/common.ts b/ui/apps/platform/src/messages/common.ts index cb368f54ae3a8..4d4a184ab592e 100644 --- a/ui/apps/platform/src/messages/common.ts +++ b/ui/apps/platform/src/messages/common.ts @@ -21,6 +21,13 @@ export const vulnerabilitySeverityLabels: Record LOW_VULNERABILITY_SEVERITY: 'Low', }); +export const vulnerabilitySeveritySortValues: Record = { + CRITICAL_VULNERABILITY_SEVERITY: 4, + IMPORTANT_VULNERABILITY_SEVERITY: 3, + MODERATE_VULNERABILITY_SEVERITY: 2, + LOW_VULNERABILITY_SEVERITY: 1, +} as const; + export const clusterTypeLabels = Object.freeze({ KUBERNETES_CLUSTER: 'Kubernetes Clusters', SWARM_CLUSTER: 'Swarm Clusters',