Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,13 @@ export const ServiceDetails = () => {
setSelectedImageVersion(version)
}}
/>

{/* Image Version Details Panel */}
{selectedImageVersion && (
<ImageVersionDetailsPanel imageVersion={selectedImageVersion} onClose={() => setSelectedImageVersion(null)} />
<ImageVersionDetailsPanel
imageVersion={selectedImageVersion}
serviceCcrn={selectedService.name}
onClose={() => setSelectedImageVersion(null)}
/>
)}
</MessagesProvider>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,108 +4,126 @@
*/

import React, { useState } from "react"
import { Panel, PanelBody, Stack, Badge, Pill, Label } from "@cloudoperators/juno-ui-components"
import { Panel, PanelBody, Stack, Badge, Pill, Label, ContentHeading } from "@cloudoperators/juno-ui-components"
import { MessagesProvider, Messages } from "@cloudoperators/juno-messages-provider"
import { ServiceImageVersion } from "../../utils"
import { ImageVersionIssuesList } from "./ImageVersionIssuesList"

type ImageVersionDetailsPanelProps = {
imageVersion: ServiceImageVersion
serviceCcrn: string
onClose: () => void
}

export const ImageVersionDetailsPanel = ({ imageVersion, onClose }: ImageVersionDetailsPanelProps) => {
export const ImageVersionDetailsPanel = ({ imageVersion, serviceCcrn, onClose }: ImageVersionDetailsPanelProps) => {
const [showOccurrences, setShowOccurrences] = useState(false)

return (
<Panel heading={`Image ${imageVersion.repository} Information`} opened={true} onClose={onClose} size="large">
<PanelBody>
<Stack gap="6" direction="vertical" className="w-full">
<Stack gap="4" direction="vertical">
{/* Component Details Row */}
<Stack gap="2" direction="horizontal">
<Label text="Image Details: " />
<Stack direction="horizontal" gap="2" wrap>
<Pill pillKey="tag" pillKeyLabel="tag" pillValue={imageVersion.tag} pillValueLabel={imageVersion.tag} />
<Pill
pillKey="repository"
pillKeyLabel="repository"
pillValue={imageVersion.repository}
pillValueLabel={imageVersion.repository}
/>
<Pill
pillKey="version"
pillKeyLabel="version"
pillValue={imageVersion.version}
pillValueLabel={imageVersion.version}
/>
</Stack>
</Stack>

{/* Issues Count Row */}
<Stack gap="2" direction="horizontal">
<Label text="Number of Issues: " />
<Stack direction="horizontal" gap="4" alignment="center">
<Stack direction="horizontal" gap="2" alignment="center">
<span>Critical:</span>
<Badge
icon="danger"
text={`${imageVersion.issueCounts?.critical || 0}`}
variant={imageVersion.issueCounts?.critical > 0 ? "danger" : "default"}
<MessagesProvider>
<Messages />
Comment thread
hodanoori marked this conversation as resolved.
Outdated
<Panel heading={`Image ${imageVersion.repository} Information`} opened={true} onClose={onClose} size="large">
<PanelBody>
<Stack gap="6" direction="vertical" className="w-full">
Comment thread
hodanoori marked this conversation as resolved.
<Stack gap="4" direction="vertical">
Comment thread
hodanoori marked this conversation as resolved.
{/* Component Details Row */}
<Stack gap="2" direction="horizontal">
<Label text="Image Details: " />
<Stack direction="horizontal" gap="2" wrap>
<Pill
pillKey="tag"
pillKeyLabel="tag"
pillValue={imageVersion.tag}
pillValueLabel={imageVersion.tag}
/>
</Stack>
<Stack direction="horizontal" gap="2" alignment="center">
<span>High:</span>
<Badge
icon="warning"
text={`${imageVersion.issueCounts?.high || 0}`}
variant={imageVersion.issueCounts?.high > 0 ? "warning" : "default"}
<Pill
pillKey="repository"
pillKeyLabel="repository"
pillValue={imageVersion.repository}
pillValueLabel={imageVersion.repository}
/>
<Pill
pillKey="version"
pillKeyLabel="version"
pillValue={imageVersion.version}
pillValueLabel={imageVersion.version}
/>
</Stack>
<Stack direction="horizontal" gap="2" alignment="center">
<span>Low:</span>
<span>{imageVersion.issueCounts?.low || 0}</span>
</Stack>
</Stack>
</Stack>

{/* Occurrences Section */}
<Stack gap="2" direction="vertical">
<Stack gap="2" direction="horizontal" alignment="center">
<Label text="Image Instances: " />
<a
href="#"
onClick={(e) => {
e.preventDefault()
setShowOccurrences(!showOccurrences)
}}
className="hover:underline text-sm"
>
{showOccurrences ? "Hide Occurrences" : "Show Occurrences"} (
{imageVersion.componetInstancesCount || 0})
</a>
{/* Issues Count Row */}
<Stack gap="2" direction="horizontal">
<Label text="Number of Issues: " />
<Stack direction="horizontal" gap="4" alignment="center">
<Stack direction="horizontal" gap="2" alignment="center">
<span>Critical:</span>
<Badge
icon="danger"
text={`${imageVersion.issueCounts?.critical || 0}`}
variant={imageVersion.issueCounts?.critical > 0 ? "danger" : "default"}
/>
<Pill
pillKey="repository"
pillKeyLabel="repository"
pillValue={imageVersion.repository}
pillValueLabel={imageVersion.repository}
/>
<Pill
pillKey="version"
pillKeyLabel="version"
pillValue={imageVersion.version}
pillValueLabel={imageVersion.version}
/>
</Stack>
</Stack>
</Stack>
{showOccurrences && (
<Stack gap="4" direction="vertical" className="pl-4">
{imageVersion.componentInstances && imageVersion.componentInstances.length > 0 ? (
<Stack gap="2" direction="vertical">
{imageVersion.componentInstances.map((componentInstance) => (
<Stack key={`${componentInstance?.ccrn}-${componentInstance?.id}`} gap="2" direction="vertical">
<span>{componentInstance?.region || "-"}</span>
<span>{componentInstance?.cluster || "-"}</span>
<span>{componentInstance?.namespace || "-"}</span>
<span>{componentInstance?.pod || "-"}</span>
<span>{componentInstance?.container || "-"}</span>
</Stack>
))}
</Stack>
) : (
<span className="text-theme-light">No image instances found</span>
)}
{/* Occurrences Section */}
<Stack gap="2" direction="vertical">
<Stack gap="2" direction="horizontal" alignment="center">
<Label text="Image Instances: " />
<a
href="#"
onClick={(e) => {
e.preventDefault()
setShowOccurrences(!showOccurrences)
}}
className="hover:underline text-sm"
>
{showOccurrences ? "Hide Occurrences" : "Show Occurrences"} (
{imageVersion.componetInstancesCount || 0})
</a>
</Stack>
)}
{showOccurrences && (
<Stack gap="4" direction="vertical" className="pl-4">
{imageVersion.componentInstances && imageVersion.componentInstances.length > 0 ? (
<Stack gap="2" direction="vertical">
{imageVersion.componentInstances.map((componentInstance) => (
<Stack
key={`${componentInstance?.ccrn}-${componentInstance?.id}`}
gap="2"
direction="vertical"
>
<span>{componentInstance?.cluster || "-"}</span>
<span>{componentInstance?.pod || "-"}</span>
<span>{componentInstance?.container || "-"}</span>
</Stack>
))}
</Stack>
) : (
<span className="text-theme-light">No image instances found</span>
)}
</Stack>
)}
</Stack>

{/* Second Section: Issues List */}
<Stack gap="4" direction="vertical">
<ContentHeading>Issues</ContentHeading>
<ImageVersionIssuesList serviceCcrn={serviceCcrn} imageVersion={imageVersion.version} />
</Stack>
</Stack>
</Stack>
</Stack>
</PanelBody>
</Panel>
</PanelBody>
</Panel>
</MessagesProvider>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React, { useState } from "react"
import { DataGridRow, DataGridCell, Badge, Stack } from "@cloudoperators/juno-ui-components"
import { Icon } from "@cloudoperators/juno-ui-components"
import { IssueIcon } from "../../../common/IssueIcon/IssueIcon"
import { IssueTimestamp } from "../../../common/IssueTimestamp/IssueTimestamp"

type Issue = {
Comment thread
hodanoori marked this conversation as resolved.
Outdated
severity: string
name: string
earliestTargetRemediation: string
description: string
sourceLink: string
}

const cellSeverityClasses = (severity: string) => {
let borderColor = "border-text-theme-default"
switch (severity.toLowerCase()) {
Comment thread
hodanoori marked this conversation as resolved.
Outdated
case "critical":
borderColor = "border-theme-danger"
break
case "high":
borderColor = "border-theme-warning"
break
case "medium":
borderColor = "border-theme-info"
break
}

return `
border-l-2
${borderColor}
h-full
pl-5
`
}

type ImageVersionIssueListItemProps = {
issue: Issue
}

export const ImageVersionIssueListItem = ({ issue }: ImageVersionIssueListItemProps) => {
const [isExpanded, setIsExpanded] = useState(false)

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

return (
<DataGridRow>
<DataGridCell className="pl-0">
<div className={cellSeverityClasses(issue.severity)}>
<IssueIcon severity={issue.severity} />
</div>
</DataGridCell>

<DataGridCell className="whitespace-nowrap">
<Stack gap="2" direction="vertical">
<span>{issue.name}</span>
<a href={issue.sourceLink} target="_blank" rel="noopener noreferrer" className="link-hover">
Comment thread
hodanoori marked this conversation as resolved.
Outdated
<Stack gap="1.5" alignment="center">
<Icon icon="openInNew" size="16" />
<span>Issue source</span>
</Stack>
</a>
</Stack>
</DataGridCell>
<DataGridCell className="whitespace-nowrap">
<IssueTimestamp targetDate={issue.earliestTargetRemediation} />
</DataGridCell>
<DataGridCell>
{isExpanded ? (
Comment thread
hodanoori marked this conversation as resolved.
Outdated
<Stack gap="2" direction="vertical">
<span>{issue.description}</span>
<a href="#" onClick={toggleDescription} className="link-hover">
Show less
</a>
</Stack>
) : (
<Stack gap="2" direction="vertical">
<span>{issue.description.substring(0, 95)}...</span>
<a href="#" onClick={toggleDescription} className="link-hover">
Show more
</a>
</Stack>
)}
</DataGridCell>
</DataGridRow>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React, { useEffect } from "react"
import { DataGrid, DataGridRow, DataGridHeadCell, Stack, Icon } from "@cloudoperators/juno-ui-components"
import { EmptyDataGridRow } from "../../../common/EmptyDataGridRow/EmptyDataGridRow"
import { useFetchServiceImageVersionIssues } from "../../useFetchServiceImageVersionIssues"
import { ImageVersionIssueListItem } from "./ImageVersionIssueListItem"
import { useActions as useMessageActions } from "@cloudoperators/juno-messages-provider"

type ImageVersionIssuesListProps = {
serviceCcrn: string
imageVersion: string
}

export const ImageVersionIssuesList = ({ serviceCcrn, imageVersion }: ImageVersionIssuesListProps) => {
const { addMessage } = useMessageActions()
const {
issues,
loading: isLoading,
error,
} = useFetchServiceImageVersionIssues({
serviceCcrn,
imageVersion,
})

useEffect(() => {
if (error) {
addMessage({
variant: "error",
text: error,
})
}
}, [error])
return (
<Stack gap="4" direction="vertical">
Comment thread
hodanoori marked this conversation as resolved.
Outdated
<DataGrid columns={4} minContentColumns={[0, 1, 2]}>
<DataGridRow>
<DataGridHeadCell>
<Icon icon="monitorHeart" />
</DataGridHeadCell>
<DataGridHeadCell>Issue</DataGridHeadCell>
<DataGridHeadCell>Target Date</DataGridHeadCell>
<DataGridHeadCell>Description</DataGridHeadCell>
</DataGridRow>

{isLoading ? (
<EmptyDataGridRow colSpan={4}>Loading issues...</EmptyDataGridRow>
) : issues.length === 0 ? (
<EmptyDataGridRow colSpan={4}>No issues found.</EmptyDataGridRow>
) : (
!error && issues.map((issue, index) => <ImageVersionIssueListItem key={index} issue={issue} />)
)}
</DataGrid>
</Stack>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@
*/

export * from "./ImageVersionDetailsPanel"
export * from "./ImageVersionIssuesList"
export * from "./ImageVersionIssueListItem"
Loading
Loading