Skip to content
Draft
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
3 changes: 2 additions & 1 deletion apps/greenhouse/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
"vite": "7.2.7",
"vite-plugin-svgr": "4.5.0",
"vitest": "3.2.4",
"zustand": "4.5.7"
"zustand": "4.5.7",
"react-error-boundary": "6.0.0"
},
"scripts": {
"lint": "eslint",
Expand Down
20 changes: 18 additions & 2 deletions apps/greenhouse/src/Shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import React, { StrictMode } from "react"
import { createBrowserHistory, createHashHistory, createRouter, RouterProvider } from "@tanstack/react-router"
import { AppShellProvider } from "@cloudoperators/juno-ui-components"
import { MessagesProvider } from "@cloudoperators/juno-messages-provider"
import { createClient } from "@cloudoperators/juno-k8s-client"
import Auth from "./components/Auth"
import styles from "./styles.css?inline"
import StoreProvider from "./components/StoreProvider"
import StoreProvider, { useGlobalsApiEndpoint } from "./components/StoreProvider"
import { AuthProvider, useAuth } from "./components/AuthProvider"
import { routeTree } from "./routeTree.gen"

Expand All @@ -18,6 +19,8 @@ const router = createRouter({
routeTree,
context: {
appProps: undefined!,
apiClient: null,
organization: undefined!,
},
})

Expand Down Expand Up @@ -50,8 +53,21 @@ const getBasePath = (auth: any) => {
return orgString ? orgString.split(":")[1] : undefined
}

const getOrganization = (auth: unknown) => {
// @ts-expect-error - auth?.data type needs to be properly defined
return auth?.data?.raw?.groups?.find((g: any) => g.startsWith("organization:"))?.split(":")[1]
}

function App(props: AppProps) {
const auth = useAuth()
const apiEndpoint = useGlobalsApiEndpoint()
// @ts-expect-error - useAuth return type is not properly typed
const token = auth?.data?.JWT
// Create k8s client if apiEndpoint and token are available
// @ts-expect-error - apiEndpoint type needs to be properly typed as string
const apiClient = apiEndpoint && token ? createClient({ apiEndpoint, token }) : null
const organization = getOrganization(auth)

/*
* Dynamically change the type of history on the router
* based on the enableHashedRouting prop. This ensures that
Expand All @@ -60,7 +76,7 @@ function App(props: AppProps) {
*/
router.update({
basepath: getBasePath(auth),
context: { appProps: props },
context: { appProps: props, apiClient, organization },
history: props.enableHashedRouting ? createHashHistory() : createBrowserHistory(),
})
return <RouterProvider router={router} />
Expand Down
8 changes: 4 additions & 4 deletions apps/greenhouse/src/components/admin/Layout/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import { useNavigate, useMatches, AnySchema } from "@tanstack/react-router"
import { TopNavigation, TopNavigationItem } from "@cloudoperators/juno-ui-components"

export const navigationItems = [
{
label: "Plugin Presets",
value: "/admin/plugin-presets",
},
{
label: "Clusters",
value: "/admin/clusters",
Expand All @@ -16,10 +20,6 @@ export const navigationItems = [
label: "Teams",
value: "/admin/teams",
},
{
label: "Plugin Presets",
value: "/admin/plugin-presets",
},
] as const

type NavigationItem = (typeof navigationItems)[number]
Expand Down
19 changes: 16 additions & 3 deletions apps/greenhouse/src/components/admin/Layout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,30 @@ import React from "react"
import { Container } from "@cloudoperators/juno-ui-components"
import { Breadcrumb } from "./Breadcrumb"
import { Navigation } from "./Navigation"
import { ErrorMessage } from "../common/ErrorBoundary/ErrorMessage"
import { Outlet } from "@tanstack/react-router"

type LayoutProps = {
children: React.ReactNode
error?: Error
}

export const Layout = ({ children }: LayoutProps) => (
export const Layout = ({ error }: LayoutProps) => (
<>
<Navigation />
<Container py px>
<Breadcrumb />
<Container px={false}>{children}</Container>
{/*
This ensures that if an error was not caught by a sub-route,
it is caught and displayed here keeping breadcrumb and the navigation visible,
providing a consistent layout for error handling.
*/}
{error ? (
<ErrorMessage error={error} />
) : (
<Container px={false}>
<Outlet />
</Container>
)}
</Container>
</>
)
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 } from "react"
import { DataGridRow, DataGridCell, Button, Icon } from "@cloudoperators/juno-ui-components"
import { EmptyDataGridRow } from "../../common/EmptyDataGridRow"
import { PluginPreset } from "../../types/k8sTypes"
import { FilterSettings } from "../PluginPresetsFilter"

interface DataRowsProps {
filterSettings: FilterSettings
pluginPresetsPromise: Promise<PluginPreset[]>
colSpan: number
}

const getReadyCondition = (preset: PluginPreset) => {
return preset.status?.statusConditions?.conditions?.find((condition) => condition.type === "Ready")
}

export const DataRows = ({ filterSettings, pluginPresetsPromise, colSpan }: DataRowsProps) => {
const pluginPresets = use(pluginPresetsPromise)

// TODO: Just for demonstration. Optimized filtering to be expected from backend in future.
const filteredPresets = pluginPresets?.filter((preset) => {
if (!filterSettings?.searchTerm) return true

const searchTerm = filterSettings.searchTerm.toLowerCase()
const presetName = preset.metadata?.name?.toLowerCase() || ""
const pluginDefinition = (
preset.spec?.plugin?.pluginDefinitionRef?.name ||
preset.spec?.plugin?.pluginDefinition ||
""
).toLowerCase()

return presetName.includes(searchTerm) || pluginDefinition.includes(searchTerm)
})

if (!filteredPresets || filteredPresets.length === 0) {
return <EmptyDataGridRow colSpan={colSpan}>No plugin presets found.</EmptyDataGridRow>
}

return (
<>
{filteredPresets.map((preset: PluginPreset, idx: number) => (
<DataGridRow key={idx}>
<DataGridCell>
<Icon
icon={
getReadyCondition(preset)?.type === "Ready" && getReadyCondition(preset)?.status === "True"
? "checkCircle"
: "error"
}
color={
getReadyCondition(preset)?.type === "Ready" && getReadyCondition(preset)?.status === "True"
? "text-theme-success"
: "text-theme-danger"
}
/>
</DataGridCell>
<DataGridCell>
{preset.status?.readyPlugins || 0}/{preset.status?.totalPlugins || 0}
</DataGridCell>
<DataGridCell>{preset.metadata?.name}</DataGridCell>
<DataGridCell>
{preset.spec?.plugin?.pluginDefinitionRef.name || preset.spec?.plugin?.pluginDefinition}
</DataGridCell>
<DataGridCell>
{getReadyCondition(preset)?.type === "Ready" && getReadyCondition(preset)?.status !== "True"
? getReadyCondition(preset)?.message
: ""}
</DataGridCell>
<DataGridCell className="whitespace-nowrap">
<Button size="small" variant="primary">
View details
</Button>
</DataGridCell>
</DataGridRow>
))}
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
* SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React, { act } from "react"
import { render, screen } from "@testing-library/react"
import { PluginPresetsDataGrid } from "./index"
import { PluginPreset } from "../../types/k8sTypes"

const mockPluginPresets: PluginPreset[] = [
{
metadata: {
name: "preset-1",
},
spec: {
clusterSelector: {},
deletionPolicy: "Delete",
plugin: {
pluginDefinitionRef: {
name: "plugin-def-1",
},
deletionPolicy: "Delete",
pluginDefinition: "plugin-def-1",
},
},
status: {
readyPlugins: 2,
totalPlugins: 3,
statusConditions: {
conditions: [
{
lastTransitionTime: "2024-10-01T12:00:00Z",
type: "Ready",
status: "True",
message: "",
},
],
},
},
},
{
metadata: {
name: "preset-2",
},
spec: {
clusterSelector: {},
deletionPolicy: "Delete",
plugin: {
pluginDefinitionRef: {
name: "plugin-def-2",
},
deletionPolicy: "Delete",
pluginDefinition: "plugin-def-2",
},
},
status: {
readyPlugins: 0,
totalPlugins: 2,
statusConditions: {
conditions: [
{
lastTransitionTime: "2024-10-01T12:00:00Z",
type: "Ready",
status: "False",
message: "Some error occurred",
},
],
},
},
},
]

describe("PluginPresetsDataGrid", () => {
it("should render loading and column headers while the data is being fetched", async () => {
const mockPluginPresetsPromise = Promise.resolve(mockPluginPresets)
render(<PluginPresetsDataGrid filterSettings={{}} pluginPresetsPromise={mockPluginPresetsPromise} />)

// Loading should be gone
expect(screen.queryByText("Loading...")).toBeInTheDocument()

// Check for column headers
expect(screen.getByText("Instances")).toBeInTheDocument()
expect(screen.getByText("Name")).toBeInTheDocument()
expect(screen.getByText("Plugin Definition")).toBeInTheDocument()
expect(screen.getByText("Message")).toBeInTheDocument()
expect(screen.getByText("Actions")).toBeInTheDocument()
})

it("should render the data", async () => {
const mockPluginPresetsPromise = Promise.resolve(mockPluginPresets)
await act(async () => {
render(<PluginPresetsDataGrid filterSettings={{}} pluginPresetsPromise={mockPluginPresetsPromise} />)
})

// Loading should be gone
expect(screen.queryByText("Loading...")).not.toBeInTheDocument()

// Check for column headers
expect(screen.getByText("Instances")).toBeInTheDocument()
expect(screen.getByText("Name")).toBeInTheDocument()
expect(screen.getByText("Plugin Definition")).toBeInTheDocument()
expect(screen.getByText("Message")).toBeInTheDocument()
expect(screen.getByText("Actions")).toBeInTheDocument()

// Check for data
expect(screen.getByText("2/3")).toBeInTheDocument()
expect(screen.getByText("preset-1")).toBeInTheDocument()
expect(screen.getByText("preset-2")).toBeInTheDocument()
expect(screen.getByText("0/2")).toBeInTheDocument()
})

it("should render the error message while fetching data", async () => {
const mockPluginPresetsPromise = Promise.reject(new Error("Something went wrong"))
await act(async () => {
render(<PluginPresetsDataGrid filterSettings={{}} pluginPresetsPromise={mockPluginPresetsPromise} />)
})

// Loading should be gone
expect(screen.queryByText("Loading...")).not.toBeInTheDocument()

// Check for column headers
expect(screen.getByText("Instances")).toBeInTheDocument()
expect(screen.getByText("Name")).toBeInTheDocument()
expect(screen.getByText("Plugin Definition")).toBeInTheDocument()
expect(screen.getByText("Message")).toBeInTheDocument()
expect(screen.getByText("Actions")).toBeInTheDocument()

// Check for error
expect(screen.getByText("Error: Something went wrong")).toBeInTheDocument()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
* SPDX-License-Identifier: Apache-2.0
*/

import React, { Suspense } from "react"
import { DataGrid, DataGridRow, DataGridHeadCell, Icon } from "@cloudoperators/juno-ui-components"
import { DataRows } from "./DataRows"
import { LoadingDataRow } from "../../common/LoadingDataRow"
import { ErrorBoundary } from "../../common/ErrorBoundary"
import { getErrorDataRowComponent } from "../../common/getErrorDataRow"
import { PluginPreset } from "../../types/k8sTypes"
import { FilterSettings } from "../PluginPresetsFilter"

const COLUMN_SPAN = 6

interface PluginPresetsDataGridProps {
filterSettings: FilterSettings
pluginPresetsPromise: Promise<PluginPreset[]>
}

export const PluginPresetsDataGrid = ({ filterSettings, pluginPresetsPromise }: PluginPresetsDataGridProps) => {
return (
<div className="datagrid-hover">
<DataGrid minContentColumns={[0, 1, 5]} columns={COLUMN_SPAN}>
<DataGridRow>
<DataGridHeadCell>
<Icon icon="monitorHeart" />
</DataGridHeadCell>
<DataGridHeadCell>Instances</DataGridHeadCell>
<DataGridHeadCell>Name</DataGridHeadCell>
<DataGridHeadCell>Plugin Definition</DataGridHeadCell>
<DataGridHeadCell>Message</DataGridHeadCell>
<DataGridHeadCell>Actions</DataGridHeadCell>
</DataGridRow>

<ErrorBoundary
displayErrorMessage
fallbackRender={getErrorDataRowComponent({ colspan: COLUMN_SPAN })}
resetKeys={[pluginPresetsPromise]}
>
<Suspense fallback={<LoadingDataRow colSpan={COLUMN_SPAN} />}>
<DataRows
filterSettings={filterSettings}
pluginPresetsPromise={pluginPresetsPromise}
colSpan={COLUMN_SPAN}
/>
</Suspense>
</ErrorBoundary>
</DataGrid>
</div>
)
}
Loading
Loading