Skip to content

Commit 6ce69e4

Browse files
authored
feat(heureka): adds vulnerability details panel (#1117)
* feat(heureka): adds vulnerability details panel with services list only * chore(heureka): fixes eslint * chore(heureka): adds pagination to services table * chore(heureka): adds support groups * chore(heureka): adds vulnerability details panel * Create moody-steaks-lay.md * chore(heureka): show services as links * chore(heureka): removes the redundant changeset * chore(heureka): improve type handling in utils
1 parent 3d829cb commit 6ce69e4

13 files changed

Lines changed: 424 additions & 30 deletions

File tree

.changeset/moody-steaks-lay.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": patch
3+
"@cloudoperators/juno-app-greenhouse": patch
4+
---
5+
6+
feat(heureka): adds vulnerability details panel

apps/heureka/src/api/fetchVulnerabilities.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,26 +12,28 @@ import { getActiveVulnerabilityFilter } from "../components/Vulnerabilities/util
1212
type FetchVulnerabilitiesParams = Pick<RouteContext, "queryClient" | "apiClient"> & {
1313
filterSettings: FilterSettings
1414
after?: string | null
15+
afterServices?: string | null
1516
}
1617

1718
export const fetchVulnerabilities = ({
1819
queryClient,
1920
apiClient,
2021
filterSettings,
2122
after,
23+
afterServices,
2224
}: FetchVulnerabilitiesParams): Promise<ApolloQueryResult<GetVulnerabilitiesQuery>> => {
2325
const filter = getActiveVulnerabilityFilter(filterSettings)
2426
return queryClient.ensureQueryData({
25-
queryKey: ["vulnerabilities", JSON.stringify(filter), after],
27+
queryKey: ["vulnerabilities", JSON.stringify(filter), after, afterServices],
2628
queryFn: () =>
2729
apiClient.query({
2830
query: GetVulnerabilitiesDocument,
2931
variables: {
3032
first: 20,
3133
after,
3234
filter,
33-
firstServices: 10,
34-
afterServices: null,
35+
firstServices: 134, // Get all services to avoid pagination
36+
afterServices,
3537
},
3638
}),
3739
})

apps/heureka/src/components/Vulnerabilities/VulnerabilitiesList/VulnerabilitiesDataRows/VulnerabilityDataRow.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { Vulnerability } from "../../utils"
1111

1212
type VulnerabilityDataRowProps = {
1313
vulnerability: Vulnerability
14+
selected: boolean
15+
onItemClick: () => void
1416
}
1517

1618
const cellSeverityClasses = (severity: string) => {
@@ -39,17 +41,18 @@ const getIconForSeverity = (severity: string) => {
3941
return <Icon icon={iconMap[severityLower] || "help"} color={iconColor} />
4042
}
4143

42-
export const VulnerabilityDataRow = ({ vulnerability }: VulnerabilityDataRowProps) => {
44+
export const VulnerabilityDataRow = ({ vulnerability, selected, onItemClick }: VulnerabilityDataRowProps) => {
4345
const [isExpanded, setIsExpanded] = useState(false)
4446
const { needsExpansion, textRef } = useTextOverflow(vulnerability.description)
4547

4648
const toggleDescription = (e: React.MouseEvent) => {
4749
e.preventDefault()
50+
e.stopPropagation()
4851
setIsExpanded(!isExpanded)
4952
}
5053

5154
return (
52-
<DataGridRow>
55+
<DataGridRow className={`cursor-pointer ${selected ? "active" : ""}`} onClick={onItemClick}>
5356
<DataGridCell className="pl-0">
5457
<div className={cellSeverityClasses(vulnerability.severity)}>{getIconForSeverity(vulnerability.severity)}</div>
5558
</DataGridCell>

apps/heureka/src/components/Vulnerabilities/VulnerabilitiesList/VulnerabilitiesDataRows/index.tsx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import React, { use } from "react"
6+
import React, { use, useCallback } from "react"
7+
import { useNavigate, useSearch } from "@tanstack/react-router"
78
import { EmptyDataGridRow } from "../../../common/EmptyDataGridRow"
89
import { getNormalizedVulnerabilitiesResponse, Vulnerability } from "../../utils"
910
import { ApolloQueryResult } from "@apollo/client"
@@ -15,9 +16,21 @@ type VulnerabilitiesDataRowsProps = {
1516
}
1617

1718
export const VulnerabilitiesDataRows = ({ vulnerabilitiesPromise }: VulnerabilitiesDataRowsProps) => {
19+
const navigate = useNavigate()
20+
const { vulnerability } = useSearch({ from: "/vulnerabilities/" })
1821
const { error, data } = use(vulnerabilitiesPromise)
1922
const { vulnerabilities } = getNormalizedVulnerabilitiesResponse(data)
2023

24+
const openVulnerabilityPanel = useCallback(
25+
(vuln: Vulnerability) => {
26+
navigate({
27+
to: "/vulnerabilities",
28+
search: (prev) => ({ ...prev, vulnerability: vuln.name }),
29+
})
30+
},
31+
[navigate]
32+
)
33+
2134
if (error) {
2235
return <EmptyDataGridRow colSpan={5}>Error loading vulnerabilities: {error.message}</EmptyDataGridRow>
2336
}
@@ -26,5 +39,12 @@ export const VulnerabilitiesDataRows = ({ vulnerabilitiesPromise }: Vulnerabilit
2639
return <EmptyDataGridRow colSpan={5}>No vulnerabilities found! 🚀</EmptyDataGridRow>
2740
}
2841

29-
return vulnerabilities.map((vuln: Vulnerability) => <VulnerabilityDataRow key={vuln.name} vulnerability={vuln} />)
42+
return vulnerabilities.map((vuln: Vulnerability) => (
43+
<VulnerabilityDataRow
44+
key={vuln.name}
45+
vulnerability={vuln}
46+
selected={vuln.name === vulnerability}
47+
onItemClick={() => openVulnerabilityPanel(vuln)}
48+
/>
49+
))
3050
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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, Suspense } from "react"
7+
import { useNavigate } from "@tanstack/react-router"
8+
import { Stack, Spinner } from "@cloudoperators/juno-ui-components"
9+
import { Vulnerability } from "../../utils"
10+
import { ApolloQueryResult } from "@apollo/client"
11+
import { GetVulnerabilitiesQuery } from "../../../../generated/graphql"
12+
import { getNormalizedVulnerabilitiesResponse } from "../../utils"
13+
14+
type VulnerabilityServicesProps = {
15+
vulnerabilityName: string
16+
vulnerabilitiesPromise: Promise<ApolloQueryResult<GetVulnerabilitiesQuery>>
17+
onServiceClick?: (serviceCcrn: string) => void
18+
}
19+
20+
export const VulnerabilityServices = ({
21+
vulnerabilityName,
22+
vulnerabilitiesPromise,
23+
onServiceClick,
24+
}: VulnerabilityServicesProps) => {
25+
const navigate = useNavigate()
26+
27+
const handleServiceClick = (serviceCcrn: string) => {
28+
if (onServiceClick) {
29+
onServiceClick(serviceCcrn)
30+
} else {
31+
navigate({
32+
to: "/services/$service",
33+
params: { service: serviceCcrn },
34+
})
35+
}
36+
}
37+
38+
// Use the promise passed from the parent
39+
const { data } = use(vulnerabilitiesPromise)
40+
41+
// Get vulnerability data from the response
42+
const { vulnerabilities } = getNormalizedVulnerabilitiesResponse(data)
43+
const vulnerabilityData = vulnerabilities.find((vuln: Vulnerability) => vuln.name === vulnerabilityName)
44+
45+
if (!vulnerabilityData) {
46+
return <div className="text-sm text-theme-light">Vulnerability not found: {vulnerabilityName}</div>
47+
}
48+
49+
const services = vulnerabilityData.services || []
50+
51+
if (services.length === 0) {
52+
return <div className="text-sm text-theme-light">No services affected by this vulnerability.</div>
53+
}
54+
55+
return (
56+
<div className="mb-4">
57+
<Suspense
58+
fallback={
59+
<Stack gap="2" alignment="center">
60+
<div>Loading</div>
61+
<Spinner variant="primary"></Spinner>
62+
</Stack>
63+
}
64+
>
65+
<Stack gap="4" direction="horizontal" wrap>
66+
{services.map((service, index) => (
67+
<a
68+
key={index}
69+
href="#"
70+
onClick={(e) => {
71+
e.preventDefault()
72+
handleServiceClick(service.ccrn)
73+
}}
74+
className="link-hover"
75+
>
76+
{service.ccrn}
77+
</a>
78+
))}
79+
</Stack>
80+
</Suspense>
81+
</div>
82+
)
83+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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 { Vulnerability } from "../../utils"
8+
import { ApolloQueryResult } from "@apollo/client"
9+
import { GetVulnerabilitiesQuery } from "../../../../generated/graphql"
10+
import { getNormalizedVulnerabilitiesResponse } from "../../utils"
11+
12+
type VulnerabilityServicesTotalCountProps = {
13+
vulnerabilitiesPromise: Promise<ApolloQueryResult<GetVulnerabilitiesQuery>>
14+
vulnerabilityName: string
15+
}
16+
17+
export const VulnerabilityServicesTotalCount = ({
18+
vulnerabilitiesPromise,
19+
vulnerabilityName,
20+
}: VulnerabilityServicesTotalCountProps) => {
21+
const { data } = use(vulnerabilitiesPromise)
22+
const { vulnerabilities } = getNormalizedVulnerabilitiesResponse(data)
23+
const vulnerabilityData = vulnerabilities.find((vuln: Vulnerability) => vuln.name === vulnerabilityName)
24+
const { servicesCount } = vulnerabilityData || { servicesCount: 0 }
25+
26+
return servicesCount
27+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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, { Suspense, use } from "react"
7+
import { Stack, Pill, Spinner } from "@cloudoperators/juno-ui-components"
8+
import { Vulnerability } from "../../utils"
9+
import { ApolloQueryResult } from "@apollo/client"
10+
import { GetVulnerabilitiesQuery } from "../../../../generated/graphql"
11+
import { getNormalizedVulnerabilitiesResponse } from "../../utils"
12+
13+
type VulnerabilitySupportGroupsProps = {
14+
vulnerabilitiesPromise: Promise<ApolloQueryResult<GetVulnerabilitiesQuery>>
15+
vulnerabilityName: string
16+
}
17+
18+
export const VulnerabilitySupportGroups = ({
19+
vulnerabilitiesPromise,
20+
vulnerabilityName,
21+
}: VulnerabilitySupportGroupsProps) => {
22+
const { data } = use(vulnerabilitiesPromise)
23+
const { vulnerabilities } = getNormalizedVulnerabilitiesResponse(data)
24+
const vulnerabilityData = vulnerabilities.find((vuln: Vulnerability) => vuln.name === vulnerabilityName)
25+
26+
if (!vulnerabilityData) {
27+
return null
28+
}
29+
30+
return (
31+
<Stack gap="1" direction="horizontal" wrap>
32+
<Suspense
33+
fallback={
34+
<Stack gap="2" alignment="center">
35+
<div>Loading</div>
36+
<Spinner variant="primary"></Spinner>
37+
</Stack>
38+
}
39+
>
40+
{vulnerabilityData.supportGroups?.map((group: string) => (
41+
<Pill key={group} pillValue={group} pillValueLabel={group} />
42+
))}
43+
</Suspense>
44+
</Stack>
45+
)
46+
}

0 commit comments

Comments
 (0)