Repository: cloudoperators/juno
Pull request #809: feat(greenhouse): pluginAdmin filtering
Author: login: TilmanHaupt
Created: 2025-03-11T15:31:33Z | Merged: 2025-03-14T13:36:58Z

# Summary

Closes #754 

1. The list is sorted in ascending order.
2. Search and regex apply to the entire pluginConfigItem element.
3. Filter by status.
4. Filter by label values, which are parsed.

# Testing Instructions

<!-- Describe the steps needed to test this pull request. -->

Filter & search in ORG > PLUGINS

# Checklist

<!-- Ensure that your pull request meets the following requirements. -->

- [x] I have performed a self-review of my code.
- [x] I have commented my code, particularly in hard-to-understand areas.
- [x] I have added tests that prove my fix is effective or that my feature works.
- [x] New and existing unit tests pass locally with my changes.
- [ ] I have made corresponding changes to the documentation (if applicable).
- [x] My changes generate no new warnings or errors.
- [x] I have created a changeset for my changes.

# PR Manifesto

Review the [PR Manifesto](https://github.com/cloudoperators/juno/blob/main/docs/pr_manifesto.md) for best practises.


Linked issue #754: [Feature](greenhouse-org-admin): Filtering for Plugin List
**Description**
We would like to add the possibility to filter the Plugin list within the greenhouse org-admin Area

**Problem Statement**
Currently we have an unsorted, unfilterable list of Plugins within the org-admin area. With increasing number of deployed plugins it becomes more and more unusable. E.g. I like to highlight `clusterNames` via the browser search to be able to focus on a set of Plugins

**Proposed Solution**
Add sorting and filtering options to the list.
Filter by:
- `Plugin.Spec.ClusterName`
- `Plugin.Spec.PluginDefinition`
- `Plugin.Status.StatusConditions.Ready = true/false/unknown`
- `Plugin.Metadata.Label[key] = [value]`

Sort by:
- `Plugin.Spec.DisplayName` (default)
- `Plugin.Spec.ClusterName` then `Plugin.Spec.DisplayName`



**Additional Context**
The `Plugin` list screen is going to be replaced by a list/view only showing `PluginPresets`. So this should just be a quick solution. Maybe *sorting* is superflous if default sorting by `displayName` is implemented.

Linked issue comments: user: login: uwe-mayer
created_at: 2025-03-04T10:06:35Z
body: Updated Ticket description with ask for filtering by labels...
🙏 
author_association: CONTRIBUTOR
user: login: TilmanHaupt
created_at: 2025-03-06T16:17:57Z
body: @uwe-mayer Regarding: Plugin.Metadata.Label[key] = [value] - is it needed to connect more then one label - like in supernova?  Is there something how i can get these labels? I mean i could parse them all from all the items, but that seems quite long lasting and not very efficient if there are a huge amount of configs. A Alternative could be AppProps and a Helmchart - what do you think?


author_association: CONTRIBUTOR

Diff:
diff --git a/.changeset/dull-geckos-own.md b/.changeset/dull-geckos-own.md
new file mode 100644
index 0000000000..2727947267
--- /dev/null
+++ b/.changeset/dull-geckos-own.md
@@ -0,0 +1,5 @@
+---
+"@cloudoperators/juno-app-greenhouse": minor
+---
+
+Plugin admin can now filter pluginConfigurationItems
diff --git a/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugindefinitions/components/PluginDefinitionPanel.tsx b/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugindefinitions/components/PluginDefinitionPanel.tsx
index 8271f99cb4..1c955b22cf 100644
--- a/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugindefinitions/components/PluginDefinitionPanel.tsx
+++ b/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugindefinitions/components/PluginDefinitionPanel.tsx
@@ -32,7 +32,6 @@ const PluginDefinitionPanel = () => {
 
   return (
     <Panel
-      // @ts-expect-error TS(2550): Property 'includes' does not exist on type 'string... Remove this comment to see the full error message
       opened={["showPluginDefinition", "showPluginDefinitionDetail", "editPlugin"].includes(panel)}
       onClose={onCloseDefinitionPanel}
       size="large"
diff --git a/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/components/PluginDetail.tsx b/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/components/PluginDetail.tsx
index cb1df4c055..c30c9b2147 100644
--- a/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/components/PluginDetail.tsx
+++ b/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/components/PluginDetail.tsx
@@ -28,9 +28,7 @@ import { buildExternalServicesUrls } from "./buildExternalServicesUrls"
 // Renders the plugin details panel
 const PluginDetail = () => {
   const pluginConfig = usePluginConfig()
-  // @ts-expect-error TS(2339): Property 'setShowDetailsFor' does not exist on type 'never'.
   const { setShowDetailsFor } = usePluginActions()
-  // @ts-expect-error TS(2339): Property 'setPanel' does not exist on type 'never'.
   const { setPanel } = useGlobalsActions()
   const panel = usePanel()
   const showDetailsFor = useShowDetailsFor()
@@ -40,7 +38,6 @@ const PluginDetail = () => {
     if (!showDetailsFor || !pluginConfig) {
       return
     }
-    // @ts-expect-error TS(2339): Property 'setPlugin' does not exist on type 'never'.
     setPlugin(pluginConfig.find((p: any) => p.metadata.uid === showDetailsFor))
   }, [showDetailsFor, pluginConfig])
 
diff --git a/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/components/PluginList.tsx b/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/components/PluginList.tsx
index b7bb99c08f..30b1a3b80f 100644
--- a/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/components/PluginList.tsx
+++ b/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/components/PluginList.tsx
@@ -1,5 +1,5 @@
 /*
- * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors
+ * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
  * SPDX-License-Identifier: Apache-2.0
  */
 
@@ -9,19 +9,17 @@ import {
   DataGridHeadCell,
   DataGridRow,
   DataGridCell,
-  DataGridToolbar,
   Button,
-  ButtonRow,
   Icon,
   Stack,
 } from "@cloudoperators/juno-ui-components"
-import { usePluginConfig, useGlobalsActions } from "./StoreProvider"
+import { useFilteredPluginConfigs, useGlobalsActions } from "./StoreProvider"
 import Plugin from "./Plugin"
+import Toolbar from "./Toolbar"
 
 // Renders the list of plugins
 const PluginList = () => {
-  const pluginConfig = usePluginConfig()
-  // @ts-ignore
+  const pluginConfig = useFilteredPluginConfigs()
   const { setPanel } = useGlobalsActions()
 
   const onShowPluginDefinition = () => {
@@ -30,11 +28,12 @@ const PluginList = () => {
 
   return (
     <>
-      <DataGridToolbar>
-        <ButtonRow>
+      <Stack className="mb-4 mt-4">
+        <div className="ml-auto">
           <Button onClick={onShowPluginDefinition}>Add Plugin</Button>
-        </ButtonRow>
-      </DataGridToolbar>
+        </div>
+      </Stack>
+      <Toolbar />
       <DataGrid columns={5} cellVerticalAlignment="top" className="plugins" minContentColumns={[0, 4]}>
         {pluginConfig && (
           <DataGridRow>
@@ -47,27 +46,23 @@ const PluginList = () => {
             <DataGridHeadCell>Edit</DataGridHeadCell>
           </DataGridRow>
         )}
-        {
-          // @ts-ignore
-          pluginConfig?.length > 0 ? (
-            // @ts-ignore
-            pluginConfig?.map((plugin: any) => {
-              return <Plugin key={plugin?.metadata?.uid} plugin={plugin} />
-            })
-          ) : (
-            <DataGridRow className="no-hover">
-              <DataGridCell colSpan={4}>
-                <Stack gap="3">
-                  <Icon icon="info" color="text-theme-info" />
-                  <div>
-                    It seems that no plugins have been created yet. Do you want to
-                    <a onClick={onShowPluginDefinition}> create</a> a new one?
-                  </div>
-                </Stack>
-              </DataGridCell>
-            </DataGridRow>
-          )
-        }
+        {pluginConfig?.length > 0 ? (
+          pluginConfig?.map((plugin: any) => {
+            return <Plugin key={plugin?.metadata?.uid} plugin={plugin} />
+          })
+        ) : (
+          <DataGridRow className="no-hover">
+            <DataGridCell colSpan={4}>
+              <Stack gap="3">
+                <Icon icon="info" color="text-theme-info" />
+                <div>
+                  It seems that no plugins have been created yet. Do you want to
+                  <a onClick={onShowPluginDefinition}> create</a> a new one?
+                </div>
+              </Stack>
+            </DataGridCell>
+          </DataGridRow>
+        )}
       </DataGrid>
     </>
   )
diff --git a/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/components/StoreProvider.tsx b/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/components/StoreProvider.tsx
index 64fce64a08..a1bf0fc3a4 100644
--- a/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/components/StoreProvider.tsx
+++ b/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/components/StoreProvider.tsx
@@ -1,21 +1,42 @@
 /*
- * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors
+ * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
  * SPDX-License-Identifier: Apache-2.0
  */
 
 import React, { createContext, useContext } from "react"
+import { createStore, useStore, StoreApi } from "zustand"
 
-import { useStore as create } from "zustand"
-import createStore from "../lib/store"
+import createPluginSlice, { PluginSlice } from "../lib/store/createPluginSlice"
+import createGlobalSlice, { GlobalsSlice } from "../lib/store/createGlobalsSlice"
 
-// @ts-expect-error TS(2554): Expected 1 arguments, but got 0.
-const StoreContext = createContext()
-const StoreProvider = ({ children }: any) => (
-  <StoreContext.Provider value={createStore()}>{children}</StoreContext.Provider>
-)
-// @ts-ignore
-const useAppStore = (selector: any) => create(useContext(StoreContext), selector)
+const StoreContext = createContext<StoreApi<AppState> | null>(null)
 
+export type AppState = PluginSlice & GlobalsSlice
+
+interface StoreProviderProps {
+  children: React.ReactNode
+}
+
+export const StoreProvider = ({ children }: StoreProviderProps) => {
+  return (
+    <StoreContext.Provider
+      value={createStore<AppState>((set, get, store) => ({
+        ...createPluginSlice(set, get, store),
+        ...createGlobalSlice(set, get, store),
+      }))}
+    >
+      {children}
+    </StoreContext.Provider>
+  )
+}
+
+const useAppStore = <T,>(selector: (state: AppState) => T): T => {
+  const store = useContext(StoreContext)
+  if (!store) {
+    throw new Error("useAppStore must be used within a StoreProvider")
+  }
+  return useStore(store, selector)
+}
 export const useGlobalsUrlStateKey = () => useAppStore((state: any) => state.globals.urlStateKey)
 export const useApiEndpoint = () => useAppStore((state: any) => state.globals.apiEndpoint)
 export const useNamespace = () => useAppStore((state: any) => state.globals.namespace)
@@ -24,9 +45,13 @@ export const useShowDefinitionPanel = () => useAppStore((state: any) => state.gl
 export const usePanel = () => useAppStore((state: any) => state.globals.panel)
 export const useGlobalsActions = () => useAppStore((state: any) => state.globals.actions)
 
-/// Plugin
+// Plugin
 export const usePluginConfig = () => useAppStore((state: any) => state.plugin.pluginConfig)
+export const useFilteredPluginConfigs = () => useAppStore((state: any) => state.plugin.filteredPluginConfigs)
 export const useShowDetailsFor = () => useAppStore((state: any) => state.plugin.showDetailsFor)
+export const useSearchTerm = () => useAppStore((state: any) => state.plugin.searchTerm)
+export const useLabelValueFilter = () => useAppStore((state: any) => state.plugin.labelValueFilter)
+export const useLabelValuesFilters = () => useAppStore((state: any) => state.plugin.labelValuesFilters)
 export const usePluginActions = () => useAppStore((state: any) => state.plugin.actions)
 
 export default StoreProvider
diff --git a/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/components/Toolbar.tsx b/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/components/Toolbar.tsx
new file mode 100644
index 0000000000..c186f963f2
--- /dev/null
+++ b/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/components/Toolbar.tsx
@@ -0,0 +1,150 @@
+/*
+ * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { useState, useEffect } from "react"
+import { SearchInput, Select, SelectOption, Stack, InputGroup, Pill } from "@cloudoperators/juno-ui-components"
+import { usePluginActions, usePluginConfig, useLabelValuesFilters } from "./StoreProvider"
+import { LabelValuesFilter } from "../lib/store/createPluginSlice"
+import { SelectContext } from "../../../../../../../../../../packages/ui-components/build/src/components/Select/Select.component"
+import { getStatusCondition } from "../hooks/helper"
+const filtersStyles = `
+  bg-theme-background-lvl-1
+  py-2
+  px-4
+  my-px
+`
+
+const Toolbar = () => {
+  const { setSearchTerm, addLabelValueFilter, removeLabelValueFilter } = usePluginActions()
+  const labelValuesFilters = useLabelValuesFilters()
+  const [labelFilters, setLabelFilters] = useState<LabelValuesFilter[]>()
+  const [selectedLabel, setSelectedLabel] = useState<string>("")
+  const [availableValues, setAvailableValues] = useState<string[]>([])
+  const [selectedValue, setSelectedValue] = useState<string | undefined>(undefined)
+
+  const pluginConfig = usePluginConfig()
+  const handleSearchChange = (value: any) => {
+    // debounce setSearchTerm to avoid unnecessary re-renders
+    const debouncedSearchTerm = setTimeout(() => {
+      setSearchTerm(value.target.value)
+    }, 500)
+
+    return () => clearTimeout(debouncedSearchTerm)
+  }
+
+  useEffect(() => {
+    if (!pluginConfig) return
+    const labelMap = new Map<string, Set<string>>()
+
+    // collects all labels and states for ready.
+    const values = new Set<string>()
+    pluginConfig.forEach((item: any) => {
+      const statusCondition = getStatusCondition(item)
+      if (statusCondition) {
+        values.add(statusCondition)
+      }
+      const labels = item?.metadata?.labels
+      if (labels && typeof labels === "object") {
+        Object.entries(labels).forEach(([key, value]) => {
+          if (!labelMap.has(key)) {
+            labelMap.set(key, new Set())
+          }
+          if (value && typeof value === "string") {
+            labelMap.get(key)?.add(value)
+          } else {
+            return
+          }
+        })
+      }
+    })
+    labelMap.set("Ready", values)
+
+    setLabelFilters(
+      Array.from(labelMap.entries()).map(([label, values]) => ({
+        label,
+        value: Array.from(values),
+      }))
+    )
+  }, [pluginConfig])
+
+  const handleLabelChange = (value?: string | number | string[] | undefined) => {
+    setSelectedValue(undefined)
+
+    if (!labelFilters) return
+
+    const selectedValue = String(value)
+    setSelectedLabel(selectedValue)
+
+    const filter = labelFilters.find((filter) => filter?.label === selectedValue)
+    setAvailableValues(filter?.value || [])
+  }
+
+  const handleValueChange = (value?: string | number | string[] | undefined) => {
+    addLabelValueFilter({ label: selectedLabel, value: value })
+  }
+
+  const handleRemoveFilter = (label: string, value: string) => {
+    removeLabelValueFilter({ label: label, value: value })
+  }
+
+  return (
+    <Stack direction="vertical" gap="4" className={`filters ${filtersStyles}`}>
+      <Stack alignment="center" gap="8">
+        <InputGroup>
+          <Select
+            required
+            label="Label"
+            value={selectedLabel}
+            className="filter-label-select w-64 mb-0"
+            onChange={handleLabelChange}
+            truncateOptions
+          >
+            {labelFilters &&
+              labelFilters.map((filter) => (
+                <SelectOption key={filter?.label} label={filter?.label} value={filter?.label} />
+              ))}
+          </Select>
+
+          <Select
+            name="filterValue"
+            className="filter-value-select w-96 bg-theme-background-lvl-0"
+            value={selectedValue ? selectedValue : "Select..."}
+            onChange={handleValueChange}
+            truncateOptions
+          >
+            {availableValues.map((value) => (
+              <SelectOption key={value} label={value} value={value} />
+            ))}
+          </Select>
+        </InputGroup>
+
+        <SearchInput
+          placeholder="search term or regular expression"
+          className="w-96 ml-auto"
+          value={""}
+          onSearch={(value: any) => setSearchTerm(value)}
+          onClear={() => setSearchTerm("")}
+          onChange={(value: any) => handleSearchChange(value)}
+        />
+      </Stack>
+      <Stack gap="2" wrap={true}>
+        {labelValuesFilters &&
+          labelValuesFilters.map((labelValues: LabelValuesFilter) =>
+            labelValues?.value.map((value: string, i: number) => (
+              <Pill
+                key={labelValues?.label + i}
+                pillKey={labelValues?.label}
+                pillValue={value}
+                onClose={(e: any, _: any) => handleRemoveFilter(labelValues?.label, value)}
+                closeable
+              />
+            ))
+          )}
+      </Stack>
+    </Stack>
+  )
+}
+
+export default Toolbar
diff --git a/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/hooks/helper.ts b/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/hooks/helper.ts
new file mode 100644
index 0000000000..18a3b431fd
--- /dev/null
+++ b/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/hooks/helper.ts
@@ -0,0 +1,15 @@
+/*
+ * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export type StatusConditionFilter = "True" | "False" | "Unknown"
+
+export function getStatusCondition(item: any): StatusConditionFilter {
+  const conditions = item?.status?.statusConditions?.conditions ?? []
+
+  const condition = conditions.find((condition: any) => condition?.type === "Ready")
+  if (!condition || !condition.status) return "Unknown"
+
+  return condition?.status
+}
diff --git a/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/hooks/useUrlState.ts b/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/hooks/useUrlState.ts
index d997ddea5c..583f997248 100644
--- a/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/hooks/useUrlState.ts
+++ b/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/hooks/useUrlState.ts
@@ -13,7 +13,6 @@ const DETAILS_FOR = "d"
 
 const useUrlState = () => {
   const [isURLRead, setIsURLRead] = useState(false)
-  // @ts-ignore
   const { setShowDetailsFor } = usePluginActions()
   const detailsFor = useShowDetailsFor()
 
diff --git a/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/lib/store/createGlobalsSlice.ts b/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/lib/store/createGlobalsSlice.ts
index 73c3e8dbba..76b0c13f1a 100644
--- a/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/lib/store/createGlobalsSlice.ts
+++ b/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/lib/store/createGlobalsSlice.ts
@@ -2,8 +2,33 @@
  * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors
  * SPDX-License-Identifier: Apache-2.0
  */
+import { StateCreator } from "zustand"
 
-const createGlobalsSlice = (set: any) => ({
+// Define the state structure
+interface GlobalState {
+  urlStateKey: string
+  apiEndpoint: string | null
+  token: string
+  namespace: string
+  showDefinitionPanel: boolean
+  panel: string | null
+}
+
+// Define the actions (functions to modify state)
+interface GlobalActions {
+  initialize: (apiEndpoint: string, token: string, namespace: string) => void
+  setUrlStateKey: (newUrlStateKey: string) => void
+  setPanel: (showPanel: string | null) => void
+  setEndpoint: (apiEndpoint: string) => void
+  setShowDefinitionPanel: (showDefinitionPanel: boolean) => void
+}
+
+// Define the complete slice (state + actions)
+export interface GlobalsSlice {
+  globals: GlobalState & { actions: GlobalActions }
+}
+
+const createGlobalsSlice: StateCreator<GlobalsSlice, [], [], GlobalsSlice> = (set) => ({
   globals: {
     urlStateKey: "",
     apiEndpoint: null,
diff --git a/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/lib/store/createPluginSlice.test.tsx b/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/lib/store/createPluginSlice.test.tsx
index ba08444e41..6a65a1bb50 100644
--- a/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/lib/store/createPluginSlice.test.tsx
+++ b/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/lib/store/createPluginSlice.test.tsx
@@ -1,5 +1,5 @@
 /*
- * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors
+ * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
  * SPDX-License-Identifier: Apache-2.0
  */
 
@@ -21,18 +21,19 @@ describe("createPluginSlice", () => {
       )
 
       act(() => {
-        // @ts-ignore
         store.result.current.pluginActions.setPluginConfig([
-          { id: "onePlugin", disabled: false },
-          { id: "alert", disabled: false },
-          { id: "example app", disabled: true },
+          {
+            spec: { displayName: "onePlugin", disabled: false },
+          },
+          { spec: { displayName: "alert", disabled: false } },
+          { spec: { displayName: "example app", disabled: true } },
         ])
       })
 
       expect(store.result.current.pluginConfig).toEqual([
-        { id: "alert", disabled: false },
-        { id: "onePlugin", disabled: false },
-        { id: "example app", disabled: true },
+        { spec: { displayName: "alert", disabled: false } },
+        { spec: { displayName: "onePlugin", disabled: false } },
+        { spec: { displayName: "example app", disabled: true } },
       ])
     })
     it("test if pluginConfig is saved correctly without disabled field", () => {
@@ -46,18 +47,17 @@ describe("createPluginSlice", () => {
       )
 
       act(() => {
-        // @ts-ignore
         store.result.current.pluginActions.setPluginConfig([
-          { id: "onePlugin", disabled: false },
-          { id: "alert" },
-          { id: "example app", disabled: true },
+          { spec: { displayName: "onePlugin", disabled: false } },
+          { spec: { displayName: "alert" } },
+          { spec: { displayName: "example app", disabled: true } },
         ])
       })
 
       expect(store.result.current.pluginConfig).toEqual([
-        { id: "alert" },
-        { id: "onePlugin", disabled: false },
-        { id: "example app", disabled: true },
+        { spec: { displayName: "alert" } },
+        { spec: { displayName: "onePlugin", disabled: false } },
+        { spec: { displayName: "example app", disabled: true } },
       ])
     })
   })
diff --git a/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/lib/store/createPluginSlice.ts b/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/lib/store/createPluginSlice.ts
index f6149ab472..c8916a54dd 100644
--- a/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/lib/store/createPluginSlice.ts
+++ b/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/lib/store/createPluginSlice.ts
@@ -1,51 +1,97 @@
 /*
- * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors
+ * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
  * SPDX-License-Identifier: Apache-2.0
  */
 
-function sortPluginConfigItems(items: any) {
-  return items.sort((a: any, b: any) => {
+import { StateCreator } from "zustand"
+import { getStatusCondition } from "../../hooks/helper"
+
+interface PluginState {
+  pluginConfig: PluginConfigItem[] | null
+  filteredPluginConfigs: PluginConfigItem[] | null
+  showDetailsFor: any | null
+  searchTerm: string
+  labelValuesFilters: LabelValuesFilter[] | undefined
+}
+
+export type LabelValueFilter = { label: string; value: string } | undefined
+export type LabelValuesFilter = { label: string; value: string[] } | undefined
+interface PluginConfigItem {
+  metadata: {
+    uid: string
+    labels?: { [key: string]: string }
+    name?: string
+    [key: string]: unknown
+  }
+  spec: {
+    disabled?: boolean
+    displayName?: string
+    [key: string]: unknown
+  }
+  [key: string]: unknown
+}
+
+interface PluginActions {
+  setPluginConfig: (pluginConfig: PluginConfigItem[]) => void
+  addPluginConfigItems: (pluginConfigItems: PluginConfigItem[]) => void
+  modifyPluginConfigItems: (modifiedItems: PluginConfigItem[]) => void
+  deletePluginConfigItems: (pluginConfigItems: PluginConfigItem[]) => void
+  setShowDetailsFor: (showDetailsFor: any | null) => void
+  setSearchTerm: (searchTerm: string) => void
+  addLabelValueFilter: (labelValueFilter: LabelValueFilter) => void
+  removeLabelValueFilter: (labelValueFilter: LabelValueFilter) => void
+  filterItems: () => void
+}
+
+export interface PluginSlice {
+  plugin: PluginState & { actions: PluginActions }
+}
+
+function sortPluginConfigItems(items: PluginConfigItem[]) {
+  return items.sort((a, b) => {
+    // First, sort by `disabled` status
     if (a?.spec?.disabled && !b?.spec?.disabled) {
       return 1
     } else if (!a?.spec?.disabled && b?.spec?.disabled) {
       return -1
-    } else {
-      return a?.metadata?.uid.localeCompare(b?.metadata?.uid)
     }
+
+    // Then, sort by displayName or metadata.name
+    const nameA = a?.spec?.displayName || a?.metadata?.name || ""
+    const nameB = b?.spec?.displayName || b?.metadata?.name || ""
+
+    return nameA.localeCompare(nameB)
   })
 }
 
-function uniqPluginConfigItems(items: any) {
-  return items.filter((item: any, index: any, array: any) => array.indexOf(item) === index)
+function uniqPluginConfigItems(items: PluginConfigItem[]) {
+  return items.filter((item, index: any, array: any) => array.indexOf(item) === index)
 }
 
-// @ts-expect-error TS(7006): Parameter 'set' implicitly has an 'any' type.
-const createPluginSlice = (set, get) => ({
+const createPluginSlice: StateCreator<PluginSlice, [], [], PluginSlice> = (set, get, store) => ({
   plugin: {
     pluginConfig: null,
+    filteredPluginConfigs: null,
     showDetailsFor: null,
+    searchTerm: "",
+    statusConditionFilter: "All",
+    labelValuesFilters: undefined,
 
     actions: {
-      setPluginConfig: (pluginConfig: any) => {
+      setPluginConfig: (pluginConfig) => {
         // Sort plugins by id alphabetically, but put disabled plugins at the end
-        let sortedPlugins = pluginConfig.sort((a: any, b: any) => {
-          if (a?.disabled && !b?.disabled) {
-            return 1
-          } else if (!a?.disabled && b?.disabled) {
-            return -1
-          } else {
-            return a.id.localeCompare(b.id)
-          }
-        })
+        let sortedPlugins = sortPluginConfigItems(pluginConfig)
+
         set((state: any) => ({
           plugin: {
             ...state.plugin,
             pluginConfig: sortedPlugins,
           },
         }))
+        get().plugin.actions.filterItems()
       },
 
-      addPluginConfigItems: (pluginConfigItems: any) => {
+      addPluginConfigItems: (pluginConfigItems) => {
         const items = (get().plugin.pluginConfig || []).slice()
         let newItems = uniqPluginConfigItems([...items, ...pluginConfigItems])
         newItems = sortPluginConfigItems(newItems)
@@ -56,8 +102,9 @@ const createPluginSlice = (set, get) => ({
             pluginConfig: newItems,
           },
         }))
+        get().plugin.actions.filterItems()
       },
-      modifyPluginConfigItems: (modifiedItems: any) => {
+      modifyPluginConfigItems: (modifiedItems) => {
         const items = (get().plugin.pluginConfig || []).slice()
 
         const updatedItems = items.map((item: any) => {
@@ -76,8 +123,9 @@ const createPluginSlice = (set, get) => ({
             pluginConfig: newItems,
           },
         }))
+        get().plugin.actions.filterItems()
       },
-      deletePluginConfigItems: (pluginConfigItems: any) => {
+      deletePluginConfigItems: (pluginConfigItems) => {
         const items = (get().plugin.pluginConfig || []).slice() // Get items
 
         let updatedItems = items.filter((item: any) => {
@@ -93,12 +141,138 @@ const createPluginSlice = (set, get) => ({
             pluginConfig: newItems,
           },
         }))
+        get().plugin.actions.filterItems()
       },
 
-      setShowDetailsFor: (showDetailsFor: any) =>
+      setShowDetailsFor: (showDetailsFor: any) => {
         set((state: any) => ({
           plugin: { ...state.plugin, showDetailsFor: showDetailsFor },
-        })),
+        }))
+      },
+
+      setSearchTerm: (searchTerm: string) => {
+        set((state: any) => ({
+          plugin: { ...state.plugin, searchTerm: searchTerm },
+        }))
+        get().plugin.actions.filterItems()
+      },
+
+      addLabelValueFilter: (labelValueFilter: LabelValueFilter) => {
+        if (!labelValueFilter) return
+
+        set((state: any) => {
+          const existingFilters = state.plugin.labelValuesFilters || []
+
+          // Check if the label already exists
+          const existingFilterIndex = existingFilters.findIndex(
+            (f: LabelValuesFilter) => f?.label === labelValueFilter.label
+          )
+
+          let updatedFilters
+
+          if (existingFilterIndex !== -1) {
+            // Label exists → Add unique value
+            updatedFilters = [...existingFilters]
+            updatedFilters[existingFilterIndex] = {
+              label: labelValueFilter.label,
+              value: Array.from(new Set([...updatedFilters[existingFilterIndex].value, labelValueFilter.value])), // Ensure uniqueness
+            }
+          } else {
+            // Label does not exist → Add new entry
+            updatedFilters = [...existingFilters, { label: labelValueFilter.label, value: [labelValueFilter.value] }]
+          }
+
+          return {
+            plugin: {
+              ...state.plugin,
+              labelValuesFilters: updatedFilters,
+            },
+          }
+        })
+
+        get().plugin.actions.filterItems()
+      },
+
+      removeLabelValueFilter: (labelValueFilter: LabelValueFilter) => {
+        if (!labelValueFilter) return
+        set((state: any) => {
+          const existingFilters = state.plugin.labelValuesFilters || []
+          const selectedLabelValues = existingFilters.find(
+            (filter: LabelValuesFilter) => filter?.label === labelValueFilter.label
+          )
+
+          const filteredArray = selectedLabelValues.value.filter((item: string) => item !== labelValueFilter.value)
+
+          let updatedFilters
+
+          if (filteredArray.length === 0) {
+            updatedFilters = existingFilters.filter(
+              (filter: LabelValuesFilter) => filter?.label !== labelValueFilter.label
+            )
+          } else {
+            updatedFilters = existingFilters.map((filter: LabelValuesFilter) =>
+              filter?.label === labelValueFilter.label ? { ...filter, value: filteredArray } : filter
+            )
+          }
+
+          return {
+            plugin: {
+              ...state.plugin,
+              labelValuesFilters: updatedFilters,
+            },
+          }
+        })
+        get().plugin.actions.filterItems()
+      },
+
+      filterItems: () => {
+        let items = (get().plugin.pluginConfig || []).slice()
+
+        const searchTerm = get().plugin.searchTerm
+        let labelValuesFilters = get().plugin.labelValuesFilters
+
+        // Filter by getStatusCondition / Ready
+        if (labelValuesFilters?.length && items) {
+          const readyFilter = labelValuesFilters.find((filter) => filter?.label === "Ready")
+
+          if (readyFilter) {
+            items = items.filter((item) => {
+              const statusCondition = getStatusCondition(item)
+              return statusCondition && readyFilter.value.includes(statusCondition)
+            })
+
+            // Remove the "Ready" filter from labelValuesFilters
+            labelValuesFilters = labelValuesFilters.filter((filter) => filter?.label !== "Ready")
+          }
+        }
+
+        // Filter by LabelValueFilter
+        if (labelValuesFilters?.length && items) {
+          items = items.filter((item: any) => {
+            const labels = item.metadata?.labels || {}
+
+            // Check if at least one filter matches
+            return labelValuesFilters.every(
+              (filter) => filter && filter.value.some((val) => labels[filter.label] === val)
+            )
+          })
+        }
+
+        // Filter SearchTerm
+        if (searchTerm && items) {
+          items = items.filter((item: any) => {
+            const itemString = JSON.stringify(item)
+            const re = new RegExp(searchTerm, "i")
+            if (itemString.match(re)) {
+              return true
+            } else return false
+          })
+        }
+
+        set((state: any) => ({
+          plugin: { ...state.plugin, filteredPluginConfigs: items },
+        }))
+      },
     },
   },
 })
diff --git a/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/lib/store/index.ts b/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/lib/store/index.ts
index f8330a2876..baf6fab288 100644
--- a/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/lib/store/index.ts
+++ b/apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/lib/store/index.ts
@@ -3,16 +3,18 @@
  * SPDX-License-Identifier: Apache-2.0
  */
 
-import { createStore } from "zustand"
+import { create } from "zustand"
 import { devtools } from "zustand/middleware"
-import createGlobalsSlice from "./createGlobalsSlice"
-import createPluginSlice from "./createPluginSlice"
+import createGlobalsSlice, { GlobalsSlice } from "./createGlobalsSlice"
+import createPluginSlice, { PluginSlice } from "./createPluginSlice"
 
-export default () =>
-  createStore(
-    devtools((set, get) => ({
-      // @ts-expect-error TS(2554): Expected 1 arguments, but got 2.
-      ...createGlobalsSlice(set, get),
-      ...createPluginSlice(set, get),
-    }))
-  )
+type StoreState = GlobalsSlice & PluginSlice
+
+const useStore = create<StoreState>()(
+  devtools((set, get, store) => ({
+    ...createGlobalsSlice(set, get, store),
+    ...createPluginSlice(set, get, store),
+  }))
+)
+
+export default useStore
diff --git a/apps/greenhouse/src/components/core-apps/org-admin/components/secrets/components/StoreProvider.tsx b/apps/greenhouse/src/components/core-apps/org-admin/components/secrets/components/StoreProvider.tsx
index f5b52fb4b5..ee9f6e721c 100644
--- a/apps/greenhouse/src/components/core-apps/org-admin/components/secrets/components/StoreProvider.tsx
+++ b/apps/greenhouse/src/components/core-apps/org-admin/components/secrets/components/StoreProvider.tsx
@@ -23,7 +23,7 @@ export const useAuthData = () => useAppStore((state: any) => state.globals.authD
 
 export const useGlobalsActions = () => useAppStore((state: any) => state.globals.actions)
 
-/// Plugin
+// Plugin
 export const usePluginConfig = () => useAppStore((state: any) => state.plugin.pluginConfig)
 export const useShowDetailsFor = () => useAppStore((state: any) => state.plugin.showDetailsFor)
 
diff --git a/apps/supernova/src/components/filters/FilterSelect.tsx b/apps/supernova/src/components/filters/FilterSelect.tsx
index 1a6ec42858..66d881a1e2 100644
--- a/apps/supernova/src/components/filters/FilterSelect.tsx
+++ b/apps/supernova/src/components/filters/FilterSelect.tsx
@@ -1,5 +1,5 @@
 /*
- * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors
+ * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
  * SPDX-License-Identifier: Apache-2.0
  */
 
diff --git a/apps/supernova/src/components/filters/Filters.tsx b/apps/supernova/src/components/filters/Filters.tsx
index 770b22f342..c758c818ed 100644
--- a/apps/supernova/src/components/filters/Filters.tsx
+++ b/apps/supernova/src/components/filters/Filters.tsx
@@ -1,5 +1,5 @@
 /*
- * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors
+ * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
  * SPDX-License-Identifier: Apache-2.0
  */
 
diff --git a/apps/supernova/src/lib/createAlertsSlice.tsx b/apps/supernova/src/lib/createAlertsSlice.tsx
index 92ee88a1f0..3346e9a7b4 100644
--- a/apps/supernova/src/lib/createAlertsSlice.tsx
+++ b/apps/supernova/src/lib/createAlertsSlice.tsx
@@ -1,5 +1,5 @@
 /*
- * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors
+ * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
  * SPDX-License-Identifier: Apache-2.0
  */
 
diff --git a/apps/supernova/src/lib/createFiltersSlice.tsx b/apps/supernova/src/lib/createFiltersSlice.tsx
index 15896b6bdc..9e77feee97 100644
--- a/apps/supernova/src/lib/createFiltersSlice.tsx
+++ b/apps/supernova/src/lib/createFiltersSlice.tsx
@@ -1,5 +1,5 @@
 /*
- * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors
+ * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
  * SPDX-License-Identifier: Apache-2.0
  */
 


Reviews:
user: login: edda
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T19:03:27Z
user: login: edda
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T19:13:36Z
user: login: TilmanHaupt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T23:09:15Z
user: login: TilmanHaupt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T23:12:24Z
user: login: edda
body: I'm going to approve but have two comments:
1. The message at the top of the page should have a margin-bottom so that it has some space to the "Add Plugin" button.
2. I'm not sure if the `truncateOptions` option does us any favors here with the filter labels being as long as they are. We can keep it like this for now and see what it will look like in the live system but I'm expecting this to be not really useable and we'll have to come up with a different solution, e.g. parsing the labels or going with the word-break solution instead.
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T13:02:00Z
user: login: edda
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T17:12:48Z
user: login: andypf
body: Great job, only a few comments
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-14T11:56:04Z
user: login: andypf
body: Please address my comments
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-14T11:59:42Z
user: login: TilmanHaupt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T12:05:39Z
user: login: andypf
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-14T12:39:14Z
user: login: TilmanHaupt
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T12:45:20Z
user: login: edda
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T13:19:12Z

Review comments:
diff_hunk: @@ -0,0 +1,133 @@
+import React, { useState, useEffect } from "react"
+import { SearchInput, Select, SelectOption, Stack, InputGroup } from "@cloudoperators/juno-ui-components"
+import { usePluginActions, usePluginConfig, useStatusConditionFilter } from "./StoreProvider"
+import { StatusConditionFilter, LabelValuesFilter } from "../lib/store/createPluginSlice"
+const filtersStyles = `
+  bg-theme-background-lvl-1
+  py-2
+  px-4
+  my-px
+`
+
+const Toolbar = () => {
+  const { setSearchTerm, setStatusConditionFilter, setLabelValueFilter } = usePluginActions()
+  const [labelFilters, setLabelFilters] = useState<LabelValuesFilter[]>()
+  const [selectedLabel, setSelectedLabel] = useState<string>("All")
+  const [availableValues, setAvailableValues] = useState<string[]>([])
+  const [selectedValue, setSelectedValue] = useState<string | undefined>(undefined)
+
+  const statusOptions: StatusConditionFilter[] = ["All", "True", "False", "Unknown"]
+  const statusConditionFilter = useStatusConditionFilter()
+  const pluginConfig = usePluginConfig()
+  const handleSearchChange = (value: any) => {
+    // debounce setSearchTerm to avoid unnecessary re-renders
+    const debouncedSearchTerm = setTimeout(() => {
+      setSearchTerm(value.target.value)
+    }, 500)
+
+    return () => clearTimeout(debouncedSearchTerm)
+  }
+
+  useEffect(() => {
+    if (!pluginConfig) return
+    const labelMap = new Map<string, Set<string>>()
+
+    pluginConfig.forEach((item: any) => {
+      const labels = item?.metadata?.labels
+      if (labels && typeof labels === "object") {
+        Object.entries(labels).forEach(([key, value]) => {
+          if (!labelMap.has(key)) {
+            labelMap.set(key, new Set())
+          }
+          if (typeof value === "string") {
+            labelMap.get(key)?.add(value)
+          }
+        })
+      }
+    })
+
+    setLabelFilters(
+      Array.from(labelMap.entries()).map(([label, values]) => ({
+        label,
+        value: Array.from(values),
+      }))
+    )
+  }, [pluginConfig])
+
+  const handleLabelChange = (value?: string | number | string[] | undefined) => {
+    setSelectedValue(undefined)
+    setLabelValueFilter(undefined)
+
+    if (!labelFilters) return
+
+    const selectedValue = typeof value === "string" ? value : "All"
+    setSelectedLabel(selectedValue)
+
+    if (selectedValue == "All") {
+      setAvailableValues([])
+    } else {
+      const filter = labelFilters.find((filter) => filter?.label === selectedValue)
+      setAvailableValues(filter?.value || [])
+    }
+  }
+
+  const handleValueChange = (value?: string | number | string[] | undefined) => {
+    setSelectedValue(value !== undefined ? String(value) : "")
+    setLabelValueFilter({ label: selectedLabel, value: value })
+  }
+
+  return (
+    <Stack alignment="center" gap="8" className={`filters ${filtersStyles}`}>
+      <Stack alignment="center" gap="4" className={`filters ${filtersStyles}`}>
+        <Select
path: apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/components/Toolbar.tsx
user: login: edda
body: I am not a fan of this filter section. You have an extra `Select` for the ready condition and pair it with our established pattern of the combo filter element where you first select the thing you filter by and then the value. Please add the filter for the ready condition inside the combo element.
created_at: 2025-03-11T19:01:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,133 @@
+import React, { useState, useEffect } from "react"
+import { SearchInput, Select, SelectOption, Stack, InputGroup } from "@cloudoperators/juno-ui-components"
+import { usePluginActions, usePluginConfig, useStatusConditionFilter } from "./StoreProvider"
+import { StatusConditionFilter, LabelValuesFilter } from "../lib/store/createPluginSlice"
+const filtersStyles = `
+  bg-theme-background-lvl-1
+  py-2
+  px-4
+  my-px
+`
+
+const Toolbar = () => {
+  const { setSearchTerm, setStatusConditionFilter, setLabelValueFilter } = usePluginActions()
+  const [labelFilters, setLabelFilters] = useState<LabelValuesFilter[]>()
+  const [selectedLabel, setSelectedLabel] = useState<string>("All")
+  const [availableValues, setAvailableValues] = useState<string[]>([])
+  const [selectedValue, setSelectedValue] = useState<string | undefined>(undefined)
+
+  const statusOptions: StatusConditionFilter[] = ["All", "True", "False", "Unknown"]
+  const statusConditionFilter = useStatusConditionFilter()
+  const pluginConfig = usePluginConfig()
+  const handleSearchChange = (value: any) => {
+    // debounce setSearchTerm to avoid unnecessary re-renders
+    const debouncedSearchTerm = setTimeout(() => {
+      setSearchTerm(value.target.value)
+    }, 500)
+
+    return () => clearTimeout(debouncedSearchTerm)
+  }
+
+  useEffect(() => {
+    if (!pluginConfig) return
+    const labelMap = new Map<string, Set<string>>()
+
+    pluginConfig.forEach((item: any) => {
+      const labels = item?.metadata?.labels
+      if (labels && typeof labels === "object") {
+        Object.entries(labels).forEach(([key, value]) => {
+          if (!labelMap.has(key)) {
+            labelMap.set(key, new Set())
+          }
+          if (typeof value === "string") {
+            labelMap.get(key)?.add(value)
+          }
+        })
+      }
+    })
+
+    setLabelFilters(
+      Array.from(labelMap.entries()).map(([label, values]) => ({
+        label,
+        value: Array.from(values),
+      }))
+    )
+  }, [pluginConfig])
+
+  const handleLabelChange = (value?: string | number | string[] | undefined) => {
+    setSelectedValue(undefined)
+    setLabelValueFilter(undefined)
+
+    if (!labelFilters) return
+
+    const selectedValue = typeof value === "string" ? value : "All"
+    setSelectedLabel(selectedValue)
+
+    if (selectedValue == "All") {
+      setAvailableValues([])
+    } else {
+      const filter = labelFilters.find((filter) => filter?.label === selectedValue)
+      setAvailableValues(filter?.value || [])
+    }
+  }
+
+  const handleValueChange = (value?: string | number | string[] | undefined) => {
+    setSelectedValue(value !== undefined ? String(value) : "")
+    setLabelValueFilter({ label: selectedLabel, value: value })
+  }
+
+  return (
+    <Stack alignment="center" gap="8" className={`filters ${filtersStyles}`}>
+      <Stack alignment="center" gap="4" className={`filters ${filtersStyles}`}>
+        <Select
+          required
+          label="Ready"
+          value={statusConditionFilter}
+          className="filter-label-select w-32 mb-0"
+          onChange={(value: any) => setStatusConditionFilter(value)}
+        >
+          {statusOptions.map((option) => (
+            <SelectOption key={option} label={option} value={option} />
+          ))}
+        </Select>
+        <InputGroup>
+          <Select
+            required
+            label="Label"
+            value={selectedLabel}
+            className="filter-label-select w-64 mb-0"
+            onChange={handleLabelChange}
+          >
+            <SelectOption key="All" label="All" value="All" />
+            {labelFilters &&
+              labelFilters.map((filter) => (
+                <SelectOption key={filter?.label} label={filter?.label} value={filter?.label} />
+              ))}
+          </Select>
+
+          <Select
+            name="filterValue"
+            className="filter-value-select w-96 bg-theme-background-lvl-0"
+            value={selectedValue ? selectedValue : "Select..."}
+            onChange={handleValueChange}
+          >
+            {availableValues.map((value) => (
+              <SelectOption key={value} label={value} value={value} />
+            ))}
+          </Select>
+        </InputGroup>
path: apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/components/Toolbar.tsx
user: login: edda
body: You forgot to add the filter pills so it's not possible to unselect a selected filter
created_at: 2025-03-11T19:01:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,11 +28,12 @@ const PluginList = () => {
 
   return (
     <>
-      <DataGridToolbar>
-        <ButtonRow>
+      <Stack className="mb-4">
path: apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/components/PluginList.tsx
user: login: edda
body: Please change this back to the DataGridToolbar to give the impression of one Toolbar block with buttons and filters.
created_at: 2025-03-11T19:03:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,133 @@
+import React, { useState, useEffect } from "react"
+import { SearchInput, Select, SelectOption, Stack, InputGroup } from "@cloudoperators/juno-ui-components"
+import { usePluginActions, usePluginConfig, useStatusConditionFilter } from "./StoreProvider"
+import { StatusConditionFilter, LabelValuesFilter } from "../lib/store/createPluginSlice"
+const filtersStyles = `
+  bg-theme-background-lvl-1
+  py-2
+  px-4
+  my-px
+`
+
+const Toolbar = () => {
+  const { setSearchTerm, setStatusConditionFilter, setLabelValueFilter } = usePluginActions()
+  const [labelFilters, setLabelFilters] = useState<LabelValuesFilter[]>()
+  const [selectedLabel, setSelectedLabel] = useState<string>("All")
+  const [availableValues, setAvailableValues] = useState<string[]>([])
+  const [selectedValue, setSelectedValue] = useState<string | undefined>(undefined)
+
+  const statusOptions: StatusConditionFilter[] = ["All", "True", "False", "Unknown"]
+  const statusConditionFilter = useStatusConditionFilter()
+  const pluginConfig = usePluginConfig()
+  const handleSearchChange = (value: any) => {
+    // debounce setSearchTerm to avoid unnecessary re-renders
+    const debouncedSearchTerm = setTimeout(() => {
+      setSearchTerm(value.target.value)
+    }, 500)
+
+    return () => clearTimeout(debouncedSearchTerm)
+  }
+
+  useEffect(() => {
+    if (!pluginConfig) return
+    const labelMap = new Map<string, Set<string>>()
+
+    pluginConfig.forEach((item: any) => {
+      const labels = item?.metadata?.labels
+      if (labels && typeof labels === "object") {
+        Object.entries(labels).forEach(([key, value]) => {
+          if (!labelMap.has(key)) {
+            labelMap.set(key, new Set())
+          }
+          if (typeof value === "string") {
+            labelMap.get(key)?.add(value)
+          }
+        })
+      }
+    })
+
+    setLabelFilters(
+      Array.from(labelMap.entries()).map(([label, values]) => ({
+        label,
+        value: Array.from(values),
+      }))
+    )
+  }, [pluginConfig])
+
+  const handleLabelChange = (value?: string | number | string[] | undefined) => {
+    setSelectedValue(undefined)
+    setLabelValueFilter(undefined)
+
+    if (!labelFilters) return
+
+    const selectedValue = typeof value === "string" ? value : "All"
+    setSelectedLabel(selectedValue)
+
+    if (selectedValue == "All") {
+      setAvailableValues([])
+    } else {
+      const filter = labelFilters.find((filter) => filter?.label === selectedValue)
+      setAvailableValues(filter?.value || [])
+    }
+  }
+
+  const handleValueChange = (value?: string | number | string[] | undefined) => {
+    setSelectedValue(value !== undefined ? String(value) : "")
+    setLabelValueFilter({ label: selectedLabel, value: value })
+  }
+
+  return (
+    <Stack alignment="center" gap="8" className={`filters ${filtersStyles}`}>
+      <Stack alignment="center" gap="4" className={`filters ${filtersStyles}`}>
+        <Select
+          required
+          label="Ready"
+          value={statusConditionFilter}
+          className="filter-label-select w-32 mb-0"
+          onChange={(value: any) => setStatusConditionFilter(value)}
+        >
+          {statusOptions.map((option) => (
+            <SelectOption key={option} label={option} value={option} />
+          ))}
+        </Select>
+        <InputGroup>
+          <Select
+            required
+            label="Label"
+            value={selectedLabel}
+            className="filter-label-select w-64 mb-0"
+            onChange={handleLabelChange}
+          >
+            <SelectOption key="All" label="All" value="All" />
+            {labelFilters &&
+              labelFilters.map((filter) => (
+                <SelectOption key={filter?.label} label={filter?.label} value={filter?.label} />
path: apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/components/Toolbar.tsx
user: login: edda
body: At least in the demo preview some of the values for this  are too long for the available width. You can solve this either with the available config props on the `Select` component (look into `truncateOptions`, this will truncate option labels that are too long). Alternatively if you don't want to truncate because it renders the value unreadable you can add a tailwind classname which forces the string to break if necessary: `break-words` on the `SelectOption`.
created_at: 2025-03-11T19:13:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,133 @@
+import React, { useState, useEffect } from "react"
+import { SearchInput, Select, SelectOption, Stack, InputGroup } from "@cloudoperators/juno-ui-components"
+import { usePluginActions, usePluginConfig, useStatusConditionFilter } from "./StoreProvider"
+import { StatusConditionFilter, LabelValuesFilter } from "../lib/store/createPluginSlice"
+const filtersStyles = `
+  bg-theme-background-lvl-1
+  py-2
+  px-4
+  my-px
+`
+
+const Toolbar = () => {
+  const { setSearchTerm, setStatusConditionFilter, setLabelValueFilter } = usePluginActions()
+  const [labelFilters, setLabelFilters] = useState<LabelValuesFilter[]>()
+  const [selectedLabel, setSelectedLabel] = useState<string>("All")
+  const [availableValues, setAvailableValues] = useState<string[]>([])
+  const [selectedValue, setSelectedValue] = useState<string | undefined>(undefined)
+
+  const statusOptions: StatusConditionFilter[] = ["All", "True", "False", "Unknown"]
+  const statusConditionFilter = useStatusConditionFilter()
+  const pluginConfig = usePluginConfig()
+  const handleSearchChange = (value: any) => {
+    // debounce setSearchTerm to avoid unnecessary re-renders
+    const debouncedSearchTerm = setTimeout(() => {
+      setSearchTerm(value.target.value)
+    }, 500)
+
+    return () => clearTimeout(debouncedSearchTerm)
+  }
+
+  useEffect(() => {
+    if (!pluginConfig) return
+    const labelMap = new Map<string, Set<string>>()
+
+    pluginConfig.forEach((item: any) => {
+      const labels = item?.metadata?.labels
+      if (labels && typeof labels === "object") {
+        Object.entries(labels).forEach(([key, value]) => {
+          if (!labelMap.has(key)) {
+            labelMap.set(key, new Set())
+          }
+          if (typeof value === "string") {
+            labelMap.get(key)?.add(value)
+          }
+        })
+      }
+    })
+
+    setLabelFilters(
+      Array.from(labelMap.entries()).map(([label, values]) => ({
+        label,
+        value: Array.from(values),
+      }))
+    )
+  }, [pluginConfig])
+
+  const handleLabelChange = (value?: string | number | string[] | undefined) => {
+    setSelectedValue(undefined)
+    setLabelValueFilter(undefined)
+
+    if (!labelFilters) return
+
+    const selectedValue = typeof value === "string" ? value : "All"
+    setSelectedLabel(selectedValue)
+
+    if (selectedValue == "All") {
+      setAvailableValues([])
+    } else {
+      const filter = labelFilters.find((filter) => filter?.label === selectedValue)
+      setAvailableValues(filter?.value || [])
+    }
+  }
+
+  const handleValueChange = (value?: string | number | string[] | undefined) => {
+    setSelectedValue(value !== undefined ? String(value) : "")
+    setLabelValueFilter({ label: selectedLabel, value: value })
+  }
+
+  return (
+    <Stack alignment="center" gap="8" className={`filters ${filtersStyles}`}>
+      <Stack alignment="center" gap="4" className={`filters ${filtersStyles}`}>
+        <Select
+          required
+          label="Ready"
+          value={statusConditionFilter}
+          className="filter-label-select w-32 mb-0"
+          onChange={(value: any) => setStatusConditionFilter(value)}
+        >
+          {statusOptions.map((option) => (
+            <SelectOption key={option} label={option} value={option} />
+          ))}
+        </Select>
+        <InputGroup>
+          <Select
+            required
+            label="Label"
+            value={selectedLabel}
+            className="filter-label-select w-64 mb-0"
+            onChange={handleLabelChange}
+          >
+            <SelectOption key="All" label="All" value="All" />
+            {labelFilters &&
+              labelFilters.map((filter) => (
+                <SelectOption key={filter?.label} label={filter?.label} value={filter?.label} />
+              ))}
+          </Select>
+
+          <Select
+            name="filterValue"
+            className="filter-value-select w-96 bg-theme-background-lvl-0"
+            value={selectedValue ? selectedValue : "Select..."}
+            onChange={handleValueChange}
+          >
+            {availableValues.map((value) => (
+              <SelectOption key={value} label={value} value={value} />
+            ))}
+          </Select>
+        </InputGroup>
path: apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/components/Toolbar.tsx
user: login: TilmanHaupt
body: A Filter automatically deselects, if you select another one. If there should be a combination of filters, i will need some time for another version. 
created_at: 2025-03-11T23:09:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,133 @@
+import React, { useState, useEffect } from "react"
+import { SearchInput, Select, SelectOption, Stack, InputGroup } from "@cloudoperators/juno-ui-components"
+import { usePluginActions, usePluginConfig, useStatusConditionFilter } from "./StoreProvider"
+import { StatusConditionFilter, LabelValuesFilter } from "../lib/store/createPluginSlice"
+const filtersStyles = `
+  bg-theme-background-lvl-1
+  py-2
+  px-4
+  my-px
+`
+
+const Toolbar = () => {
+  const { setSearchTerm, setStatusConditionFilter, setLabelValueFilter } = usePluginActions()
+  const [labelFilters, setLabelFilters] = useState<LabelValuesFilter[]>()
+  const [selectedLabel, setSelectedLabel] = useState<string>("All")
+  const [availableValues, setAvailableValues] = useState<string[]>([])
+  const [selectedValue, setSelectedValue] = useState<string | undefined>(undefined)
+
+  const statusOptions: StatusConditionFilter[] = ["All", "True", "False", "Unknown"]
+  const statusConditionFilter = useStatusConditionFilter()
+  const pluginConfig = usePluginConfig()
+  const handleSearchChange = (value: any) => {
+    // debounce setSearchTerm to avoid unnecessary re-renders
+    const debouncedSearchTerm = setTimeout(() => {
+      setSearchTerm(value.target.value)
+    }, 500)
+
+    return () => clearTimeout(debouncedSearchTerm)
+  }
+
+  useEffect(() => {
+    if (!pluginConfig) return
+    const labelMap = new Map<string, Set<string>>()
+
+    pluginConfig.forEach((item: any) => {
+      const labels = item?.metadata?.labels
+      if (labels && typeof labels === "object") {
+        Object.entries(labels).forEach(([key, value]) => {
+          if (!labelMap.has(key)) {
+            labelMap.set(key, new Set())
+          }
+          if (typeof value === "string") {
+            labelMap.get(key)?.add(value)
+          }
+        })
+      }
+    })
+
+    setLabelFilters(
+      Array.from(labelMap.entries()).map(([label, values]) => ({
+        label,
+        value: Array.from(values),
+      }))
+    )
+  }, [pluginConfig])
+
+  const handleLabelChange = (value?: string | number | string[] | undefined) => {
+    setSelectedValue(undefined)
+    setLabelValueFilter(undefined)
+
+    if (!labelFilters) return
+
+    const selectedValue = typeof value === "string" ? value : "All"
+    setSelectedLabel(selectedValue)
+
+    if (selectedValue == "All") {
+      setAvailableValues([])
+    } else {
+      const filter = labelFilters.find((filter) => filter?.label === selectedValue)
+      setAvailableValues(filter?.value || [])
+    }
+  }
+
+  const handleValueChange = (value?: string | number | string[] | undefined) => {
+    setSelectedValue(value !== undefined ? String(value) : "")
+    setLabelValueFilter({ label: selectedLabel, value: value })
+  }
+
+  return (
+    <Stack alignment="center" gap="8" className={`filters ${filtersStyles}`}>
+      <Stack alignment="center" gap="4" className={`filters ${filtersStyles}`}>
+        <Select
path: apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/components/Toolbar.tsx
user: login: TilmanHaupt
body: I was refraining from supernova-style filtering because of speed / road map restaints, but when I hear the rest of the input i can change that. 
created_at: 2025-03-11T23:12:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,21 +1,43 @@
 /*
- * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors
+ * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
  * SPDX-License-Identifier: Apache-2.0
  */
 
 import React, { createContext, useContext } from "react"
+import { createStore, useStore, StoreApi } from "zustand"
 
-import { useStore as create } from "zustand"
-import createStore from "../lib/store"
+import createPluginSlice, { PluginSlice } from "../lib/store/createPluginSlice"
+import createGlobalSlice, { GlobalsSlice } from "../lib/store/createGlobalsSlice"
 
-// @ts-expect-error TS(2554): Expected 1 arguments, but got 0.
-const StoreContext = createContext()
-const StoreProvider = ({ children }: any) => (
-  <StoreContext.Provider value={createStore()}>{children}</StoreContext.Provider>
-)
-// @ts-ignore
-const useAppStore = (selector: any) => create(useContext(StoreContext), selector)
+const StoreContext = createContext<StoreApi<AppState> | null>(null)
 
+export type AppState = PluginSlice & GlobalsSlice
+
+interface StoreProviderProps {
+  options?: Record<string, any> // should be defined later on for the props
path: apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/components/StoreProvider.tsx
user: login: andypf
body: Do we really need any here? Don't we know the possible types?
`string | number | boolean | object`
created_at: 2025-03-14T11:50:49Z
author_association: COLLABORATOR
diff_hunk: @@ -26,7 +48,11 @@ export const useGlobalsActions = () => useAppStore((state: any) => state.globals
 
 /// Plugin
path: apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/components/StoreProvider.tsx
user: login: andypf
body: remove one slash please
created_at: 2025-03-14T11:51:16Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,133 @@
+import React, { useState, useEffect } from "react"
+import { SearchInput, Select, SelectOption, Stack, InputGroup } from "@cloudoperators/juno-ui-components"
+import { usePluginActions, usePluginConfig, useStatusConditionFilter } from "./StoreProvider"
+import { StatusConditionFilter, LabelValuesFilter } from "../lib/store/createPluginSlice"
+const filtersStyles = `
+  bg-theme-background-lvl-1
+  py-2
+  px-4
+  my-px
+`
+
+const Toolbar = () => {
+  const { setSearchTerm, setStatusConditionFilter, setLabelValueFilter } = usePluginActions()
+  const [labelFilters, setLabelFilters] = useState<LabelValuesFilter[]>()
+  const [selectedLabel, setSelectedLabel] = useState<string>("All")
+  const [availableValues, setAvailableValues] = useState<string[]>([])
+  const [selectedValue, setSelectedValue] = useState<string | undefined>(undefined)
+
+  const statusOptions: StatusConditionFilter[] = ["All", "True", "False", "Unknown"]
+  const statusConditionFilter = useStatusConditionFilter()
+  const pluginConfig = usePluginConfig()
+  const handleSearchChange = (value: any) => {
+    // debounce setSearchTerm to avoid unnecessary re-renders
+    const debouncedSearchTerm = setTimeout(() => {
+      setSearchTerm(value.target.value)
+    }, 500)
+
+    return () => clearTimeout(debouncedSearchTerm)
+  }
+
+  useEffect(() => {
+    if (!pluginConfig) return
+    const labelMap = new Map<string, Set<string>>()
+
+    pluginConfig.forEach((item: any) => {
+      const labels = item?.metadata?.labels
+      if (labels && typeof labels === "object") {
+        Object.entries(labels).forEach(([key, value]) => {
+          if (!labelMap.has(key)) {
+            labelMap.set(key, new Set())
+          }
+          if (typeof value === "string") {
+            labelMap.get(key)?.add(value)
+          }
+        })
+      }
+    })
+
+    setLabelFilters(
+      Array.from(labelMap.entries()).map(([label, values]) => ({
+        label,
+        value: Array.from(values),
+      }))
+    )
+  }, [pluginConfig])
+
+  const handleLabelChange = (value?: string | number | string[] | undefined) => {
+    setSelectedValue(undefined)
+    setLabelValueFilter(undefined)
+
+    if (!labelFilters) return
+
+    const selectedValue = typeof value === "string" ? value : "All"
+    setSelectedLabel(selectedValue)
+
+    if (selectedValue == "All") {
+      setAvailableValues([])
+    } else {
+      const filter = labelFilters.find((filter) => filter?.label === selectedValue)
+      setAvailableValues(filter?.value || [])
+    }
+  }
+
+  const handleValueChange = (value?: string | number | string[] | undefined) => {
+    setSelectedValue(value !== undefined ? String(value) : "")
+    setLabelValueFilter({ label: selectedLabel, value: value })
+  }
+
+  return (
+    <Stack alignment="center" gap="8" className={`filters ${filtersStyles}`}>
+      <Stack alignment="center" gap="4" className={`filters ${filtersStyles}`}>
+        <Select
+          required
+          label="Ready"
+          value={statusConditionFilter}
+          className="filter-label-select w-32 mb-0"
+          onChange={(value: any) => setStatusConditionFilter(value)}
+        >
+          {statusOptions.map((option) => (
+            <SelectOption key={option} label={option} value={option} />
+          ))}
+        </Select>
+        <InputGroup>
+          <Select
+            required
+            label="Label"
+            value={selectedLabel}
+            className="filter-label-select w-64 mb-0"
+            onChange={handleLabelChange}
+          >
+            <SelectOption key="All" label="All" value="All" />
+            {labelFilters &&
+              labelFilters.map((filter) => (
+                <SelectOption key={filter?.label} label={filter?.label} value={filter?.label} />
path: apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/components/Toolbar.tsx
user: login: andypf
body: Is it resolved? I don't see the mentioned className
created_at: 2025-03-14T11:53:19Z
author_association: COLLABORATOR
diff_hunk: @@ -1,48 +1,80 @@
 /*
- * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors
+ * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
  * SPDX-License-Identifier: Apache-2.0
  */
 
+import { StateCreator } from "zustand"
+import { getStatusCondition } from "../../hooks/helper"
+
+interface PluginState {
+  pluginConfig: any[] | null
+  filteredPluginConfigs: any[] | null
+  showDetailsFor: any | null
+  searchTerm: string
+  labelValuesFilters: LabelValuesFilter[] | undefined
+}
+
+export type LabelValueFilter = { label: string; value: string } | undefined
+export type LabelValuesFilter = { label: string; value: string[] } | undefined
+
+interface PluginActions {
+  setPluginConfig: (pluginConfig: any[]) => void
+  addPluginConfigItems: (pluginConfigItems: any[]) => void
+  modifyPluginConfigItems: (modifiedItems: any[]) => void
+  deletePluginConfigItems: (pluginConfigItems: any[]) => void
+  setShowDetailsFor: (showDetailsFor: any | null) => void
+  setSearchTerm: (searchTerm: string) => void
+  addLabelValueFilter: (labelValueFilter: LabelValueFilter) => void
+  removeLabelValueFilter: (labelValueFilter: LabelValueFilter) => void
+  filterItems: () => void
+}
+
+export interface PluginSlice {
+  plugin: PluginState & { actions: PluginActions }
+}
+
 function sortPluginConfigItems(items: any) {
path: apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/lib/store/createPluginSlice.ts
user: login: andypf
body: Is the type of items unknown? Maybe using zod helps?
created_at: 2025-03-14T11:55:03Z
author_association: COLLABORATOR
diff_hunk: @@ -1,21 +1,43 @@
 /*
- * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors
+ * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
  * SPDX-License-Identifier: Apache-2.0
  */
 
 import React, { createContext, useContext } from "react"
+import { createStore, useStore, StoreApi } from "zustand"
 
-import { useStore as create } from "zustand"
-import createStore from "../lib/store"
+import createPluginSlice, { PluginSlice } from "../lib/store/createPluginSlice"
+import createGlobalSlice, { GlobalsSlice } from "../lib/store/createGlobalsSlice"
 
-// @ts-expect-error TS(2554): Expected 1 arguments, but got 0.
-const StoreContext = createContext()
-const StoreProvider = ({ children }: any) => (
-  <StoreContext.Provider value={createStore()}>{children}</StoreContext.Provider>
-)
-// @ts-ignore
-const useAppStore = (selector: any) => create(useContext(StoreContext), selector)
+const StoreContext = createContext<StoreApi<AppState> | null>(null)
 
+export type AppState = PluginSlice & GlobalsSlice
+
+interface StoreProviderProps {
+  options?: Record<string, any> // should be defined later on for the props
path: apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/components/StoreProvider.tsx
user: login: TilmanHaupt
body: i think it can be deleted completly.
created_at: 2025-03-14T12:05:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,133 @@
+import React, { useState, useEffect } from "react"
+import { SearchInput, Select, SelectOption, Stack, InputGroup } from "@cloudoperators/juno-ui-components"
+import { usePluginActions, usePluginConfig, useStatusConditionFilter } from "./StoreProvider"
+import { StatusConditionFilter, LabelValuesFilter } from "../lib/store/createPluginSlice"
+const filtersStyles = `
+  bg-theme-background-lvl-1
+  py-2
+  px-4
+  my-px
+`
+
+const Toolbar = () => {
+  const { setSearchTerm, setStatusConditionFilter, setLabelValueFilter } = usePluginActions()
+  const [labelFilters, setLabelFilters] = useState<LabelValuesFilter[]>()
+  const [selectedLabel, setSelectedLabel] = useState<string>("All")
+  const [availableValues, setAvailableValues] = useState<string[]>([])
+  const [selectedValue, setSelectedValue] = useState<string | undefined>(undefined)
+
+  const statusOptions: StatusConditionFilter[] = ["All", "True", "False", "Unknown"]
+  const statusConditionFilter = useStatusConditionFilter()
+  const pluginConfig = usePluginConfig()
+  const handleSearchChange = (value: any) => {
+    // debounce setSearchTerm to avoid unnecessary re-renders
+    const debouncedSearchTerm = setTimeout(() => {
+      setSearchTerm(value.target.value)
+    }, 500)
+
+    return () => clearTimeout(debouncedSearchTerm)
+  }
+
+  useEffect(() => {
+    if (!pluginConfig) return
+    const labelMap = new Map<string, Set<string>>()
+
+    pluginConfig.forEach((item: any) => {
+      const labels = item?.metadata?.labels
+      if (labels && typeof labels === "object") {
+        Object.entries(labels).forEach(([key, value]) => {
+          if (!labelMap.has(key)) {
+            labelMap.set(key, new Set())
+          }
+          if (typeof value === "string") {
+            labelMap.get(key)?.add(value)
+          }
+        })
+      }
+    })
+
+    setLabelFilters(
+      Array.from(labelMap.entries()).map(([label, values]) => ({
+        label,
+        value: Array.from(values),
+      }))
+    )
+  }, [pluginConfig])
+
+  const handleLabelChange = (value?: string | number | string[] | undefined) => {
+    setSelectedValue(undefined)
+    setLabelValueFilter(undefined)
+
+    if (!labelFilters) return
+
+    const selectedValue = typeof value === "string" ? value : "All"
+    setSelectedLabel(selectedValue)
+
+    if (selectedValue == "All") {
+      setAvailableValues([])
+    } else {
+      const filter = labelFilters.find((filter) => filter?.label === selectedValue)
+      setAvailableValues(filter?.value || [])
+    }
+  }
+
+  const handleValueChange = (value?: string | number | string[] | undefined) => {
+    setSelectedValue(value !== undefined ? String(value) : "")
+    setLabelValueFilter({ label: selectedLabel, value: value })
+  }
+
+  return (
+    <Stack alignment="center" gap="8" className={`filters ${filtersStyles}`}>
+      <Stack alignment="center" gap="4" className={`filters ${filtersStyles}`}>
+        <Select
+          required
+          label="Ready"
+          value={statusConditionFilter}
+          className="filter-label-select w-32 mb-0"
+          onChange={(value: any) => setStatusConditionFilter(value)}
+        >
+          {statusOptions.map((option) => (
+            <SelectOption key={option} label={option} value={option} />
+          ))}
+        </Select>
+        <InputGroup>
+          <Select
+            required
+            label="Label"
+            value={selectedLabel}
+            className="filter-label-select w-64 mb-0"
+            onChange={handleLabelChange}
+          >
+            <SelectOption key="All" label="All" value="All" />
+            {labelFilters &&
+              labelFilters.map((filter) => (
+                <SelectOption key={filter?.label} label={filter?.label} value={filter?.label} />
path: apps/greenhouse/src/components/core-apps/org-admin/components/plugins/plugins/components/Toolbar.tsx
user: login: TilmanHaupt
body: it is. Its a prop from the select

created_at: 2025-03-14T12:45:20Z
author_association: CONTRIBUTOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-03-11T15:31:36Z
body: ###  🦋  Changeset detected

Latest commit: a6013fa82dca3a10ba032e2529950a86ce1cc7bf

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name                                | Type  |
| ----------------------------------- | ----- |
| @cloudoperators/juno-app-greenhouse | Minor |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/cloudoperators/juno/new/tilman-plugin-filter?filename=.changeset/calm-apples-exist.md&value=---%0A%22%40cloudoperators%2Fjuno-app-greenhouse%22%3A%20patch%0A%22%40cloudoperators%2Fjuno-app-supernova%22%3A%20patch%0A---%0A%0Afeat(greenhouse)%3A%20pluginAdmin%20filtering%0A)


author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-11T15:33:59Z
body: [PR Preview Action](https://github.com/rossjrw/pr-preview-action) v1.6.0
:---:
Preview removed because the pull request was closed.
2025-03-14 13:37 UTC
<!-- Sticky Pull Request Commentpr-preview -->
author_association: CONTRIBUTOR
user: login: uwe-mayer
created_at: 2025-03-12T10:46:20Z
body: Is it a big change to implement [initialFilters](https://github.com/cloudoperators/juno/tree/b9349c7bb9df19cd6dfcbd2cf08d8fa9b269fa3b/apps/supernova#initial-filters-optional) as they are available as `prop` on supernova?

Just thinking about prefiltering the content according to `support_group` claim from the token. Would be nice to have the functionality already in place. But do not want to block or delay this PR.

WDYT @edda ?
author_association: CONTRIBUTOR
user: login: ArtieReus
created_at: 2025-03-13T13:00:57Z
body: I have to UX Remarks to fix:
- Space between message and button
![Screenshot 2025-03-13 at 13 59 41](https://github.com/user-attachments/assets/b3cb0900-f068-4f64-9eec-085e48d9659d)
- After setting the value of a filter reset the input field
![Screenshot 2025-03-13 at 13 59 47](https://github.com/user-attachments/assets/e1bba5f7-fefd-438c-8454-2dc23498ba5d)

author_association: COLLABORATOR
user: login: TilmanHaupt
created_at: 2025-03-13T13:05:29Z
body: > I'm going to approve but have two comments:
> 
> 1. The message at the top of the page should have a margin-bottom so that it has some space to the "Add Plugin" button.
> 2. I'm not sure if the `truncateOptions` option does us any favors here with the filter labels being as long as they are. We can keep it like this for now and see what it will look like in the live system but I'm expecting this to be not really useable and we'll have to come up with a different solution, e.g. parsing the labels or going with the word-break solution instead.

1. Will be done
2. True 
author_association: CONTRIBUTOR
user: login: uwe-mayer
created_at: 2025-03-13T13:17:20Z
body: Thanks for this!

I think I am missing filtering for:
`Plugin.Spec.ClusterName`
and
`Plugin.Spec.PluginDefinition`

as in the [feature request](https://github.com/cloudoperators/juno/issues/754)

or am I missing something?
author_association: CONTRIBUTOR