Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Juno contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from "react"
import { render, screen } from "@testing-library/react"
import { Conditions } from "./Conditions"
import { Cluster } from "../../types/k8sTypes"

describe("Conditions", () => {
it("should render conditions heading and readiness badges", () => {
const mockCluster: Cluster = {
apiVersion: "v1",
kind: "Cluster",
metadata: { name: "test-cluster", creationTimestamp: "" },
spec: {
accessMode: "direct",
kubeConfig: {
maxTokenValidity: 72,
},
},
status: {
statusConditions: {
conditions: [
{
type: "Ready",
status: "True",
lastTransitionTime: "2026-01-01T00:00:00Z",
},
],
},
},
}

render(<Conditions clusters={mockCluster} />)

expect(screen.getByText("Conditions")).toBeInTheDocument()
expect(screen.getByText("Ready")).toBeInTheDocument()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Juno contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from "react"
import { Stack, ContentHeading } from "@cloudoperators/juno-ui-components"

import { Cluster } from "../../types/k8sTypes"
import ReadinessConditions from "../../common/ReadinessConditions"

type ConditionsProps = {
clusters: Cluster
}

export const Conditions: React.FC<ConditionsProps> = ({ clusters }) => (
<Stack direction="vertical" gap="4">
<ContentHeading>Conditions</ContentHeading>
<ReadinessConditions conditions={clusters.status?.statusConditions?.conditions || []} enableDetailsView />
</Stack>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Juno contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from "react"
import { render, screen } from "@testing-library/react"
import { Details } from "./Details"
import { Cluster } from "../../types/k8sTypes"

describe("Details", () => {
it("should render cluster details", () => {
const mockCluster: Cluster = {
apiVersion: "greenhouse.sap/v1alpha1",
kind: "Cluster",
metadata: {
name: "test-cluster",
creationTimestamp: "2026-01-01T00:00:00Z",
labels: {
"greenhouse.sap/owned-by": "test-team",
},
annotations: {
"greenhouse.sap/cluster-connectivity": "test-oidc",
},
},
spec: {
accessMode: "direct",
kubeConfig: {
maxTokenValidity: 72,
},
},
status: {
kubernetesVersion: "v1.20.0",
nodes: {
ready: 1,
total: 1,
},
statusConditions: {
conditions: [
{
type: "Ready",
status: "True",
lastTransitionTime: "2026-01-01T00:00:00Z",
},
],
},
},
}

render(<Details clusters={mockCluster} />)

expect(screen.getByText("Details")).toBeInTheDocument()
expect(screen.getByText("test-cluster")).toBeInTheDocument()
expect(screen.getByText("v1.20.0")).toBeInTheDocument()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Juno contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React from "react"
import {
DataGrid,
DataGridRow,
DataGridHeadCell,
DataGridCell,
Pill,
Stack,
ContentHeading,
} from "@cloudoperators/juno-ui-components"

import { formatAge } from "../../utils"
import YamlViewer from "../../common/YamlViewer"
import { Cluster } from "../../types/k8sTypes"
import { CONNECTIVITY_LABEL, NO_VALUE_DEFAULT, SUPPORT_GROUP_LABEL } from "../../constants"

interface DetailsProps {
clusters: Cluster
}

interface NodesStatus {
ready: number
total: number
}

const NodeStatus: React.FC<{ nodes: NodesStatus }> = ({ nodes }) => {
const { ready, total } = nodes
const notReady = total - ready

// If not ready nodes exist, make the text red
const notReadyColor = notReady > 0 ? "text-theme-danger" : ""

return (
<div className="inline-block">
{total} nodes ({ready} ready, <span className={notReadyColor}>{notReady} not ready</span>)
</div>
Comment thread
guoda-puidokaite marked this conversation as resolved.
)
}

export const Details: React.FC<DetailsProps> = ({ clusters }) => (
<Stack gap="4" direction="vertical" className="flex-1">
<ContentHeading>Details</ContentHeading>
<DataGrid columns={2} minContentColumns={[0]}>
<DataGridRow>
<DataGridHeadCell nowrap>Name</DataGridHeadCell>
<DataGridCell>{clusters.metadata?.name ?? NO_VALUE_DEFAULT}</DataGridCell>
</DataGridRow>
<DataGridRow>
<DataGridHeadCell nowrap>Age</DataGridHeadCell>
<DataGridCell>{formatAge(clusters.metadata?.creationTimestamp || 0) ?? NO_VALUE_DEFAULT}</DataGridCell>
</DataGridRow>
<DataGridRow>
<DataGridHeadCell nowrap>Version</DataGridHeadCell>
<DataGridCell>{clusters.status?.kubernetesVersion ?? NO_VALUE_DEFAULT}</DataGridCell>
</DataGridRow>
<DataGridRow>
<DataGridHeadCell nowrap>Connectivity</DataGridHeadCell>
<DataGridCell>{clusters.metadata?.annotations?.[CONNECTIVITY_LABEL] ?? NO_VALUE_DEFAULT}</DataGridCell>
</DataGridRow>
<DataGridRow>
<DataGridHeadCell nowrap>Support Group</DataGridHeadCell>
<DataGridCell>{clusters.metadata?.labels?.[SUPPORT_GROUP_LABEL] ?? NO_VALUE_DEFAULT}</DataGridCell>
</DataGridRow>
{clusters.metadata?.labels && Object.keys(clusters.metadata.labels).length > 0 && (
<DataGridRow>
<DataGridHeadCell nowrap>Labels</DataGridHeadCell>
<DataGridCell>
<Stack gap="2" wrap={true}>
{Object.entries(clusters.metadata.labels).map(([key, value], index) => (
<Pill key={key} pillKey={key} pillValue={value} />
))}
</Stack>
</DataGridCell>
</DataGridRow>
)}
<DataGridRow>
<DataGridHeadCell nowrap>Annotations</DataGridHeadCell>
<DataGridCell>
{clusters.metadata?.annotations && Object.keys(clusters.metadata.annotations).length > 0 ? (
<YamlViewer value={clusters.metadata?.annotations} />
) : (
"No Annotations"
)}
</DataGridCell>
</DataGridRow>
<DataGridRow>
<DataGridHeadCell nowrap>Nodes</DataGridHeadCell>
<DataGridCell>
<NodeStatus
nodes={{
ready: clusters?.status?.nodes?.ready ?? 0,
total: clusters?.status?.nodes?.total ?? 0,
}}
/>
</DataGridCell>
</DataGridRow>
</DataGrid>
</Stack>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Juno contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React, { act, Suspense } from "react"
import {
createMemoryHistory,
createRootRoute,
createRoute,
createRouter,
Outlet,
RouterProvider,
} from "@tanstack/react-router"
import { render, screen } from "@testing-library/react"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { PluginInstances } from "./PluginInstances"
import { mockPlugins, MockPluginsResponse } from "../../__mocks__/plugins"

const renderComponent = async (mockPromise: Promise<MockPluginsResponse | unknown>) => {
const rootRoute = createRootRoute({
component: () => <Outlet />,
})
const testRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/admin/clusters/$clusterName",
component: () => (
<QueryClientProvider
client={
new QueryClient({
defaultOptions: {
Comment thread
guoda-puidokaite marked this conversation as resolved.
queries: {
retry: false,
},
},
})
}
>
<Suspense fallback={<div>Loading...</div>}>
<PluginInstances />
</Suspense>
</QueryClientProvider>
),
})
const routeTree = rootRoute.addChildren([testRoute])
const router = createRouter({
routeTree: routeTree,
defaultPendingMinMs: 0,
context: {
apiClient: {
get() {
return mockPromise
},
},
user: {
organization: "test-org",
supportGroups: [],
},
},
history: createMemoryHistory({
initialEntries: ["/admin/clusters/test-cluster"],
}),
})
return await act(async () => render(<RouterProvider router={router} />))
}

describe("PluginInstances", () => {
it("should render plugin instances table", async () => {
await renderComponent(new Promise<MockPluginsResponse>((resolve) => resolve(mockPlugins)))

expect(screen.getByText("Plugin Instances")).toBeInTheDocument()
expect(screen.getByText("Plugin Name")).toBeInTheDocument()
expect(screen.getByText("Plugin Preset")).toBeInTheDocument()
expect(screen.getByText("Status")).toBeInTheDocument()
expect(screen.getByText("plugin-1")).toBeInTheDocument()
expect(screen.getByText("plugin-2")).toBeInTheDocument()
}, 20000)
Comment thread
guoda-puidokaite marked this conversation as resolved.
})
Loading
Loading