New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add pagination and sorting to image single table #5258
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,7 @@ import { | |
GridItem, | ||
Label, | ||
PageSection, | ||
Pagination, | ||
pluralize, | ||
Spinner, | ||
Split, | ||
|
@@ -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; | ||
|
@@ -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> | ||
|
@@ -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>([]); | ||
Comment on lines
98
to
99
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you share this at next team meeting? Map and Set remove a security concern about overwriting Object properties. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure and I can share the What's the security concern you are talking about in this context? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Whether possible (see below) or implausible, huge number of reported vulnerabilities are about possible object prototype pollution. Map and Set remove it as even a theoretical risk. } else if (cleanRoot !== '__proto__') {
obj[cleanRoot] = leaf;
} |
||
|
||
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> | ||
|
@@ -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 />}> | ||
|
@@ -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> | ||
</> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reader observations, not necessarily change request:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The idea here was to create a local variable that enforced the order specific to this component. I had thought about just making the default order in the hook CriticalToLow, but didn't want to have reliance on an order that is pretty much arbitrary in all other use cases. |
||
|
||
const disabledColor100 = 'var(--pf-global--disabled-color--100)'; | ||
const disabledColor200 = 'var(--pf-global--disabled-color--200)'; | ||
|
@@ -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]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you write a short comment (maybe above at Or, dare I ask, are the classic
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are definitely a handful of different representations of severity, both in this section and others. We're still deciding on how we want to standardize this data in this feature, at least as it pertains to mapping server responses to display values, etc. Currently the API will return the I could alias the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, related, I found that we removed the Do you see any reason to not add this back in? Possibly a discussion for the team meeting too. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That sounds familiar. Was me the we? Almost certainly add it back, with discussion to remove any confusion within the team. Can you search how much duplication of severity constants, icon, labels, and so on? I have mixed feelings about pro and con:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was some light discussion in this PR regarding As for duplications: It looks like the We also have
The new Workload CVEs intermixes the short style with the long style. There are some .js files that use values like There are also gql queries that reference resolvers like My general feeling is that "CVE Severity" is a concept that is used in enough places that it might be useful to keep it in a global Side note: Is |
||
const isHidden = hiddenSeverities.has(vulnSeverity); | ||
|
||
let textColor = ''; | ||
let text = `${count} ${vulnerabilitySeverityLabels[severity]}`; | ||
let text = `${count.total} ${vulnerabilitySeverityLabels[vulnSeverity]}`; | ||
|
||
if (isHidden) { | ||
textColor = disabledColor100; | ||
|
@@ -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> | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is from a proof-of-concept of client side pagination. Since we are not using client-side pagination, we can get this data directly from the query.