Skip to content

Commit

Permalink
Add pagination and sorting to image single table
Browse files Browse the repository at this point in the history
  • Loading branch information
dvail committed Mar 15, 2023
1 parent c1f9b05 commit 770f3b1
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 83 deletions.
Expand Up @@ -21,6 +21,7 @@ import {
TabsComponent,
Text,
Title,
Pagination,
} from '@patternfly/react-core';
import { ExclamationCircleIcon, InfoCircleIcon } from '@patternfly/react-icons';

Expand All @@ -29,64 +30,46 @@ 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<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;
};

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 = (
<Bullseye>
Expand All @@ -100,34 +83,35 @@ function ImageSingleVulnerabilities({ imageId }: ImageSingleVulnerabilitiesProps
</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 @@ -139,12 +123,7 @@ function ImageSingleVulnerabilities({ imageId }: ImageSingleVulnerabilitiesProps
<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 @@ -153,10 +132,25 @@ function ImageSingleVulnerabilities({ imageId }: ImageSingleVulnerabilitiesProps
)}
</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
imageVulnerabilities={data.image.imageVulnerabilities}
imageVulnerabilities={vulnerabilities}
getSortParams={getSortParams}
/>
</div>
</>
Expand Down
Expand Up @@ -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<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 = {
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 @@ -32,13 +36,14 @@ function BySeveritySummaryCard({
<CardTitle>{title}</CardTitle>
<CardBody>
<Grid className="pf-u-pl-sm">
{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;
Expand All @@ -48,16 +53,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
Expand Up @@ -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<FixableStatus, number | 'hidden'>;
cveStatusCounts: ImageVulnerabilityCounter;
hiddenStatuses: Set<FixableStatus>;
};

Expand All @@ -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;

Expand Down
Expand Up @@ -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 (
<TableComposable>
<Thead>
<Tr>
<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 once aggregate sorting is available in BE */}
<Th>Affected components</Th>
<Th>First discovered</Th>
</Tr>
Expand Down

0 comments on commit 770f3b1

Please sign in to comment.