diff --git a/open-lens/integration/__tests__/cluster-pages.tests.ts b/open-lens/integration/__tests__/cluster-pages.tests.ts index 30bbc79ed9a3..b5c72446613a 100644 --- a/open-lens/integration/__tests__/cluster-pages.tests.ts +++ b/open-lens/integration/__tests__/cluster-pages.tests.ts @@ -106,14 +106,13 @@ describeIf(minikubeReady(TEST_NAMESPACE))("Minikube based tests", () => { await navigateToPods(frame); - const namespacesSelector = await frame.waitForSelector( - ".NamespaceSelect", - ); + const namespacesSelector = await frame.waitForSelector(".namespace-select-filter .menu .non-icon label"); await namespacesSelector.click(); await namespacesSelector.type(TEST_NAMESPACE); - await namespacesSelector.press("Enter"); - await namespacesSelector.click(); + await frame.page().keyboard.press("Tab", { delay: 10 }); + await frame.page().keyboard.press("Tab", { delay: 10 }); + await frame.page().keyboard.press("Enter", { delay: 10 }); await frame.click(".Icon.new-dock-tab"); diff --git a/packages/core/src/extensions/renderer-api/components.ts b/packages/core/src/extensions/renderer-api/components.ts index 36ce3bba0a64..2d1a89112d2b 100644 --- a/packages/core/src/extensions/renderer-api/components.ts +++ b/packages/core/src/extensions/renderer-api/components.ts @@ -94,7 +94,7 @@ export * from "../../renderer/components/stepper"; export * from "../../renderer/components/wizard"; export * from "../../renderer/components/workloads-pods/pod-details-list"; export * from "../../renderer/components/namespaces/namespace-select"; -export * from "../../renderer/components/namespaces/namespace-select-filter"; +export * from "../../renderer/components/namespace-select-filter/component"; export * from "../../renderer/components/layout/sub-title"; export * from "../../renderer/components/input/search-input"; export * from "../../renderer/components/chart/bar-chart"; diff --git a/packages/core/src/features/cluster/__snapshots__/custom-resources-in-sidebar.test.tsx.snap b/packages/core/src/features/cluster/__snapshots__/custom-resources-in-sidebar.test.tsx.snap index fb6f87ae7c51..74303d3ab114 100644 --- a/packages/core/src/features/cluster/__snapshots__/custom-resources-in-sidebar.test.tsx.snap +++ b/packages/core/src/features/cluster/__snapshots__/custom-resources-in-sidebar.test.tsx.snap @@ -293,82 +293,48 @@ exports[`cluster - custom resources in sidebar renders 1`] = ` Overview
- -
+
+
+ Namespaces: +
- - -
+ class="gradient right" + />
+ + + expand_more + +
@@ -844,82 +810,48 @@ exports[`cluster - custom resources in sidebar when custom resource definitions Overview
- -
+
+
+ Namespaces: +
- - -
+ class="gradient right" + />
+ + + expand_more + +
@@ -1359,82 +1291,48 @@ exports[`cluster - custom resources in sidebar when custom resource exists rende Overview
- -
+
+
+ Namespaces: +
- - -
+ class="gradient right" + />
+ + + expand_more + +
@@ -1910,82 +1808,48 @@ exports[`cluster - custom resources in sidebar when custom resource exists when Overview
- -
+
+
+ Namespaces: +
- - -
+ class="gradient right" + />
+ + + expand_more + +
@@ -2481,82 +2345,48 @@ exports[`cluster - custom resources in sidebar when custom resource exists when Overview
- -
+
+
+ Namespaces: +
- - -
+ class="gradient right" + />
+ + + expand_more + +
@@ -3032,82 +2862,48 @@ exports[`cluster - custom resources in sidebar when custom resource exists when Overview
- -
+
+
+ Namespaces: +
- - -
+ class="gradient right" + />
+ + + expand_more + +
@@ -3614,82 +3410,48 @@ exports[`cluster - custom resources in sidebar when custom resource exists when Overview
- -
+
+
+ Namespaces: +
- - -
+ class="gradient right" + />
+ + + expand_more + +
@@ -4216,82 +3978,48 @@ exports[`cluster - custom resources in sidebar when custom resource exists when Overview
- -
+
+
+ Namespaces: +
- - -
+ class="gradient right" + />
+ + + expand_more + +
diff --git a/packages/core/src/features/cluster/__snapshots__/legacy-extension-adding-cluster-frame-components.test.tsx.snap b/packages/core/src/features/cluster/__snapshots__/legacy-extension-adding-cluster-frame-components.test.tsx.snap index cfcea5deb305..f3e9803053c5 100644 --- a/packages/core/src/features/cluster/__snapshots__/legacy-extension-adding-cluster-frame-components.test.tsx.snap +++ b/packages/core/src/features/cluster/__snapshots__/legacy-extension-adding-cluster-frame-components.test.tsx.snap @@ -293,82 +293,48 @@ exports[`legacy extension adding cluster frame components given custom component Overview
- -
+
+
+ Namespaces: +
- - -
+ class="gradient right" + />
+ + + expand_more + +
diff --git a/packages/core/src/features/cluster/__snapshots__/sidebar-and-tab-navigation-for-core.test.tsx.snap b/packages/core/src/features/cluster/__snapshots__/sidebar-and-tab-navigation-for-core.test.tsx.snap index a27c6874174c..bd98267b2ba0 100644 --- a/packages/core/src/features/cluster/__snapshots__/sidebar-and-tab-navigation-for-core.test.tsx.snap +++ b/packages/core/src/features/cluster/__snapshots__/sidebar-and-tab-navigation-for-core.test.tsx.snap @@ -322,82 +322,48 @@ exports[`cluster - sidebar and tab navigation for core given core registrations Overview
- -
+
+
+ Namespaces: +
- - -
+ class="gradient right" + />
+ + + expand_more + +
@@ -866,82 +832,48 @@ exports[`cluster - sidebar and tab navigation for core given core registrations Overview
- -
+
+
+ Namespaces: +
- - -
+ class="gradient right" + />
+ + + expand_more + +
@@ -1430,82 +1362,48 @@ exports[`cluster - sidebar and tab navigation for core given core registrations Overview
- -
+
+
+ Namespaces: +
- - -
+ class="gradient right" + />
+ + + expand_more + +
@@ -2857,82 +2755,48 @@ exports[`cluster - sidebar and tab navigation for core given core registrations Overview
- -
+
+
+ Namespaces: +
- - -
+ class="gradient right" + />
+ + + expand_more + +
@@ -3401,82 +3265,48 @@ exports[`cluster - sidebar and tab navigation for core given core registrations Overview
- -
+
+
+ Namespaces: +
- - -
+ class="gradient right" + />
+ + + expand_more + +
diff --git a/packages/core/src/features/cluster/__snapshots__/sidebar-and-tab-navigation-for-extensions.test.tsx.snap b/packages/core/src/features/cluster/__snapshots__/sidebar-and-tab-navigation-for-extensions.test.tsx.snap index a64f9c2aa273..e3490380a3c0 100644 --- a/packages/core/src/features/cluster/__snapshots__/sidebar-and-tab-navigation-for-extensions.test.tsx.snap +++ b/packages/core/src/features/cluster/__snapshots__/sidebar-and-tab-navigation-for-extensions.test.tsx.snap @@ -322,82 +322,48 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit Overview
- -
+
+
+ Namespaces: +
- - -
+ class="gradient right" + />
+ + + expand_more + +
@@ -866,82 +832,48 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit Overview
- -
+
+
+ Namespaces: +
- - -
+ class="gradient right" + />
+ + + expand_more + +
@@ -1446,82 +1378,48 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit Overview
- -
+
+
+ Namespaces: +
- - -
+ class="gradient right" + />
+ + + expand_more + +
@@ -3486,82 +3384,48 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit Overview
- -
+
+
+ Namespaces: +
- - -
+ class="gradient right" + />
+ + + expand_more + +
@@ -4030,82 +3894,48 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit Overview
- -
+
+
+ Namespaces: +
- - -
+ class="gradient right" + />
+ + + expand_more + +
diff --git a/packages/core/src/features/cluster/__snapshots__/visibility-of-sidebar-items.test.tsx.snap b/packages/core/src/features/cluster/__snapshots__/visibility-of-sidebar-items.test.tsx.snap index 85601255c3c7..9972010a42e7 100644 --- a/packages/core/src/features/cluster/__snapshots__/visibility-of-sidebar-items.test.tsx.snap +++ b/packages/core/src/features/cluster/__snapshots__/visibility-of-sidebar-items.test.tsx.snap @@ -293,82 +293,48 @@ exports[`cluster - visibility of sidebar items given kube resource for route is Overview
- -
+
+
+ Namespaces: +
- - -
+ class="gradient right" + />
+ + + expand_more + +
@@ -848,82 +814,48 @@ exports[`cluster - visibility of sidebar items given kube resource for route is Overview
- -
+
+
+ Namespaces: +
- - -
+ class="gradient right" + />
+ + + expand_more + +
diff --git a/packages/core/src/features/cluster/__snapshots__/workload-overview.test.tsx.snap b/packages/core/src/features/cluster/__snapshots__/workload-overview.test.tsx.snap index 1a4f905eaacc..17280877e33c 100644 --- a/packages/core/src/features/cluster/__snapshots__/workload-overview.test.tsx.snap +++ b/packages/core/src/features/cluster/__snapshots__/workload-overview.test.tsx.snap @@ -307,82 +307,48 @@ exports[`workload overview when navigating to workload overview renders 1`] = ` Overview
- -
+
+
+ Namespaces: +
- - -
+ class="gradient right" + />
+ + + expand_more + +
diff --git a/packages/core/src/features/cluster/extension-api/__snapshots__/disable-cluster-pages-when-cluster-is-not-relevant.test.tsx.snap b/packages/core/src/features/cluster/extension-api/__snapshots__/disable-cluster-pages-when-cluster-is-not-relevant.test.tsx.snap index 9e2aa77d4511..516098a9c4da 100644 --- a/packages/core/src/features/cluster/extension-api/__snapshots__/disable-cluster-pages-when-cluster-is-not-relevant.test.tsx.snap +++ b/packages/core/src/features/cluster/extension-api/__snapshots__/disable-cluster-pages-when-cluster-is-not-relevant.test.tsx.snap @@ -689,82 +689,48 @@ exports[`disable-cluster-pages-when-cluster-is-not-relevant given extension shou Overview
- -
+
+
+ Namespaces: +
- - -
+ class="gradient right" + />
+ + + expand_more + +
@@ -1206,82 +1172,48 @@ exports[`disable-cluster-pages-when-cluster-is-not-relevant given not yet known Overview
- -
+
+
+ Namespaces: +
- - -
+ class="gradient right" + />
+ + + expand_more + +
diff --git a/packages/core/src/features/cluster/extension-api/__snapshots__/disable-sidebar-items-when-cluster-is-not-relevant.test.tsx.snap b/packages/core/src/features/cluster/extension-api/__snapshots__/disable-sidebar-items-when-cluster-is-not-relevant.test.tsx.snap index 62dc47d37c30..b6c81d4d7d28 100644 --- a/packages/core/src/features/cluster/extension-api/__snapshots__/disable-sidebar-items-when-cluster-is-not-relevant.test.tsx.snap +++ b/packages/core/src/features/cluster/extension-api/__snapshots__/disable-sidebar-items-when-cluster-is-not-relevant.test.tsx.snap @@ -312,82 +312,48 @@ exports[`disable sidebar items when cluster is not relevant given extension shou Overview
- -
+
+
+ Namespaces: +
- - -
+ class="gradient right" + />
+ + + expand_more + +
@@ -829,82 +795,48 @@ exports[`disable sidebar items when cluster is not relevant given extension shou Overview
- -
+
+
+ Namespaces: +
- - -
+ class="gradient right" + />
+ + + expand_more + +
@@ -1346,82 +1278,48 @@ exports[`disable sidebar items when cluster is not relevant given not yet known Overview
- -
+
+
+ Namespaces: +
- - -
+ class="gradient right" + />
+ + + expand_more + +
diff --git a/packages/core/src/features/cluster/kube-object-details/extension-api/__snapshots__/disable-kube-object-detail-items-when-cluster-is-not-relevant.test.tsx.snap b/packages/core/src/features/cluster/kube-object-details/extension-api/__snapshots__/disable-kube-object-detail-items-when-cluster-is-not-relevant.test.tsx.snap index 140d800d9905..6ed73c49b310 100644 --- a/packages/core/src/features/cluster/kube-object-details/extension-api/__snapshots__/disable-kube-object-detail-items-when-cluster-is-not-relevant.test.tsx.snap +++ b/packages/core/src/features/cluster/kube-object-details/extension-api/__snapshots__/disable-kube-object-detail-items-when-cluster-is-not-relevant.test.tsx.snap @@ -365,82 +365,48 @@ exports[`disable kube object detail items when cluster is not relevant given ext Overview
- -
+
+
+ Namespaces: +
- - -
+ class="gradient right" + />
+ + + expand_more + +
@@ -948,82 +914,48 @@ exports[`disable kube object detail items when cluster is not relevant given ext Overview
- -
+
+
+ Namespaces: +
- - -
+ class="gradient right" + />
+ + + expand_more + +
@@ -1531,82 +1463,48 @@ exports[`disable kube object detail items when cluster is not relevant given not Overview
- -
+
+
+ Namespaces: +
- - -
+ class="gradient right" + />
+ + + expand_more + +
diff --git a/packages/core/src/features/cluster/kube-object-details/extension-api/__snapshots__/reactively-hide-kube-object-detail-item.test.tsx.snap b/packages/core/src/features/cluster/kube-object-details/extension-api/__snapshots__/reactively-hide-kube-object-detail-item.test.tsx.snap index aa580d82f69e..311a2e839a65 100644 --- a/packages/core/src/features/cluster/kube-object-details/extension-api/__snapshots__/reactively-hide-kube-object-detail-item.test.tsx.snap +++ b/packages/core/src/features/cluster/kube-object-details/extension-api/__snapshots__/reactively-hide-kube-object-detail-item.test.tsx.snap @@ -360,82 +360,48 @@ exports[`reactively hide kube object detail item renders 1`] = ` Overview
- -
+
+
+ Namespaces: +
- - -
+ class="gradient right" + />
+ + + expand_more + +
@@ -948,82 +914,48 @@ exports[`reactively hide kube object detail item when the item is shown renders Overview
- -
+
+
+ Namespaces: +
- - -
+ class="gradient right" + />
+ + + expand_more + +
diff --git a/packages/core/src/features/cluster/namespaces/__snapshots__/edit-namespace-from-previously-opened-tab.test.tsx.snap b/packages/core/src/features/cluster/namespaces/__snapshots__/edit-namespace-from-previously-opened-tab.test.tsx.snap index af16c7648e53..26bb97969ec3 100644 --- a/packages/core/src/features/cluster/namespaces/__snapshots__/edit-namespace-from-previously-opened-tab.test.tsx.snap +++ b/packages/core/src/features/cluster/namespaces/__snapshots__/edit-namespace-from-previously-opened-tab.test.tsx.snap @@ -319,82 +319,48 @@ exports[`cluster/namespaces - edit namespaces from previously opened tab given t Overview
- -
+
+
+ Namespaces: +
- - -
+ class="gradient right" + />
+ + + expand_more + +
@@ -871,82 +837,48 @@ exports[`cluster/namespaces - edit namespaces from previously opened tab given t Overview
- -
+
+
+ Namespaces: +
- - -
+ class="gradient right" + />
+ + + expand_more + +
diff --git a/packages/core/src/features/cluster/workloads/__snapshots__/pods.test.tsx.snap b/packages/core/src/features/cluster/workloads/__snapshots__/pods.test.tsx.snap index d037610871a9..1447bfb3f6b3 100644 --- a/packages/core/src/features/cluster/workloads/__snapshots__/pods.test.tsx.snap +++ b/packages/core/src/features/cluster/workloads/__snapshots__/pods.test.tsx.snap @@ -311,82 +311,48 @@ exports[`workloads / pods when navigating to workloads / pods view given a names 1 item
- -
+
+
+ Namespace: default +
- - -
+ class="gradient right" + />
+ + + expand_more + +
- -
+
+
+ Namespace: default +
- - -
+ class="gradient right" + />
+ + + expand_more + +
- -
+
+
+ Namespace: default +
- - -
+ class="gradient right" + />
+ + + expand_more + +
- -
+
+
+ Namespaces: +
- - -
+ class="gradient right" + />
+ + + expand_more + +
@@ -816,82 +782,48 @@ exports[`disable workloads overview details when cluster is not relevant given e Overview
- -
+
+
+ Namespaces: +
- - -
+ class="gradient right" + />
+ + + expand_more + +
@@ -1333,82 +1265,48 @@ exports[`disable workloads overview details when cluster is not relevant given n Overview
- -
+
+
+ Namespaces: +
- - -
+ class="gradient right" + />
+ + + expand_more + +
diff --git a/packages/core/src/features/helm-charts/installing-chart/__snapshots__/installing-helm-chart-from-new-tab.test.ts.snap b/packages/core/src/features/helm-charts/installing-chart/__snapshots__/installing-helm-chart-from-new-tab.test.ts.snap index d7ca67d88416..258dec4e874f 100644 --- a/packages/core/src/features/helm-charts/installing-chart/__snapshots__/installing-helm-chart-from-new-tab.test.ts.snap +++ b/packages/core/src/features/helm-charts/installing-chart/__snapshots__/installing-helm-chart-from-new-tab.test.ts.snap @@ -294,82 +294,48 @@ exports[`installing helm chart from new tab given tab for installing chart was n Overview
- -
+
+
+ Namespaces: default, some-other-namespace +
- - -
+ class="gradient right" + />
+ + + expand_more + +
@@ -10437,82 +10403,48 @@ exports[`installing helm chart from new tab given tab for installing chart was n 0 items
- -
+
+
+ Namespaces: default, some-other-namespace +
- - -
+ class="gradient right" + />
+ + + expand_more + +
- -
+
+
+ Namespaces: default, some-other-namespace +
- - -
+ class="gradient right" + />
+ + + expand_more + +
@@ -821,82 +787,48 @@ exports[`installing helm chart from previously opened tab given tab for installi Overview
- -
+
+
+ Namespaces: default, some-other-namespace +
- - -
+ class="gradient right" + />
+ + + expand_more + +
diff --git a/packages/core/src/features/helm-charts/upgrade-chart/__snapshots__/upgrade-chart-new-tab.test.ts.snap b/packages/core/src/features/helm-charts/upgrade-chart/__snapshots__/upgrade-chart-new-tab.test.ts.snap index 11789fe6bbc7..49d7c61295cf 100644 --- a/packages/core/src/features/helm-charts/upgrade-chart/__snapshots__/upgrade-chart-new-tab.test.ts.snap +++ b/packages/core/src/features/helm-charts/upgrade-chart/__snapshots__/upgrade-chart-new-tab.test.ts.snap @@ -311,82 +311,48 @@ exports[`New Upgrade Helm Chart Dock Tab given a namespace is selected when navi 0 items
- -
+
+
+ Namespace: my-second-namespace +
- - -
+ class="gradient right" + />
+ + + expand_more + +
- -
+
+
+ Namespace: my-second-namespace +
- - -
+ class="gradient right" + />
+ + + expand_more + +
- -
+
+
+ Namespace: my-second-namespace +
- - -
+ class="gradient right" + />
+ + + expand_more + +
- -
+
+
+ Namespace: my-second-namespace +
- - -
+ class="gradient right" + />
+ + + expand_more + +
- -
+
+
+ Namespace: my-second-namespace +
- - -
+ class="gradient right" + />
+ + + expand_more + +
- -
+
+
+ Namespace: my-second-namespace +
- - -
+ class="gradient right" + />
+ + + expand_more + +
- -
+
+
+ Namespace: my-second-namespace +
- - -
+ class="gradient right" + />
+ + + expand_more + +
- -
+
+
+ Namespaces: some-namespace, some-other-namespace +
- - -
+ class="gradient right" + />
+ + + expand_more + +
- -
+
+
+ Namespaces: some-namespace, some-other-namespace +
- - -
+ class="gradient right" + />
+ + + expand_more + +
- -
+
+
+ Namespaces: some-namespace, some-other-namespace +
- - -
+ class="gradient right" + />
+ + + expand_more + +
- -
+
+
+ Namespaces: some-namespace, some-other-namespace +
- - -
+ class="gradient right" + />
+ + + expand_more + +
- -
+
+
+ Namespaces: some-namespace, some-other-namespace +
- - -
+ class="gradient right" + />
+ + + expand_more + +
- -
+
+
+ Namespaces: some-namespace, some-other-namespace +
- - -
+ class="gradient right" + />
+ + + expand_more + +
- -
+
+
+ Namespaces: some-namespace, some-other-namespace +
- - -
+ class="gradient right" + />
+ + + expand_more + +
- -
+
+
+ Namespaces: some-namespace, some-other-namespace +
- - -
+ class="gradient right" + />
+ + + expand_more + +
- -
+
+
+ Namespaces: some-namespace, some-other-namespace +
- - -
+ class="gradient right" + />
+ + + expand_more + +
- -
+
+
+ Namespaces: some-namespace, some-other-namespace +
- - -
+ class="gradient right" + />
+ + + expand_more + +
- -
+
+
+ Namespaces: some-namespace, some-other-namespace +
- - -
+ class="gradient right" + />
+ + + expand_more + +
- -
+
+
+ Namespaces: some-namespace, some-other-namespace +
- - -
+ class="gradient right" + />
+ + + expand_more + +
- -
+
+
+ Namespaces: some-namespace, some-other-namespace +
- - -
+ class="gradient right" + />
+ + + expand_more + +
- -
+
+
+ Namespaces: some-namespace, some-other-namespace +
- - -
+ class="gradient right" + />
+ + + expand_more + +
- -
+
+
+ Namespaces: some-namespace, some-other-namespace +
- - -
+ class="gradient right" + />
+ + + expand_more + +
- -
+
+
+ Namespaces: some-namespace, some-other-namespace +
- - -
+ class="gradient right" + />
+ + + expand_more + +
- -
+
+
+ Namespaces: some-namespace, some-other-namespace +
- - -
+ class="gradient right" + />
+ + + expand_more + +
- -
+
+
+ Namespaces: some-namespace, some-other-namespace +
- - -
+ class="gradient right" + />
+ + + expand_more + +
- -
+
+
+ Namespaces: +
- - -
+ class="gradient right" + />
+ + + expand_more + +
@@ -1154,82 +1120,48 @@ exports[`download logs options in logs dock tab opening pod logs when logs not a Overview
- -
+
+
+ Namespaces: +
- - -
+ class="gradient right" + />
+ + + expand_more + +
diff --git a/packages/core/src/renderer/components/config-vertical-pod-autoscalers/vpa.tsx b/packages/core/src/renderer/components/config-vertical-pod-autoscalers/vpa.tsx index 242c66a7d247..9cffbd95acaf 100644 --- a/packages/core/src/renderer/components/config-vertical-pod-autoscalers/vpa.tsx +++ b/packages/core/src/renderer/components/config-vertical-pod-autoscalers/vpa.tsx @@ -14,9 +14,9 @@ import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import { SiblingsInTabLayout } from "../layout/siblings-in-tab-layout"; import { KubeObjectAge } from "../kube-object/age"; import type { VerticalPodAutoscalerStore } from "./store"; -import type { FilterByNamespace } from "../namespaces/namespace-select-filter-model/filter-by-namespace.injectable"; +import type { FilterByNamespace } from "../namespaces/filter-by-namespace.injectable"; import { withInjectables } from "@ogre-tools/injectable-react"; -import filterByNamespaceInjectable from "../namespaces/namespace-select-filter-model/filter-by-namespace.injectable"; +import filterByNamespaceInjectable from "../namespaces/filter-by-namespace.injectable"; import verticalPodAutoscalerStoreInjectable from "./store.injectable"; enum columnId { diff --git a/packages/core/src/renderer/components/helm-releases/releases.tsx b/packages/core/src/renderer/components/helm-releases/releases.tsx index 514e7593e7bb..9a31a9e10b77 100644 --- a/packages/core/src/renderer/components/helm-releases/releases.tsx +++ b/packages/core/src/renderer/components/helm-releases/releases.tsx @@ -11,7 +11,7 @@ import type { HelmRelease } from "../../../common/k8s-api/endpoints/helm-release import { withInjectables } from "@ogre-tools/injectable-react"; import type { ItemListStore } from "../item-object-list"; import { ItemListLayout } from "../item-object-list"; -import { NamespaceSelectFilter } from "../namespaces/namespace-select-filter"; +import { NamespaceSelectFilter } from "../namespace-select-filter/component"; import { kebabCase } from "lodash/fp"; import { HelmReleaseMenu } from "./release-menu"; import { ReleaseRollbackDialog } from "./dialog/dialog"; diff --git a/packages/core/src/renderer/components/kube-object-list-layout/__snapshots__/kube-object-list-layout.test.tsx.snap b/packages/core/src/renderer/components/kube-object-list-layout/__snapshots__/kube-object-list-layout.test.tsx.snap index 21c6b0c90fc2..aed75cccc456 100644 --- a/packages/core/src/renderer/components/kube-object-list-layout/__snapshots__/kube-object-list-layout.test.tsx.snap +++ b/packages/core/src/renderer/components/kube-object-list-layout/__snapshots__/kube-object-list-layout.test.tsx.snap @@ -21,82 +21,48 @@ exports[`kube-object-list-layout given pod store renders 1`] = ` 0 items
- -
+
+
+ All namespaces +
- - -
+ class="gradient right" + />
+ + + expand_more + +
diff --git a/packages/core/src/renderer/components/kube-object-list-layout/kube-object-list-layout.tsx b/packages/core/src/renderer/components/kube-object-list-layout/kube-object-list-layout.tsx index 9280d55ac89e..72e393fe73b7 100644 --- a/packages/core/src/renderer/components/kube-object-list-layout/kube-object-list-layout.tsx +++ b/packages/core/src/renderer/components/kube-object-list-layout/kube-object-list-layout.tsx @@ -14,7 +14,7 @@ import type { KubeJsonApiDataFor, KubeObject } from "@k8slens/kube-object"; import type { ItemListLayoutProps, ItemListStore } from "../item-object-list/list-layout"; import { ItemListLayout } from "../item-object-list/list-layout"; import { KubeObjectMenu } from "../kube-object-menu"; -import { NamespaceSelectFilter } from "../namespaces/namespace-select-filter"; +import { NamespaceSelectFilter } from "../namespace-select-filter/component"; import { ResourceKindMap, ResourceNames } from "../../utils/rbac"; import { Icon } from "@k8slens/icon"; import { TooltipPosition } from "@k8slens/tooltip"; diff --git a/packages/core/src/renderer/components/namespace-select-filter/__snapshots__/namespace-select-filter.test.tsx.snap b/packages/core/src/renderer/components/namespace-select-filter/__snapshots__/namespace-select-filter.test.tsx.snap new file mode 100644 index 000000000000..a7a0c15d6663 --- /dev/null +++ b/packages/core/src/renderer/components/namespace-select-filter/__snapshots__/namespace-select-filter.test.tsx.snap @@ -0,0 +1,7065 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` once the subscribe resolves renders 1`] = ` + +
+
+ + +`; + +exports[` once the subscribe resolves when clicked renders 1`] = ` + +
+
+ + +`; + +exports[` once the subscribe resolves when clicked when 'test-2' is clicked renders 1`] = ` + +
+
+ + +`; + +exports[` once the subscribe resolves when clicked when 'test-2' is clicked when clicked again renders 1`] = ` + +
+
+ + +`; + +exports[` once the subscribe resolves when clicked when 'test-2' is clicked when clicked again when 'test-1' is clicked renders 1`] = ` + +
+
+ + +`; + +exports[` once the subscribe resolves when clicked when 'test-2' is clicked when clicked again when 'test-1' is clicked when clicked again, then holding down multi select key when 'test-3' is clicked renders 1`] = ` + +
+
+ + +`; + +exports[` once the subscribe resolves when clicked when clicked again renders 1`] = ` + +
+
+ + +`; + +exports[` once the subscribe resolves when clicked when clicking the remove from selection button for 'test-2' renders 1`] = ` + +
+
+ + +`; + +exports[` once the subscribe resolves when clicked when clicking the remove from selection button for 'test-2' when clicking the remove from selection button for 'test-3' renders 1`] = ` + +
+
+ + +`; + +exports[` once the subscribe resolves when clicked when clicking the remove from selection button for 'test-2' when clicking the remove from selection button for 'test-3' when clicking the add to selection button for 'test-2' renders 1`] = ` + +
+
+ + +`; + +exports[` once the subscribe resolves when clicked when clicking the remove from selection button for 'test-2' when clicking the remove from selection button for 'test-3' when clicking the add to selection button for 'test-2' when clicking the 'select only' button for 'test-5' renders 1`] = ` + +
+
+ + +`; + +exports[` once the subscribe resolves when clicked when typing a glob style filter into the filter input renders 1`] = ` + +
+
+ + +`; + +exports[` once the subscribe resolves when clicked when typing in the filter input renders 1`] = ` + +
+
+ + +`; + +exports[` once the subscribe resolves when clicked when typing in the filter input when pressing the 'Enter' key renders 1`] = ` + +
+
+ + +`; + +exports[` once the subscribe resolves when menu expand icon is clicked renders 1`] = ` + +
+
+ + +`; + +exports[` once the subscribe resolves when menu expand icon is clicked when menu expand icon is clicked again renders 1`] = ` + +
+
+ + +`; + +exports[` once the subscribe resolves with thousands of namespaces renders 1`] = ` + +
+
+ + +`; + +exports[` once the subscribe resolves with thousands of namespaces when menu expand icon is clicked renders 1`] = ` + +
+
+ + +`; + +exports[` once the subscribe resolves with thousands of namespaces when menu expand icon is clicked when menu expand icon is clicked again renders 1`] = ` + +
+
+ + +`; diff --git a/packages/core/src/renderer/components/namespace-select-filter/component.tsx b/packages/core/src/renderer/components/namespace-select-filter/component.tsx new file mode 100644 index 000000000000..fba7de3d300e --- /dev/null +++ b/packages/core/src/renderer/components/namespace-select-filter/component.tsx @@ -0,0 +1,207 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import "./namespace-select-filter.scss"; + +import React, { useEffect, useRef } from "react"; +import { observer } from "mobx-react"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import type { NamespaceSelectFilterModel, NamespaceSelectFilterOption } from "./model.injectable"; +import namespaceSelectFilterModelInjectable, { selectAllNamespaces } from "./model.injectable"; +import { VariableSizeList } from "react-window"; +import { Icon } from "@k8slens/icon"; +import { cssNames, prevDefault } from "@k8slens/utilities"; +import { addWindowEventListener } from "../../window/event-listener.injectable"; +import { TooltipPosition } from "@k8slens/tooltip"; + +interface NamespaceSelectFilterProps { + id: string; +} + +interface Dependencies { + model: NamespaceSelectFilterModel; +} + +const Gradient = ({ type }: { type: "left" | "right" }) => ( +
+); + +const NamespaceSelectFilterMenu = observer(({ id, model }: Dependencies & NamespaceSelectFilterProps) => { + const selectedOptions = model.selectedOptions.get(); + const prefix = selectedOptions.length === 1 + ? "Namespace" + : "Namespaces"; + + return ( +
+
+ model.filterText.set(event.target.value)} + onClick={model.menu.open} + onKeyDown={model.input.onKeyDown} + data-testid="namespace-select-filter-input" + /> + + + +
+ +
+ ); +}); + +const rowHeight = 29; + +const NonInjectedNamespaceSelectFilter = observer(({ model, id }: Dependencies & NamespaceSelectFilterProps) => { + const divRef = useRef(null); + + useEffect(() => { + return addWindowEventListener("click", (event) => { + if (!model.menu.isOpen.get()) { + return; + } + + if (divRef.current?.contains(event.target as Node)) { + return; + } + + model.menu.close(); + }); + }, []); + + return ( +
+ + {model.menu.isOpen.get() && ( +
+ rowHeight} + itemCount={model.filteredOptions.get().length} + itemData={{ + items: model.filteredOptions.get(), + model, + }} + overscanCount={5} + innerElementType={"ul"} + > + {NamespaceSelectFilterRow} + +
+ )} +
+ ); +}); + +interface FilterRowProps { + index: number; + style: React.CSSProperties; + data: { + model: NamespaceSelectFilterModel; + items: NamespaceSelectFilterOption[]; + }; +} + +const renderSingleOptionIcons = (namespace: string, option: NamespaceSelectFilterOption, model: NamespaceSelectFilterModel) => { + if (model.isOptionSelected(option)) { + return ( + model.deselect(namespace))} + /> + ); + } + + return ( + model.select(namespace))} + /> + ); +}; + +const NamespaceSelectFilterRow = observer(({ index, style, data: { model, items }}: FilterRowProps) => { + const option = items[index]; + + return ( +
  • model.onClick(option)} + data-testid={`namespace-select-filter-option-${option.label}`} + > + {option.value === selectAllNamespaces + ? All Namespaces + : ( + <> + model.onClick(option))} + tooltip={{ + preferredPositions: TooltipPosition.LEFT, + children: `Select only ${option.value}`, + }} + data-testid={`namespace-select-filter-option-${option.value}-select-only`} + /> + {option.value} + {renderSingleOptionIcons(option.value, option, model)} + + )} +
  • + ); +}); + +export const NamespaceSelectFilter = withInjectables(NonInjectedNamespaceSelectFilter, { + getProps: (di, props) => ({ + model: di.inject(namespaceSelectFilterModelInjectable), + ...props, + }), +}); diff --git a/packages/core/src/renderer/components/namespaces/namespace-select-filter-model/is-selection-key.injectable.ts b/packages/core/src/renderer/components/namespace-select-filter/is-selection-key.injectable.ts similarity index 76% rename from packages/core/src/renderer/components/namespaces/namespace-select-filter-model/is-selection-key.injectable.ts rename to packages/core/src/renderer/components/namespace-select-filter/is-selection-key.injectable.ts index 3562b8a1665b..25779bcc521c 100644 --- a/packages/core/src/renderer/components/namespaces/namespace-select-filter-model/is-selection-key.injectable.ts +++ b/packages/core/src/renderer/components/namespace-select-filter/is-selection-key.injectable.ts @@ -4,7 +4,7 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import type React from "react"; -import isMacInjectable from "../../../../common/vars/is-mac.injectable"; +import isMacInjectable from "../../../common/vars/is-mac.injectable"; export type IsMultiSelectionKey = (event: React.KeyboardEvent) => boolean; @@ -12,10 +12,9 @@ const isMultiSelectionKeyInjectable = getInjectable({ id: "is-multi-selection-key", instantiate: (di): IsMultiSelectionKey => { const isMac = di.inject(isMacInjectable); + const specificKey = isMac ? "Meta" : "Control"; - return isMac - ? ({ key }) => key === "Meta" - : ({ key }) => key === "Control"; + return ({ key }) => key === specificKey; }, }); diff --git a/packages/core/src/renderer/components/namespace-select-filter/model.injectable.ts b/packages/core/src/renderer/components/namespace-select-filter/model.injectable.ts new file mode 100644 index 000000000000..ba0338bbd175 --- /dev/null +++ b/packages/core/src/renderer/components/namespace-select-filter/model.injectable.ts @@ -0,0 +1,205 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import namespaceStoreInjectable from "../namespaces/store.injectable"; +import isMultiSelectionKeyInjectable from "./is-selection-key.injectable"; +import clusterFrameContextForNamespacedResourcesInjectable from "../../cluster-frame-context/for-namespaced-resources.injectable"; +import type { IComputedValue, IObservableValue } from "mobx"; +import { action, comparer, computed, observable } from "mobx"; +import GlobToRegExp from "glob-to-regexp"; +import { observableCrate } from "@k8slens/utilities"; + + +export const selectAllNamespaces = Symbol("all-namespaces-selected"); + +export type SelectAllNamespaces = typeof selectAllNamespaces; +export interface NamespaceSelectFilterOption { + value: string | SelectAllNamespaces; + label: string; + id: string | SelectAllNamespaces; +} + +export interface NamespaceSelectFilterModel { + readonly options: IComputedValue; + readonly filteredOptions: IComputedValue; + readonly selectedOptions: IComputedValue; + readonly menu: { + open: () => void; + close: () => void; + toggle: () => void; + readonly isOpen: IComputedValue; + readonly hasSelectedAll: IComputedValue; + onKeyDown: React.KeyboardEventHandler; + onKeyUp: React.KeyboardEventHandler; + }; + readonly input: { + onKeyDown: React.KeyboardEventHandler; + }; + onClick: (options: NamespaceSelectFilterOption) => void; + deselect: (namespace: string) => void; + select: (namespace: string) => void; + readonly filterText: IObservableValue; + reset: () => void; + isOptionSelected: (option: NamespaceSelectFilterOption) => boolean; +} + +enum SelectMenuState { + Close = "close", + Open = "open", +} + +const filterBasedOnText = (filterText: string) => { + const regexp = new RegExp(GlobToRegExp(filterText, { extended: true, flags: "gi" })); + + return (options: NamespaceSelectFilterOption) => { + if (options.value === selectAllNamespaces) { + return true; + } + + return Boolean(options.value.match(regexp)); + }; +}; + +const namespaceSelectFilterModelInjectable = getInjectable({ + id: "namespace-select-filter-model", + + instantiate: (di) => { + const namespaceStore = di.inject(namespaceStoreInjectable); + const isMultiSelectionKey = di.inject(isMultiSelectionKeyInjectable); + const context = di.inject(clusterFrameContextForNamespacedResourcesInjectable); + + let didToggle = false; + let isMultiSelection = false; + const menuState = observableCrate(SelectMenuState.Close, [{ + from: SelectMenuState.Close, + to: SelectMenuState.Open, + onTransition: () => { + optionsSortingSelected.replace(selectedNames.get()); + didToggle = false; + }, + }]); + const filterText = observable.box(""); + const selectedNames = computed(() => new Set(context.contextNamespaces), { + equals: comparer.structural, + }); + const optionsSortingSelected = observable.set(selectedNames.get()); + const sortNamespacesByIfTheyHaveBeenSelected = (left: string, right: string) => { + const isLeftSelected = optionsSortingSelected.has(left); + const isRightSelected = optionsSortingSelected.has(right); + + if (isLeftSelected === isRightSelected) { + return 0; + } + + return isRightSelected + ? 1 + : -1; + }; + const options = computed((): NamespaceSelectFilterOption[] => [ + { + value: selectAllNamespaces, + label: "All Namespaces", + id: "all-namespaces", + }, + ...context + .allNamespaces + .sort(sortNamespacesByIfTheyHaveBeenSelected) + .map(namespace => ({ + value: namespace, + label: namespace, + id: namespace, + })), + ]); + const filteredOptions = computed(() => options.get().filter(filterBasedOnText(filterText.get()))); + const selectedOptions = computed(() => options.get().filter(model.isOptionSelected)); + const menuIsOpen = computed(() => menuState.get() === SelectMenuState.Open); + const isOptionSelected: NamespaceSelectFilterModel["isOptionSelected"] = (option) => { + if (option.value === selectAllNamespaces) { + return false; + } + + return selectedNames.get().has(option.value); + }; + + const model: NamespaceSelectFilterModel = { + options, + filteredOptions, + selectedOptions, + menu: { + close: action(() => { + menuState.set(SelectMenuState.Close); + filterText.set(""); + }), + open: action(() => { + menuState.set(SelectMenuState.Open); + }), + toggle: () => { + if (menuIsOpen.get()) { + model.menu.close(); + } else { + model.menu.open(); + } + }, + isOpen: menuIsOpen, + hasSelectedAll: computed(() => namespaceStore.areAllSelectedImplicitly), + onKeyDown: (event) => { + if (isMultiSelectionKey(event)) { + isMultiSelection = true; + } else if (event.key === "Escape") { + model.menu.close(); + } + }, + onKeyUp: (event) => { + if (isMultiSelectionKey(event)) { + isMultiSelection = false; + + if (didToggle) { + model.menu.close(); + } + } + }, + }, + input: { + onKeyDown: (event) => { + if (event.key === "Enter") { + const options = filteredOptions.get().slice(1); + + if (options.length >= 1) { + model.onClick(options[0]); + } + } + }, + }, + onClick: action((option) => { + if (option.value === selectAllNamespaces) { + namespaceStore.selectAll(); + model.menu.close(); + } else if (isMultiSelection) { + didToggle = true; + namespaceStore.toggleSingle(option.value); + } else { + namespaceStore.selectSingle(option.value); + model.menu.close(); + } + }), + deselect: action((namespace) => { + namespaceStore.deselectSingle(namespace); + }), + select: action((namespace) => { + namespaceStore.includeSingle(namespace); + }), + filterText, + reset: action(() => { + isMultiSelection = false; + model.menu.close(); + }), + isOptionSelected, + }; + + return model; + }, +}); + +export default namespaceSelectFilterModelInjectable; diff --git a/packages/core/src/renderer/components/namespace-select-filter/namespace-select-filter.scss b/packages/core/src/renderer/components/namespace-select-filter/namespace-select-filter.scss new file mode 100644 index 000000000000..268a3c69819e --- /dev/null +++ b/packages/core/src/renderer/components/namespace-select-filter/namespace-select-filter.scss @@ -0,0 +1,83 @@ +.namespace-select-filter { + width: 300px; + position: relative; + + .list-container { + position: absolute; + top: 30px; + z-index: 10; + background: var(--mainBackground); + border-radius: var(--border-radius); + + li.option { + cursor: pointer; + padding: calc(var(--padding) / 2) var(--padding); + list-style: none; + + &:hover { + color: white; + } + + .selected-icon:hover { + color: var(--buttonAccentBackground); + } + + .add-selection-icon:hover { + color: var(--colorSuccess); + } + } + } + + .menu { + width: 300px; + border-radius: var(--border-radius); + border: 1px solid; + border-color: var(--halfGray); + padding: calc(var(--padding) / 2) var(--padding); + display: flex; + + .non-icon { + width: -webkit-fill-available; + position: relative; + + input { + width: 100%; + background: transparent; + } + + label { + white-space: nowrap; + overflow: scroll hidden!important; + text-overflow: unset!important; + margin-left: -16px; + padding-left: 8px; + padding-right: 8px; + width: calc(100% + 4px); + position: absolute; + left: 9px; + + &::-webkit-scrollbar { + display: none; + } + } + + .gradient { + position: absolute; + width: 10px; + top: 0; + height: 20px; + z-index: 21; + + &.left { + left: -7px; + background: linear-gradient(to right, var(--contentColor) 0%, rgba(255, 255, 255, 0) 100%); + } + + &.right { + right: 2px; + background: linear-gradient(to left, var(--contentColor) 0%, rgba(255, 255, 255, 0) 100%); + } + } + } + } +} diff --git a/packages/core/src/renderer/components/namespace-select-filter/namespace-select-filter.test.tsx b/packages/core/src/renderer/components/namespace-select-filter/namespace-select-filter.test.tsx new file mode 100644 index 000000000000..65c15a4ff122 --- /dev/null +++ b/packages/core/src/renderer/components/namespace-select-filter/namespace-select-filter.test.tsx @@ -0,0 +1,561 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { AsyncFnMock } from "@async-fn/jest"; +import asyncFn from "@async-fn/jest"; +import type { DiContainer } from "@ogre-tools/injectable"; +import type { RenderResult } from "@testing-library/react"; +import { fireEvent } from "@testing-library/react"; +import React from "react"; +import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; +import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; +import { Cluster } from "../../../common/cluster/cluster"; +import type { Fetch } from "../../../common/fetch/fetch.injectable"; +import fetchInjectable from "../../../common/fetch/fetch.injectable"; +import { Namespace } from "@k8slens/kube-object"; +import { createMockResponseFromString } from "../../../test-utils/mock-responses"; +import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable"; +import { getDiForUnitTesting } from "../../getDiForUnitTesting"; +import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable"; +import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable"; +import type { Disposer } from "@k8slens/utilities"; +import { array, disposer } from "@k8slens/utilities"; +import { renderFor } from "../test-utils/renderFor"; +import { NamespaceSelectFilter } from "./component"; +import type { NamespaceStore } from "../namespaces/store"; +import namespaceStoreInjectable from "../namespaces/store.injectable"; +import userEvent from "@testing-library/user-event"; + +function createNamespace(name: string): Namespace { + return new Namespace({ + apiVersion: "v1", + kind: "Namespace", + metadata: { + name, + resourceVersion: "1", + selfLink: `/api/v1/namespaces/${name}`, + uid: `${name}-1`, + }, + }); +} + +describe("", () => { + let di: DiContainer; + let namespaceStore: NamespaceStore; + let fetchMock: AsyncFnMock; + let result: RenderResult; + let cleanup: Disposer; + + beforeEach(() => { + di = getDiForUnitTesting(); + di.unoverride(subscribeStoresInjectable); + + di.override(directoryForUserDataInjectable, () => "/some-user-store-path"); + di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs"); + di.override(storesAndApisCanBeCreatedInjectable, () => true); + + fetchMock = asyncFn(); + di.override(fetchInjectable, () => fetchMock); + + di.override(hostedClusterInjectable, () => new Cluster({ + contextName: "some-context-name", + id: "some-cluster-id", + kubeConfigPath: "/some-path-to-a-kubeconfig", + })); + + namespaceStore = di.inject(namespaceStoreInjectable); + + const subscribeStores = di.inject(subscribeStoresInjectable); + + cleanup = disposer(subscribeStores([namespaceStore])); + + const render = renderFor(di); + + result = render(( + + )); + }); + + afterEach(() => { + cleanup(); + }); + + describe("once the subscribe resolves", () => { + beforeEach(async () => { + await fetchMock.resolveSpecific([ + "https://127.0.0.1:12345/api-kube/api/v1/namespaces", + ], createMockResponseFromString("https://127.0.0.1:12345/api-kube/api/v1/namespaces", JSON.stringify({ + apiVersion: "v1", + kind: "NamespaceList", + metadata: {}, + items: [ + createNamespace("test-1"), + createNamespace("test-2"), + createNamespace("test-3"), + createNamespace("test-4"), + createNamespace("test-5"), + createNamespace("test-6"), + createNamespace("test-7"), + createNamespace("test-8"), + createNamespace("test-9"), + createNamespace("test-10"), + createNamespace("test-11"), + createNamespace("test-12"), + createNamespace("test-13"), + ], + }))); + }); + + it("renders", () => { + expect(result.baseElement).toMatchSnapshot(); + }); + + describe("when menu expand icon is clicked", () => { + beforeEach(() => { + result.getByTestId("namespace-select-filter-expand-icon").click(); + }); + + it("renders", () => { + expect(result.baseElement).toMatchSnapshot(); + }); + + it("menu is open", () => { + expect(result.getByTestId("namespace-select-filter-list-container")).toBeInTheDocument(); + }); + + describe("when menu expand icon is clicked again", () => { + beforeEach(() => { + result.getByTestId("namespace-select-filter-expand-icon").click(); + }); + + it("renders", () => { + expect(result.baseElement).toMatchSnapshot(); + }); + + it("menu is closed", () => { + expect(result.queryByTestId("namespace-select-filter-list-container")).not.toBeInTheDocument(); + }); + }); + }); + + describe("when clicked", () => { + beforeEach(() => { + result.getByTestId("namespace-select-filter-input").click(); + }); + + it("renders", () => { + expect(result.baseElement).toMatchSnapshot(); + }); + + it("opens menu", () => { + expect(result.getByTestId("namespace-select-filter-list-container")).toBeInTheDocument(); + }); + + it("has all namespaces selected in the store", () => { + expect(namespaceStore.contextNamespaces.length).toBe(13); + }); + + describe("when 'test-2' is clicked", () => { + beforeEach(() => { + result.getByTestId("namespace-select-filter-option-test-2").click(); + }); + + it("renders", () => { + expect(result.baseElement).toMatchSnapshot(); + }); + + it("has only 'test-2' is selected in the store", () => { + expect(namespaceStore.contextNamespaces).toEqual(["test-2"]); + }); + + it("closes menu", () => { + expect(result.queryByTestId("namespace-select-filter-list-container")).toBeNull(); + }); + + describe("when clicked again", () => { + beforeEach(() => { + result.getByTestId("namespace-select-filter-input").click(); + }); + + it("renders", () => { + expect(result.baseElement).toMatchSnapshot(); + }); + + it("shows 'test-2' as selected", () => { + expect(result.queryByTestId("namespace-select-filter-option-test-2-selected")).toBeInTheDocument(); + }); + + it("does not show 'test-1' as selected", () => { + expect(result.queryByTestId("namespace-select-filter-option-test-1-selected")).toBeNull(); + }); + + describe("when 'test-1' is clicked", () => { + beforeEach(() => { + result.getByTestId("namespace-select-filter-option-test-1").click(); + }); + + it("renders", () => { + expect(result.baseElement).toMatchSnapshot(); + }); + + it("has only 'test-1' is selected in the store", () => { + expect(namespaceStore.contextNamespaces).toEqual(["test-1"]); + }); + + it("closes menu", () => { + expect(result.queryByTestId("namespace-select-filter-list-container")).toBeNull(); + }); + + describe("when clicked again, then holding down multi select key", () => { + beforeEach(() => { + const filter = result.getByTestId("namespace-select-filter-input"); + + filter.click(); + fireEvent.keyDown(filter, { key: "Meta" }); + }); + + describe("when 'test-3' is clicked", () => { + beforeEach(() => { + result.getByTestId("namespace-select-filter-option-test-3").click(); + }); + + it("renders", () => { + expect(result.baseElement).toMatchSnapshot(); + }); + + it("has both 'test-1' and 'test-3' as selected in the store", () => { + expect(new Set(namespaceStore.contextNamespaces)).toEqual(new Set(["test-1", "test-3"])); + }); + + it("keeps menu open", () => { + expect(result.getByTestId("namespace-select-filter-list-container")).toBeInTheDocument(); + }); + + it("does not show 'kube-system' as selected", () => { + expect(result.queryByTestId("namespace-select-filter-option-kube-system-selected")).toBeNull(); + }); + + describe("when 'test-13' is clicked", () => { + beforeEach(() => { + result.getByTestId("namespace-select-filter-option-test-13").click(); + }); + + it("has all of 'test-1', 'test-3', and 'test-13' selected in the store", () => { + expect(new Set(namespaceStore.contextNamespaces)).toEqual(new Set(["test-1", "test-3", "test-13"])); + }); + + it("keeps menu open", () => { + expect(result.getByTestId("namespace-select-filter-list-container")).toBeInTheDocument(); + }); + + it("'test-13' is not sorted to the top of the list", () => { + expect(result.getByTestId("namespace-select-filter-option-test-13").previousSibling).not.toBe(null); + }); + }); + + describe("when releasing multi select key", () => { + beforeEach(() => { + const filter = result.getByTestId("namespace-select-filter-input"); + + fireEvent.keyUp(filter, { key: "Meta" }); + }); + + it("closes menu", () => { + expect(result.queryByTestId("namespace-select-filter-list-container")).toBeNull(); + }); + }); + }); + + describe("when releasing multi select key", () => { + beforeEach(() => { + const filter = result.getByTestId("namespace-select-filter-input"); + + fireEvent.keyUp(filter, { key: "Meta" }); + }); + + it("keeps menu open", () => { + expect(result.getByTestId("namespace-select-filter-list-container")).toBeInTheDocument(); + }); + }); + }); + }); + }); + }); + + describe("when multi-selection key is pressed", () => { + beforeEach(() => { + const filter = result.getByTestId("namespace-select-filter-input"); + + fireEvent.keyDown(filter, { key: "Meta" }); + }); + + it("should show placeholder text as 'All namespaces'", () => { + expect(result.getByTestId("namespace-select-filter")).toHaveTextContent("All namespaces"); + }); + + describe("when 'test-2' is clicked", () => { + beforeEach(() => { + result.getByTestId("namespace-select-filter-option-test-2").click(); + }); + + it("should not show placeholder text as 'All namespaces'", () => { + expect(result.getByTestId("namespace-select-filter")).not.toHaveTextContent("All namespaces"); + }); + + describe("when 'test-2' is clicked", () => { + beforeEach(() => { + result.getByTestId("namespace-select-filter-option-test-2").click(); + }); + + it("should not show placeholder as 'All namespaces'", () => { + expect(result.getByTestId("namespace-select-filter")).not.toHaveTextContent("All namespaces"); + }); + + describe("when multi-selection key is raised", () => { + beforeEach(() => { + const filter = result.getByTestId("namespace-select-filter-input"); + + fireEvent.keyUp(filter, { key: "Meta" }); + }); + + it("should show placeholder text as 'All namespaces'", () => { + expect(result.getByTestId("namespace-select-filter")).not.toHaveTextContent("All namespaces"); + }); + }); + }); + }); + }); + + describe("when clicked again", () => { + beforeEach(() => { + result.getByTestId("namespace-select-filter-input").click(); + }); + + it("renders", () => { + expect(result.baseElement).toMatchSnapshot(); + }); + + it("menu is still open", () => { + expect(result.getByTestId("namespace-select-filter-list-container")).toBeInTheDocument(); + }); + }); + + describe("when typing in the filter input", () => { + beforeEach(() => { + userEvent.type(result.getByTestId("namespace-select-filter-input"), "1"); + }); + + it("renders", () => { + expect(result.baseElement).toMatchSnapshot(); + }); + + it("does show something in the input", () => { + expect(result.getByTestId("namespace-select-filter-input")).toHaveValue("1"); + }); + + it("doesn't show anything in the label", () => { + expect(result.getByTestId("namespace-select-filter-label")).toBeEmptyDOMElement(); + }); + + it("menu is still open", () => { + expect(result.getByTestId("namespace-select-filter-list-container")).toBeInTheDocument(); + }); + + it("shows 'test-1' option", () => { + expect(result.getByTestId("namespace-select-filter-option-test-1")).toBeInTheDocument(); + }); + + it("shows 'test-10' option", () => { + expect(result.getByTestId("namespace-select-filter-option-test-10")).toBeInTheDocument(); + }); + + it("does not show 'test-2' option", () => { + expect(result.queryByTestId("namespace-select-filter-option-test-2")).not.toBeInTheDocument(); + }); + + describe("when pressing the 'Enter' key", () => { + beforeEach(() => { + userEvent.keyboard("{enter}"); + }); + + it("renders", () => { + expect(result.baseElement).toMatchSnapshot(); + }); + + it("closes the menu", () => { + expect(result.queryByTestId("namespace-select-filter-list-container")).not.toBeInTheDocument(); + }); + + it("has only 'test-1' as selected in the store", () => { + expect(namespaceStore.contextNamespaces).toEqual(["test-1"]); + }); + }); + }); + + describe("when typing a glob style filter into the filter input", () => { + beforeEach(() => { + userEvent.type(result.getByTestId("namespace-select-filter-input"), "1*"); + }); + + it("renders", () => { + expect(result.baseElement).toMatchSnapshot(); + }); + + it("menu is still open", () => { + expect(result.getByTestId("namespace-select-filter-list-container")).toBeInTheDocument(); + }); + + it("shows 'test-1' option", () => { + expect(result.getByTestId("namespace-select-filter-option-test-1")).toBeInTheDocument(); + }); + + it("shows 'test-10' option", () => { + expect(result.getByTestId("namespace-select-filter-option-test-10")).toBeInTheDocument(); + }); + + it("does not show 'test-2' option", () => { + expect(result.queryByTestId("namespace-select-filter-option-test-2")).not.toBeInTheDocument(); + }); + }); + + describe("when clicking the remove from selection button for 'test-2'", () => { + beforeEach(() => { + result.getByTestId("namespace-select-filter-option-test-2-selected").click(); + }); + + it("renders", () => { + expect(result.baseElement).toMatchSnapshot(); + }); + + it("shows an 'add to selection' button for 'test-2'", () => { + expect(result.getByTestId("namespace-select-filter-option-test-2-add-to-selection")).toBeInTheDocument(); + }); + + it("does not have 'test-2' as selected in the store", () => { + expect(namespaceStore.contextNamespaces.includes("test-2")).toBe(false); + }); + + it("menu is still open", () => { + expect(result.getByTestId("namespace-select-filter-list-container")).toBeInTheDocument(); + }); + + describe("when clicking the remove from selection button for 'test-3'", () => { + beforeEach(() => { + result.getByTestId("namespace-select-filter-option-test-3-selected").click(); + }); + + it("renders", () => { + expect(result.baseElement).toMatchSnapshot(); + }); + + it("shows an 'add to selection' button for 'test-3'", () => { + expect(result.getByTestId("namespace-select-filter-option-test-3-add-to-selection")).toBeInTheDocument(); + }); + + it("does not have 'test-3' as selected in the store", () => { + expect(namespaceStore.contextNamespaces.includes("test-3")).toBe(false); + }); + + it("menu is still open", () => { + expect(result.getByTestId("namespace-select-filter-list-container")).toBeInTheDocument(); + }); + + describe("when clicking the add to selection button for 'test-2'", () => { + beforeEach(() => { + result.getByTestId("namespace-select-filter-option-test-2-add-to-selection").click(); + }); + + it("renders", () => { + expect(result.baseElement).toMatchSnapshot(); + }); + + it("shows a 'remove from selection' button for 'test-2'", () => { + expect(result.getByTestId("namespace-select-filter-option-test-2-selected")).toBeInTheDocument(); + }); + + it("does have 'test-2' as selected in the store", () => { + expect(namespaceStore.contextNamespaces.includes("test-2")).toBe(true); + }); + + it("menu is still open", () => { + expect(result.getByTestId("namespace-select-filter-list-container")).toBeInTheDocument(); + }); + + describe("when clicking the 'select only' button for 'test-5'", () => { + beforeEach(() => { + result.getByTestId("namespace-select-filter-option-test-5-select-only").click(); + }); + + it("renders", () => { + expect(result.baseElement).toMatchSnapshot(); + }); + + it("only has 'test-5' as selected in the store", () => { + expect(namespaceStore.contextNamespaces).toEqual(["test-5"]); + }); + + it("menu is now closed", () => { + expect(result.queryByTestId("namespace-select-filter-list-container")).not.toBeInTheDocument(); + }); + }); + }); + }); + }); + }); + }); + + describe("once the subscribe resolves with thousands of namespaces", () => { + beforeEach(async () => { + await fetchMock.resolveSpecific([ + "https://127.0.0.1:12345/api-kube/api/v1/namespaces", + ], createMockResponseFromString("https://127.0.0.1:12345/api-kube/api/v1/namespaces", JSON.stringify({ + apiVersion: "v1", + kind: "NamespaceList", + metadata: {}, + items: array.filled(20000, undefined).map((_, i) => createNamespace(`test-${i}`)), + }))); + }); + + it("renders", () => { + expect(result.baseElement).toMatchSnapshot(); + }); + + describe("when menu expand icon is clicked", () => { + beforeEach(() => { + result.getByTestId("namespace-select-filter-expand-icon").click(); + }); + + it("renders", () => { + expect(result.baseElement).toMatchSnapshot(); + }); + + it("menu is open", () => { + expect(result.getByTestId("namespace-select-filter-list-container")).toBeInTheDocument(); + }); + + it("does not show all items in the DOM", () => { + expect(result.queryByTestId("namespace-select-filter-option-test-1500")).not.toBeInTheDocument(); + }); + + it("does show some items in the DOM", () => { + expect(result.getByTestId("namespace-select-filter-option-test-10")).toBeInTheDocument(); + }); + + describe("when menu expand icon is clicked again", () => { + beforeEach(() => { + result.getByTestId("namespace-select-filter-expand-icon").click(); + }); + + it("renders", () => { + expect(result.baseElement).toMatchSnapshot(); + }); + + it("menu is closed", () => { + expect(result.queryByTestId("namespace-select-filter-list-container")).not.toBeInTheDocument(); + }); + }); + }); + }); +}); diff --git a/packages/core/src/renderer/components/namespaces/__snapshots__/namespace-select-filter.test.tsx.snap b/packages/core/src/renderer/components/namespaces/__snapshots__/namespace-select-filter.test.tsx.snap deleted file mode 100644 index a03fe634d7bc..000000000000 --- a/packages/core/src/renderer/components/namespaces/__snapshots__/namespace-select-filter.test.tsx.snap +++ /dev/null @@ -1,1698 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` once the subscribe resolves renders 1`] = ` - -
    -
    -
    - - -
    -
    -
    - All namespaces -
    -
    - -
    -
    -
    - - -
    -
    -
    -
    -
    - -`; - -exports[` once the subscribe resolves when clicked renders 1`] = ` - -
    -
    -
    - - -
    -
    -
    - All namespaces -
    -
    - -
    -
    -
    - - -
    -
    -
    -
    -
    -
    -
    -
    -
    - All Namespaces -
    -
    -
    - - - layers - - - - test-1 - - - - check - - -
    -
    -
    -
    - - - layers - - - - test-10 - - - - check - - -
    -
    -
    -
    - - - layers - - - - test-11 - - - - check - - -
    -
    -
    -
    - - - layers - - - - test-12 - - - - check - - -
    -
    -
    -
    - - - layers - - - - test-13 - - - - check - - -
    -
    -
    -
    - - - layers - - - - test-2 - - - - check - - -
    -
    -
    -
    - - - layers - - - - test-3 - - - - check - - -
    -
    -
    -
    - - - layers - - - - test-4 - - - - check - - -
    -
    -
    -
    - - - layers - - - - test-5 - - - - check - - -
    -
    -
    -
    - - - layers - - - - test-6 - - - - check - - -
    -
    -
    -
    - - - layers - - - - test-7 - - - - check - - -
    -
    -
    -
    - - - layers - - - - test-8 - - - - check - - -
    -
    -
    -
    - - - layers - - - - test-9 - - - - check - - -
    -
    -
    -
    -
    - -`; - -exports[` once the subscribe resolves when clicked when 'test-2' is clicked renders 1`] = ` - -
    -
    -
    - - -
    -
    -
    - Namespace: test-2 -
    -
    - -
    -
    -
    - - -
    -
    -
    -
    -
    - -`; - -exports[` once the subscribe resolves when clicked when 'test-2' is clicked when clicked again renders 1`] = ` - -
    -
    -
    - - -
    -
    -
    - Namespace: test-2 -
    -
    - -
    -
    -
    - - -
    -
    -
    -
    -
    -
    -
    -
    -
    - All Namespaces -
    -
    -
    - - - layers - - - - test-2 - - - - check - - -
    -
    -
    -
    - - - layers - - - - test-1 - -
    -
    -
    -
    - - - layers - - - - test-10 - -
    -
    -
    -
    - - - layers - - - - test-11 - -
    -
    -
    -
    - - - layers - - - - test-12 - -
    -
    -
    -
    - - - layers - - - - test-13 - -
    -
    -
    -
    - - - layers - - - - test-3 - -
    -
    -
    -
    - - - layers - - - - test-4 - -
    -
    -
    -
    - - - layers - - - - test-5 - -
    -
    -
    -
    - - - layers - - - - test-6 - -
    -
    -
    -
    - - - layers - - - - test-7 - -
    -
    -
    -
    - - - layers - - - - test-8 - -
    -
    -
    -
    - - - layers - - - - test-9 - -
    -
    -
    -
    -
    - -`; - -exports[` once the subscribe resolves when clicked when 'test-2' is clicked when clicked again when 'test-1' is clicked renders 1`] = ` - -
    -
    -
    - - -
    -
    -
    - Namespace: test-1 -
    -
    - -
    -
    -
    - - -
    -
    -
    -
    -
    - -`; - -exports[` once the subscribe resolves when clicked when 'test-2' is clicked when clicked again when 'test-1' is clicked when clicked again, then holding down multi select key when 'test-3' is clicked renders 1`] = ` - -
    -
    -
    - - -
    -
    -
    - Namespaces: test-1, test-3 -
    -
    - -
    -
    -
    - - -
    -
    -
    -
    -
    -
    -
    -
    -
    - All Namespaces -
    -
    -
    - - - layers - - - - test-1 - - - - check - - -
    -
    -
    -
    - - - layers - - - - test-2 - -
    -
    -
    -
    - - - layers - - - - test-10 - -
    -
    -
    -
    - - - layers - - - - test-11 - -
    -
    -
    -
    - - - layers - - - - test-12 - -
    -
    -
    -
    - - - layers - - - - test-13 - -
    -
    -
    -
    - - - layers - - - - test-3 - - - - check - - -
    -
    -
    -
    - - - layers - - - - test-4 - -
    -
    -
    -
    - - - layers - - - - test-5 - -
    -
    -
    -
    - - - layers - - - - test-6 - -
    -
    -
    -
    - - - layers - - - - test-7 - -
    -
    -
    -
    - - - layers - - - - test-8 - -
    -
    -
    -
    - - - layers - - - - test-9 - -
    -
    -
    -
    -
    - -`; diff --git a/packages/core/src/renderer/components/namespaces/namespace-select-filter-model/filter-by-namespace.injectable.ts b/packages/core/src/renderer/components/namespaces/filter-by-namespace.injectable.ts similarity index 90% rename from packages/core/src/renderer/components/namespaces/namespace-select-filter-model/filter-by-namespace.injectable.ts rename to packages/core/src/renderer/components/namespaces/filter-by-namespace.injectable.ts index fff1c37202f8..4848cb562460 100644 --- a/packages/core/src/renderer/components/namespaces/namespace-select-filter-model/filter-by-namespace.injectable.ts +++ b/packages/core/src/renderer/components/namespaces/filter-by-namespace.injectable.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import namespaceStoreInjectable from "../store.injectable"; +import namespaceStoreInjectable from "./store.injectable"; export type FilterByNamespace = (namespace: string) => void; diff --git a/packages/core/src/renderer/components/namespaces/namespace-select-badge.tsx b/packages/core/src/renderer/components/namespaces/namespace-select-badge.tsx index 088cfe603e97..97020e02887a 100644 --- a/packages/core/src/renderer/components/namespaces/namespace-select-badge.tsx +++ b/packages/core/src/renderer/components/namespaces/namespace-select-badge.tsx @@ -8,11 +8,8 @@ import React from "react"; import { withInjectables } from "@ogre-tools/injectable-react"; import type { BadgeProps } from "../badge"; import { Badge } from "../badge"; -import type { - FilterByNamespace, -} from "./namespace-select-filter-model/filter-by-namespace.injectable"; -import filterByNamespaceInjectable - from "./namespace-select-filter-model/filter-by-namespace.injectable"; +import type { FilterByNamespace } from "./filter-by-namespace.injectable"; +import filterByNamespaceInjectable from "./filter-by-namespace.injectable"; import { prevDefault, cssNames } from "@k8slens/utilities"; export interface NamespaceSelectBadgeProps extends BadgeProps { @@ -49,10 +46,8 @@ export function NamespaceSelectBadgeNonInjected( } export const NamespaceSelectBadge = withInjectables(NamespaceSelectBadgeNonInjected, { - getProps(di, props) { - return { - ...props, - filterByNamespace: di.inject(filterByNamespaceInjectable), - }; - }, + getProps: (di, props) => ({ + ...props, + filterByNamespace: di.inject(filterByNamespaceInjectable), + }), }); diff --git a/packages/core/src/renderer/components/namespaces/namespace-select-filter-model/namespace-select-filter-model.injectable.ts b/packages/core/src/renderer/components/namespaces/namespace-select-filter-model/namespace-select-filter-model.injectable.ts deleted file mode 100644 index d84292410b56..000000000000 --- a/packages/core/src/renderer/components/namespaces/namespace-select-filter-model/namespace-select-filter-model.injectable.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { namespaceSelectFilterModelFor } from "./namespace-select-filter-model"; -import { getInjectable } from "@ogre-tools/injectable"; -import namespaceStoreInjectable from "../store.injectable"; -import isMultiSelectionKeyInjectable from "./is-selection-key.injectable"; -import clusterFrameContextForNamespacedResourcesInjectable from "../../../cluster-frame-context/for-namespaced-resources.injectable"; - -const namespaceSelectFilterModelInjectable = getInjectable({ - id: "namespace-select-filter-model", - - instantiate: (di) => namespaceSelectFilterModelFor({ - namespaceStore: di.inject(namespaceStoreInjectable), - isMultiSelectionKey: di.inject(isMultiSelectionKeyInjectable), - context: di.inject(clusterFrameContextForNamespacedResourcesInjectable), - }), -}); - -export default namespaceSelectFilterModelInjectable; diff --git a/packages/core/src/renderer/components/namespaces/namespace-select-filter-model/namespace-select-filter-model.tsx b/packages/core/src/renderer/components/namespaces/namespace-select-filter-model/namespace-select-filter-model.tsx deleted file mode 100644 index c0ad194dfbc1..000000000000 --- a/packages/core/src/renderer/components/namespaces/namespace-select-filter-model/namespace-select-filter-model.tsx +++ /dev/null @@ -1,182 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import React from "react"; -import type { IComputedValue } from "mobx"; -import { observable, action, computed, comparer } from "mobx"; -import type { NamespaceStore } from "../store"; -import type { ActionMeta, MultiValue } from "react-select"; -import { Icon } from "@k8slens/icon"; -import type { SelectOption } from "../../select"; -import { observableCrate } from "@k8slens/utilities"; -import type { IsMultiSelectionKey } from "./is-selection-key.injectable"; -import type { ClusterContext } from "../../../cluster-frame-context/cluster-frame-context"; - -interface Dependencies { - context: ClusterContext; - namespaceStore: NamespaceStore; - isMultiSelectionKey: IsMultiSelectionKey; -} - -export const selectAllNamespaces = Symbol("all-namespaces-selected"); - -export type SelectAllNamespaces = typeof selectAllNamespaces; -export type NamespaceSelectFilterOption = SelectOption; - -export interface NamespaceSelectFilterModel { - readonly options: IComputedValue; - readonly menu: { - open: () => void; - close: () => void; - readonly isOpen: IComputedValue; - }; - onChange: (newValue: MultiValue, actionMeta: ActionMeta) => void; - onClick: () => void; - onKeyDown: React.KeyboardEventHandler; - onKeyUp: React.KeyboardEventHandler; - reset: () => void; - isOptionSelected: (option: NamespaceSelectFilterOption) => boolean; - formatOptionLabel: (option: NamespaceSelectFilterOption) => JSX.Element; -} - -enum SelectMenuState { - Close = "close", - Open = "open", -} - -export function namespaceSelectFilterModelFor(dependencies: Dependencies): NamespaceSelectFilterModel { - const { isMultiSelectionKey, namespaceStore, context } = dependencies; - - let didToggle = false; - let isMultiSelection = false; - const menuState = observableCrate(SelectMenuState.Close, [{ - from: SelectMenuState.Close, - to: SelectMenuState.Open, - onTransition: () => { - optionsSortingSelected.replace(selectedNames.get()); - didToggle = false; - }, - }]); - const selectedNames = computed(() => new Set(context.contextNamespaces), { - equals: comparer.structural, - }); - const optionsSortingSelected = observable.set(selectedNames.get()); - const sortNamespacesByIfTheyHaveBeenSelected = (left: string, right: string) => { - const isLeftSelected = optionsSortingSelected.has(left); - const isRightSelected = optionsSortingSelected.has(right); - - if (isLeftSelected === isRightSelected) { - return 0; - } - - return isRightSelected - ? 1 - : -1; - }; - const options = computed((): readonly NamespaceSelectFilterOption[] => [ - { - value: selectAllNamespaces, - label: "All Namespaces", - id: "all-namespaces", - }, - ...context - .allNamespaces - .sort(sortNamespacesByIfTheyHaveBeenSelected) - .map(namespace => ({ - value: namespace, - label: namespace, - id: namespace, - })), - ]); - const menuIsOpen = computed(() => menuState.get() === SelectMenuState.Open); - const isOptionSelected: NamespaceSelectFilterModel["isOptionSelected"] = (option) => { - if (option.value === selectAllNamespaces) { - return false; - } - - return selectedNames.get().has(option.value); - }; - - const model: NamespaceSelectFilterModel = { - options, - menu: { - close: action(() => { - menuState.set(SelectMenuState.Close); - }), - open: action(() => { - menuState.set(SelectMenuState.Open); - }), - isOpen: menuIsOpen, - }, - onChange: (_, action) => { - switch (action.action) { - case "clear": - namespaceStore.selectAll(); - break; - case "deselect-option": - case "select-option": - if (action.option) { - didToggle = true; - - if (action.option.value === selectAllNamespaces) { - namespaceStore.selectAll(); - } else if (isMultiSelection) { - namespaceStore.toggleSingle(action.option.value); - } else { - namespaceStore.selectSingle(action.option.value); - } - } - break; - } - }, - onClick: () => { - if (!menuIsOpen.get()) { - model.menu.open(); - } else if (!isMultiSelection) { - model.menu.close(); - } - }, - onKeyDown: (event) => { - if (isMultiSelectionKey(event)) { - isMultiSelection = true; - } - }, - onKeyUp: (event) => { - if (isMultiSelectionKey(event)) { - isMultiSelection = false; - - if (didToggle) { - model.menu.close(); - } - } - }, - reset: action(() => { - isMultiSelection = false; - model.menu.close(); - }), - isOptionSelected, - formatOptionLabel: (option) => { - if (option.value === selectAllNamespaces) { - return <>All Namespaces; - } - - return ( -
    - - {option.value} - {isOptionSelected(option) && ( - - )} -
    - ); - }, - }; - - return model; -} diff --git a/packages/core/src/renderer/components/namespaces/namespace-select-filter.scss b/packages/core/src/renderer/components/namespaces/namespace-select-filter.scss deleted file mode 100644 index f227976b46a7..000000000000 --- a/packages/core/src/renderer/components/namespaces/namespace-select-filter.scss +++ /dev/null @@ -1,78 +0,0 @@ -@include theme-light { - .NamespaceSelectFilter { - --gradientColor: white; - } -} - -.NamespaceSelectFilterParent { - max-width: 300px; -} - -.NamespaceSelectFilter { - --gradientColor: var(--select-menu-bgc); - - .Select { - &__placeholder { - white-space: nowrap; - overflow: scroll hidden!important; - text-overflow: unset!important; - margin-left: -8px; - padding-left: 8px; - margin-right: -8px; - padding-right: 8px; - line-height: 1.1; - - &::-webkit-scrollbar { - display: none; - } - } - - &__value-container { - position: relative; - - &::before, &::after { - content: ' '; - position: absolute; - z-index: 20; - display: block; - width: 8px; - height: var(--font-size); - } - - &::before { - left: 0; - background: linear-gradient(to right, var(--gradientColor) 0px, transparent); - } - - &::after { - right: 0; - background: linear-gradient(to left, var(--gradientColor) 0px, transparent); - } - } - } -} - -.NamespaceSelectFilterMenu { - right: 0; - - .Select { - &__menu-list { - max-width: 400px; - } - - &__option { - white-space: normal; - word-break: break-all; - padding: 4px 8px; - border-radius: 3px; - - &--is-selected:not(&--is-focused) { - background: transparent; - } - } - } - - .Icon { - margin-right: $margin * 0.5; - } -} diff --git a/packages/core/src/renderer/components/namespaces/namespace-select-filter.test.tsx b/packages/core/src/renderer/components/namespaces/namespace-select-filter.test.tsx deleted file mode 100644 index a07bb6c981ac..000000000000 --- a/packages/core/src/renderer/components/namespaces/namespace-select-filter.test.tsx +++ /dev/null @@ -1,297 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { AsyncFnMock } from "@async-fn/jest"; -import asyncFn from "@async-fn/jest"; -import type { DiContainer } from "@ogre-tools/injectable"; -import type { RenderResult } from "@testing-library/react"; -import { fireEvent } from "@testing-library/react"; -import React from "react"; -import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; -import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; -import { Cluster } from "../../../common/cluster/cluster"; -import type { Fetch } from "../../../common/fetch/fetch.injectable"; -import fetchInjectable from "../../../common/fetch/fetch.injectable"; -import { Namespace } from "@k8slens/kube-object"; -import { createMockResponseFromString } from "../../../test-utils/mock-responses"; -import hostedClusterInjectable from "../../cluster-frame-context/hosted-cluster.injectable"; -import { getDiForUnitTesting } from "../../getDiForUnitTesting"; -import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable"; -import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable"; -import type { Disposer } from "@k8slens/utilities"; -import { disposer } from "@k8slens/utilities"; -import { renderFor } from "../test-utils/renderFor"; -import { NamespaceSelectFilter } from "./namespace-select-filter"; -import type { NamespaceStore } from "./store"; -import namespaceStoreInjectable from "./store.injectable"; - -function createNamespace(name: string): Namespace { - return new Namespace({ - apiVersion: "v1", - kind: "Namespace", - metadata: { - name, - resourceVersion: "1", - selfLink: `/api/v1/namespaces/${name}`, - uid: `${name}-1`, - }, - }); -} - -describe("", () => { - let di: DiContainer; - let namespaceStore: NamespaceStore; - let fetchMock: AsyncFnMock; - let result: RenderResult; - let cleanup: Disposer; - - beforeEach(() => { - di = getDiForUnitTesting(); - di.unoverride(subscribeStoresInjectable); - - di.override(directoryForUserDataInjectable, () => "/some-user-store-path"); - di.override(directoryForKubeConfigsInjectable, () => "/some-kube-configs"); - di.override(storesAndApisCanBeCreatedInjectable, () => true); - - fetchMock = asyncFn(); - di.override(fetchInjectable, () => fetchMock); - - di.override(hostedClusterInjectable, () => new Cluster({ - contextName: "some-context-name", - id: "some-cluster-id", - kubeConfigPath: "/some-path-to-a-kubeconfig", - })); - - namespaceStore = di.inject(namespaceStoreInjectable); - - const subscribeStores = di.inject(subscribeStoresInjectable); - - cleanup = disposer(subscribeStores([namespaceStore])); - - const render = renderFor(di); - - result = render(( - - )); - }); - - afterEach(() => { - cleanup(); - }); - - describe("once the subscribe resolves", () => { - beforeEach(async () => { - await fetchMock.resolveSpecific([ - "https://127.0.0.1:12345/api-kube/api/v1/namespaces", - ], createMockResponseFromString("https://127.0.0.1:12345/api-kube/api/v1/namespaces", JSON.stringify({ - apiVersion: "v1", - kind: "NamespaceList", - metadata: {}, - items: [ - createNamespace("test-1"), - createNamespace("test-2"), - createNamespace("test-3"), - createNamespace("test-4"), - createNamespace("test-5"), - createNamespace("test-6"), - createNamespace("test-7"), - createNamespace("test-8"), - createNamespace("test-9"), - createNamespace("test-10"), - createNamespace("test-11"), - createNamespace("test-12"), - createNamespace("test-13"), - ], - }))); - }); - - it("renders", () => { - expect(result.baseElement).toMatchSnapshot(); - }); - - describe("when clicked", () => { - beforeEach(() => { - result.getByTestId("namespace-select-filter").click(); - }); - - it("renders", () => { - expect(result.baseElement).toMatchSnapshot(); - }); - - it("opens menu", () => { - expect(result.baseElement.querySelector("#react-select-namespace-select-filter-listbox")).not.toBeNull(); - }); - - describe("when 'test-2' is clicked", () => { - beforeEach(() => { - result.getByText("test-2").click(); - }); - - it("renders", () => { - expect(result.baseElement).toMatchSnapshot(); - }); - - it("has only 'test-2' is selected in the store", () => { - expect(namespaceStore.contextNamespaces).toEqual(["test-2"]); - }); - - it("closes menu", () => { - expect(result.baseElement.querySelector("#react-select-namespace-select-filter-listbox")).toBeNull(); - }); - - describe("when clicked again", () => { - beforeEach(() => { - result.getByTestId("namespace-select-filter").click(); - }); - - it("renders", () => { - expect(result.baseElement).toMatchSnapshot(); - }); - - it("shows 'test-2' as selected", () => { - expect(result.queryByTestId("namespace-select-filter-option-test-2-selected")).not.toBeNull(); - }); - - it("does not show 'test-1' as selected", () => { - expect(result.queryByTestId("namespace-select-filter-option-test-1-selected")).toBeNull(); - }); - - describe("when 'test-1' is clicked", () => { - beforeEach(() => { - result.getByText("test-1").click(); - }); - - it("renders", () => { - expect(result.baseElement).toMatchSnapshot(); - }); - - it("has only 'test-1' is selected in the store", () => { - expect(namespaceStore.contextNamespaces).toEqual(["test-1"]); - }); - - it("closes menu", () => { - expect(result.baseElement.querySelector("#react-select-namespace-select-filter-listbox")).toBeNull(); - }); - - describe("when clicked again, then holding down multi select key", () => { - beforeEach(() => { - const filter = result.getByTestId("namespace-select-filter"); - - filter.click(); - fireEvent.keyDown(filter, { key: "Meta" }); - }); - - describe("when 'test-3' is clicked", () => { - beforeEach(() => { - result.getByText("test-3").click(); - }); - - it("renders", () => { - expect(result.baseElement).toMatchSnapshot(); - }); - - it("has both 'test-1' and 'test-3' as selected in the store", () => { - expect(new Set(namespaceStore.contextNamespaces)).toEqual(new Set(["test-1", "test-3"])); - }); - - it("keeps menu open", () => { - expect(result.baseElement.querySelector("#react-select-namespace-select-filter-listbox")).not.toBeNull(); - }); - - it("does not show 'kube-system' as selected", () => { - expect(result.queryByTestId("namespace-select-filter-option-kube-system-selected")).toBeNull(); - }); - - describe("when 'test-13' is clicked", () => { - beforeEach(() => { - result.getByText("test-13").click(); - }); - - it("has all of 'test-1', 'test-3', and 'test-13' selected in the store", () => { - expect(new Set(namespaceStore.contextNamespaces)).toEqual(new Set(["test-1", "test-3", "test-13"])); - }); - - it("'test-13' is not sorted to the top of the list", () => { - const topLevelElement = result.getByText("test-13").parentElement?.parentElement as HTMLElement; - - expect(topLevelElement.previousSibling).not.toBe(null); - }); - }); - - describe("when releasing multi select key", () => { - beforeEach(() => { - const filter = result.getByTestId("namespace-select-filter"); - - fireEvent.keyUp(filter, { key: "Meta" }); - }); - - it("closes menu", () => { - expect(result.baseElement.querySelector("#react-select-namespace-select-filter-listbox")).toBeNull(); - }); - }); - }); - - describe("when releasing multi select key", () => { - beforeEach(() => { - const filter = result.getByTestId("namespace-select-filter"); - - fireEvent.keyUp(filter, { key: "Meta" }); - }); - - it("keeps menu open", () => { - expect(result.baseElement.querySelector("#react-select-namespace-select-filter-listbox")).not.toBeNull(); - }); - }); - }); - }); - }); - }); - - describe("when multi-selection key is pressed", () => { - beforeEach(() => { - const filter = result.getByTestId("namespace-select-filter"); - - fireEvent.keyDown(filter, { key: "Meta" }); - }); - - it("should show placeholder text as 'All namespaces'", () => { - expect(result.baseElement.querySelector("#react-select-namespace-select-filter-placeholder")).toHaveTextContent("All namespaces"); - }); - - describe("when 'test-2' is clicked", () => { - beforeEach(() => { - result.getByText("test-2").click(); - }); - - it("should not show placeholder text as 'All namespaces'", () => { - expect(result.baseElement.querySelector("#react-select-namespace-select-filter-placeholder")).not.toHaveTextContent("All namespaces"); - }); - - describe("when 'test-2' is clicked", () => { - beforeEach(() => { - result.getByText("test-2").click(); - }); - - it("should not show placeholder as 'All namespaces'", () => { - expect(result.baseElement.querySelector("#react-select-namespace-select-filter-placeholder")).not.toHaveTextContent("All namespaces"); - }); - - describe("when multi-selection key is raised", () => { - beforeEach(() => { - const filter = result.getByTestId("namespace-select-filter"); - - fireEvent.keyUp(filter, { key: "Meta" }); - }); - - it("should show placeholder text as 'All namespaces'", () => { - expect(result.baseElement.querySelector("#react-select-namespace-select-filter-placeholder")).not.toHaveTextContent("All namespaces"); - }); - }); - }); - }); - }); - }); - }); -}); diff --git a/packages/core/src/renderer/components/namespaces/namespace-select-filter.tsx b/packages/core/src/renderer/components/namespaces/namespace-select-filter.tsx deleted file mode 100644 index de1c2a8cd663..000000000000 --- a/packages/core/src/renderer/components/namespaces/namespace-select-filter.tsx +++ /dev/null @@ -1,95 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import "./namespace-select-filter.scss"; - -import React from "react"; -import { observer } from "mobx-react"; -import type { PlaceholderProps } from "react-select"; -import { components } from "react-select"; -import type { NamespaceStore } from "./store"; -import { Select } from "../select"; -import { withInjectables } from "@ogre-tools/injectable-react"; -import type { NamespaceSelectFilterModel, NamespaceSelectFilterOption, SelectAllNamespaces } from "./namespace-select-filter-model/namespace-select-filter-model"; -import namespaceSelectFilterModelInjectable from "./namespace-select-filter-model/namespace-select-filter-model.injectable"; -import namespaceStoreInjectable from "./store.injectable"; - -interface NamespaceSelectFilterProps { - id: string; -} - -interface Dependencies { - model: NamespaceSelectFilterModel; -} - -const NonInjectedNamespaceSelectFilter = observer(({ model, id }: Dependencies & NamespaceSelectFilterProps) => ( -
    - - id={id} - isMulti={true} - isClearable={false} - menuIsOpen={model.menu.isOpen.get()} - components={{ Placeholder }} - closeMenuOnSelect={false} - controlShouldRenderValue={false} - onChange={model.onChange} - onBlur={model.reset} - formatOptionLabel={model.formatOptionLabel} - options={model.options.get()} - className="NamespaceSelect NamespaceSelectFilter" - menuClass="NamespaceSelectFilterMenu" - isOptionSelected={model.isOptionSelected} - hideSelectedOptions={false} - /> -
    -)); - -export const NamespaceSelectFilter = withInjectables(NonInjectedNamespaceSelectFilter, { - getProps: (di, props) => ({ - model: di.inject(namespaceSelectFilterModelInjectable), - ...props, - }), -}); - -export interface CustomPlaceholderProps extends PlaceholderProps {} - -interface PlaceholderDependencies { - namespaceStore: NamespaceStore; -} - -const NonInjectedPlaceholder = observer(({ namespaceStore, ...props }: CustomPlaceholderProps & PlaceholderDependencies) => { - const getPlaceholder = () => { - const namespaces = namespaceStore.contextNamespaces; - - if (namespaceStore.areAllSelectedImplicitly || namespaces.length === 0) { - return "All namespaces"; - } - - const prefix = namespaces.length === 1 - ? "Namespace" - : "Namespaces"; - - return `${prefix}: ${namespaces.join(", ")}`; - }; - - return ( - - {getPlaceholder()} - - ); -}); - -const Placeholder = withInjectables( NonInjectedPlaceholder, { - getProps: (di, props) => ({ - namespaceStore: di.inject(namespaceStoreInjectable), - ...props, - }), -}); diff --git a/packages/core/src/renderer/components/namespaces/store.ts b/packages/core/src/renderer/components/namespaces/store.ts index 1bbb4446dd23..cce4a2e4c1e5 100644 --- a/packages/core/src/renderer/components/namespaces/store.ts +++ b/packages/core/src/renderer/components/namespaces/store.ts @@ -171,6 +171,20 @@ export class NamespaceStore extends KubeObjectStore { this.dependencies.storage.set([...nextState]); } + deselectSingle(namespace: string) { + const nextState = new Set(this.contextNamespaces); + + nextState.delete(namespace); + this.dependencies.storage.set([...nextState]); + } + + includeSingle(namespace: string) { + const nextState = new Set(this.contextNamespaces); + + nextState.add(namespace); + this.dependencies.storage.set([...nextState]); + } + /** * Makes the given namespace the sole selected namespace */ diff --git a/packages/core/src/renderer/components/workloads-overview/overview.tsx b/packages/core/src/renderer/components/workloads-overview/overview.tsx index 3ea0a8c2d71a..69c51d214efc 100644 --- a/packages/core/src/renderer/components/workloads-overview/overview.tsx +++ b/packages/core/src/renderer/components/workloads-overview/overview.tsx @@ -13,7 +13,7 @@ import type { JobStore } from "../workloads-jobs/store"; import type { CronJobStore } from "../workloads-cronjobs/store"; import type { IComputedValue } from "mobx"; import { makeObservable, observable, reaction } from "mobx"; -import { NamespaceSelectFilter } from "../namespaces/namespace-select-filter"; +import { NamespaceSelectFilter } from "../namespace-select-filter/component"; import { Icon } from "@k8slens/icon"; import { TooltipPosition } from "@k8slens/tooltip"; import { withInjectables } from "@ogre-tools/injectable-react"; diff --git a/packages/core/src/renderer/frames/cluster-frame/__snapshots__/cluster-frame.test.tsx.snap b/packages/core/src/renderer/frames/cluster-frame/__snapshots__/cluster-frame.test.tsx.snap index 3d12aff98752..b589c1b5ddfc 100644 --- a/packages/core/src/renderer/frames/cluster-frame/__snapshots__/cluster-frame.test.tsx.snap +++ b/packages/core/src/renderer/frames/cluster-frame/__snapshots__/cluster-frame.test.tsx.snap @@ -1323,82 +1323,48 @@ exports[` given cluster without list nodes, but with namespaces Overview
    - -
    +
    +
    + All namespaces +
    - - -
    + class="gradient right" + />
    + + + expand_more + +
    diff --git a/packages/core/src/renderer/window/event-listener.injectable.ts b/packages/core/src/renderer/window/event-listener.injectable.ts index 631fec116e1f..4be1022424f6 100644 --- a/packages/core/src/renderer/window/event-listener.injectable.ts +++ b/packages/core/src/renderer/window/event-listener.injectable.ts @@ -9,10 +9,10 @@ import type { Disposer } from "@k8slens/utilities"; export type AddWindowEventListener = typeof addWindowEventListener; export type WindowEventListener = (this: Window, ev: WindowEventMap[K]) => any; -function addWindowEventListener(type: K, listener: WindowEventListener, options?: boolean | AddEventListenerOptions): Disposer { +export function addWindowEventListener(type: K, listener: WindowEventListener, options?: boolean | AddEventListenerOptions): Disposer { window.addEventListener(type, listener, options); - return () => void window.removeEventListener(type, listener); + return () => void window.removeEventListener(type, listener, options); } const windowAddEventListenerInjectable = getInjectable({ diff --git a/packages/utility-features/utilities/src/type-narrowing.ts b/packages/utility-features/utilities/src/type-narrowing.ts index 68d526523768..8c545608df25 100644 --- a/packages/utility-features/utilities/src/type-narrowing.ts +++ b/packages/utility-features/utilities/src/type-narrowing.ts @@ -123,7 +123,8 @@ export function isDefined(val: T | undefined | null): val is T { return val != null; } -export function isFunction(val: unknown): val is (...args: unknown[]) => unknown { +// @ts-expect-error 2677 +export function isFunction(val: T): val is Extract extends never ? ((...args: unknown[]) => unknown) : Extract { return typeof val === "function"; }