Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/moody-steaks-lay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@cloudoperators/juno-app-heureka": patch
"@cloudoperators/juno-app-greenhouse": patch
---

feat(heureka): adds vulnerability details panel
8 changes: 5 additions & 3 deletions apps/heureka/src/api/fetchVulnerabilities.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,28 @@ import { getActiveVulnerabilityFilter } from "../components/Vulnerabilities/util
type FetchVulnerabilitiesParams = Pick<RouteContext, "queryClient" | "apiClient"> & {
filterSettings: FilterSettings
after?: string | null
afterServices?: string | null
Comment thread
hodanoori marked this conversation as resolved.
}

export const fetchVulnerabilities = ({
queryClient,
apiClient,
filterSettings,
after,
afterServices,
}: FetchVulnerabilitiesParams): Promise<ApolloQueryResult<GetVulnerabilitiesQuery>> => {
const filter = getActiveVulnerabilityFilter(filterSettings)
return queryClient.ensureQueryData({
queryKey: ["vulnerabilities", JSON.stringify(filter), after],
queryKey: ["vulnerabilities", JSON.stringify(filter), after, afterServices],
queryFn: () =>
apiClient.query({
query: GetVulnerabilitiesDocument,
variables: {
first: 20,
after,
filter,
firstServices: 10,
afterServices: null,
firstServices: 134, // Get all services to avoid pagination
afterServices,
},
}),
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { Vulnerability } from "../../utils"

type VulnerabilityDataRowProps = {
vulnerability: Vulnerability
selected: boolean
onItemClick: () => void
}

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

export const VulnerabilityDataRow = ({ vulnerability }: VulnerabilityDataRowProps) => {
export const VulnerabilityDataRow = ({ vulnerability, selected, onItemClick }: VulnerabilityDataRowProps) => {
const [isExpanded, setIsExpanded] = useState(false)
const { needsExpansion, textRef } = useTextOverflow(vulnerability.description)

const toggleDescription = (e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
setIsExpanded(!isExpanded)
}

return (
<DataGridRow>
<DataGridRow className={`cursor-pointer ${selected ? "active" : ""}`} onClick={onItemClick}>
<DataGridCell className="pl-0">
<div className={cellSeverityClasses(vulnerability.severity)}>{getIconForSeverity(vulnerability.severity)}</div>
</DataGridCell>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
* SPDX-License-Identifier: Apache-2.0
*/

import React, { use } from "react"
import React, { use, useCallback } from "react"
import { useNavigate, useSearch } from "@tanstack/react-router"
import { EmptyDataGridRow } from "../../../common/EmptyDataGridRow"
import { getNormalizedVulnerabilitiesResponse, Vulnerability } from "../../utils"
import { ApolloQueryResult } from "@apollo/client"
Expand All @@ -15,9 +16,21 @@ type VulnerabilitiesDataRowsProps = {
}

export const VulnerabilitiesDataRows = ({ vulnerabilitiesPromise }: VulnerabilitiesDataRowsProps) => {
const navigate = useNavigate()
const { vulnerability } = useSearch({ from: "/vulnerabilities/" })
const { error, data } = use(vulnerabilitiesPromise)
const { vulnerabilities } = getNormalizedVulnerabilitiesResponse(data)

const openVulnerabilityPanel = useCallback(
(vuln: Vulnerability) => {
navigate({
to: "/vulnerabilities",
search: (prev) => ({ ...prev, vulnerability: vuln.name }),
})
},
[navigate]
)

if (error) {
return <EmptyDataGridRow colSpan={5}>Error loading vulnerabilities: {error.message}</EmptyDataGridRow>
}
Expand All @@ -26,5 +39,12 @@ export const VulnerabilitiesDataRows = ({ vulnerabilitiesPromise }: Vulnerabilit
return <EmptyDataGridRow colSpan={5}>No vulnerabilities found! 🚀</EmptyDataGridRow>
}

return vulnerabilities.map((vuln: Vulnerability) => <VulnerabilityDataRow key={vuln.name} vulnerability={vuln} />)
return vulnerabilities.map((vuln: Vulnerability) => (
<VulnerabilityDataRow
key={vuln.name}
vulnerability={vuln}
selected={vuln.name === vulnerability}
onItemClick={() => openVulnerabilityPanel(vuln)}
/>
))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React, { use, Suspense } from "react"
import { useNavigate } from "@tanstack/react-router"
import { Stack, Spinner } from "@cloudoperators/juno-ui-components"
import { Vulnerability } from "../../utils"
import { ApolloQueryResult } from "@apollo/client"
import { GetVulnerabilitiesQuery } from "../../../../generated/graphql"
import { getNormalizedVulnerabilitiesResponse } from "../../utils"

type VulnerabilityServicesProps = {
vulnerabilityName: string
vulnerabilitiesPromise: Promise<ApolloQueryResult<GetVulnerabilitiesQuery>>
onServiceClick?: (serviceCcrn: string) => void
}

export const VulnerabilityServices = ({
vulnerabilityName,
vulnerabilitiesPromise,
onServiceClick,
}: VulnerabilityServicesProps) => {
const navigate = useNavigate()

const handleServiceClick = (serviceCcrn: string) => {
if (onServiceClick) {
onServiceClick(serviceCcrn)
} else {
navigate({
to: "/services/$service",
params: { service: serviceCcrn },
})
}
}

// Use the promise passed from the parent
const { data } = use(vulnerabilitiesPromise)

// Get vulnerability data from the response
const { vulnerabilities } = getNormalizedVulnerabilitiesResponse(data)
const vulnerabilityData = vulnerabilities.find((vuln: Vulnerability) => vuln.name === vulnerabilityName)

if (!vulnerabilityData) {
return <div className="text-sm text-theme-light">Vulnerability not found: {vulnerabilityName}</div>
}

const services = vulnerabilityData.services || []

if (services.length === 0) {
return <div className="text-sm text-theme-light">No services affected by this vulnerability.</div>
}

return (
<div className="mb-4">
<Suspense
fallback={
<Stack gap="2" alignment="center">
<div>Loading</div>
<Spinner variant="primary"></Spinner>
</Stack>
}
>
<Stack gap="4" direction="horizontal" wrap>
{services.map((service, index) => (
<a
key={index}
href="#"
onClick={(e) => {
e.preventDefault()
handleServiceClick(service.ccrn)
}}
className="link-hover"
>
{service.ccrn}
</a>
))}
</Stack>
</Suspense>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React, { use } from "react"
import { Vulnerability } from "../../utils"
import { ApolloQueryResult } from "@apollo/client"
import { GetVulnerabilitiesQuery } from "../../../../generated/graphql"
import { getNormalizedVulnerabilitiesResponse } from "../../utils"

type VulnerabilityServicesTotalCountProps = {
vulnerabilitiesPromise: Promise<ApolloQueryResult<GetVulnerabilitiesQuery>>
vulnerabilityName: string
}

export const VulnerabilityServicesTotalCount = ({
vulnerabilitiesPromise,
vulnerabilityName,
}: VulnerabilityServicesTotalCountProps) => {
const { data } = use(vulnerabilitiesPromise)
const { vulnerabilities } = getNormalizedVulnerabilitiesResponse(data)
const vulnerabilityData = vulnerabilities.find((vuln: Vulnerability) => vuln.name === vulnerabilityName)
const { servicesCount } = vulnerabilityData || { servicesCount: 0 }

return servicesCount
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React, { Suspense, use } from "react"
import { Stack, Pill, Spinner } from "@cloudoperators/juno-ui-components"
import { Vulnerability } from "../../utils"
import { ApolloQueryResult } from "@apollo/client"
import { GetVulnerabilitiesQuery } from "../../../../generated/graphql"
import { getNormalizedVulnerabilitiesResponse } from "../../utils"

type VulnerabilitySupportGroupsProps = {
vulnerabilitiesPromise: Promise<ApolloQueryResult<GetVulnerabilitiesQuery>>
vulnerabilityName: string
}

export const VulnerabilitySupportGroups = ({
vulnerabilitiesPromise,
vulnerabilityName,
}: VulnerabilitySupportGroupsProps) => {
const { data } = use(vulnerabilitiesPromise)
const { vulnerabilities } = getNormalizedVulnerabilitiesResponse(data)
const vulnerabilityData = vulnerabilities.find((vuln: Vulnerability) => vuln.name === vulnerabilityName)

if (!vulnerabilityData) {
return null
}

return (
<Stack gap="1" direction="horizontal" wrap>
<Suspense
fallback={
<Stack gap="2" alignment="center">
<div>Loading</div>
<Spinner variant="primary"></Spinner>
</Stack>
}
>
{vulnerabilityData.supportGroups?.map((group: string) => (
<Pill key={group} pillValue={group} pillValueLabel={group} />
))}
</Suspense>
</Stack>
)
}
Loading
Loading