From 10099b33a8a3feb73c8ec1b3a1f91536e3fc5c39 Mon Sep 17 00:00:00 2001 From: I531348 Date: Mon, 27 Apr 2026 08:49:20 +0200 Subject: [PATCH 01/12] clusters dashboard Signed-off-by: I531348 --- .../Overview/Conditions.test.tsx | 41 ++++++ .../ClusterDetail/Overview/Conditions.tsx | 20 +++ .../ClusterDetail/Overview/Details.test.tsx | 36 +++++ .../admin/ClusterDetail/Overview/Details.tsx | 96 ++++++++++++ .../Overview/PluginInstances.test.tsx | 76 ++++++++++ .../Overview/PluginInstances.tsx | 126 ++++++++++++++++ .../admin/ClusterDetail/Overview/index.tsx | 31 ++++ .../admin/ClusterDetail/index.test.tsx | 77 ++++++++++ .../components/admin/ClusterDetail/index.tsx | 75 ++++++++++ .../admin/Clusters/ClustersFilters.tsx | 122 ++++++++++++++++ .../Clusters/ClustersGrid/DataRows/index.tsx | 88 +++++++++++ .../Clusters/ClustersGrid/index.test.tsx | 101 +++++++++++++ .../admin/Clusters/ClustersGrid/index.tsx | 47 ++++++ .../src/components/admin/Clusters/index.tsx | 68 +++++++++ .../admin/Layout/Breadcrumb.test.tsx | 2 +- .../components/admin/Layout/Navigation.tsx | 4 + .../admin/api/clusters/fetchCluster.ts | 18 +++ .../admin/api/clusters/fetchClusters.ts | 83 +++++++++++ .../api/clusters/fetchClustersFilters.ts | 40 +++++ .../api/plugins/fetchPluginsByCluster.ts | 26 ++++ .../src/components/admin/constants.ts | 5 +- .../src/components/admin/types/k8sTypes.ts | 3 +- apps/greenhouse/src/components/admin/utils.ts | 26 +++- apps/greenhouse/src/routeTree.gen.ts | 138 +++++++++++++++--- .../admin/clusters/$clusterName/index.tsx | 11 ++ .../admin/clusters/$clusterName/route.tsx | 15 ++ .../src/routes/admin/clusters/index.tsx | 70 +++++++++ .../src/routes/admin/clusters/route.tsx | 16 ++ .../admin/{clusters.tsx => oldclusters.tsx} | 6 +- 29 files changed, 1443 insertions(+), 24 deletions(-) create mode 100644 apps/greenhouse/src/components/admin/ClusterDetail/Overview/Conditions.test.tsx create mode 100644 apps/greenhouse/src/components/admin/ClusterDetail/Overview/Conditions.tsx create mode 100644 apps/greenhouse/src/components/admin/ClusterDetail/Overview/Details.test.tsx create mode 100644 apps/greenhouse/src/components/admin/ClusterDetail/Overview/Details.tsx create mode 100644 apps/greenhouse/src/components/admin/ClusterDetail/Overview/PluginInstances.test.tsx create mode 100644 apps/greenhouse/src/components/admin/ClusterDetail/Overview/PluginInstances.tsx create mode 100644 apps/greenhouse/src/components/admin/ClusterDetail/Overview/index.tsx create mode 100644 apps/greenhouse/src/components/admin/ClusterDetail/index.test.tsx create mode 100644 apps/greenhouse/src/components/admin/ClusterDetail/index.tsx create mode 100644 apps/greenhouse/src/components/admin/Clusters/ClustersFilters.tsx create mode 100644 apps/greenhouse/src/components/admin/Clusters/ClustersGrid/DataRows/index.tsx create mode 100644 apps/greenhouse/src/components/admin/Clusters/ClustersGrid/index.test.tsx create mode 100644 apps/greenhouse/src/components/admin/Clusters/ClustersGrid/index.tsx create mode 100644 apps/greenhouse/src/components/admin/Clusters/index.tsx create mode 100644 apps/greenhouse/src/components/admin/api/clusters/fetchCluster.ts create mode 100644 apps/greenhouse/src/components/admin/api/clusters/fetchClusters.ts create mode 100644 apps/greenhouse/src/components/admin/api/clusters/fetchClustersFilters.ts create mode 100644 apps/greenhouse/src/components/admin/api/plugins/fetchPluginsByCluster.ts create mode 100644 apps/greenhouse/src/routes/admin/clusters/$clusterName/index.tsx create mode 100644 apps/greenhouse/src/routes/admin/clusters/$clusterName/route.tsx create mode 100644 apps/greenhouse/src/routes/admin/clusters/index.tsx create mode 100644 apps/greenhouse/src/routes/admin/clusters/route.tsx rename apps/greenhouse/src/routes/admin/{clusters.tsx => oldclusters.tsx} (89%) diff --git a/apps/greenhouse/src/components/admin/ClusterDetail/Overview/Conditions.test.tsx b/apps/greenhouse/src/components/admin/ClusterDetail/Overview/Conditions.test.tsx new file mode 100644 index 0000000000..bb07962958 --- /dev/null +++ b/apps/greenhouse/src/components/admin/ClusterDetail/Overview/Conditions.test.tsx @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from "react" +import { render, screen } from "@testing-library/react" +import { Conditions } from "./Conditions" +import { PluginPreset } from "../../types/k8sTypes" + +describe("Conditions", () => { + it("should render conditions heading and readiness badges", () => { + const mockPluginPreset: PluginPreset = { + metadata: { name: "test-preset" }, + spec: { + deletionPolicy: "Delete", + plugin: { + pluginDefinitionRef: { name: "test-plugin" }, + deletionPolicy: "Delete", + pluginDefinition: "test-plugin", + }, + }, + status: { + statusConditions: { + conditions: [ + { + type: "Ready", + status: "True", + lastTransitionTime: "2026-01-01T00:00:00Z", + }, + ], + }, + }, + } + + render() + + expect(screen.getByText("Conditions")).toBeInTheDocument() + expect(screen.getByText("Ready")).toBeInTheDocument() + }) +}) diff --git a/apps/greenhouse/src/components/admin/ClusterDetail/Overview/Conditions.tsx b/apps/greenhouse/src/components/admin/ClusterDetail/Overview/Conditions.tsx new file mode 100644 index 0000000000..588ce308d7 --- /dev/null +++ b/apps/greenhouse/src/components/admin/ClusterDetail/Overview/Conditions.tsx @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from "react" +import { Stack, ContentHeading } from "@cloudoperators/juno-ui-components" +import { PluginPreset } from "../../types/k8sTypes" +import ReadinessConditions from "../../common/ReadinessConditions" + +type ConditionsProps = { + pluginPreset: PluginPreset +} + +export const Conditions: React.FC = ({ pluginPreset }) => ( + + Conditions + + +) diff --git a/apps/greenhouse/src/components/admin/ClusterDetail/Overview/Details.test.tsx b/apps/greenhouse/src/components/admin/ClusterDetail/Overview/Details.test.tsx new file mode 100644 index 0000000000..3bc33479cf --- /dev/null +++ b/apps/greenhouse/src/components/admin/ClusterDetail/Overview/Details.test.tsx @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from "react" +import { render, screen } from "@testing-library/react" +import { Details } from "./Details" +import { PluginPreset } from "../../types/k8sTypes" + +describe("Details", () => { + it("should render plugin preset details", () => { + const mockPluginPreset: PluginPreset = { + metadata: { + name: "test-preset", + labels: { + "greenhouse.sap/owned-by": "test-team", + }, + }, + spec: { + deletionPolicy: "Delete", + plugin: { + pluginDefinitionRef: { name: "test-plugin-def" }, + deletionPolicy: "Delete", + pluginDefinition: "test-plugin-def", + }, + }, + } + + render(
) + + expect(screen.getByText("Details")).toBeInTheDocument() + expect(screen.getByText("test-preset")).toBeInTheDocument() + expect(screen.getByText("test-plugin-def")).toBeInTheDocument() + }) +}) diff --git a/apps/greenhouse/src/components/admin/ClusterDetail/Overview/Details.tsx b/apps/greenhouse/src/components/admin/ClusterDetail/Overview/Details.tsx new file mode 100644 index 0000000000..9927ef71c2 --- /dev/null +++ b/apps/greenhouse/src/components/admin/ClusterDetail/Overview/Details.tsx @@ -0,0 +1,96 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from "react" +import { + DataGrid, + DataGridRow, + DataGridHeadCell, + DataGridCell, + Pill, + Stack, + ContentHeading, +} from "@cloudoperators/juno-ui-components" +import { PluginPreset } from "../../types/k8sTypes" +import { CONNECTIVITY_GROUP_LABEL, NO_VALUE_DEFAULT } from "../../constants" +import { formatAge } from "../../utils" + +interface DetailsProps { + pluginPreset: PluginPreset +} + +const NodeStatus = ({ nodes }) => { + const ready = nodes?.ready || 0 + const total = nodes?.total || 0 + + // Determine the color class based on readiness + const statusColor = ready === total ? "text-theme-success" : "text-theme-warning" + + return ( +
+ {ready}/{total} nodes ready +
+ ) +} + +export const Details: React.FC = ({ pluginPreset }) => ( + + Details + + + Name + {pluginPreset.metadata?.name ?? NO_VALUE_DEFAULT} + + + Age + {formatAge(pluginPreset.metadata?.creationTimestamp || 0) ?? NO_VALUE_DEFAULT} + + + Version + {pluginPreset.status?.kubernetesVersion ?? NO_VALUE_DEFAULT} + + + Connectivity + {pluginPreset.metadata?.annotations?.[CONNECTIVITY_GROUP_LABEL]} + + + Support Group + {pluginPreset.metadata?.labels?.["greenhouse.sap/owned-by"] ?? NO_VALUE_DEFAULT} + + {pluginPreset.metadata?.labels && Object.keys(pluginPreset.metadata.labels).length > 0 && ( + + Labels + + + {Object.entries(pluginPreset.metadata.labels).map(([key, value]) => ( + + ))} + + + + )} + + Annotations + + {pluginPreset.metadata?.annotations && Object.keys(pluginPreset.metadata.annotations).length > 0 ? ( + + {Object.entries(pluginPreset.metadata.annotations).map(([key, value]) => ( + + ))} + + ) : ( + "No Annotations" + )} + + + + Node Status + + + + + + +) diff --git a/apps/greenhouse/src/components/admin/ClusterDetail/Overview/PluginInstances.test.tsx b/apps/greenhouse/src/components/admin/ClusterDetail/Overview/PluginInstances.test.tsx new file mode 100644 index 0000000000..6c889e10c8 --- /dev/null +++ b/apps/greenhouse/src/components/admin/ClusterDetail/Overview/PluginInstances.test.tsx @@ -0,0 +1,76 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { act } from "react" +import { + createMemoryHistory, + createRootRoute, + createRoute, + createRouter, + Outlet, + RouterProvider, +} from "@tanstack/react-router" +import { render, screen } from "@testing-library/react" +import { QueryClient, QueryClientProvider } from "@tanstack/react-query" +import { PluginInstances } from "./PluginInstances" +import { mockPlugins, MockPluginsResponse } from "../../__mocks__/plugins" + +const renderComponent = async (mockPromise: Promise) => { + const rootRoute = createRootRoute({ + component: () => , + }) + const testRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/admin/plugin-presets/$pluginPresetName", + component: () => ( + + + + ), + }) + const routeTree = rootRoute.addChildren([testRoute]) + const router = createRouter({ + routeTree: routeTree, + defaultPendingMinMs: 0, + context: { + apiClient: { + get() { + return mockPromise + }, + }, + user: { + organization: "test-org", + supportGroups: [], + }, + }, + history: createMemoryHistory({ + initialEntries: ["/admin/plugin-presets/test-preset"], + }), + }) + return await act(async () => render()) +} + +describe("PluginInstances", () => { + it("should render plugin instances table", async () => { + await renderComponent(new Promise((resolve) => resolve(mockPlugins))) + + expect(screen.getByText("Plugin Instances")).toBeInTheDocument() + expect(screen.getByText("Plugin Name")).toBeInTheDocument() + expect(screen.getByText("Cluster")).toBeInTheDocument() + expect(screen.getByText("Status")).toBeInTheDocument() + expect(screen.getByText("plugin-1")).toBeInTheDocument() + expect(screen.getByText("plugin-2")).toBeInTheDocument() + }) +}) diff --git a/apps/greenhouse/src/components/admin/ClusterDetail/Overview/PluginInstances.tsx b/apps/greenhouse/src/components/admin/ClusterDetail/Overview/PluginInstances.tsx new file mode 100644 index 0000000000..1a0dc77dc0 --- /dev/null +++ b/apps/greenhouse/src/components/admin/ClusterDetail/Overview/PluginInstances.tsx @@ -0,0 +1,126 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { Suspense } from "react" +import { useParams, useRouteContext, useNavigate } from "@tanstack/react-router" +import { useSuspenseQuery } from "@tanstack/react-query" +import { + DataGrid, + DataGridRow, + DataGridHeadCell, + DataGridCell, + Icon, + Stack, + ContentHeading, + PopupMenu, + PopupMenuOptions, + PopupMenuItem, +} from "@cloudoperators/juno-ui-components" +import { LoadingDataRow } from "../../common/LoadingDataRow" +import { ErrorBoundary } from "../../common/ErrorBoundary" +import { getErrorDataRowComponent } from "../../common/getErrorDataRow" +import { Plugin } from "../../types/k8sTypes" +import { FETCH_PLUGINS_BY_CLUSTER_CACHE_KEY, fetchPluginsByCluster } from "../../api/plugins/fetchPluginsByCluster" + +const isPluginReady = (plugin: Plugin) => { + return plugin.status?.statusConditions?.conditions?.some((c) => c.type === "Ready" && c.status === "True") ?? false +} + +const COLUMN_SPAN = 4 + +const DataRows = ({ colSpan, plugins }: { colSpan: number; plugins: Plugin[] }) => { + const navigate = useNavigate({ from: "/admin/clusters/$clusterName" }) + + if (plugins.length === 0) { + return ( + + No plugin instances found for this plugin preset. + + ) + } + + return ( + <> + {plugins.map((plugin) => { + const ready = isPluginReady(plugin) + const navigateToDetails = () => { + navigate({ + to: "/admin/plugin-presets/$pluginPresetName/plugin-instances/$pluginInstance", + params: { + pluginPresetName: plugin.metadata?.ownerReferences?.[0].name, + pluginInstance: plugin.metadata?.name || "", + }, + }) + } + + return ( + + + + + {plugin.metadata?.name} + {ready ? "Ready" : "Not Ready"} + + e.stopPropagation()}> + + + + + + + ) + })} + + ) +} + +export const PluginInstances = () => { + const { clusterName } = useParams({ from: "/admin/clusters/$clusterName" }) + const { apiClient, user } = useRouteContext({ from: "/admin/clusters/$clusterName" }) + + const { data: plugins } = useSuspenseQuery({ + queryKey: [FETCH_PLUGINS_BY_CLUSTER_CACHE_KEY, user.organization, clusterName], + queryFn: () => fetchPluginsByCluster({ apiClient, namespace: user.organization, clusterName }), + }) + + const total = plugins?.length ?? 0 + const ready = plugins?.filter(isPluginReady).length ?? 0 + const notReady = total - ready + + return ( + + + Plugin Instances + + + +
+ {`${total} plugin instances`} + {`(${ready} ready, ${notReady} not ready)`} +
+
+ + + + + + Status + + + Plugin Name + Status + + + + + }> + + + + +
+
+ ) +} diff --git a/apps/greenhouse/src/components/admin/ClusterDetail/Overview/index.tsx b/apps/greenhouse/src/components/admin/ClusterDetail/Overview/index.tsx new file mode 100644 index 0000000000..e32a5799f4 --- /dev/null +++ b/apps/greenhouse/src/components/admin/ClusterDetail/Overview/index.tsx @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React from "react" +import { Container } from "@cloudoperators/juno-ui-components" +import { Details } from "./Details" +import { PluginPreset } from "../../types/k8sTypes" +import { PluginInstances } from "./PluginInstances" +import { Conditions } from "./Conditions" + +const Section = ({ children, ...rest }: React.HTMLAttributes) => ( + + {children} + +) + +export const Overview = ({ pluginPreset }: { pluginPreset: PluginPreset }) => ( + <> +
+ +
+
+
+
+
+ +
+ +) diff --git a/apps/greenhouse/src/components/admin/ClusterDetail/index.test.tsx b/apps/greenhouse/src/components/admin/ClusterDetail/index.test.tsx new file mode 100644 index 0000000000..03fcd1bd31 --- /dev/null +++ b/apps/greenhouse/src/components/admin/ClusterDetail/index.test.tsx @@ -0,0 +1,77 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { act } from "react" +import { + createMemoryHistory, + createRootRoute, + createRoute, + createRouter, + Outlet, + RouterProvider, +} from "@tanstack/react-router" +import { render, screen } from "@testing-library/react" +import { QueryClient, QueryClientProvider } from "@tanstack/react-query" +import { MessagesProvider } from "@cloudoperators/juno-messages-provider" +import { ClusterDetail } from "./index" +import { mockPluginPresets } from "../__mocks__/pluginPresets" + +const renderComponent = async (mockPromise: Promise) => { + const rootRoute = createRootRoute({ + component: () => , + }) + const testRoute = createRoute({ + getParentRoute: () => rootRoute, + path: "/admin/plugin-presets/$pluginPresetName", + component: () => ( + + + + + + ), + }) + const routeTree = rootRoute.addChildren([testRoute]) + const router = createRouter({ + routeTree: routeTree, + defaultPendingMinMs: 0, + context: { + apiClient: { + get() { + return mockPromise + }, + }, + user: { + organization: "test-org", + supportGroups: [], + }, + }, + history: createMemoryHistory({ + initialEntries: ["/admin/plugin-presets/preset-1"], + }), + }) + return await act(async () => render()) +} + +describe("PluginPresetDetail", () => { + it("should render plugin preset detail with tabs", async () => { + const mockPreset = mockPluginPresets.items[0] + await renderComponent(new Promise((resolve) => resolve(mockPreset))) + + expect(screen.getByRole("heading", { name: "preset-1" })).toBeInTheDocument() + expect(await screen.findByText("Overview")).toBeInTheDocument() + expect(screen.getByText("YAML")).toBeInTheDocument() + }) +}) diff --git a/apps/greenhouse/src/components/admin/ClusterDetail/index.tsx b/apps/greenhouse/src/components/admin/ClusterDetail/index.tsx new file mode 100644 index 0000000000..0d3f831c31 --- /dev/null +++ b/apps/greenhouse/src/components/admin/ClusterDetail/index.tsx @@ -0,0 +1,75 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useCallback } from "react" +import { useParams, useRouteContext } from "@tanstack/react-router" +import { useQuery, useQueryClient } from "@tanstack/react-query" +import { Container, Tabs, TabList, Tab, TabPanel, Stack, ContentHeading } from "@cloudoperators/juno-ui-components" +import { fetchCluster, FETCH_CLUSTER_CACHE_KEY } from "../api/clusters/fetchCluster" +import { Overview } from "./Overview" +import { PluginPreset } from "../types/k8sTypes" +import { ErrorMessage } from "../common/ErrorBoundary/ErrorMessage" +import YamlViewer from "../common/YamlViewer" +import { ReconcileButton } from "../common/ReconcileButton" +import { FETCH_CLUSTERS_CACHE_KEY } from "../api/clusters/fetchClusters" + +const PluginPresetDetailContent = ({ cluster }: { cluster: PluginPreset }) => ( + + + + + + + + + + + + + + + + +) + +export const ClusterDetail = () => { + const { clusterName } = useParams({ from: "/admin/clusters/$clusterName" }) + const { apiClient, user } = useRouteContext({ from: "/admin/clusters/$clusterName" }) + const queryClient = useQueryClient() + + const { + data: cluster, + isLoading, + error, + } = useQuery({ + queryKey: [FETCH_CLUSTER_CACHE_KEY, user.organization, clusterName], + queryFn: () => fetchCluster({ apiClient, namespace: user.organization, clusterName }), + }) + + const handleReconcile = useCallback(() => { + // Invalidate and refetch the plugin preset + queryClient.invalidateQueries({ queryKey: [FETCH_CLUSTERS_CACHE_KEY, user.organization, clusterName] }) + }, [queryClient, user.organization, clusterName]) + + return ( + + + + {clusterName} + + + +

Cluster configuration and instance status

+ {isLoading &&

Loading...

} + {!isLoading && error && } + {!isLoading && !error && cluster && } +
+ ) +} diff --git a/apps/greenhouse/src/components/admin/Clusters/ClustersFilters.tsx b/apps/greenhouse/src/components/admin/Clusters/ClustersFilters.tsx new file mode 100644 index 0000000000..d3a5b1e659 --- /dev/null +++ b/apps/greenhouse/src/components/admin/Clusters/ClustersFilters.tsx @@ -0,0 +1,122 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP SE or an SAP affiliate company and Juno contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import React, { useCallback } from "react" +import { useLoaderData, useNavigate, useRouteContext } from "@tanstack/react-router" +import { FilterSettings, SelectedFilter } from "../common/types" +import { getFiltersForUrl } from "../utils" +import { SELECTED_FILTER_PREFIX } from "../constants" +import { Stack, InputGroup, Button, SearchInput } from "@cloudoperators/juno-ui-components/index" +import { SelectedFilters } from "../common/SelectedFilters" +import { useQuery } from "@tanstack/react-query" +import { FilterSelect } from "../common/FilterSelect" +import { FETCH_CLUSTERS_FILTERS_CACHE_KEY, fetchClustersFilters } from "../api/clusters/fetchClustersFilters" + +export const PluginPresetsFilters = () => { + const navigate = useNavigate() + const { apiClient, user } = useRouteContext({ from: "/admin/clusters" }) + const { filterSettings } = useLoaderData({ from: "/admin/clusters/" }) + const { + data: filters, + isLoading, + error, + } = useQuery({ + queryKey: [FETCH_CLUSTERS_FILTERS_CACHE_KEY, user.organization], + queryFn: () => + fetchClustersFilters({ + apiClient, + namespace: user.organization, + }), + }) + + const updateFilters = useCallback( + (updatedFilterSettings: FilterSettings) => { + navigate({ + to: "/admin/clusters", + search: (prev) => { + const newFilterParams = getFiltersForUrl(updatedFilterSettings) + const cleanedPrev = Object.fromEntries( + Object.entries(prev).filter(([key]) => !key.startsWith(SELECTED_FILTER_PREFIX)) + ) + return { + ...cleanedPrev, + ...newFilterParams, + } + }, + }) + }, + [navigate] + ) + + const handleFilterDelete = useCallback( + (filterToRemove: SelectedFilter) => { + updateFilters({ + ...filterSettings, + selectedFilters: filterSettings.selectedFilters?.filter( + (filter) => !(filter.id === filterToRemove.id && filter.value === filterToRemove.value) + ), + }) + }, + [filterSettings, updateFilters] + ) + + return ( + + + + { + const filterExists = filterSettings.selectedFilters?.some( + (filter) => filter.id === selectedFilter.id && filter.value === selectedFilter.value + ) + //only add the filter if it does not already exist + if (!filterExists) { + updateFilters({ + ...filterSettings, + selectedFilters: [...(filterSettings.selectedFilters || []), selectedFilter], + }) + } + }} + /> + +