From 211a3dbfd3a86e4d580adde983cb9e884c5b7e60 Mon Sep 17 00:00:00 2001 From: Taimoor Aslam Date: Fri, 21 Nov 2025 14:04:32 +0100 Subject: [PATCH] feat(greenhouse): plugin presets overview --- apps/greenhouse/package.json | 3 +- apps/greenhouse/src/Shell.tsx | 20 +- .../components/admin/Layout/Navigation.tsx | 8 +- .../src/components/admin/Layout/index.tsx | 19 +- .../PluginPresetsDataGrid/DataRows.tsx | 83 + .../PluginPresetsDataGrid/index.test.tsx | 132 ++ .../PluginPresetsDataGrid/index.tsx | 53 + .../PluginPresets/PluginPresetsFilter.tsx | 44 + .../components/admin/PluginPresets/index.tsx | 27 + .../components/admin/api/plugin-presets.ts | 12 + .../admin/common/EmptyDataGridRow.tsx | 18 + .../ErrorBoundary/ErrorMessage.test.tsx | 34 + .../common/ErrorBoundary/ErrorMessage.tsx | 26 + .../admin/common/ErrorBoundary/index.test.tsx | 96 + .../admin/common/ErrorBoundary/index.tsx | 27 + .../admin/common/LoadingDataRow.test.tsx | 16 + .../admin/common/LoadingDataRow.tsx | 16 + .../admin/common/getErrorDataRow.tsx | 21 + .../src/components/admin/types/k8sTypes.ts | 8 + .../src/components/admin/types/schema.d.ts | 1556 +++++++++++++++++ apps/greenhouse/src/routes/__root.tsx | 3 + apps/greenhouse/src/routes/admin/index.tsx | 2 +- .../src/routes/admin/plugin-presets.tsx | 29 +- apps/greenhouse/src/routes/admin/route.tsx | 16 +- pnpm-lock.yaml | 200 +-- 25 files changed, 2277 insertions(+), 192 deletions(-) create mode 100644 apps/greenhouse/src/components/admin/PluginPresets/PluginPresetsDataGrid/DataRows.tsx create mode 100644 apps/greenhouse/src/components/admin/PluginPresets/PluginPresetsDataGrid/index.test.tsx create mode 100644 apps/greenhouse/src/components/admin/PluginPresets/PluginPresetsDataGrid/index.tsx create mode 100644 apps/greenhouse/src/components/admin/PluginPresets/PluginPresetsFilter.tsx create mode 100644 apps/greenhouse/src/components/admin/PluginPresets/index.tsx create mode 100644 apps/greenhouse/src/components/admin/api/plugin-presets.ts create mode 100644 apps/greenhouse/src/components/admin/common/EmptyDataGridRow.tsx create mode 100644 apps/greenhouse/src/components/admin/common/ErrorBoundary/ErrorMessage.test.tsx create mode 100644 apps/greenhouse/src/components/admin/common/ErrorBoundary/ErrorMessage.tsx create mode 100644 apps/greenhouse/src/components/admin/common/ErrorBoundary/index.test.tsx create mode 100644 apps/greenhouse/src/components/admin/common/ErrorBoundary/index.tsx create mode 100644 apps/greenhouse/src/components/admin/common/LoadingDataRow.test.tsx create mode 100644 apps/greenhouse/src/components/admin/common/LoadingDataRow.tsx create mode 100644 apps/greenhouse/src/components/admin/common/getErrorDataRow.tsx create mode 100644 apps/greenhouse/src/components/admin/types/k8sTypes.ts create mode 100644 apps/greenhouse/src/components/admin/types/schema.d.ts diff --git a/apps/greenhouse/package.json b/apps/greenhouse/package.json index 20ae0dd671..df7da6a226 100644 --- a/apps/greenhouse/package.json +++ b/apps/greenhouse/package.json @@ -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", diff --git a/apps/greenhouse/src/Shell.tsx b/apps/greenhouse/src/Shell.tsx index b86415c69c..eabe570624 100644 --- a/apps/greenhouse/src/Shell.tsx +++ b/apps/greenhouse/src/Shell.tsx @@ -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" @@ -18,6 +19,8 @@ const router = createRouter({ routeTree, context: { appProps: undefined!, + apiClient: null, + organization: undefined!, }, }) @@ -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 @@ -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 diff --git a/apps/greenhouse/src/components/admin/Layout/Navigation.tsx b/apps/greenhouse/src/components/admin/Layout/Navigation.tsx index 445132ec20..28428d81ba 100644 --- a/apps/greenhouse/src/components/admin/Layout/Navigation.tsx +++ b/apps/greenhouse/src/components/admin/Layout/Navigation.tsx @@ -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", @@ -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] diff --git a/apps/greenhouse/src/components/admin/Layout/index.tsx b/apps/greenhouse/src/components/admin/Layout/index.tsx index 23bae61086..95bad3904b 100644 --- a/apps/greenhouse/src/components/admin/Layout/index.tsx +++ b/apps/greenhouse/src/components/admin/Layout/index.tsx @@ -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) => ( <> - {children} + {/* + 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 ? ( + + ) : ( + + + + )} ) diff --git a/apps/greenhouse/src/components/admin/PluginPresets/PluginPresetsDataGrid/DataRows.tsx b/apps/greenhouse/src/components/admin/PluginPresets/PluginPresetsDataGrid/DataRows.tsx new file mode 100644 index 0000000000..0a3f587c95 --- /dev/null +++ b/apps/greenhouse/src/components/admin/PluginPresets/PluginPresetsDataGrid/DataRows.tsx @@ -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 + 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 No plugin presets found. + } + + return ( + <> + {filteredPresets.map((preset: PluginPreset, idx: number) => ( + + + + + + {preset.status?.readyPlugins || 0}/{preset.status?.totalPlugins || 0} + + {preset.metadata?.name} + + {preset.spec?.plugin?.pluginDefinitionRef.name || preset.spec?.plugin?.pluginDefinition} + + + {getReadyCondition(preset)?.type === "Ready" && getReadyCondition(preset)?.status !== "True" + ? getReadyCondition(preset)?.message + : ""} + + + + + + ))} + + ) +} diff --git a/apps/greenhouse/src/components/admin/PluginPresets/PluginPresetsDataGrid/index.test.tsx b/apps/greenhouse/src/components/admin/PluginPresets/PluginPresetsDataGrid/index.test.tsx new file mode 100644 index 0000000000..69269bece9 --- /dev/null +++ b/apps/greenhouse/src/components/admin/PluginPresets/PluginPresetsDataGrid/index.test.tsx @@ -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() + + // 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() + }) + + // 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() + }) + + // 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() + }) +}) diff --git a/apps/greenhouse/src/components/admin/PluginPresets/PluginPresetsDataGrid/index.tsx b/apps/greenhouse/src/components/admin/PluginPresets/PluginPresetsDataGrid/index.tsx new file mode 100644 index 0000000000..19db950e7f --- /dev/null +++ b/apps/greenhouse/src/components/admin/PluginPresets/PluginPresetsDataGrid/index.tsx @@ -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 +} + +export const PluginPresetsDataGrid = ({ filterSettings, pluginPresetsPromise }: PluginPresetsDataGridProps) => { + return ( +
+ + + + + + Instances + Name + Plugin Definition + Message + Actions + + + + }> + + + + +
+ ) +} diff --git a/apps/greenhouse/src/components/admin/PluginPresets/PluginPresetsFilter.tsx b/apps/greenhouse/src/components/admin/PluginPresets/PluginPresetsFilter.tsx new file mode 100644 index 0000000000..a6c41aaa2a --- /dev/null +++ b/apps/greenhouse/src/components/admin/PluginPresets/PluginPresetsFilter.tsx @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useCallback } from "react" +import { useLoaderData, useNavigate } from "@tanstack/react-router" +import { Stack, SearchInput, InputGroup } from "@cloudoperators/juno-ui-components" + +export type FilterSettings = { + searchTerm?: string +} + +export const PluginPresetsFilter = () => { + const navigate = useNavigate() + const { filterSettings } = useLoaderData({ from: "/admin/plugin-presets" }) + + const handleSearchChange = useCallback( + (value?: string) => { + navigate({ + to: "/admin/plugin-presets", + search: (prev) => ({ + ...prev, + searchTerm: value, + }), + }) + }, + [navigate] + ) + + return ( + + + handleSearchChange(undefined)} + /> + + + ) +} diff --git a/apps/greenhouse/src/components/admin/PluginPresets/index.tsx b/apps/greenhouse/src/components/admin/PluginPresets/index.tsx new file mode 100644 index 0000000000..1e523116d7 --- /dev/null +++ b/apps/greenhouse/src/components/admin/PluginPresets/index.tsx @@ -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 from "react" +import { Container, ContentHeading } from "@cloudoperators/juno-ui-components" +import { useLoaderData } from "@tanstack/react-router" +import { PluginPresetsDataGrid } from "./PluginPresetsDataGrid" +import { PluginPresetsFilter } from "./PluginPresetsFilter" + +export const PluginPresets = () => { + const { pluginPresetsPromise, filterSettings } = useLoaderData({ from: "/admin/plugin-presets" }) + + return ( + <> + + Plugin Presets Overview +

Manage and monitor plugin preset configurations across all clusters

+
+ + + + + + ) +} diff --git a/apps/greenhouse/src/components/admin/api/plugin-presets.ts b/apps/greenhouse/src/components/admin/api/plugin-presets.ts new file mode 100644 index 0000000000..784aef8616 --- /dev/null +++ b/apps/greenhouse/src/components/admin/api/plugin-presets.ts @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { PluginPreset } from "../types/k8sTypes" + +export const fetchPluginPresets = async (client: any, namespace: string): Promise => { + const response = await client.get(`/apis/greenhouse.sap/v1alpha1/namespaces/${namespace}/pluginpresets`) + + return response.items || [] +} diff --git a/apps/greenhouse/src/components/admin/common/EmptyDataGridRow.tsx b/apps/greenhouse/src/components/admin/common/EmptyDataGridRow.tsx new file mode 100644 index 0000000000..608acea9f2 --- /dev/null +++ b/apps/greenhouse/src/components/admin/common/EmptyDataGridRow.tsx @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from "react" +import { DataGridCell, DataGridRow } from "@cloudoperators/juno-ui-components" + +type EmptyDataGridRowProps = { + colSpan: number + children: React.ReactNode +} + +export const EmptyDataGridRow = ({ colSpan, children }: EmptyDataGridRowProps) => ( + + {children} + +) diff --git a/apps/greenhouse/src/components/admin/common/ErrorBoundary/ErrorMessage.test.tsx b/apps/greenhouse/src/components/admin/common/ErrorBoundary/ErrorMessage.test.tsx new file mode 100644 index 0000000000..953dffdcdd --- /dev/null +++ b/apps/greenhouse/src/components/admin/common/ErrorBoundary/ErrorMessage.test.tsx @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2025 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 { ErrorMessage } from "./ErrorMessage" + +describe("ErrorMessage", () => { + it("renders error message with name and message", () => { + const error = new Error("Something went wrong") + error.name = "TestError" + render() + const errorText = screen.getByText("TestError: Something went wrong") + expect(errorText).toBeInTheDocument() + }) + + it("renders error message with default name", () => { + const error = new Error("Something went wrong") + error.name = "" + render() + const errorText = screen.getByText("Error: Something went wrong") + expect(errorText).toBeInTheDocument() + }) + + it("renders default message when error message is empty", () => { + const error = new Error("") + error.name = "TestError" + render() + const errorText = screen.getByText("TestError: Something went wrong") + expect(errorText).toBeInTheDocument() + }) +}) diff --git a/apps/greenhouse/src/components/admin/common/ErrorBoundary/ErrorMessage.tsx b/apps/greenhouse/src/components/admin/common/ErrorBoundary/ErrorMessage.tsx new file mode 100644 index 0000000000..371c51e2b4 --- /dev/null +++ b/apps/greenhouse/src/components/admin/common/ErrorBoundary/ErrorMessage.tsx @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from "react" +import { Icon, Stack } from "@cloudoperators/juno-ui-components" + +type ErrorMessageProps = { + error: Error +} + +export const ErrorMessage = ({ error }: ErrorMessageProps) => { + const errorName = error.name ? `${error.name}: ` : "Error: " + const errorMessage = error.message || "Something went wrong" + + return ( + + + + {errorName} + {errorMessage} + + + ) +} diff --git a/apps/greenhouse/src/components/admin/common/ErrorBoundary/index.test.tsx b/apps/greenhouse/src/components/admin/common/ErrorBoundary/index.test.tsx new file mode 100644 index 0000000000..7eaa62809a --- /dev/null +++ b/apps/greenhouse/src/components/admin/common/ErrorBoundary/index.test.tsx @@ -0,0 +1,96 @@ +/* + * 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 { render, screen } from "@testing-library/react" +import { FallbackProps } from "react-error-boundary" +import { ErrorBoundary } from "./index" + +const Component = () =>
Some Component
+const ComponentThatThrows = () => { + useEffect(() => { + throw new Error("Test error message") + }, []) + return
Will not render
+} + +const CustomFallback = ({ error }: FallbackProps) =>
Custom fallback: {error?.message}
+ +describe("ErrorBoundary", () => { + let consoleSpy: any + + beforeEach(() => { + // mock console.error to suppress the error message being printed to the console + consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}) + }) + + afterEach(() => { + // restore console.error to its original implementation + consoleSpy.mockRestore() + }) + + it("should render children when no error occurs", () => { + render( + + + + ) + expect(screen.getByText("Some Component")).toBeInTheDocument() + }) + + describe("when error occurs", () => { + it("should render null when displayErrorMessage is false", () => { + const { container } = render( + + + + ) + expect(container.firstChild).toBeNull() + expect(screen.queryByText("Some Component")).not.toBeInTheDocument() + }) + + it("should render null when displayErrorMessage is undefined", () => { + const { container } = render( + + + + ) + expect(container.firstChild).toBeNull() + expect(screen.queryByText("Some Component")).not.toBeInTheDocument() + }) + + it("should render default ErrorMessage when displayErrorMessage is true and no fallback provided", () => { + render( + + + + ) + expect(screen.getByText("Error: Test error message")).toBeInTheDocument() + expect(screen.queryByText("Some Component")).not.toBeInTheDocument() + }) + + it("should render custom fallback when displayErrorMessage is true and fallback is provided", () => { + render( + + + + ) + expect(screen.getByText("Custom fallback: Test error message")).toBeInTheDocument() + expect(screen.queryByText("Error: Test error message")).not.toBeInTheDocument() + expect(screen.queryByText("Some Component")).not.toBeInTheDocument() + }) + + it("should render null when displayErrorMessage is false even if fallback is provided", () => { + const { container } = render( + + + + ) + expect(container.firstChild).toBeNull() + expect(screen.queryByText("Custom fallback: Test error message")).not.toBeInTheDocument() + expect(screen.queryByText("Some Component")).not.toBeInTheDocument() + }) + }) +}) diff --git a/apps/greenhouse/src/components/admin/common/ErrorBoundary/index.tsx b/apps/greenhouse/src/components/admin/common/ErrorBoundary/index.tsx new file mode 100644 index 0000000000..b7e91398e9 --- /dev/null +++ b/apps/greenhouse/src/components/admin/common/ErrorBoundary/index.tsx @@ -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, { ReactNode } from "react" +import { ErrorBoundary as ReactErrorBoundary, FallbackProps } from "react-error-boundary" +import { ErrorMessage } from "./ErrorMessage" + +export const ErrorBoundary = ({ + children, + displayErrorMessage, + fallbackRender, + resetKeys, +}: { + children: ReactNode + displayErrorMessage?: boolean + fallbackRender?: (props: FallbackProps) => ReactNode + resetKeys?: any +}) => ( + null} + > + {children} + +) diff --git a/apps/greenhouse/src/components/admin/common/LoadingDataRow.test.tsx b/apps/greenhouse/src/components/admin/common/LoadingDataRow.test.tsx new file mode 100644 index 0000000000..967e3a50eb --- /dev/null +++ b/apps/greenhouse/src/components/admin/common/LoadingDataRow.test.tsx @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2025 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 { LoadingDataRow } from "./LoadingDataRow" + +describe("LoadingDataRow", () => { + it("should correctly render", () => { + render() + const loadingText = screen.getByText("Loading...") + expect(loadingText).toBeInTheDocument() + }) +}) diff --git a/apps/greenhouse/src/components/admin/common/LoadingDataRow.tsx b/apps/greenhouse/src/components/admin/common/LoadingDataRow.tsx new file mode 100644 index 0000000000..de0045d201 --- /dev/null +++ b/apps/greenhouse/src/components/admin/common/LoadingDataRow.tsx @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from "react" +import { Stack } from "@cloudoperators/juno-ui-components/index" +import { EmptyDataGridRow } from "./EmptyDataGridRow" + +export const LoadingDataRow = ({ colSpan }: { colSpan: number }) => ( + + +
Loading...
+
+
+) diff --git a/apps/greenhouse/src/components/admin/common/getErrorDataRow.tsx b/apps/greenhouse/src/components/admin/common/getErrorDataRow.tsx new file mode 100644 index 0000000000..0b46d52945 --- /dev/null +++ b/apps/greenhouse/src/components/admin/common/getErrorDataRow.tsx @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from "react" +import { Stack } from "@cloudoperators/juno-ui-components/index" +import { EmptyDataGridRow } from "./EmptyDataGridRow" +import { ErrorMessage } from "./ErrorBoundary/ErrorMessage" + +export const getErrorDataRowComponent = ({ colspan }: { colspan: number }) => { + const ErrorDataRow = ({ error }: { error: Error }) => ( + + + + + + ) + ErrorDataRow.displayName = "ErrorDataRow" + return ErrorDataRow +} diff --git a/apps/greenhouse/src/components/admin/types/k8sTypes.ts b/apps/greenhouse/src/components/admin/types/k8sTypes.ts new file mode 100644 index 0000000000..cc8e01bbe3 --- /dev/null +++ b/apps/greenhouse/src/components/admin/types/k8sTypes.ts @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { components } from "./schema" + +export type PluginPreset = components["schemas"]["PluginPreset"] diff --git a/apps/greenhouse/src/components/admin/types/schema.d.ts b/apps/greenhouse/src/components/admin/types/schema.d.ts new file mode 100644 index 0000000000..20235210b6 --- /dev/null +++ b/apps/greenhouse/src/components/admin/types/schema.d.ts @@ -0,0 +1,1556 @@ +/* + * SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export type paths = Record +export type webhooks = Record +export interface components { + schemas: { + /** + * Catalog + * @description Catalog is the Schema for the catalogs API. + */ + Catalog: { + /** + * @description APIVersion defines the versioned schema of this representation of an object. + * Servers should convert recognized schemas to the latest internal value, and + * may reject unrecognized values. + * More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + */ + apiVersion?: string + /** + * @description Kind is a string value representing the REST resource this object represents. + * Servers may infer this from the endpoint the client submits requests to. + * Cannot be updated. + * In CamelCase. + * More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + */ + kind?: string + metadata?: { + name?: string + namespace?: string + /** Format: uuid */ + uid?: string + resourceVersion?: string + /** Format: date-time */ + creationTimestamp?: string + /** Format: date-time */ + deletionTimestamp?: string + labels?: { + [key: string]: string + } + annotations?: { + [key: string]: string + } + } + /** @description CatalogSpec defines the desired state of Catalog. */ + spec?: { + /** @description Sources contains the list of Git Repository source to resolve PluginDefinitions / ClusterPluginDefinitions from */ + sources: { + /** @description Interval defines how often to reconcile the Git repository source */ + interval?: string + /** @description Overrides are the PluginDefinition overrides to be applied */ + overrides?: { + /** + * @description Alias is the alias to apply to the PluginDefinition Name via Kustomize patches + * For SourceType Helm, this field is passed to postRender Kustomize patch + */ + alias?: string + /** @description Name is the name of the PluginDefinition to patch with an alias */ + name: string + /** @description Repository is the repository to override in the PluginDefinition .spec.helmChart.repository */ + repository?: string + }[] + /** + * @description Ref is the git reference (branch, tag, or SHA) to resolve PluginDefinitions from + * precedence: SHA > Tag > Branch + * if not specified, defaults to the branch "main" + */ + ref?: { + branch?: string + sha?: string + tag?: string + } + /** @description Repository - the Git repository URL */ + repository: string + /** + * @description Resources contains the list of path to PluginDefinition files + * e.g. ["plugins/plugin-a.yaml", "plugins/plugin-b.yaml"] + * glob patterns are supported, e.g. ["plugins/*.yaml", "more-plugins/**\/plugindefinition.yaml"] + */ + resources: string[] + /** + * @description SecretName is the name of v1.Secret containing credentials to access the Git repository + * the secret must be in the same namespace as the Catalog resource + * + * GitHub App Example: + */ + secretName?: string + }[] + } + /** @description CatalogStatus defines the observed state of Catalog. */ + status?: { + /** @description Inventory contains list of internal artifacts generated by Catalog */ + inventory?: { + [key: string]: { + kind: string + message?: string + name: string + ready?: string + }[] + } + /** @description LastReconciledAt contains the value when the reconcile was last triggered via annotation. */ + lastReconciledAt?: string + /** @description StatusConditions contain the different conditions that constitute the status of the Catalog */ + statusConditions?: { + conditions?: { + /** + * Format: date-time + * @description LastTransitionTime is the last time the condition transitioned from one status to another. + */ + lastTransitionTime: string + /** @description Message is an optional human-readable message indicating details about the last transition. */ + message?: string + /** @description Reason is a one-word, CamelCase reason for the condition's last transition. */ + reason?: string + /** @description Status of the condition. */ + status: string + /** @description Type of the condition. */ + type: string + }[] + } + } + } + /** + * ClusterKubeconfig + * @description ClusterKubeconfig is the Schema for the clusterkubeconfigs API + * ObjectMeta.OwnerReferences is used to link the ClusterKubeconfig to the Cluster + * ObjectMeta.Generation is used to detect changes in the ClusterKubeconfig and sync local kubeconfig files + * ObjectMeta.Name is designed to be the same with the Cluster name + */ + ClusterKubeconfig: { + /** + * @description APIVersion defines the versioned schema of this representation of an object. + * Servers should convert recognized schemas to the latest internal value, and + * may reject unrecognized values. + * More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + */ + apiVersion?: string + /** + * @description Kind is a string value representing the REST resource this object represents. + * Servers may infer this from the endpoint the client submits requests to. + * Cannot be updated. + * In CamelCase. + * More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + */ + kind?: string + metadata?: { + name?: string + namespace?: string + /** Format: uuid */ + uid?: string + resourceVersion?: string + /** Format: date-time */ + creationTimestamp?: string + /** Format: date-time */ + deletionTimestamp?: string + labels?: { + [key: string]: string + } + annotations?: { + [key: string]: string + } + } + /** + * @description ClusterKubeconfigSpec stores the kubeconfig data for the cluster + * The idea is to use kubeconfig data locally with minimum effort (with local tools or plain kubectl): + * kubectl get cluster-kubeconfig $NAME -o yaml | yq -y .spec.kubeconfig + */ + spec?: { + /** + * @description ClusterKubeconfigData stores the kubeconfig data ready to use kubectl or other local tooling + * It is a simplified version of clientcmdapi.Config: https://pkg.go.dev/k8s.io/client-go/tools/clientcmd/api#Config + */ + kubeconfig?: { + apiVersion?: string + clusters?: { + cluster: { + /** Format: byte */ + "certificate-authority-data"?: string + server?: string + } + name: string + }[] + contexts?: { + context?: { + cluster: string + namespace?: string + user: string + } + name: string + }[] + "current-context"?: string + kind?: string + preferences?: Record + users?: { + name: string + user?: { + /** @description AuthProviderConfig holds the configuration for a specified auth provider. */ + "auth-provider"?: { + config?: { + [key: string]: string + } + name: string + } + /** Format: byte */ + "client-certificate-data"?: string + /** Format: byte */ + "client-key-data"?: string + } + }[] + } + } + status?: { + /** + * @description A StatusConditions contains a list of conditions. + * Only one condition of a given type may exist in the list. + */ + statusConditions?: { + conditions?: { + /** + * Format: date-time + * @description LastTransitionTime is the last time the condition transitioned from one status to another. + */ + lastTransitionTime: string + /** @description Message is an optional human-readable message indicating details about the last transition. */ + message?: string + /** @description Reason is a one-word, CamelCase reason for the condition's last transition. */ + reason?: string + /** @description Status of the condition. */ + status: string + /** @description Type of the condition. */ + type: string + }[] + } + } + } + /** + * ClusterPluginDefinition + * @description ClusterPluginDefinition is the Schema for the clusterplugindefinitions API. + */ + ClusterPluginDefinition: { + /** + * @description APIVersion defines the versioned schema of this representation of an object. + * Servers should convert recognized schemas to the latest internal value, and + * may reject unrecognized values. + * More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + */ + apiVersion?: string + /** + * @description Kind is a string value representing the REST resource this object represents. + * Servers may infer this from the endpoint the client submits requests to. + * Cannot be updated. + * In CamelCase. + * More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + */ + kind?: string + metadata?: { + name?: string + namespace?: string + /** Format: uuid */ + uid?: string + resourceVersion?: string + /** Format: date-time */ + creationTimestamp?: string + /** Format: date-time */ + deletionTimestamp?: string + labels?: { + [key: string]: string + } + annotations?: { + [key: string]: string + } + } + /** @description PluginDefinitionSpec defines the desired state of PluginDefinitionSpec */ + spec?: { + /** @description Description provides additional details of the pluginDefinition. */ + description?: string + /** @description DisplayName provides a human-readable label for the pluginDefinition. */ + displayName?: string + /** + * @description DocMarkDownUrl specifies the URL to the markdown documentation file for this plugin. + * Source needs to allow all CORS origins. + */ + docMarkDownUrl?: string + /** @description HelmChart specifies where the Helm Chart for this pluginDefinition can be found. */ + helmChart?: { + /** @description Name of the HelmChart chart. */ + name: string + /** @description Repository of the HelmChart chart. */ + repository: string + /** @description Version of the HelmChart chart. */ + version: string + } + /** + * @description Icon specifies the icon to be used for this plugin in the Greenhouse UI. + * Icons can be either: + * - A string representing a juno icon in camel case from this list: https://github.com/sapcc/juno/blob/main/libs/juno-ui-components/src/components/Icon/Icon.component.js#L6-L52 + * - A publicly accessible image reference to a .png file. Will be displayed 100x100px + */ + icon?: string + /** @description RequiredValues is a list of values required to create an instance of this PluginDefinition. */ + options?: { + /** @description Default provides a default value for the option */ + default?: unknown + /** @description Description provides a human-readable text for the value as shown in the UI. */ + description?: string + /** @description DisplayName provides a human-readable label for the configuration option */ + displayName?: string + /** @description Name/Key of the config option. */ + name: string + /** @description Regex specifies a match rule for validating configuration options. */ + regex?: string + /** @description Required indicates that this config option is required */ + required: boolean + /** + * @description Type of this configuration option. + * @enum {string} + */ + type: "string" | "secret" | "bool" | "int" | "list" | "map" + }[] + /** @description UIApplication specifies a reference to a UI application */ + uiApplication?: { + /** @description Name of the UI application. */ + name: string + /** + * @description URL specifies the url to a built javascript asset. + * By default, assets are loaded from the Juno asset server using the provided name and version. + */ + url?: string + /** @description Version of the frontend application. */ + version: string + } + /** @description Version of this pluginDefinition */ + version: string + /** + * Format: int32 + * @description Weight configures the order in which Plugins are shown in the Greenhouse UI. + * Defaults to alphabetical sorting if not provided or on conflict. + */ + weight?: number + } + /** @description ClusterPluginDefinitionStatus defines the observed state of ClusterPluginDefinition. */ + status?: { + /** @description StatusConditions contain the different conditions that constitute the status of the Plugin. */ + statusConditions?: { + conditions?: { + /** + * Format: date-time + * @description LastTransitionTime is the last time the condition transitioned from one status to another. + */ + lastTransitionTime: string + /** @description Message is an optional human-readable message indicating details about the last transition. */ + message?: string + /** @description Reason is a one-word, CamelCase reason for the condition's last transition. */ + reason?: string + /** @description Status of the condition. */ + status: string + /** @description Type of the condition. */ + type: string + }[] + } + } + } + /** + * Cluster + * @description Cluster is the Schema for the clusters API + */ + Cluster: { + /** + * @description APIVersion defines the versioned schema of this representation of an object. + * Servers should convert recognized schemas to the latest internal value, and + * may reject unrecognized values. + * More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + */ + apiVersion?: string + /** + * @description Kind is a string value representing the REST resource this object represents. + * Servers may infer this from the endpoint the client submits requests to. + * Cannot be updated. + * In CamelCase. + * More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + */ + kind?: string + metadata?: { + name?: string + namespace?: string + /** Format: uuid */ + uid?: string + resourceVersion?: string + /** Format: date-time */ + creationTimestamp?: string + /** Format: date-time */ + deletionTimestamp?: string + labels?: { + [key: string]: string + } + annotations?: { + [key: string]: string + } + } + /** @description ClusterSpec defines the desired state of the Cluster. */ + spec?: { + /** + * @description AccessMode configures how the cluster is accessed from the Greenhouse operator. + * @enum {string} + */ + accessMode: "direct" + /** @description KubeConfig contains specific values for `KubeConfig` for the cluster. */ + kubeConfig?: { + /** + * Format: int32 + * @description MaxTokenValidity specifies the maximum duration for which a token remains valid in hours. + * @default 72 + */ + maxTokenValidity: number + } + } + /** @description ClusterStatus defines the observed state of Cluster */ + status?: { + /** + * Format: date-time + * @description BearerTokenExpirationTimestamp reflects the expiration timestamp of the bearer token used to access the cluster. + */ + bearerTokenExpirationTimestamp?: string + /** @description KubernetesVersion reflects the detected Kubernetes version of the cluster. */ + kubernetesVersion?: string + /** @description Nodes contain a short summary of nodes count and not ready nodes status. */ + nodes?: { + /** @description NotReady is slice of non-ready nodes status details. */ + notReady?: { + /** + * Format: date-time + * @description LastTransitionTime represents latest transition time of status. + */ + lastTransitionTime?: string + /** @description Message represents the error message. */ + message?: string + /** @description Name of the node. */ + name: string + }[] + /** + * Format: int32 + * @description ReadyNodes represent the number of ready nodes in the cluster. + */ + ready?: number + /** + * Format: int32 + * @description Total represent the number of all the nodes in the cluster. + */ + total?: number + } + /** @description StatusConditions contain the different conditions that constitute the status of the Cluster. */ + statusConditions?: { + conditions?: { + /** + * Format: date-time + * @description LastTransitionTime is the last time the condition transitioned from one status to another. + */ + lastTransitionTime: string + /** @description Message is an optional human-readable message indicating details about the last transition. */ + message?: string + /** @description Reason is a one-word, CamelCase reason for the condition's last transition. */ + reason?: string + /** @description Status of the condition. */ + status: string + /** @description Type of the condition. */ + type: string + }[] + } + } + } + /** + * Organization + * @description Organization is the Schema for the organizations API + */ + Organization: { + /** + * @description APIVersion defines the versioned schema of this representation of an object. + * Servers should convert recognized schemas to the latest internal value, and + * may reject unrecognized values. + * More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + */ + apiVersion?: string + /** + * @description Kind is a string value representing the REST resource this object represents. + * Servers may infer this from the endpoint the client submits requests to. + * Cannot be updated. + * In CamelCase. + * More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + */ + kind?: string + metadata?: { + name?: string + namespace?: string + /** Format: uuid */ + uid?: string + resourceVersion?: string + /** Format: date-time */ + creationTimestamp?: string + /** Format: date-time */ + deletionTimestamp?: string + labels?: { + [key: string]: string + } + annotations?: { + [key: string]: string + } + } + /** @description OrganizationSpec defines the desired state of Organization */ + spec?: { + /** @description Authentication configures the organizations authentication mechanism. */ + authentication?: { + /** @description OIDConfig configures the OIDC provider. */ + oidc?: { + /** @description ClientIDReference references the Kubernetes secret containing the client id. */ + clientIDReference: { + /** @description Key in the secret to select the value from. */ + key: string + /** @description Name of the secret in the same namespace. */ + name: string + } + /** @description ClientSecretReference references the Kubernetes secret containing the client secret. */ + clientSecretReference: { + /** @description Key in the secret to select the value from. */ + key: string + /** @description Name of the secret in the same namespace. */ + name: string + } + /** @description Issuer is the URL of the identity service. */ + issuer: string + /** + * @description OAuth2ClientRedirectURIs are a registered set of redirect URIs. When redirecting from the idproxy to + * the client application, the URI requested to redirect to must be contained in this list. + */ + oauth2ClientRedirectURIs?: string[] + /** + * @description RedirectURI is the redirect URI to be used for the OIDC flow against the upstream IdP. + * If none is specified, the Greenhouse ID proxy will be used. + */ + redirectURI?: string + } + /** @description SCIMConfig configures the SCIM client. */ + scim?: { + /** + * @description AuthType defined possible authentication type + * @default basic + * @enum {string} + */ + authType: "basic" | "token" + /** @description URL to the SCIM server. */ + baseURL: string + /** @description Password to be used for basic authentication. */ + basicAuthPw?: { + /** @description Secret references the secret containing the value. */ + secret?: { + /** @description Key in the secret to select the value from. */ + key: string + /** @description Name of the secret in the same namespace. */ + name: string + } + } + /** @description User to be used for basic authentication. */ + basicAuthUser?: { + /** @description Secret references the secret containing the value. */ + secret?: { + /** @description Key in the secret to select the value from. */ + key: string + /** @description Name of the secret in the same namespace. */ + name: string + } + } + /** @description BearerHeader to be used to defined bearer token header */ + bearerHeader?: string + /** @description BearerPrefix to be used to defined bearer token prefix */ + bearerPrefix?: string + /** @description BearerToken to be used for bearer token authorization */ + bearerToken?: { + /** @description Secret references the secret containing the value. */ + secret?: { + /** @description Key in the secret to select the value from. */ + key: string + /** @description Name of the secret in the same namespace. */ + name: string + } + } + } + } + /** @description ConfigMapRef allows to reference organizational config map. */ + configMapRef?: string + /** @description Description provides additional details of the organization. */ + description?: string + /** + * @description DisplayName is an optional name for the organization to be displayed in the Greenhouse UI. + * Defaults to a normalized version of metadata.name. + */ + displayName?: string + /** @description MappedOrgAdminIDPGroup is the IDP group ID identifying org admins */ + mappedOrgAdminIdPGroup?: string + } + /** @description OrganizationStatus defines the observed state of an Organization */ + status?: { + /** @description StatusConditions contain the different conditions that constitute the status of the Organization. */ + statusConditions?: { + conditions?: { + /** + * Format: date-time + * @description LastTransitionTime is the last time the condition transitioned from one status to another. + */ + lastTransitionTime: string + /** @description Message is an optional human-readable message indicating details about the last transition. */ + message?: string + /** @description Reason is a one-word, CamelCase reason for the condition's last transition. */ + reason?: string + /** @description Status of the condition. */ + status: string + /** @description Type of the condition. */ + type: string + }[] + } + } + } + /** + * PluginDefinition + * @description PluginDefinition is the Schema for the PluginDefinitions API + */ + PluginDefinition: { + /** + * @description APIVersion defines the versioned schema of this representation of an object. + * Servers should convert recognized schemas to the latest internal value, and + * may reject unrecognized values. + * More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + */ + apiVersion?: string + /** + * @description Kind is a string value representing the REST resource this object represents. + * Servers may infer this from the endpoint the client submits requests to. + * Cannot be updated. + * In CamelCase. + * More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + */ + kind?: string + metadata?: { + name?: string + namespace?: string + /** Format: uuid */ + uid?: string + resourceVersion?: string + /** Format: date-time */ + creationTimestamp?: string + /** Format: date-time */ + deletionTimestamp?: string + labels?: { + [key: string]: string + } + annotations?: { + [key: string]: string + } + } + /** @description PluginDefinitionSpec defines the desired state of PluginDefinitionSpec */ + spec?: { + /** @description Description provides additional details of the pluginDefinition. */ + description?: string + /** @description DisplayName provides a human-readable label for the pluginDefinition. */ + displayName?: string + /** + * @description DocMarkDownUrl specifies the URL to the markdown documentation file for this plugin. + * Source needs to allow all CORS origins. + */ + docMarkDownUrl?: string + /** @description HelmChart specifies where the Helm Chart for this pluginDefinition can be found. */ + helmChart?: { + /** @description Name of the HelmChart chart. */ + name: string + /** @description Repository of the HelmChart chart. */ + repository: string + /** @description Version of the HelmChart chart. */ + version: string + } + /** + * @description Icon specifies the icon to be used for this plugin in the Greenhouse UI. + * Icons can be either: + * - A string representing a juno icon in camel case from this list: https://github.com/sapcc/juno/blob/main/libs/juno-ui-components/src/components/Icon/Icon.component.js#L6-L52 + * - A publicly accessible image reference to a .png file. Will be displayed 100x100px + */ + icon?: string + /** @description RequiredValues is a list of values required to create an instance of this PluginDefinition. */ + options?: { + /** @description Default provides a default value for the option */ + default?: unknown + /** @description Description provides a human-readable text for the value as shown in the UI. */ + description?: string + /** @description DisplayName provides a human-readable label for the configuration option */ + displayName?: string + /** @description Name/Key of the config option. */ + name: string + /** @description Regex specifies a match rule for validating configuration options. */ + regex?: string + /** @description Required indicates that this config option is required */ + required: boolean + /** + * @description Type of this configuration option. + * @enum {string} + */ + type: "string" | "secret" | "bool" | "int" | "list" | "map" + }[] + /** @description UIApplication specifies a reference to a UI application */ + uiApplication?: { + /** @description Name of the UI application. */ + name: string + /** + * @description URL specifies the url to a built javascript asset. + * By default, assets are loaded from the Juno asset server using the provided name and version. + */ + url?: string + /** @description Version of the frontend application. */ + version: string + } + /** @description Version of this pluginDefinition */ + version: string + /** + * Format: int32 + * @description Weight configures the order in which Plugins are shown in the Greenhouse UI. + * Defaults to alphabetical sorting if not provided or on conflict. + */ + weight?: number + } + /** @description PluginDefinitionStatus defines the observed state of PluginDefinition */ + status?: { + /** @description StatusConditions contain the different conditions that constitute the status of the Plugin. */ + statusConditions?: { + conditions?: { + /** + * Format: date-time + * @description LastTransitionTime is the last time the condition transitioned from one status to another. + */ + lastTransitionTime: string + /** @description Message is an optional human-readable message indicating details about the last transition. */ + message?: string + /** @description Reason is a one-word, CamelCase reason for the condition's last transition. */ + reason?: string + /** @description Status of the condition. */ + status: string + /** @description Type of the condition. */ + type: string + }[] + } + } + } + /** + * PluginPreset + * @description PluginPreset is the Schema for the PluginPresets API + */ + PluginPreset: { + /** + * @description APIVersion defines the versioned schema of this representation of an object. + * Servers should convert recognized schemas to the latest internal value, and + * may reject unrecognized values. + * More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + */ + apiVersion?: string + /** + * @description Kind is a string value representing the REST resource this object represents. + * Servers may infer this from the endpoint the client submits requests to. + * Cannot be updated. + * In CamelCase. + * More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + */ + kind?: string + metadata?: { + name?: string + namespace?: string + /** Format: uuid */ + uid?: string + resourceVersion?: string + /** Format: date-time */ + creationTimestamp?: string + /** Format: date-time */ + deletionTimestamp?: string + labels?: { + [key: string]: string + } + annotations?: { + [key: string]: string + } + } + /** @description PluginPresetSpec defines the desired state of PluginPreset */ + spec?: { + /** @description ClusterOptionOverrides define plugin option values to override by the PluginPreset */ + clusterOptionOverrides?: { + clusterName: string + overrides: { + /** @description Name of the values. */ + name: string + /** + * @description Template is a Go string template that will be dynamically resolved for cluster-specific values. + * Only PluginOptionValues declared as template will be templated by the PluginController for Flux. + */ + template?: string + /** @description Value is the actual value in plain text. */ + value?: unknown + /** @description ValueFrom references a potentially confidential value in another source. */ + valueFrom?: { + /** @description Secret references the secret containing the value. */ + secret?: { + /** @description Key in the secret to select the value from. */ + key: string + /** @description Name of the secret in the same namespace. */ + name: string + } + } + }[] + }[] + /** @description ClusterSelector is a label selector to select the clusters the plugin bundle should be deployed to. */ + clusterSelector: { + /** @description matchExpressions is a list of label selector requirements. The requirements are ANDed. */ + matchExpressions?: { + /** @description key is the label key that the selector applies to. */ + key: string + /** + * @description operator represents a key's relationship to a set of values. + * Valid operators are In, NotIn, Exists and DoesNotExist. + */ + operator: string + /** + * @description values is an array of string values. If the operator is In or NotIn, + * the values array must be non-empty. If the operator is Exists or DoesNotExist, + * the values array must be empty. This array is replaced during a strategic + * merge patch. + */ + values?: string[] + }[] + /** + * @description matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + * map is equivalent to an element of matchExpressions, whose key field is "key", the + * operator is "In", and the values array contains only "value". The requirements are ANDed. + */ + matchLabels?: { + [key: string]: string + } + } + /** + * @description DeletionPolicy defines how Plugins owned by a PluginPreset are handled on deletion of the PluginPreset. + * Supported values are "Delete" and "Retain". If not set, defaults to "Delete". + * @default Delete + * @enum {string} + */ + deletionPolicy: "Delete" | "Retain" + /** @description PluginSpec is the spec of the plugin to be deployed by the PluginPreset. */ + plugin: { + /** @description ClusterName is the name of the cluster the plugin is deployed to. If not set, the plugin is deployed to the greenhouse cluster. */ + clusterName?: string + /** + * @description DeletionPolicy defines how Helm Releases created by a Plugin are handled upon deletion of the Plugin. + * Supported values are "Delete" and "Retain". If not set, defaults to "Delete". + * @default Delete + * @enum {string} + */ + deletionPolicy: "Delete" | "Retain" + /** + * @description DisplayName is an optional name for the Plugin to be displayed in the Greenhouse UI. + * This is especially helpful to distinguish multiple instances of a PluginDefinition in the same context. + * Defaults to a normalized version of metadata.name. + */ + displayName?: string + /** @description IgnoreDifferences defines paths to ignore when detecting drift between desired and actual state. */ + ignoreDifferences?: { + /** @description Group matches the APIVersion group of the resources to ignore. */ + group?: string + /** @description Kind matches the Kind of the resources to ignore. */ + kind?: string + /** @description Name matches the name of the resources to ignore. */ + name?: string + /** @description Paths is a list of JSON paths to ignore when detecting drifts. */ + paths: string[] + /** @description Version matches the APIVersion version of the resources to ignore. */ + version?: string + }[] + /** @description Values are the values for a PluginDefinition instance. */ + optionValues?: { + /** @description Name of the values. */ + name: string + /** + * @description Template is a Go string template that will be dynamically resolved for cluster-specific values. + * Only PluginOptionValues declared as template will be templated by the PluginController for Flux. + */ + template?: string + /** @description Value is the actual value in plain text. */ + value?: unknown + /** @description ValueFrom references a potentially confidential value in another source. */ + valueFrom?: { + /** @description Secret references the secret containing the value. */ + secret?: { + /** @description Key in the secret to select the value from. */ + key: string + /** @description Name of the secret in the same namespace. */ + name: string + } + } + }[] + /** + * @description PluginDefinition is the name of the PluginDefinition this instance is for. + * + * Deprecated: Use PluginDefinitionRef instead. Future releases of greenhouse will remove this field. + */ + pluginDefinition: string + /** @description PluginDefinitionRef is the reference to the (Cluster-)PluginDefinition. */ + pluginDefinitionRef: { + /** + * @description Kind of the referent. Supported values: PluginDefinition, ClusterPluginDefinition. + * @enum {string} + */ + kind?: "PluginDefinition" | "ClusterPluginDefinition" + /** @description Name of the referenced PluginDefinition or ClusterPluginDefinition resource. */ + name?: string + } + /** + * @description ReleaseName is the name of the helm release in the remote cluster to which the backend is deployed. + * If the Plugin was already deployed, the Plugin's name is used as the release name. + * If this Plugin is newly created, the releaseName is defaulted to the PluginDefinitions HelmChart name. + */ + releaseName?: string + /** + * @description ReleaseNamespace is the namespace in the remote cluster to which the backend is deployed. + * Defaults to the Greenhouse managed namespace if not set. + */ + releaseNamespace?: string + /** @description WaitFor defines other Plugins to wait for before installing this Plugin. */ + waitFor?: { + /** @description PluginRef defines a reference to the Plugin. */ + pluginRef: { + /** @description Name of the Plugin. */ + name?: string + /** @description PluginPreset is the name of the PluginPreset which creates the Plugin. */ + pluginPreset?: string + } + }[] + } + /** @description WaitFor defines other Plugins to wait for before creating the Plugin. */ + waitFor?: { + /** @description PluginRef defines a reference to the Plugin. */ + pluginRef: { + /** @description Name of the Plugin. */ + name?: string + /** @description PluginPreset is the name of the PluginPreset which creates the Plugin. */ + pluginPreset?: string + } + }[] + } + /** @description PluginPresetStatus defines the observed state of PluginPreset */ + status?: { + /** @description FailedPlugins is the number of failed Plugins managed by the PluginPreset. */ + failedPlugins?: number + /** @description PluginStatuses contains statuses of Plugins managed by the PluginPreset. */ + pluginStatuses?: { + pluginName?: string + /** @description Condition contains additional information on the state of a resource. */ + readyCondition?: { + /** + * Format: date-time + * @description LastTransitionTime is the last time the condition transitioned from one status to another. + */ + lastTransitionTime: string + /** @description Message is an optional human-readable message indicating details about the last transition. */ + message?: string + /** @description Reason is a one-word, CamelCase reason for the condition's last transition. */ + reason?: string + /** @description Status of the condition. */ + status: string + /** @description Type of the condition. */ + type: string + } + }[] + /** @description ReadyPlugins is the number of ready Plugins managed by the PluginPreset. */ + readyPlugins?: number + /** @description StatusConditions contain the different conditions that constitute the status of the PluginPreset. */ + statusConditions?: { + conditions?: { + /** + * Format: date-time + * @description LastTransitionTime is the last time the condition transitioned from one status to another. + */ + lastTransitionTime: string + /** @description Message is an optional human-readable message indicating details about the last transition. */ + message?: string + /** @description Reason is a one-word, CamelCase reason for the condition's last transition. */ + reason?: string + /** @description Status of the condition. */ + status: string + /** @description Type of the condition. */ + type: string + }[] + } + /** @description TotalPlugins is the number of Plugins in total managed by the PluginPreset. */ + totalPlugins?: number + } + } + /** + * Plugin + * @description Plugin is the Schema for the plugins API + */ + Plugin: { + /** + * @description APIVersion defines the versioned schema of this representation of an object. + * Servers should convert recognized schemas to the latest internal value, and + * may reject unrecognized values. + * More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + */ + apiVersion?: string + /** + * @description Kind is a string value representing the REST resource this object represents. + * Servers may infer this from the endpoint the client submits requests to. + * Cannot be updated. + * In CamelCase. + * More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + */ + kind?: string + metadata?: { + name?: string + namespace?: string + /** Format: uuid */ + uid?: string + resourceVersion?: string + /** Format: date-time */ + creationTimestamp?: string + /** Format: date-time */ + deletionTimestamp?: string + labels?: { + [key: string]: string + } + annotations?: { + [key: string]: string + } + } + /** @description PluginSpec defines the desired state of Plugin */ + spec?: { + /** @description ClusterName is the name of the cluster the plugin is deployed to. If not set, the plugin is deployed to the greenhouse cluster. */ + clusterName?: string + /** + * @description DeletionPolicy defines how Helm Releases created by a Plugin are handled upon deletion of the Plugin. + * Supported values are "Delete" and "Retain". If not set, defaults to "Delete". + * @default Delete + * @enum {string} + */ + deletionPolicy: "Delete" | "Retain" + /** + * @description DisplayName is an optional name for the Plugin to be displayed in the Greenhouse UI. + * This is especially helpful to distinguish multiple instances of a PluginDefinition in the same context. + * Defaults to a normalized version of metadata.name. + */ + displayName?: string + /** @description IgnoreDifferences defines paths to ignore when detecting drift between desired and actual state. */ + ignoreDifferences?: { + /** @description Group matches the APIVersion group of the resources to ignore. */ + group?: string + /** @description Kind matches the Kind of the resources to ignore. */ + kind?: string + /** @description Name matches the name of the resources to ignore. */ + name?: string + /** @description Paths is a list of JSON paths to ignore when detecting drifts. */ + paths: string[] + /** @description Version matches the APIVersion version of the resources to ignore. */ + version?: string + }[] + /** @description Values are the values for a PluginDefinition instance. */ + optionValues?: { + /** @description Name of the values. */ + name: string + /** + * @description Template is a Go string template that will be dynamically resolved for cluster-specific values. + * Only PluginOptionValues declared as template will be templated by the PluginController for Flux. + */ + template?: string + /** @description Value is the actual value in plain text. */ + value?: unknown + /** @description ValueFrom references a potentially confidential value in another source. */ + valueFrom?: { + /** @description Secret references the secret containing the value. */ + secret?: { + /** @description Key in the secret to select the value from. */ + key: string + /** @description Name of the secret in the same namespace. */ + name: string + } + } + }[] + /** + * @description PluginDefinition is the name of the PluginDefinition this instance is for. + * + * Deprecated: Use PluginDefinitionRef instead. Future releases of greenhouse will remove this field. + */ + pluginDefinition: string + /** @description PluginDefinitionRef is the reference to the (Cluster-)PluginDefinition. */ + pluginDefinitionRef: { + /** + * @description Kind of the referent. Supported values: PluginDefinition, ClusterPluginDefinition. + * @enum {string} + */ + kind?: "PluginDefinition" | "ClusterPluginDefinition" + /** @description Name of the referenced PluginDefinition or ClusterPluginDefinition resource. */ + name?: string + } + /** + * @description ReleaseName is the name of the helm release in the remote cluster to which the backend is deployed. + * If the Plugin was already deployed, the Plugin's name is used as the release name. + * If this Plugin is newly created, the releaseName is defaulted to the PluginDefinitions HelmChart name. + */ + releaseName?: string + /** + * @description ReleaseNamespace is the namespace in the remote cluster to which the backend is deployed. + * Defaults to the Greenhouse managed namespace if not set. + */ + releaseNamespace?: string + /** @description WaitFor defines other Plugins to wait for before installing this Plugin. */ + waitFor?: { + /** @description PluginRef defines a reference to the Plugin. */ + pluginRef: { + /** @description Name of the Plugin. */ + name?: string + /** @description PluginPreset is the name of the PluginPreset which creates the Plugin. */ + pluginPreset?: string + } + }[] + } + /** @description PluginStatus defines the observed state of Plugin */ + status?: { + /** @description Description provides additional details of the plugin. */ + description?: string + /** + * @description ExposedServices provides an overview of the Plugins services that are centrally exposed. + * It maps the exposed URL to the service found in the manifest. + */ + exposedServices?: { + [key: string]: { + /** @description Name is the name of the service in the target cluster. */ + name: string + /** @description Namespace is the namespace of the service in the target cluster. */ + namespace: string + /** + * Format: int32 + * @description Port is the port of the service. Zero for ingresses where port is not applicable. + */ + port?: number + /** @description Protocol is the protocol of the service. */ + protocol?: string + /** + * @description Type is the type of exposed service. + * @default service + * @enum {string} + */ + type: "service" | "ingress" + } + } + /** @description HelmChart contains a reference the helm chart used for the deployed pluginDefinition version. */ + helmChart?: { + /** @description Name of the HelmChart chart. */ + name: string + /** @description Repository of the HelmChart chart. */ + repository: string + /** @description Version of the HelmChart chart. */ + version: string + } + /** + * @description HelmReleaseStatus reflects the status of the latest HelmChart release. + * This is only configured if the pluginDefinition is backed by HelmChart. + */ + helmReleaseStatus?: { + /** @description Diff contains the difference between the deployed helm chart and the helm chart in the last reconciliation */ + diff?: string + /** + * Format: date-time + * @description FirstDeployed is the timestamp of the first deployment of the release. + */ + firstDeployed?: string + /** + * Format: date-time + * @description LastDeployed is the timestamp of the last deployment of the release. + */ + lastDeployed?: string + /** @description PluginOptionChecksum is the checksum of plugin option values. */ + pluginOptionChecksum?: string + /** @description Status is the status of a HelmChart release. */ + status: string + } + /** @description LastReconciledAt contains the value when the reconcile was last triggered via annotation. */ + lastReconciledAt?: string + /** @description StatusConditions contain the different conditions that constitute the status of the Plugin. */ + statusConditions?: { + conditions?: { + /** + * Format: date-time + * @description LastTransitionTime is the last time the condition transitioned from one status to another. + */ + lastTransitionTime: string + /** @description Message is an optional human-readable message indicating details about the last transition. */ + message?: string + /** @description Reason is a one-word, CamelCase reason for the condition's last transition. */ + reason?: string + /** @description Status of the condition. */ + status: string + /** @description Type of the condition. */ + type: string + }[] + } + /** @description UIApplication contains a reference to the frontend that is used for the deployed pluginDefinition version. */ + uiApplication?: { + /** @description Name of the UI application. */ + name: string + /** + * @description URL specifies the url to a built javascript asset. + * By default, assets are loaded from the Juno asset server using the provided name and version. + */ + url?: string + /** @description Version of the frontend application. */ + version: string + } + /** @description Version contains the latest pluginDefinition version the config was last applied with successfully. */ + version?: string + /** + * Format: int32 + * @description Weight configures the order in which Plugins are shown in the Greenhouse UI. + */ + weight?: number + } + } + /** + * TeamRoleBinding + * @description TeamRoleBinding is the Schema for the rolebindings API + */ + TeamRoleBinding: { + /** + * @description APIVersion defines the versioned schema of this representation of an object. + * Servers should convert recognized schemas to the latest internal value, and + * may reject unrecognized values. + * More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + */ + apiVersion?: string + /** + * @description Kind is a string value representing the REST resource this object represents. + * Servers may infer this from the endpoint the client submits requests to. + * Cannot be updated. + * In CamelCase. + * More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + */ + kind?: string + metadata?: { + name?: string + namespace?: string + /** Format: uuid */ + uid?: string + resourceVersion?: string + /** Format: date-time */ + creationTimestamp?: string + /** Format: date-time */ + deletionTimestamp?: string + labels?: { + [key: string]: string + } + annotations?: { + [key: string]: string + } + } + /** @description TeamRoleBindingSpec defines the desired state of a TeamRoleBinding */ + spec?: { + /** @description ClusterSelector is used to select a Cluster or Clusters the TeamRoleBinding should be deployed to. */ + clusterSelector?: { + /** @description Name of a single Cluster to select. */ + clusterName?: string + /** @description LabelSelector is a label query over a set of Clusters. */ + labelSelector?: { + /** @description matchExpressions is a list of label selector requirements. The requirements are ANDed. */ + matchExpressions?: { + /** @description key is the label key that the selector applies to. */ + key: string + /** + * @description operator represents a key's relationship to a set of values. + * Valid operators are In, NotIn, Exists and DoesNotExist. + */ + operator: string + /** + * @description values is an array of string values. If the operator is In or NotIn, + * the values array must be non-empty. If the operator is Exists or DoesNotExist, + * the values array must be empty. This array is replaced during a strategic + * merge patch. + */ + values?: string[] + }[] + /** + * @description matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + * map is equivalent to an element of matchExpressions, whose key field is "key", the + * operator is "In", and the values array contains only "value". The requirements are ANDed. + */ + matchLabels?: { + [key: string]: string + } + } + } + /** + * @description CreateNamespaces when enabled the controller will create namespaces for RoleBindings if they do not exist. + * @default false + */ + createNamespaces: boolean + /** + * @description Namespaces is a list of namespaces in the Greenhouse Clusters to apply the RoleBinding to. + * If empty, a ClusterRoleBinding will be created on the remote cluster, otherwise a RoleBinding per namespace. + */ + namespaces?: string[] + /** @description TeamRef references a Greenhouse Team by name */ + teamRef?: string + /** @description TeamRoleRef references a Greenhouse TeamRole by name */ + teamRoleRef?: string + /** @description Usernames defines list of users to add to the (Cluster-)RoleBindings */ + usernames?: string[] + } + /** @description TeamRoleBindingStatus defines the observed state of the TeamRoleBinding */ + status?: { + /** @description PropagationStatus is the list of clusters the TeamRoleBinding is applied to */ + clusters?: { + /** @description ClusterName is the name of the cluster the rbacv1 resources are created on. */ + clusterName: string + /** @description Condition is the overall Status of the rbacv1 resources created on the cluster */ + condition?: { + /** + * Format: date-time + * @description LastTransitionTime is the last time the condition transitioned from one status to another. + */ + lastTransitionTime: string + /** @description Message is an optional human-readable message indicating details about the last transition. */ + message?: string + /** @description Reason is a one-word, CamelCase reason for the condition's last transition. */ + reason?: string + /** @description Status of the condition. */ + status: string + /** @description Type of the condition. */ + type: string + } + }[] + /** @description StatusConditions contain the different conditions that constitute the status of the TeamRoleBinding. */ + statusConditions?: { + conditions?: { + /** + * Format: date-time + * @description LastTransitionTime is the last time the condition transitioned from one status to another. + */ + lastTransitionTime: string + /** @description Message is an optional human-readable message indicating details about the last transition. */ + message?: string + /** @description Reason is a one-word, CamelCase reason for the condition's last transition. */ + reason?: string + /** @description Status of the condition. */ + status: string + /** @description Type of the condition. */ + type: string + }[] + } + } + } + /** + * TeamRole + * @description TeamRole is the Schema for the TeamRoles API + */ + TeamRole: { + /** + * @description APIVersion defines the versioned schema of this representation of an object. + * Servers should convert recognized schemas to the latest internal value, and + * may reject unrecognized values. + * More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + */ + apiVersion?: string + /** + * @description Kind is a string value representing the REST resource this object represents. + * Servers may infer this from the endpoint the client submits requests to. + * Cannot be updated. + * In CamelCase. + * More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + */ + kind?: string + metadata?: { + name?: string + namespace?: string + /** Format: uuid */ + uid?: string + resourceVersion?: string + /** Format: date-time */ + creationTimestamp?: string + /** Format: date-time */ + deletionTimestamp?: string + labels?: { + [key: string]: string + } + annotations?: { + [key: string]: string + } + } + /** @description TeamRoleSpec defines the desired state of a TeamRole */ + spec?: { + /** @description AggregationRule describes how to locate ClusterRoles to aggregate into the ClusterRole on the remote cluster */ + aggregationRule?: { + /** + * @description ClusterRoleSelectors holds a list of selectors which will be used to find ClusterRoles and create the rules. + * If any of the selectors match, then the ClusterRole's permissions will be added + */ + clusterRoleSelectors?: { + /** @description matchExpressions is a list of label selector requirements. The requirements are ANDed. */ + matchExpressions?: { + /** @description key is the label key that the selector applies to. */ + key: string + /** + * @description operator represents a key's relationship to a set of values. + * Valid operators are In, NotIn, Exists and DoesNotExist. + */ + operator: string + /** + * @description values is an array of string values. If the operator is In or NotIn, + * the values array must be non-empty. If the operator is Exists or DoesNotExist, + * the values array must be empty. This array is replaced during a strategic + * merge patch. + */ + values?: string[] + }[] + /** + * @description matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + * map is equivalent to an element of matchExpressions, whose key field is "key", the + * operator is "In", and the values array contains only "value". The requirements are ANDed. + */ + matchLabels?: { + [key: string]: string + } + }[] + } + /** + * @description Labels are applied to the ClusterRole created on the remote cluster. + * This allows using TeamRoles as part of AggregationRules by other TeamRoles + */ + labels?: { + [key: string]: string + } + /** @description Rules is a list of rbacv1.PolicyRules used on a managed RBAC (Cluster)Role */ + rules?: { + /** + * @description APIGroups is the name of the APIGroup that contains the resources. If multiple API groups are specified, any action requested against one of + * the enumerated resources in any API group will be allowed. "" represents the core API group and "*" represents all API groups. + */ + apiGroups?: string[] + /** + * @description NonResourceURLs is a set of partial urls that a user should have access to. *s are allowed, but only as the full, final step in the path + * Since non-resource URLs are not namespaced, this field is only applicable for ClusterRoles referenced from a ClusterRoleBinding. + * Rules can either apply to API resources (such as "pods" or "secrets") or non-resource URL paths (such as "/api"), but not both. + */ + nonResourceURLs?: string[] + /** @description ResourceNames is an optional white list of names that the rule applies to. An empty set means that everything is allowed. */ + resourceNames?: string[] + /** @description Resources is a list of resources this rule applies to. '*' represents all resources. */ + resources?: string[] + /** @description Verbs is a list of Verbs that apply to ALL the ResourceKinds contained in this rule. '*' represents all verbs. */ + verbs: string[] + }[] + } + /** @description TeamRoleStatus defines the observed state of a TeamRole */ + status?: Record + } + /** + * Team + * @description Team is the Schema for the teams API + */ + Team: { + /** + * @description APIVersion defines the versioned schema of this representation of an object. + * Servers should convert recognized schemas to the latest internal value, and + * may reject unrecognized values. + * More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + */ + apiVersion?: string + /** + * @description Kind is a string value representing the REST resource this object represents. + * Servers may infer this from the endpoint the client submits requests to. + * Cannot be updated. + * In CamelCase. + * More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + */ + kind?: string + metadata?: { + name?: string + namespace?: string + /** Format: uuid */ + uid?: string + resourceVersion?: string + /** Format: date-time */ + creationTimestamp?: string + /** Format: date-time */ + deletionTimestamp?: string + labels?: { + [key: string]: string + } + annotations?: { + [key: string]: string + } + } + /** @description TeamSpec defines the desired state of Team */ + spec?: { + /** @description Description provides additional details of the team. */ + description?: string + /** @description URL to join the IdP group. */ + joinUrl?: string + /** @description IdP group id matching team. */ + mappedIdPGroup?: string + } + /** @description TeamStatus defines the observed state of Team */ + status?: { + members?: { + /** @description Email of the user. */ + email: string + /** @description FirstName of the user. */ + firstName: string + /** @description ID is the unique identifier of the user. */ + id: string + /** @description LastName of the user. */ + lastName: string + }[] + /** + * @description A StatusConditions contains a list of conditions. + * Only one condition of a given type may exist in the list. + */ + statusConditions: { + conditions?: { + /** + * Format: date-time + * @description LastTransitionTime is the last time the condition transitioned from one status to another. + */ + lastTransitionTime: string + /** @description Message is an optional human-readable message indicating details about the last transition. */ + message?: string + /** @description Reason is a one-word, CamelCase reason for the condition's last transition. */ + reason?: string + /** @description Status of the condition. */ + status: string + /** @description Type of the condition. */ + type: string + }[] + } + } + } + } + responses: never + parameters: never + requestBodies: never + headers: never + pathItems: never +} +export type $defs = Record +export type operations = Record diff --git a/apps/greenhouse/src/routes/__root.tsx b/apps/greenhouse/src/routes/__root.tsx index 4ac9c63f60..b1538c5e48 100644 --- a/apps/greenhouse/src/routes/__root.tsx +++ b/apps/greenhouse/src/routes/__root.tsx @@ -6,6 +6,7 @@ import React, { useEffect } from "react" import { Outlet, createRootRouteWithContext } from "@tanstack/react-router" import { useActions } from "@cloudoperators/juno-messages-provider" +import type { createClient } from "@cloudoperators/juno-k8s-client" import { AppProps } from "../Shell" import ShellLayout from "../components/layout/ShellLayout" import useApi from "../hooks/useApi" @@ -13,6 +14,8 @@ import { usePlugin } from "../components/StoreProvider" export type RouteContext = { appProps: AppProps + apiClient: ReturnType | null + organization: string } export const Route = createRootRouteWithContext()({ diff --git a/apps/greenhouse/src/routes/admin/index.tsx b/apps/greenhouse/src/routes/admin/index.tsx index af27dcf659..edc72b23fb 100644 --- a/apps/greenhouse/src/routes/admin/index.tsx +++ b/apps/greenhouse/src/routes/admin/index.tsx @@ -9,7 +9,7 @@ export const Route = createFileRoute("/admin/")({ beforeLoad: () => { // eslint-disable-next-line @typescript-eslint/only-throw-error throw redirect({ - to: "/admin/clusters", + to: "/admin/plugin-presets", search: (prev) => ({ ...prev }), }) }, diff --git a/apps/greenhouse/src/routes/admin/plugin-presets.tsx b/apps/greenhouse/src/routes/admin/plugin-presets.tsx index b63c95eabf..3df4c4ce39 100644 --- a/apps/greenhouse/src/routes/admin/plugin-presets.tsx +++ b/apps/greenhouse/src/routes/admin/plugin-presets.tsx @@ -3,22 +3,37 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from "react" import { createFileRoute } from "@tanstack/react-router" +import { z } from "zod" import { Crumb } from "../-types" +import { PluginPresets } from "../../components/admin/PluginPresets" +import { fetchPluginPresets } from "../../components/admin/api/plugin-presets" + +const searchParamsSchema = z.object({ + searchTerm: z.string().optional(), +}) export const Route = createFileRoute("/admin/plugin-presets")({ - component: RouteComponent, - loader: async () => { + component: PluginPresets, + validateSearch: (search: Record) => { + return searchParamsSchema.parse(search) + }, + loaderDeps: (search) => ({ + ...search, + }), + loader: async ({ context: { apiClient, organization }, deps: { search } }) => { const crumb: Crumb = { label: "Plugin Presets", } + const pluginPresetsPromise = fetchPluginPresets(apiClient, organization) + const filterSettings = { + searchTerm: search.searchTerm, + } + return { crumb, + pluginPresetsPromise: pluginPresetsPromise, + filterSettings, } }, }) - -function RouteComponent() { - return
Hello "/admin/plugins-presets"!
-} diff --git a/apps/greenhouse/src/routes/admin/route.tsx b/apps/greenhouse/src/routes/admin/route.tsx index 18cea9a413..8da15a7322 100644 --- a/apps/greenhouse/src/routes/admin/route.tsx +++ b/apps/greenhouse/src/routes/admin/route.tsx @@ -3,13 +3,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -import React from "react" -import { createFileRoute, Outlet } from "@tanstack/react-router" -import { Layout as AdminLayout } from "../../components/admin/Layout" +import { createFileRoute } from "@tanstack/react-router" +import { Layout } from "../../components/admin/Layout" import { Crumb } from "../-types" export const Route = createFileRoute("/admin")({ - component: RouteComponent, + component: Layout, + errorComponent: Layout, loader: () => { const crumb: Crumb = { label: "Admin", @@ -20,11 +20,3 @@ export const Route = createFileRoute("/admin")({ } }, }) - -function RouteComponent() { - return ( - - - - ) -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e84495133a..cf0ee142fa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -373,6 +373,9 @@ importers: react-dom: specifier: 19.2.1 version: 19.2.1(react@19.2.1) + react-error-boundary: + specifier: 6.0.0 + version: 6.0.0(react@19.2.1) react-markdown: specifier: 10.1.0 version: 10.1.0(@types/react@19.2.7)(react@19.2.1) @@ -766,10 +769,10 @@ importers: version: 7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.81.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) vite-plugin-dts: specifier: 4.5.4 - version: 4.5.4(@types/node@24.10.1)(rollup@4.50.1)(typescript@5.9.2)(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.81.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)) + version: 4.5.4(@types/node@24.10.1)(rollup@4.50.1)(typescript@5.9.3)(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.81.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)) vitest: specifier: 3.2.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(msw@2.12.4(@types/node@24.10.1)(typescript@5.9.2))(sass@1.81.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(msw@2.12.4(@types/node@24.10.1)(typescript@5.9.3))(sass@1.81.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) packages/messages-provider: dependencies: @@ -915,7 +918,7 @@ importers: version: 4.7.0(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.81.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)) eslint-plugin-storybook: specifier: 9.1.16 - version: 9.1.16(eslint@9.35.0(jiti@2.6.1))(storybook@9.1.16(@testing-library/dom@10.4.0)(msw@2.12.4(@types/node@24.10.1)(typescript@5.9.3))(prettier@3.6.2)(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.81.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)))(typescript@5.9.3) + version: 9.1.16(eslint@9.39.1(jiti@2.6.1))(storybook@9.1.16(@testing-library/dom@10.4.0)(msw@2.12.4(@types/node@24.10.1)(typescript@5.9.3))(prettier@3.6.2)(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.81.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)))(typescript@5.9.3) flatpickr: specifier: 4.6.13 version: 4.6.13 @@ -3461,8 +3464,8 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.46.2': - resolution: {integrity: sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==} + '@typescript-eslint/project-service@8.46.4': + resolution: {integrity: sha512-nPiRSKuvtTN+no/2N1kt2tUh/HoFzeEgOm9fQ6XQk4/ApGqjx0zFIIaLJ6wooR1HIoozvj2j6vTi/1fgAz7UYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' @@ -3477,20 +3480,14 @@ packages: resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==} engines: {node: ^16.0.0 || >=18.0.0} - '@typescript-eslint/scope-manager@8.46.2': - resolution: {integrity: sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==} + '@typescript-eslint/scope-manager@8.46.4': + resolution: {integrity: sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript-eslint/scope-manager@8.47.0': resolution: {integrity: sha512-a0TTJk4HXMkfpFkL9/WaGTNuv7JWfFTQFJd6zS9dVAjKsojmv9HT55xzbEpnZoY+VUb+YXLMp+ihMLz/UlZfDg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.46.2': - resolution: {integrity: sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/tsconfig-utils@8.46.4': resolution: {integrity: sha512-+/XqaZPIAk6Cjg7NWgSGe27X4zMGqrFqZ8atJsX3CWxH/jACqWnrWI68h7nHQld0y+k9eTTjb9r+KU4twLoo9A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3524,10 +3521,6 @@ packages: resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} engines: {node: ^16.0.0 || >=18.0.0} - '@typescript-eslint/types@8.46.2': - resolution: {integrity: sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/types@8.46.4': resolution: {integrity: sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3545,8 +3538,8 @@ packages: typescript: optional: true - '@typescript-eslint/typescript-estree@8.46.2': - resolution: {integrity: sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==} + '@typescript-eslint/typescript-estree@8.46.4': + resolution: {integrity: sha512-7oV2qEOr1d4NWNmpXLR35LvCfOkTNymY9oyW+lUHkmCno7aOmIf/hMaydnJBUTBMRCOGZh8YjkFOc8dadEoNGA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' @@ -3563,8 +3556,8 @@ packages: peerDependencies: eslint: ^7.0.0 || ^8.0.0 - '@typescript-eslint/utils@8.46.2': - resolution: {integrity: sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==} + '@typescript-eslint/utils@8.46.4': + resolution: {integrity: sha512-AbSv11fklGXV6T28dp2Me04Uw90R2iJ30g2bgLz529Koehrmkbs1r7paFqr1vPCZi7hHwYxYtxfyQMRC8QaVSg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -3581,8 +3574,8 @@ packages: resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} engines: {node: ^16.0.0 || >=18.0.0} - '@typescript-eslint/visitor-keys@8.46.2': - resolution: {integrity: sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==} + '@typescript-eslint/visitor-keys@8.46.4': + resolution: {integrity: sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript-eslint/visitor-keys@8.47.0': @@ -10433,10 +10426,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.46.2(typescript@5.9.3)': + '@typescript-eslint/project-service@8.46.4(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.46.4(typescript@5.9.3) - '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/tsconfig-utils': 8.47.0(typescript@5.9.3) + '@typescript-eslint/types': 8.47.0 debug: 4.4.1 typescript: 5.9.3 transitivePeerDependencies: @@ -10456,20 +10449,16 @@ snapshots: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - '@typescript-eslint/scope-manager@8.46.2': + '@typescript-eslint/scope-manager@8.46.4': dependencies: - '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/visitor-keys': 8.46.2 + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/visitor-keys': 8.46.4 '@typescript-eslint/scope-manager@8.47.0': dependencies: '@typescript-eslint/types': 8.47.0 '@typescript-eslint/visitor-keys': 8.47.0 - '@typescript-eslint/tsconfig-utils@8.46.2(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 - '@typescript-eslint/tsconfig-utils@8.46.4(typescript@5.9.3)': dependencies: typescript: 5.9.3 @@ -10504,8 +10493,6 @@ snapshots: '@typescript-eslint/types@6.21.0': {} - '@typescript-eslint/types@8.46.2': {} - '@typescript-eslint/types@8.46.4': {} '@typescript-eslint/types@8.47.0': {} @@ -10525,12 +10512,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.46.2(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.46.4(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.46.2(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.46.2(typescript@5.9.3) - '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/visitor-keys': 8.46.2 + '@typescript-eslint/project-service': 8.46.4(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.46.4(typescript@5.9.3) + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/visitor-keys': 8.46.4 debug: 4.4.1 fast-glob: 3.3.3 is-glob: 4.0.3 @@ -10571,13 +10558,13 @@ snapshots: - supports-color - typescript - '@typescript-eslint/utils@8.46.2(eslint@9.35.0(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.35.0(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.46.2 - '@typescript-eslint/types': 8.46.2 - '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3) - eslint: 9.35.0(jiti@2.6.1) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.46.4 + '@typescript-eslint/types': 8.46.4 + '@typescript-eslint/typescript-estree': 8.46.4(typescript@5.9.3) + eslint: 9.39.1(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -10598,9 +10585,9 @@ snapshots: '@typescript-eslint/types': 6.21.0 eslint-visitor-keys: 3.4.3 - '@typescript-eslint/visitor-keys@8.46.2': + '@typescript-eslint/visitor-keys@8.46.4': dependencies: - '@typescript-eslint/types': 8.46.2 + '@typescript-eslint/types': 8.46.4 eslint-visitor-keys: 4.2.1 '@typescript-eslint/visitor-keys@8.47.0': @@ -10638,15 +10625,6 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(msw@2.12.4(@types/node@24.10.1)(typescript@5.9.2))(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.81.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0))': - dependencies: - '@vitest/spy': 3.2.4 - estree-walker: 3.0.3 - magic-string: 0.30.19 - optionalDependencies: - msw: 2.12.4(@types/node@24.10.1)(typescript@5.9.2) - vite: 7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.81.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) - '@vitest/mocker@3.2.4(msw@2.12.4(@types/node@24.10.1)(typescript@5.9.3))(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.81.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.2.4 @@ -10723,19 +10701,6 @@ snapshots: de-indent: 1.0.2 he: 1.2.0 - '@vue/language-core@2.2.0(typescript@5.9.2)': - dependencies: - '@volar/language-core': 2.4.14 - '@vue/compiler-dom': 3.5.14 - '@vue/compiler-vue2': 2.7.16 - '@vue/shared': 3.5.14 - alien-signals: 0.4.14 - minimatch: 9.0.5 - muggle-string: 0.4.1 - path-browserify: 1.0.1 - optionalDependencies: - typescript: 5.9.2 - '@vue/language-core@2.2.0(typescript@5.9.3)': dependencies: '@volar/language-core': 2.4.14 @@ -11815,10 +11780,10 @@ snapshots: string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 - eslint-plugin-storybook@9.1.16(eslint@9.35.0(jiti@2.6.1))(storybook@9.1.16(@testing-library/dom@10.4.0)(msw@2.12.4(@types/node@24.10.1)(typescript@5.9.3))(prettier@3.6.2)(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.81.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)))(typescript@5.9.3): + eslint-plugin-storybook@9.1.16(eslint@9.39.1(jiti@2.6.1))(storybook@9.1.16(@testing-library/dom@10.4.0)(msw@2.12.4(@types/node@24.10.1)(typescript@5.9.3))(prettier@3.6.2)(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.81.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)))(typescript@5.9.3): dependencies: - '@typescript-eslint/utils': 8.46.2(eslint@9.35.0(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.35.0(jiti@2.6.1) + '@typescript-eslint/utils': 8.46.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.1(jiti@2.6.1) storybook: 9.1.16(@testing-library/dom@10.4.0)(msw@2.12.4(@types/node@24.10.1)(typescript@5.9.3))(prettier@3.6.2)(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.81.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)) transitivePeerDependencies: - supports-color @@ -13344,32 +13309,6 @@ snapshots: ms@2.1.3: {} - msw@2.12.4(@types/node@24.10.1)(typescript@5.9.2): - dependencies: - '@inquirer/confirm': 5.1.10(@types/node@24.10.1) - '@mswjs/interceptors': 0.40.0 - '@open-draft/deferred-promise': 2.2.0 - '@types/statuses': 2.0.6 - cookie: 1.0.2 - graphql: 16.12.0 - headers-polyfill: 4.0.3 - is-node-process: 1.2.0 - outvariant: 1.4.3 - path-to-regexp: 6.3.0 - picocolors: 1.1.1 - rettime: 0.7.0 - statuses: 2.0.2 - strict-event-emitter: 0.5.1 - tough-cookie: 6.0.0 - type-fest: 5.3.0 - until-async: 3.0.2 - yargs: 17.7.2 - optionalDependencies: - typescript: 5.9.2 - transitivePeerDependencies: - - '@types/node' - optional: true - msw@2.12.4(@types/node@24.10.1)(typescript@5.9.3): dependencies: '@inquirer/confirm': 5.1.10(@types/node@24.10.1) @@ -14806,25 +14745,6 @@ snapshots: - tsx - yaml - vite-plugin-dts@4.5.4(@types/node@24.10.1)(rollup@4.50.1)(typescript@5.9.2)(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.81.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)): - dependencies: - '@microsoft/api-extractor': 7.52.8(@types/node@24.10.1) - '@rollup/pluginutils': 5.3.0(rollup@4.50.1) - '@volar/typescript': 2.4.14 - '@vue/language-core': 2.2.0(typescript@5.9.2) - compare-versions: 6.1.1 - debug: 4.4.1 - kolorist: 1.8.0 - local-pkg: 1.1.1 - magic-string: 0.30.19 - typescript: 5.9.2 - optionalDependencies: - vite: 7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.81.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) - transitivePeerDependencies: - - '@types/node' - - rollup - - supports-color - vite-plugin-dts@4.5.4(@types/node@24.10.1)(rollup@4.50.1)(typescript@5.9.3)(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.81.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)): dependencies: '@microsoft/api-extractor': 7.52.8(@types/node@24.10.1) @@ -14892,50 +14812,6 @@ snapshots: tsx: 4.19.4 yaml: 2.8.0 - vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(msw@2.12.4(@types/node@24.10.1)(typescript@5.9.2))(sass@1.81.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0): - dependencies: - '@types/chai': 5.2.2 - '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(msw@2.12.4(@types/node@24.10.1)(typescript@5.9.2))(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.81.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0)) - '@vitest/pretty-format': 3.2.4 - '@vitest/runner': 3.2.4 - '@vitest/snapshot': 3.2.4 - '@vitest/spy': 3.2.4 - '@vitest/utils': 3.2.4 - chai: 5.2.0 - debug: 4.4.1 - expect-type: 1.2.1 - magic-string: 0.30.19 - pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 3.9.0 - tinybench: 2.9.0 - tinyexec: 0.3.2 - tinyglobby: 0.2.15 - tinypool: 1.1.1 - tinyrainbow: 2.0.0 - vite: 7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.81.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) - vite-node: 3.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.81.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/debug': 4.1.12 - '@types/node': 24.10.1 - '@vitest/ui': 3.2.4(vitest@3.2.4) - jsdom: 26.1.0 - transitivePeerDependencies: - - jiti - - less - - lightningcss - - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(msw@2.12.4(@types/node@24.10.1)(typescript@5.9.3))(sass@1.81.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.0): dependencies: '@types/chai': 5.2.2