Skip to content

Commit cd2a9c7

Browse files
authored
feat(heureka): adds vulnerabilities list view (#1081)
* feat(heureka): adds vulnerabilities list view * fix(heureka): adjusts formats * feat(heureka): select the top navigation based on given root in url * feat(heureka): adds tests for vulnerabilities list * Delete .tool-versions This is a local file to manage the different node installation * fix(heureka): adjusts license header of graphql.ts * chore(heureka): adds changeset * chore(heureka): display showmore btn when its needed and remove clear all from input group * chore(heureka): parallelize filter request with fetch vulnerabilities request * chore(heureka): cleanup * chore(heureka): add empty lines * chore(heureka): change issue to vulnerability column name in list view
1 parent 877920c commit cd2a9c7

26 files changed

Lines changed: 1157 additions & 50 deletions

File tree

.changeset/polite-jobs-juggle.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@cloudoperators/juno-app-heureka": minor
3+
"@cloudoperators/juno-app-greenhouse": patch
4+
---
5+
6+
Heureka: Add vulnerabilities list view
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { ApolloQueryResult } from "@apollo/client"
7+
import { GetVulnerabilitiesDocument, GetVulnerabilitiesQuery } from "../generated/graphql"
8+
import { RouteContext } from "../routes/-types"
9+
import { FilterSettings } from "../components/common/Filters/types"
10+
import { getActiveVulnerabilityFilter } from "../components/Vulnerabilities/utils"
11+
12+
type FetchVulnerabilitiesParams = Pick<RouteContext, "queryClient" | "apiClient"> & {
13+
filterSettings: FilterSettings
14+
after?: string | null
15+
}
16+
17+
export const fetchVulnerabilities = ({
18+
queryClient,
19+
apiClient,
20+
filterSettings,
21+
after,
22+
}: FetchVulnerabilitiesParams): Promise<ApolloQueryResult<GetVulnerabilitiesQuery>> => {
23+
const filter = getActiveVulnerabilityFilter(filterSettings)
24+
return queryClient.ensureQueryData({
25+
queryKey: ["vulnerabilities", JSON.stringify(filter), after],
26+
queryFn: () =>
27+
apiClient.query({
28+
query: GetVulnerabilitiesDocument,
29+
variables: {
30+
first: 20,
31+
after,
32+
filter,
33+
firstServices: 10,
34+
afterServices: null,
35+
},
36+
}),
37+
})
38+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import { ApolloQueryResult } from "@apollo/client"
7+
import { GetVulnerabilityFiltersDocument, GetVulnerabilityFiltersQuery } from "../generated/graphql"
8+
import { RouteContext } from "../routes/-types"
9+
10+
type FetchVulnerabilityFiltersParams = Pick<RouteContext, "queryClient" | "apiClient">
11+
12+
export const fetchVulnerabilityFilters = ({
13+
queryClient,
14+
apiClient,
15+
}: FetchVulnerabilityFiltersParams): Promise<ApolloQueryResult<GetVulnerabilityFiltersQuery>> =>
16+
queryClient.ensureQueryData({
17+
queryKey: ["vulnerabilityFilters"],
18+
queryFn: () => apiClient.query({ query: GetVulnerabilityFiltersDocument }),
19+
})

apps/heureka/src/components/Service/ImageVersionDetailsPanel/ImageVersionIssuesList/IssuesDataRows/IssuesDataRow/index.tsx

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66
import React, { useState } from "react"
77
import { DataGridRow, DataGridCell, Stack } from "@cloudoperators/juno-ui-components"
88
import { Icon } from "@cloudoperators/juno-ui-components"
9-
import { IssueIcon } from "./IssueIcon"
10-
import { IssueTimestamp } from "./IssueTimestamp"
11-
import { Issue, getSeverityColor } from "../../../../../Services/utils"
9+
import { IssueIcon } from "../../../../../common/IssueIcon"
10+
import { IssueTimestamp } from "../../../../../common/IssueTimestamp"
11+
import { Issue } from "../../../../../Services/utils"
12+
import { getSeverityColor, useTextOverflow } from "../../../../../../utils"
1213

1314
const cellSeverityClasses = (severity: string) => {
1415
let borderColor = getSeverityColor(severity)
@@ -27,6 +28,7 @@ type IssuesDataRowProps = {
2728

2829
export const IssuesDataRow = ({ issue }: IssuesDataRowProps) => {
2930
const [isExpanded, setIsExpanded] = useState(false)
31+
const { needsExpansion, textRef } = useTextOverflow(issue.description)
3032

3133
const toggleDescription = (e: React.MouseEvent) => {
3234
e.preventDefault()
@@ -59,15 +61,17 @@ export const IssuesDataRow = ({ issue }: IssuesDataRowProps) => {
5961
</DataGridCell>
6062
<DataGridCell>
6163
<Stack gap="2" direction="vertical">
62-
<span className={isExpanded ? "" : "whitespace-nowrap overflow-hidden text-ellipsis"}>
64+
<span ref={textRef} className={isExpanded ? "" : "whitespace-nowrap overflow-hidden text-ellipsis"}>
6365
{issue.description}
6466
</span>
65-
<a href="#" onClick={toggleDescription} className="link-hover">
66-
<Stack alignment="center">
67-
{isExpanded ? "Show less" : "Show more"}
68-
<Icon color="global-text" icon={isExpanded ? "expandLess" : "expandMore"} />
69-
</Stack>
70-
</a>
67+
{issue.description && needsExpansion && (
68+
<a href="#" onClick={toggleDescription} className="link-hover">
69+
<Stack alignment="center">
70+
{isExpanded ? "Show less" : "Show more"}
71+
<Icon color="global-text" icon={isExpanded ? "expandLess" : "expandMore"} />
72+
</Stack>
73+
</a>
74+
)}
7175
</Stack>
7276
</DataGridCell>
7377
</DataGridRow>

apps/heureka/src/components/Services/utils.ts

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -261,23 +261,6 @@ export const getNormalizedImageVersionIssuesResponse = (data: any): NormalizedIm
261261
}
262262
}
263263

264-
export const getSeverityColor = (severity: string): string => {
265-
switch (severity.toLowerCase()) {
266-
case "critical":
267-
return "text-theme-danger"
268-
case "high":
269-
return "text-theme-warning"
270-
case "medium":
271-
return "text-theme-warning"
272-
case "low":
273-
return "text-theme-info"
274-
case "none":
275-
return "text-theme-default"
276-
default:
277-
return "text-theme-default"
278-
}
279-
}
280-
281264
/**
282265
* This function converts the selected filters from the FilterSettings into a format that is accepted by the url-state-provider/v2/encode
283266
* Examples:
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import React from "react"
7+
import { render, screen, act } from "@testing-library/react"
8+
import { createMemoryHistory, createRootRoute, createRoute, Outlet, RouterProvider } from "@tanstack/react-router"
9+
import { PortalProvider } from "@cloudoperators/juno-ui-components/index"
10+
import { Vulnerabilities } from "./index"
11+
import { Filter, FilterSettings } from "../common/Filters/types"
12+
import { getTestRouter } from "../../mocks/getTestRouter"
13+
import { mockVulnerabilitiesPromise, mockVulnerabilityFiltersPromise } from "../../mocks/promises"
14+
15+
const mockFilters: Filter[] = [
16+
{
17+
displayName: "Support Group",
18+
filterName: "supportGroup",
19+
values: ["containers", "platform", "network"],
20+
},
21+
{
22+
displayName: "Severity",
23+
filterName: "severity",
24+
values: ["Critical", "High", "Medium", "Low", "None"],
25+
},
26+
]
27+
28+
const mockFilterSettings: FilterSettings = {
29+
searchTerm: "",
30+
selectedFilters: [],
31+
}
32+
33+
const renderComponent = () => {
34+
const rootRoute = createRootRoute({
35+
component: () => <Outlet />,
36+
})
37+
const testRoute = createRoute({
38+
getParentRoute: () => rootRoute,
39+
path: "/vulnerabilities/",
40+
loader: async () => ({
41+
vulnerabilitiesPromise: mockVulnerabilitiesPromise,
42+
filters: mockFilters,
43+
filterSettings: mockFilterSettings,
44+
}),
45+
component: () => (
46+
<PortalProvider>
47+
<Vulnerabilities />
48+
</PortalProvider>
49+
),
50+
})
51+
const routeTree = rootRoute.addChildren([testRoute])
52+
const router = getTestRouter({
53+
routeTree,
54+
history: createMemoryHistory({
55+
initialEntries: ["/vulnerabilities/"],
56+
}),
57+
})
58+
59+
return {
60+
...render(<RouterProvider router={router} />),
61+
router,
62+
}
63+
}
64+
65+
describe("Vulnerabilities", () => {
66+
it("should render correctly", async () => {
67+
await act(async () => renderComponent())
68+
expect(await screen.findByText("CVE-2024-1234")).toBeInTheDocument()
69+
})
70+
})
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Juno contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import React, { useCallback } from "react"
7+
import { useLoaderData, useNavigate } from "@tanstack/react-router"
8+
import { Filters } from "../common/Filters"
9+
import { FilterSettings } from "../common/Filters/types"
10+
import { getFiltersForUrl } from "./utils"
11+
12+
export const VulnerabilitiesFilters = () => {
13+
const navigate = useNavigate()
14+
const { filters, filterSettings } = useLoaderData({ from: "/vulnerabilities/" })
15+
16+
const handleFilterChange = useCallback(
17+
(updatedFilterSettings: FilterSettings) => {
18+
navigate({
19+
to: "/vulnerabilities",
20+
search: {
21+
...getFiltersForUrl(updatedFilterSettings),
22+
},
23+
})
24+
},
25+
[filterSettings, navigate]
26+
)
27+
28+
return (
29+
<Filters
30+
filters={filters}
31+
filterSettings={filterSettings}
32+
onFilterChange={handleFilterChange}
33+
searchInputPlaceholder="search term for vulnerabilities name"
34+
/>
35+
)
36+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import React, { useState } from "react"
7+
import { DataGridRow, DataGridCell, Stack, Icon, KnownIcons } from "@cloudoperators/juno-ui-components"
8+
import { getSeverityColor, useTextOverflow } from "../../../../utils"
9+
import { IssueTimestamp } from "../../../common/IssueTimestamp"
10+
import { Vulnerability } from "../../utils"
11+
12+
type VulnerabilityDataRowProps = {
13+
vulnerability: Vulnerability
14+
}
15+
16+
const cellSeverityClasses = (severity: string) => {
17+
let borderColor = getSeverityColor(severity)
18+
19+
return `
20+
border-l-2
21+
${borderColor}
22+
h-full
23+
pl-5
24+
`
25+
}
26+
27+
const getIconForSeverity = (severity: string) => {
28+
const severityLower = severity.toLowerCase()
29+
const iconColor = getSeverityColor(severity)
30+
31+
const iconMap: Record<string, KnownIcons> = {
32+
critical: "danger",
33+
high: "warning",
34+
medium: "errorOutline",
35+
low: "info",
36+
none: "help",
37+
}
38+
39+
return <Icon icon={iconMap[severityLower] || "help"} color={iconColor} />
40+
}
41+
42+
export const VulnerabilityDataRow = ({ vulnerability }: VulnerabilityDataRowProps) => {
43+
const [isExpanded, setIsExpanded] = useState(false)
44+
const { needsExpansion, textRef } = useTextOverflow(vulnerability.description)
45+
46+
const toggleDescription = (e: React.MouseEvent) => {
47+
e.preventDefault()
48+
setIsExpanded(!isExpanded)
49+
}
50+
51+
return (
52+
<DataGridRow>
53+
<DataGridCell className="pl-0">
54+
<div className={cellSeverityClasses(vulnerability.severity)}>{getIconForSeverity(vulnerability.severity)}</div>
55+
</DataGridCell>
56+
57+
<DataGridCell className="whitespace-nowrap">
58+
<Stack gap="2" direction="vertical">
59+
<span>{vulnerability.name}</span>
60+
{vulnerability.sourceUrl && vulnerability.sourceUrl !== "-" && (
61+
<a href={vulnerability.sourceUrl} target="_blank" rel="noopener noreferrer" className="link-hover">
62+
<Stack gap="1.5" alignment="center">
63+
<Icon icon="openInNew" size="16" />
64+
<span>Issue source</span>
65+
</Stack>
66+
</a>
67+
)}
68+
</Stack>
69+
</DataGridCell>
70+
71+
<DataGridCell className="whitespace-nowrap">
72+
<span>{vulnerability.servicesCount}</span>
73+
</DataGridCell>
74+
75+
<DataGridCell className="whitespace-nowrap">
76+
<IssueTimestamp targetDate={vulnerability.earliestTargetRemediationDate} />
77+
</DataGridCell>
78+
79+
<DataGridCell>
80+
<Stack gap="2" direction="vertical">
81+
<span ref={textRef} className={isExpanded ? "" : "whitespace-nowrap overflow-hidden text-ellipsis"}>
82+
{vulnerability.description}
83+
</span>
84+
{vulnerability.description && needsExpansion && (
85+
<a href="#" onClick={toggleDescription} className="link-hover">
86+
<Stack alignment="center">
87+
{isExpanded ? "Show less" : "Show more"}
88+
<Icon color="global-text" icon={isExpanded ? "expandLess" : "expandMore"} />
89+
</Stack>
90+
</a>
91+
)}
92+
</Stack>
93+
</DataGridCell>
94+
</DataGridRow>
95+
)
96+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import React, { use } from "react"
7+
import { EmptyDataGridRow } from "../../../common/EmptyDataGridRow"
8+
import { getNormalizedVulnerabilitiesResponse, Vulnerability } from "../../utils"
9+
import { ApolloQueryResult } from "@apollo/client"
10+
import { GetVulnerabilitiesQuery } from "../../../../generated/graphql"
11+
import { VulnerabilityDataRow } from "./VulnerabilityDataRow"
12+
13+
type VulnerabilitiesDataRowsProps = {
14+
vulnerabilitiesPromise: Promise<ApolloQueryResult<GetVulnerabilitiesQuery>>
15+
}
16+
17+
export const VulnerabilitiesDataRows = ({ vulnerabilitiesPromise }: VulnerabilitiesDataRowsProps) => {
18+
const { error, data } = use(vulnerabilitiesPromise)
19+
const { vulnerabilities } = getNormalizedVulnerabilitiesResponse(data)
20+
21+
if (error) {
22+
return <EmptyDataGridRow colSpan={5}>Error loading vulnerabilities: {error.message}</EmptyDataGridRow>
23+
}
24+
25+
if (vulnerabilities.length === 0) {
26+
return <EmptyDataGridRow colSpan={5}>No vulnerabilities found! 🚀</EmptyDataGridRow>
27+
}
28+
29+
return vulnerabilities.map((vuln: Vulnerability) => <VulnerabilityDataRow key={vuln.name} vulnerability={vuln} />)
30+
}

0 commit comments

Comments
 (0)