Skip to content

Commit

Permalink
Add pagination and sorting to image single table (#5258)
Browse files Browse the repository at this point in the history
  • Loading branch information
dvail committed Mar 17, 2023
1 parent 34aa0e8 commit 4cad570
Show file tree
Hide file tree
Showing 6 changed files with 151 additions and 82 deletions.
Expand Up @@ -11,6 +11,7 @@ import {
GridItem,
Label,
PageSection,
Pagination,
pluralize,
Spinner,
Split,
Expand All @@ -28,52 +29,24 @@ import { VulnerabilitySeverity } from 'types/cve.proto';
import { getAxiosErrorMessage } from 'utils/responseErrorUtils';
import useURLStringUnion from 'hooks/useURLStringUnion';
import useURLSearch from 'hooks/useURLSearch';
import useURLPagination from 'hooks/useURLPagination';
import useURLSort, { UseURLSortProps } from 'hooks/useURLSort';
import { getHasSearchApplied } from 'utils/searchUtils';
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 { ImageDetailsResponse } from './hooks/useImageDetails';
import useImageVulnerabilities from './hooks/useImageVulnerabilities';

function severityCountsFromImageVulnerabilities(
imageVulnerabilities: ImageVulnerabilitiesResponse['image']['imageVulnerabilities']
): Record<VulnerabilitySeverity, number> {
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<FixableStatus, number> {
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;
Expand All @@ -82,13 +55,23 @@ export type ImageSingleVulnerabilitiesProps = {

function ImageSingleVulnerabilities({ imageId, imageData }: ImageSingleVulnerabilitiesProps) {
const { searchFilter } = useURLSearch();
const { page, perPage, setPage, setPerPage } = useURLPagination(50);
// TODO Need to reset current page at the same time sorting changes
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 = (
<Bullseye>
Expand All @@ -102,34 +85,35 @@ function ImageSingleVulnerabilities({ imageId, imageData }: ImageSingleVulnerabi
</EmptyState>
</Bullseye>
);
} else if (loading && !data) {
} else if (loading && !vulnerabilityData) {
mainContent = (
<Bullseye>
<Spinner isSVG />
</Bullseye>
);
} 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<VulnerabilitySeverity>([]);
const hiddenStatuses = new Set<FixableStatus>([]);

const totalVulnerabilityCount = vulnerabilityData.image.imageVulnerabilityCounter.all.total;

mainContent = (
<>
<div className="pf-u-px-lg pf-u-pb-lg">
<Grid hasGutter>
<GridItem sm={12} md={6} xl2={4}>
<BySeveritySummaryCard
title="CVEs by severity"
severityCounts={severityCounts}
severityCounts={vulnerabilityData.image.imageVulnerabilityCounter}
hiddenSeverities={hiddenSeverities}
/>
</GridItem>
<GridItem sm={12} md={6} xl2={4}>
<CvesByStatusSummaryCard
cveStatusCounts={cveStatusCounts}
cveStatusCounts={vulnerabilityData.image.imageVulnerabilityCounter}
hiddenStatuses={hiddenStatuses}
/>
</GridItem>
Expand All @@ -141,12 +125,7 @@ function ImageSingleVulnerabilities({ imageId, imageData }: ImageSingleVulnerabi
<SplitItem isFilled>
<Flex alignContent={{ default: 'alignContentCenter' }}>
<Title headingLevel="h2">
{pluralize(
data.image.imageVulnerabilities.length,
'result',
'results'
)}{' '}
found
{pluralize(totalVulnerabilityCount, 'result', 'results')} found
</Title>
{getHasSearchApplied(searchFilter) && (
<Label isCompact color="blue" icon={<InfoCircleIcon />}>
Expand All @@ -155,11 +134,26 @@ function ImageSingleVulnerabilities({ imageId, imageData }: ImageSingleVulnerabi
)}
</Flex>
</SplitItem>
<SplitItem>TODO Pagination</SplitItem>
<SplitItem>
<Pagination
isCompact
itemCount={totalVulnerabilityCount}
page={page}
perPage={perPage}
onSetPage={(_, newPage) => setPage(newPage)}
onPerPageSelect={(_, newPerPage) => {
if (totalVulnerabilityCount < (page - 1) * newPerPage) {
setPage(1);
}
setPerPage(newPerPage);
}}
/>
</SplitItem>
</Split>
<SingleEntityVulnerabilitiesTable
image={imageData}
imageVulnerabilities={data.image.imageVulnerabilities}
imageVulnerabilities={vulnerabilities}
getSortParams={getSortParams}
/>
</div>
</>
Expand Down
Expand Up @@ -5,19 +5,26 @@ 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,
ImageVulnerabilityCounterKey,
} from '../hooks/useImageVulnerabilities';

export type BySeveritySummaryCardProps = {
title: string;
severityCounts: Record<VulnerabilitySeverity, number>;
severityCounts: ImageVulnerabilityCounter;
hiddenSeverities: Set<VulnerabilitySeverity>;
};

const severitiesCriticalToLow = [
'CRITICAL_VULNERABILITY_SEVERITY',
'IMPORTANT_VULNERABILITY_SEVERITY',
'MODERATE_VULNERABILITY_SEVERITY',
'LOW_VULNERABILITY_SEVERITY',
] as const;
const vulnCounterToSeverity: Record<ImageVulnerabilityCounterKey, VulnerabilitySeverity> = {
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)';
Expand All @@ -34,11 +41,12 @@ function BySeveritySummaryCard({
<Grid className="pf-u-pl-sm">
{severitiesCriticalToLow.map((severity) => {
const count = severityCounts[severity];
const hasNoResults = count === 0;
const isHidden = hiddenSeverities.has(severity);
const hasNoResults = count.total === 0;
const vulnSeverity = vulnCounterToSeverity[severity];
const isHidden = hiddenSeverities.has(vulnSeverity);

let textColor = '';
let text = `${count} ${vulnerabilitySeverityLabels[severity]}`;
let text = `${count.total} ${vulnerabilitySeverityLabels[vulnSeverity]}`;

if (isHidden) {
textColor = disabledColor100;
Expand All @@ -48,16 +56,17 @@ function BySeveritySummaryCard({
text = 'No results';
}

const Icon = SeverityIcons[severity];
const Icon: React.FC<SVGIconProps> | undefined =
SeverityIcons[vulnSeverity];

return (
<GridItem key={severity} span={6}>
<GridItem key={vulnSeverity} span={6}>
<Flex
className="pf-u-pt-sm"
spaceItems={{ default: 'spaceItemsSm' }}
alignItems={{ default: 'alignItemsCenter' }}
>
<Icon color={hasNoResults ? textColor : undefined} />
{Icon && <Icon color={hasNoResults ? textColor : undefined} />}
<Text style={{ color: textColor }}>{text}</Text>
</Flex>
</GridItem>
Expand Down
@@ -1,11 +1,24 @@
import React from 'react';
import { Card, CardTitle, CardBody, Flex, Text, Grid, GridItem } from '@patternfly/react-core';
import {
Card,
CardTitle,
CardBody,
Flex,
Grid,
GridItem,
pluralize,
Text,
} from '@patternfly/react-core';

import { CheckCircleIcon, ExclamationCircleIcon } from '@patternfly/react-icons';
import { FixableStatus } from '../types';
import {
ImageVulnerabilityCounter,
imageVulnerabilityCounterKeys,
} from '../hooks/useImageVulnerabilities';

export type CvesByStatusSummaryCardProps = {
cveStatusCounts: Record<FixableStatus, number | 'hidden'>;
cveStatusCounts: ImageVulnerabilityCounter;
hiddenStatuses: Set<FixableStatus>;
};

Expand All @@ -14,15 +27,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 `${pluralize(count, 'vulnerability', '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;

Expand Down
Expand Up @@ -17,6 +17,7 @@ import SeverityIcons from 'Components/PatternFly/SeverityIcons';
import useSet from 'hooks/useSet';
import { vulnerabilitySeverityLabels } from 'messages/common';
import { getDistanceStrictAsPhrase } from 'utils/dateUtils';
import { UseURLSortResult } from 'hooks/useURLSort';
import { ImageVulnerabilitiesResponse } from '../hooks/useImageVulnerabilities';
import { getEntityPagePath } from '../searchUtils';
import ImageComponentsTable from './ImageComponentsTable';
Expand All @@ -25,21 +26,24 @@ import { ImageDetailsResponse } from '../hooks/useImageDetails';
export type SingleEntityVulnerabilitiesTableProps = {
image: ImageDetailsResponse['image'] | undefined;
imageVulnerabilities: ImageVulnerabilitiesResponse['image']['imageVulnerabilities'];
getSortParams: UseURLSortResult['getSortParams'];
};

function SingleEntityVulnerabilitiesTable({
image,
imageVulnerabilities,
getSortParams,
}: SingleEntityVulnerabilitiesTableProps) {
const expandedRowSet = useSet<string>();
return (
<TableComposable>
<Thead>
<Tr>
<Th>{/* Header for expanded column */}</Th>
<Th>CVE</Th>
<Th>Severity</Th>
<Th>CVE status</Th>
<Th sort={getSortParams('CVE')}>CVE</Th>
<Th sort={getSortParams('Severity')}>Severity</Th>
<Th sort={getSortParams('Fixable')}>CVE Status</Th>
{/* TODO Add sorting for these columns once aggregate sorting is available in BE */}
<Th>Affected components</Th>
<Th>First discovered</Th>
</Tr>
Expand Down

0 comments on commit 4cad570

Please sign in to comment.