Skip to content

Commit

Permalink
Add images table for Workload CVE Image single page
Browse files Browse the repository at this point in the history
  • Loading branch information
dvail committed Mar 13, 2023
1 parent 4a0f7ad commit 6b6392d
Show file tree
Hide file tree
Showing 5 changed files with 269 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,37 @@ import {
EmptyStateBody,
EmptyStateIcon,
EmptyStateVariant,
Flex,
Grid,
GridItem,
Label,
PageSection,
pluralize,
Spinner,
Split,
SplitItem,
Tab,
TabTitleText,
Tabs,
TabsComponent,
Text,
Title,
Flex,
} from '@patternfly/react-core';
import { ExclamationCircleIcon } from '@patternfly/react-icons';
import { gql, useQuery } from '@apollo/client';
import { ExclamationCircleIcon, InfoCircleIcon } from '@patternfly/react-icons';

import { VulnerabilitySeverity } from 'types/cve.proto';
import { getAxiosErrorMessage } from 'utils/responseErrorUtils';
import useURLStringUnion from 'hooks/useURLStringUnion';
import useURLSearch from 'hooks/useURLSearch';
import { getHasSearchApplied } from 'utils/searchUtils';
import { cveStatusTabValues, FixableStatus } from './types';
import WorkloadTableToolbar from './WorkloadTableToolbar';
import BySeveritySummaryCard from './SummaryCards/BySeveritySummaryCard';
import CvesByStatusSummaryCard from './SummaryCards/CvesByStatusSummaryCard';

export type ImageVulnerabilitiesVariables = {
id: string;
};

export type ImageVulnerabilitiesResponse = {
image: {
imageVulnerabilities: {
severity: string;
isFixable: boolean;
}[];
};
};
import SingleEntityVulnerabilitiesTable from './SingleEntityVulnerabilitiesTable';
import useImageVulnerabilities, {
ImageVulnerabilitiesResponse,
} from './hooks/useImageVulnerabilities';

function severityCountsFromImageVulnerabilities(
imageVulnerabilities: ImageVulnerabilitiesResponse['image']['imageVulnerabilities']
Expand Down Expand Up @@ -78,30 +74,14 @@ function statusCountsFromImageVulnerabilities(
return statusCounts;
}

export const imageVulnerabilitiesQuery = gql`
query getImageVulnerabilities($id: ID!) {
image(id: $id) {
id
imageVulnerabilities {
severity
isFixable
}
}
}
`;

export type ImageSingleVulnerabilitiesProps = {
imageId: string;
};

function ImageSingleVulnerabilities({ imageId }: ImageSingleVulnerabilitiesProps) {
// TODO Needs integration with URL search filter
const { data, loading, error } = useQuery<
ImageVulnerabilitiesResponse,
ImageVulnerabilitiesVariables
>(imageVulnerabilitiesQuery, {
variables: { id: imageId },
});
const { searchFilter } = useURLSearch();
// TODO Still need to properly integrate search filter with query
const { data, loading, error } = useImageVulnerabilities(imageId, {});

const [activeTabKey, setActiveTabKey] = useURLStringUnion('cveStatus', cveStatusTabValues);

Expand Down Expand Up @@ -135,21 +115,51 @@ function ImageSingleVulnerabilities({ imageId }: ImageSingleVulnerabilitiesProps
const hiddenStatuses = new Set<FixableStatus>([]);

mainContent = (
<Grid hasGutter className="pf-u-py-md">
<GridItem sm={12} md={6} xl2={4}>
<BySeveritySummaryCard
title="CVEs by severity"
severityCounts={severityCounts}
hiddenSeverities={hiddenSeverities}
/>
</GridItem>
<GridItem sm={12} md={6} xl2={4}>
<CvesByStatusSummaryCard
cveStatusCounts={cveStatusCounts}
hiddenStatuses={hiddenStatuses}
<>
<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}
hiddenSeverities={hiddenSeverities}
/>
</GridItem>
<GridItem sm={12} md={6} xl2={4}>
<CvesByStatusSummaryCard
cveStatusCounts={cveStatusCounts}
hiddenStatuses={hiddenStatuses}
/>
</GridItem>
</Grid>
</div>
<Divider />
<div className="pf-u-p-lg">
<Split className="pf-u-pb-lg">
<SplitItem isFilled>
<Flex alignContent={{ default: 'alignContentCenter' }}>
<Title headingLevel="h2">
{pluralize(
data.image.imageVulnerabilities.length,
'result',
'results'
)}{' '}
found
</Title>
{getHasSearchApplied(searchFilter) && (
<Label isCompact color="blue" icon={<InfoCircleIcon />}>
Filtered view
</Label>
)}
</Flex>
</SplitItem>
<SplitItem>TODO Pagination</SplitItem>
</Split>
<SingleEntityVulnerabilitiesTable
vulnerabilities={data.image.imageVulnerabilities}
/>
</GridItem>
</Grid>
</div>
</>
);
}

Expand All @@ -176,15 +186,12 @@ function ImageSingleVulnerabilities({ imageId }: ImageSingleVulnerabilitiesProps
eventKey="Observed"
title={<TabTitleText>Observed CVEs</TabTitleText>}
>
<PageSection variant="light" component="div" isFilled>
<Flex
direction={{ default: 'column' }}
spaceItems={{ default: 'spaceItemsMd' }}
>
<WorkloadTableToolbar />
{mainContent}
</Flex>
</PageSection>
<div className="pf-u-px-sm pf-u-background-color-100">
<WorkloadTableToolbar />
</div>
<div className="pf-u-flex-grow-1 pf-u-background-color-100">
{mainContent}
</div>
</Tab>
<Tab
className="pf-u-display-flex pf-u-flex-direction-column pf-u-flex-grow-1"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import React from 'react';
import { Button, ButtonVariant } from '@patternfly/react-core';
import { TableComposable, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table';
import { CheckCircleIcon, ExclamationCircleIcon } from '@patternfly/react-icons';
import { SVGIconProps } from '@patternfly/react-icons/dist/js/createIcon';

import LinkShim from 'Components/PatternFly/LinkShim';
import SeverityIcons from 'Components/PatternFly/SeverityIcons';
import { vulnerabilitySeverityLabels } from 'messages/common';
import { getDistanceStrictAsPhrase } from 'utils/dateUtils';
import { ImageVulnerabilitiesResponse } from './hooks/useImageVulnerabilities';
import { getEntityPagePath } from './searchUtils';

export type SingleEntityVulnerabilitiesTableProps = {
// TODO - Note that we may need to adjust this prop type, or do some pre-processing of data
// when this component is used for the Deployment single page
vulnerabilities: ImageVulnerabilitiesResponse['image']['imageVulnerabilities'];
};

function SingleEntityVulnerabilitiesTable({
vulnerabilities,
}: SingleEntityVulnerabilitiesTableProps) {
return (
<TableComposable>
<Thead>
<Tr>
<Th>CVE</Th>
<Th>Severity</Th>
<Th>CVE Status</Th>
<Th>Affected components</Th>
<Th>First discovered</Th>
</Tr>
</Thead>
<Tbody>
{vulnerabilities.map(
({ cve, severity, isFixable, imageComponents, discoveredAtImage }) => {
const SeverityIcon: React.FC<SVGIconProps> | undefined =
SeverityIcons[severity];
const severityLabel: string | undefined =
vulnerabilitySeverityLabels[severity];

return (
<Tr key={cve}>
<Td dataLabel="CVE">
<Button
variant={ButtonVariant.link}
isInline
component={LinkShim}
href={getEntityPagePath('CVE', cve)}
>
{cve}
</Button>
</Td>
<Td dataLabel="Severity">
<span>
{SeverityIcon && (
<SeverityIcon className="pf-u-display-inline" />
)}
{severityLabel && (
<span className="pf-u-pl-sm">{severityLabel}</span>
)}
</span>
</Td>
<Td dataLabel="CVE Status">
{isFixable ? (
<span>
<CheckCircleIcon
className="pf-u-display-inline"
color="var(--pf-global--success-color--100)"
/>
<span className="pf-u-pl-sm">Fixable</span>
</span>
) : (
<span>
<ExclamationCircleIcon
className="pf-u-display-inline"
color="var(--pf-global--danger-color--100)"
/>
<span className="pf-u-pl-sm">Not fixable</span>
</span>
)}
</Td>
<Td dataLabel="Affected components">
{imageComponents.length === 1
? imageComponents[0].name
: `${imageComponents.length} components`}
</Td>
<Td dataLabel="First discovered">
{getDistanceStrictAsPhrase(discoveredAtImage, new Date())}
</Td>
</Tr>
);
}
)}
</Tbody>
</TableComposable>
);
}

export default SingleEntityVulnerabilitiesTable;
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,22 @@ import CVEStatusDropdown from './CVEStatusDropdown';

import './WorkloadTableToolbar.css';

const emptyDefaultFilters = {
Severity: [],
Fixable: [],
};

type FilterType = 'resource' | 'Severity' | 'Fixable';

type WorkloadTableToolbarProps = {
defaultFilters: DefaultFilters;
defaultFilters?: DefaultFilters;
resourceContext?: Resource;
};

function WorkloadTableToolbar({ defaultFilters, resourceContext }: WorkloadTableToolbarProps) {
function WorkloadTableToolbar({
defaultFilters = emptyDefaultFilters,
resourceContext,
}: WorkloadTableToolbarProps) {
const { searchFilter, setSearchFilter } = useURLSearch();
const severityFilterChips: ToolbarChip[] = [];
const fixableFilterChips: ToolbarChip[] = [];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { gql, useQuery } from '@apollo/client';
import { SearchFilter } from 'types/search';
import { getRequestQueryStringForSearchFilter } from 'utils/searchUtils';

export type ImageVulnerabilitiesVariables = {
id: string;
vulnQuery: string;
};

export type ImageVulnerabilitiesResponse = {
image: {
id: string;
metadata: {
v1: {
layers: {
instruction: string;
value: string;
};
} | null;
} | null;
imageVulnerabilities: {
severity: string;
isFixable: boolean;
cve: string;
summary: string;
discoveredAtImage: Date | null;
imageComponents: {
name: string;
version: string;
fixedIn: string;
location: string;
layerIndex: string | null;
}[];
}[];
};
};

export const imageVulnerabilitiesQuery = gql`
query getImageVulnerabilities($id: ID!, $vulnQuery: String!) {
image(id: $id) {
id
metadata {
v1 {
layers {
instruction
value
}
}
}
imageVulnerabilities(query: $vulnQuery) {
severity
isFixable
cve
summary
discoveredAtImage
imageComponents {
name
version
fixedIn
location
layerIndex
}
}
}
}
`;

export default function useImageVulnerabilities(imageId: string, searchFilter: SearchFilter) {
return useQuery<ImageVulnerabilitiesResponse, ImageVulnerabilitiesVariables>(
imageVulnerabilitiesQuery,
{
variables: {
id: imageId,
vulnQuery: getRequestQueryStringForSearchFilter(searchFilter),
},
}
);
}

0 comments on commit 6b6392d

Please sign in to comment.