From 151a393fb5b3a7636af769bedb942b0545dedc5b Mon Sep 17 00:00:00 2001
From: Sebastian Malton
Date: Tue, 23 May 2023 10:12:18 -0400
Subject: [PATCH 01/16] feat: Improve UX of namespace select filter
- Add better support for thousands of namespaces
- Add support for mouse only multi-selection
Signed-off-by: Sebastian Malton
---
.../namespace-select-filter-model.tsx | 150 ++++++------
.../namespaces/namespace-select-filter.scss | 117 +++++-----
.../namespaces/namespace-select-filter.tsx | 220 +++++++++++++-----
.../renderer/components/namespaces/store.ts | 14 ++
.../window/event-listener.injectable.ts | 4 +-
.../utilities/src/type-narrowing.ts | 3 +-
6 files changed, 322 insertions(+), 186 deletions(-)
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
index c0ad194dfbc1..8a3cac363c63 100644
--- 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
@@ -2,16 +2,14 @@
* 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 type React from "react";
+import type { IComputedValue, IObservableValue } 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";
+import GlobToRegExp from "glob-to-regexp";
interface Dependencies {
context: ClusterContext;
@@ -22,22 +20,31 @@ interface Dependencies {
export const selectAllNamespaces = Symbol("all-namespaces-selected");
export type SelectAllNamespaces = typeof selectAllNamespaces;
-export type NamespaceSelectFilterOption = SelectOption;
+export interface NamespaceSelectFilterOption {
+ value: string | SelectAllNamespaces;
+ label: string;
+ id: string | SelectAllNamespaces;
+}
export interface NamespaceSelectFilterModel {
- readonly options: IComputedValue;
+ 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;
};
- onChange: (newValue: MultiValue, actionMeta: ActionMeta) => void;
- onClick: () => void;
- onKeyDown: React.KeyboardEventHandler;
- onKeyUp: React.KeyboardEventHandler;
+ onClick: (options: NamespaceSelectFilterOption) => void;
+ deselect: (namespace: string) => void;
+ select: (namespace: string) => void;
+ readonly filterText: IObservableValue;
reset: () => void;
isOptionSelected: (option: NamespaceSelectFilterOption) => boolean;
- formatOptionLabel: (option: NamespaceSelectFilterOption) => JSX.Element;
}
enum SelectMenuState {
@@ -45,6 +52,18 @@ enum SelectMenuState {
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));
+ };
+};
+
export function namespaceSelectFilterModelFor(dependencies: Dependencies): NamespaceSelectFilterModel {
const { isMultiSelectionKey, namespaceStore, context } = dependencies;
@@ -58,6 +77,7 @@ export function namespaceSelectFilterModelFor(dependencies: Dependencies): Names
didToggle = false;
},
}]);
+ const filterText = observable.box("");
const selectedNames = computed(() => new Set(context.contextNamespaces), {
equals: comparer.structural,
});
@@ -74,7 +94,7 @@ export function namespaceSelectFilterModelFor(dependencies: Dependencies): Names
? 1
: -1;
};
- const options = computed((): readonly NamespaceSelectFilterOption[] => [
+ const options = computed((): NamespaceSelectFilterOption[] => [
{
value: selectAllNamespaces,
label: "All Namespaces",
@@ -89,6 +109,8 @@ export function namespaceSelectFilterModelFor(dependencies: Dependencies): Names
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) {
@@ -100,82 +122,66 @@ export function namespaceSelectFilterModelFor(dependencies: Dependencies): Names
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,
- },
- 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);
- }
+ 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();
}
- break;
- }
+ }
+ },
},
- onClick: () => {
- if (!menuIsOpen.get()) {
- model.menu.open();
- } else if (!isMultiSelection) {
+ 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();
}
- },
- onKeyDown: (event) => {
- if (isMultiSelectionKey(event)) {
- isMultiSelection = true;
- }
- },
- onKeyUp: (event) => {
- if (isMultiSelectionKey(event)) {
- isMultiSelection = false;
-
- if (didToggle) {
- model.menu.close();
- }
- }
- },
+ }),
+ deselect: action((namespace) => {
+ namespaceStore.deselectSingle(namespace);
+ }),
+ select: action((namespace) => {
+ namespaceStore.includeSingle(namespace);
+ }),
+ filterText,
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
index f227976b46a7..2eead588e132 100644
--- a/packages/core/src/renderer/components/namespaces/namespace-select-filter.scss
+++ b/packages/core/src/renderer/components/namespaces/namespace-select-filter.scss
@@ -4,75 +4,86 @@
}
}
-.NamespaceSelectFilterParent {
- max-width: 300px;
-}
+.namespace-select-filter {
+ width: 300px;
+ position: relative;
+
+ .list-container {
+ position: absolute;
+ top: 30px;
+ z-index: 10;
+ background: var(--mainBackground);
+ border-radius: var(--border-radius);
-.NamespaceSelectFilter {
- --gradientColor: var(--select-menu-bgc);
+ li.option {
+ cursor: pointer;
+ padding: calc(var(--padding) / 2) var(--padding);
+ list-style: none;
- .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;
+ &:hover {
+ color: white;
+ }
+
+ .selected-icon:hover {
+ color: var(--buttonAccentBackground);
+ }
- &::-webkit-scrollbar {
- display: none;
+ .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;
- &__value-container {
+ .non-icon {
+ width: -webkit-fill-available;
position: relative;
- &::before, &::after {
- content: ' ';
- position: absolute;
- z-index: 20;
- display: block;
- width: 8px;
- height: var(--font-size);
+ input {
+ width: 100%;
+ background: transparent;
}
- &::before {
- left: 0;
- background: linear-gradient(to right, var(--gradientColor) 0px, 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;
- &::after {
- right: 0;
- background: linear-gradient(to left, var(--gradientColor) 0px, transparent);
+ &::-webkit-scrollbar {
+ display: none;
+ }
}
- }
- }
-}
-
-.NamespaceSelectFilterMenu {
- right: 0;
- .Select {
- &__menu-list {
- max-width: 400px;
- }
+ .gradient {
+ position: absolute;
+ width: 10px;
+ top: 0;
+ height: 20px;
+ z-index: 21;
- &__option {
- white-space: normal;
- word-break: break-all;
- padding: 4px 8px;
- border-radius: 3px;
+ &.left {
+ left: -7px;
+ background: linear-gradient(to right, var(--contentColor) 0%, rgba(255, 255, 255, 0) 100%);
+ }
- &--is-selected:not(&--is-focused) {
- background: transparent;
+ &.right {
+ right: 2px;
+ background: linear-gradient(to left, var(--contentColor) 0%, rgba(255, 255, 255, 0) 100%);
+ }
}
}
}
-
- .Icon {
- margin-right: $margin * 0.5;
- }
}
diff --git a/packages/core/src/renderer/components/namespaces/namespace-select-filter.tsx b/packages/core/src/renderer/components/namespaces/namespace-select-filter.tsx
index de1c2a8cd663..b22ba43114c0 100644
--- a/packages/core/src/renderer/components/namespaces/namespace-select-filter.tsx
+++ b/packages/core/src/renderer/components/namespaces/namespace-select-filter.tsx
@@ -5,16 +5,17 @@
import "./namespace-select-filter.scss";
-import React from "react";
+import React, { useEffect, useRef } 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 type { NamespaceSelectFilterModel, NamespaceSelectFilterOption } from "./namespace-select-filter-model/namespace-select-filter-model";
+import { 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";
+import { VariableSizeList } from "react-window";
+import { Icon } from "../icon";
+import { cssNames, prevDefault } from "@k8slens/utilities";
+import { addWindowEventListener } from "../../window/event-listener.injectable";
+import { TooltipPosition } from "@k8slens/tooltip";
interface NamespaceSelectFilterProps {
id: string;
@@ -24,72 +25,175 @@ interface Dependencies {
model: NamespaceSelectFilterModel;
}
-const NonInjectedNamespaceSelectFilter = observer(({ model, id }: Dependencies & NamespaceSelectFilterProps) => (
-
-
-));
+const Gradient = ({ type }: { type: "left" | "right" }) => (
+
+);
-export const NamespaceSelectFilter = withInjectables(NonInjectedNamespaceSelectFilter, {
- getProps: (di, props) => ({
- model: di.inject(namespaceSelectFilterModelInjectable),
- ...props,
- }),
+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}
+ />
+
+
+
+
+
+
+ );
});
-export interface CustomPlaceholderProps extends PlaceholderProps {}
+const rowHeight = 29;
-interface PlaceholderDependencies {
- namespaceStore: NamespaceStore;
-}
+const NonInjectedNamespaceSelectFilter = observer(({ model, id }: Dependencies & NamespaceSelectFilterProps) => {
+ const divRef = useRef(null);
-const NonInjectedPlaceholder = observer(({ namespaceStore, ...props }: CustomPlaceholderProps & PlaceholderDependencies) => {
- const getPlaceholder = () => {
- const namespaces = namespaceStore.contextNamespaces;
+ useEffect(() => {
+ return addWindowEventListener("click", (event) => {
+ if (!model.menu.isOpen.get()) {
+ return;
+ }
- if (namespaceStore.areAllSelectedImplicitly || namespaces.length === 0) {
- return "All namespaces";
- }
+ if (divRef.current?.contains(event.target as Node)) {
+ return;
+ }
- const prefix = namespaces.length === 1
- ? "Namespace"
- : "Namespaces";
+ model.menu.close();
+ });
+ }, []);
- return `${prefix}: ${namespaces.join(", ")}`;
+ 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 (
-
- {getPlaceholder()}
-
+ model.onClick(option)}
+ >
+ {option.value === selectAllNamespaces
+ ? All Namespaces
+ : (
+ <>
+ model.onClick(option))}
+ tooltip={{
+ preferredPositions: TooltipPosition.LEFT,
+ children: `Select only ${option.value}`,
+ }}
+ />
+ {option.value}
+ {renderSingleOptionIcons(option.value, option, model)}
+ >
+ )}
+
);
});
-const Placeholder = withInjectables( NonInjectedPlaceholder, {
+export const NamespaceSelectFilter = withInjectables(NonInjectedNamespaceSelectFilter, {
getProps: (di, props) => ({
- namespaceStore: di.inject(namespaceStoreInjectable),
+ model: di.inject(namespaceSelectFilterModelInjectable),
...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/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";
}
From 5811695e492492af23bae7ef5d0de7bb7c5edecb Mon Sep 17 00:00:00 2001
From: Sebastian Malton
Date: Wed, 24 May 2023 12:09:46 -0400
Subject: [PATCH 02/16] chore: Start moving namespace-select-filter to own
folder
Signed-off-by: Sebastian Malton
---
.../src/extensions/renderer-api/components.ts | 2 +-
.../config-vertical-pod-autoscalers/vpa.tsx | 4 ++--
.../components/helm-releases/releases.tsx | 2 +-
.../kube-object-list-layout.tsx | 2 +-
.../is-selection-key.injectable.ts | 2 +-
.../namespace-select-filter-model.injectable.ts | 4 ++--
.../namespace-select-filter-model.tsx | 4 ++--
.../namespace-select-filter.scss | 0
.../namespace-select-filter.test.tsx | 4 ++--
.../namespace-select-filter.tsx | 6 +++---
.../filter-by-namespace.injectable.ts | 2 +-
.../namespaces/namespace-select-badge.tsx | 17 ++++++-----------
.../components/workloads-overview/overview.tsx | 2 +-
13 files changed, 23 insertions(+), 28 deletions(-)
rename packages/core/src/renderer/components/{namespaces/namespace-select-filter-model => namespace-select-filter}/is-selection-key.injectable.ts (89%)
rename packages/core/src/renderer/components/{namespaces/namespace-select-filter-model => namespace-select-filter}/namespace-select-filter-model.injectable.ts (85%)
rename packages/core/src/renderer/components/{namespaces/namespace-select-filter-model => namespace-select-filter}/namespace-select-filter-model.tsx (97%)
rename packages/core/src/renderer/components/{namespaces => namespace-select-filter}/namespace-select-filter.scss (100%)
rename packages/core/src/renderer/components/{namespaces => namespace-select-filter}/namespace-select-filter.test.tsx (98%)
rename packages/core/src/renderer/components/{namespaces => namespace-select-filter}/namespace-select-filter.tsx (97%)
rename packages/core/src/renderer/components/namespaces/{namespace-select-filter-model => }/filter-by-namespace.injectable.ts (90%)
diff --git a/packages/core/src/extensions/renderer-api/components.ts b/packages/core/src/extensions/renderer-api/components.ts
index 36ce3bba0a64..594c2751b991 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/namespace-select-filter";
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/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..b6a58155e157 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/namespace-select-filter";
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/kube-object-list-layout.tsx b/packages/core/src/renderer/components/kube-object-list-layout/kube-object-list-layout.tsx
index 9280d55ac89e..52c79ccbe249 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/namespace-select-filter";
import { ResourceKindMap, ResourceNames } from "../../utils/rbac";
import { Icon } from "@k8slens/icon";
import { TooltipPosition } from "@k8slens/tooltip";
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 89%
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..28393ddc48fd 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;
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/namespace-select-filter/namespace-select-filter-model.injectable.ts
similarity index 85%
rename from packages/core/src/renderer/components/namespaces/namespace-select-filter-model/namespace-select-filter-model.injectable.ts
rename to packages/core/src/renderer/components/namespace-select-filter/namespace-select-filter-model.injectable.ts
index d84292410b56..b73b339ff2bd 100644
--- a/packages/core/src/renderer/components/namespaces/namespace-select-filter-model/namespace-select-filter-model.injectable.ts
+++ b/packages/core/src/renderer/components/namespace-select-filter/namespace-select-filter-model.injectable.ts
@@ -4,9 +4,9 @@
*/
import { namespaceSelectFilterModelFor } from "./namespace-select-filter-model";
import { getInjectable } from "@ogre-tools/injectable";
-import namespaceStoreInjectable from "../store.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 clusterFrameContextForNamespacedResourcesInjectable from "../../cluster-frame-context/for-namespaced-resources.injectable";
const namespaceSelectFilterModelInjectable = getInjectable({
id: "namespace-select-filter-model",
diff --git a/packages/core/src/renderer/components/namespaces/namespace-select-filter-model/namespace-select-filter-model.tsx b/packages/core/src/renderer/components/namespace-select-filter/namespace-select-filter-model.tsx
similarity index 97%
rename from packages/core/src/renderer/components/namespaces/namespace-select-filter-model/namespace-select-filter-model.tsx
rename to packages/core/src/renderer/components/namespace-select-filter/namespace-select-filter-model.tsx
index 8a3cac363c63..ae56cdc40a4a 100644
--- a/packages/core/src/renderer/components/namespaces/namespace-select-filter-model/namespace-select-filter-model.tsx
+++ b/packages/core/src/renderer/components/namespace-select-filter/namespace-select-filter-model.tsx
@@ -5,10 +5,10 @@
import type React from "react";
import type { IComputedValue, IObservableValue } from "mobx";
import { observable, action, computed, comparer } from "mobx";
-import type { NamespaceStore } from "../store";
+import type { NamespaceStore } from "../namespaces/store";
import { observableCrate } from "@k8slens/utilities";
import type { IsMultiSelectionKey } from "./is-selection-key.injectable";
-import type { ClusterContext } from "../../../cluster-frame-context/cluster-frame-context";
+import type { ClusterContext } from "../../cluster-frame-context/cluster-frame-context";
import GlobToRegExp from "glob-to-regexp";
interface Dependencies {
diff --git a/packages/core/src/renderer/components/namespaces/namespace-select-filter.scss b/packages/core/src/renderer/components/namespace-select-filter/namespace-select-filter.scss
similarity index 100%
rename from packages/core/src/renderer/components/namespaces/namespace-select-filter.scss
rename to packages/core/src/renderer/components/namespace-select-filter/namespace-select-filter.scss
diff --git a/packages/core/src/renderer/components/namespaces/namespace-select-filter.test.tsx b/packages/core/src/renderer/components/namespace-select-filter/namespace-select-filter.test.tsx
similarity index 98%
rename from packages/core/src/renderer/components/namespaces/namespace-select-filter.test.tsx
rename to packages/core/src/renderer/components/namespace-select-filter/namespace-select-filter.test.tsx
index a07bb6c981ac..c5567e2da658 100644
--- a/packages/core/src/renderer/components/namespaces/namespace-select-filter.test.tsx
+++ b/packages/core/src/renderer/components/namespace-select-filter/namespace-select-filter.test.tsx
@@ -24,8 +24,8 @@ 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";
+import type { NamespaceStore } from "../namespaces/store";
+import namespaceStoreInjectable from "../namespaces/store.injectable";
function createNamespace(name: string): Namespace {
return new Namespace({
diff --git a/packages/core/src/renderer/components/namespaces/namespace-select-filter.tsx b/packages/core/src/renderer/components/namespace-select-filter/namespace-select-filter.tsx
similarity index 97%
rename from packages/core/src/renderer/components/namespaces/namespace-select-filter.tsx
rename to packages/core/src/renderer/components/namespace-select-filter/namespace-select-filter.tsx
index b22ba43114c0..884188507d53 100644
--- a/packages/core/src/renderer/components/namespaces/namespace-select-filter.tsx
+++ b/packages/core/src/renderer/components/namespace-select-filter/namespace-select-filter.tsx
@@ -8,9 +8,9 @@ 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 "./namespace-select-filter-model/namespace-select-filter-model";
-import { selectAllNamespaces } from "./namespace-select-filter-model/namespace-select-filter-model";
-import namespaceSelectFilterModelInjectable from "./namespace-select-filter-model/namespace-select-filter-model.injectable";
+import type { NamespaceSelectFilterModel, NamespaceSelectFilterOption } from "./namespace-select-filter-model";
+import { selectAllNamespaces } from "./namespace-select-filter-model";
+import namespaceSelectFilterModelInjectable from "./namespace-select-filter-model.injectable";
import { VariableSizeList } from "react-window";
import { Icon } from "../icon";
import { cssNames, prevDefault } from "@k8slens/utilities";
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/workloads-overview/overview.tsx b/packages/core/src/renderer/components/workloads-overview/overview.tsx
index 3ea0a8c2d71a..39d4601fefa4 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/namespace-select-filter";
import { Icon } from "@k8slens/icon";
import { TooltipPosition } from "@k8slens/tooltip";
import { withInjectables } from "@ogre-tools/injectable-react";
From 8fc9fc7a6663f527446050da47a71fbed313f285 Mon Sep 17 00:00:00 2001
From: Sebastian Malton
Date: Wed, 24 May 2023 12:10:27 -0400
Subject: [PATCH 03/16] chore: Refactor isSelectionKey
Signed-off-by: Sebastian Malton
---
.../namespace-select-filter/is-selection-key.injectable.ts | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/packages/core/src/renderer/components/namespace-select-filter/is-selection-key.injectable.ts b/packages/core/src/renderer/components/namespace-select-filter/is-selection-key.injectable.ts
index 28393ddc48fd..25779bcc521c 100644
--- a/packages/core/src/renderer/components/namespace-select-filter/is-selection-key.injectable.ts
+++ b/packages/core/src/renderer/components/namespace-select-filter/is-selection-key.injectable.ts
@@ -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;
},
});
From b619eb56d8c3e178b78973363613d128e7a656d5 Mon Sep 17 00:00:00 2001
From: Sebastian Malton
Date: Wed, 24 May 2023 12:13:19 -0400
Subject: [PATCH 04/16] chore: Consolidate namespace-select-filter model into
single file
Signed-off-by: Sebastian Malton
---
.../model.injectable.ts | 191 ++++++++++++++++++
...amespace-select-filter-model.injectable.ts | 21 --
.../namespace-select-filter-model.tsx | 188 -----------------
.../namespace-select-filter.tsx | 5 +-
4 files changed, 193 insertions(+), 212 deletions(-)
create mode 100644 packages/core/src/renderer/components/namespace-select-filter/model.injectable.ts
delete mode 100644 packages/core/src/renderer/components/namespace-select-filter/namespace-select-filter-model.injectable.ts
delete mode 100644 packages/core/src/renderer/components/namespace-select-filter/namespace-select-filter-model.tsx
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..7594de276fe4
--- /dev/null
+++ b/packages/core/src/renderer/components/namespace-select-filter/model.injectable.ts
@@ -0,0 +1,191 @@
+/**
+ * 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;
+ };
+ 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();
+ }
+ }
+ },
+ },
+ 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-model.injectable.ts b/packages/core/src/renderer/components/namespace-select-filter/namespace-select-filter-model.injectable.ts
deleted file mode 100644
index b73b339ff2bd..000000000000
--- a/packages/core/src/renderer/components/namespace-select-filter/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 "../namespaces/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/namespace-select-filter/namespace-select-filter-model.tsx b/packages/core/src/renderer/components/namespace-select-filter/namespace-select-filter-model.tsx
deleted file mode 100644
index ae56cdc40a4a..000000000000
--- a/packages/core/src/renderer/components/namespace-select-filter/namespace-select-filter-model.tsx
+++ /dev/null
@@ -1,188 +0,0 @@
-/**
- * Copyright (c) OpenLens Authors. All rights reserved.
- * Licensed under MIT License. See LICENSE in root directory for more information.
- */
-import type React from "react";
-import type { IComputedValue, IObservableValue } from "mobx";
-import { observable, action, computed, comparer } from "mobx";
-import type { NamespaceStore } from "../namespaces/store";
-import { observableCrate } from "@k8slens/utilities";
-import type { IsMultiSelectionKey } from "./is-selection-key.injectable";
-import type { ClusterContext } from "../../cluster-frame-context/cluster-frame-context";
-import GlobToRegExp from "glob-to-regexp";
-
-interface Dependencies {
- context: ClusterContext;
- namespaceStore: NamespaceStore;
- isMultiSelectionKey: IsMultiSelectionKey;
-}
-
-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;
- };
- 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));
- };
-};
-
-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 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();
- }
- }
- },
- },
- 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;
-}
diff --git a/packages/core/src/renderer/components/namespace-select-filter/namespace-select-filter.tsx b/packages/core/src/renderer/components/namespace-select-filter/namespace-select-filter.tsx
index 884188507d53..56c45cea5a40 100644
--- a/packages/core/src/renderer/components/namespace-select-filter/namespace-select-filter.tsx
+++ b/packages/core/src/renderer/components/namespace-select-filter/namespace-select-filter.tsx
@@ -8,9 +8,8 @@ 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 "./namespace-select-filter-model";
-import { selectAllNamespaces } from "./namespace-select-filter-model";
-import namespaceSelectFilterModelInjectable from "./namespace-select-filter-model.injectable";
+import type { NamespaceSelectFilterModel, NamespaceSelectFilterOption } from "./model.injectable";
+import namespaceSelectFilterModelInjectable, { selectAllNamespaces } from "./model.injectable";
import { VariableSizeList } from "react-window";
import { Icon } from "../icon";
import { cssNames, prevDefault } from "@k8slens/utilities";
From ce11ef0a2e5fc054c3b856232bb6514447d7d442 Mon Sep 17 00:00:00 2001
From: Sebastian Malton
Date: Wed, 24 May 2023 12:13:51 -0400
Subject: [PATCH 05/16] chore: Rename namespace-select-filter component file
Signed-off-by: Sebastian Malton
---
packages/core/src/extensions/renderer-api/components.ts | 2 +-
.../core/src/renderer/components/helm-releases/releases.tsx | 2 +-
.../kube-object-list-layout/kube-object-list-layout.tsx | 2 +-
.../{namespace-select-filter.tsx => component.tsx} | 0
.../namespace-select-filter/namespace-select-filter.test.tsx | 2 +-
.../src/renderer/components/workloads-overview/overview.tsx | 2 +-
6 files changed, 5 insertions(+), 5 deletions(-)
rename packages/core/src/renderer/components/namespace-select-filter/{namespace-select-filter.tsx => component.tsx} (100%)
diff --git a/packages/core/src/extensions/renderer-api/components.ts b/packages/core/src/extensions/renderer-api/components.ts
index 594c2751b991..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/namespace-select-filter/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/renderer/components/helm-releases/releases.tsx b/packages/core/src/renderer/components/helm-releases/releases.tsx
index b6a58155e157..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 "../namespace-select-filter/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/kube-object-list-layout.tsx b/packages/core/src/renderer/components/kube-object-list-layout/kube-object-list-layout.tsx
index 52c79ccbe249..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 "../namespace-select-filter/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/namespace-select-filter.tsx b/packages/core/src/renderer/components/namespace-select-filter/component.tsx
similarity index 100%
rename from packages/core/src/renderer/components/namespace-select-filter/namespace-select-filter.tsx
rename to packages/core/src/renderer/components/namespace-select-filter/component.tsx
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
index c5567e2da658..6f24d416ce59 100644
--- 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
@@ -23,7 +23,7 @@ import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-create
import type { Disposer } from "@k8slens/utilities";
import { disposer } from "@k8slens/utilities";
import { renderFor } from "../test-utils/renderFor";
-import { NamespaceSelectFilter } from "./namespace-select-filter";
+import { NamespaceSelectFilter } from "./component";
import type { NamespaceStore } from "../namespaces/store";
import namespaceStoreInjectable from "../namespaces/store.injectable";
diff --git a/packages/core/src/renderer/components/workloads-overview/overview.tsx b/packages/core/src/renderer/components/workloads-overview/overview.tsx
index 39d4601fefa4..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 "../namespace-select-filter/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";
From fa7bc2ee6bf2ce0c08d0187d00095954abb1e8e9 Mon Sep 17 00:00:00 2001
From: Sebastian Malton
Date: Wed, 24 May 2023 14:07:49 -0400
Subject: [PATCH 06/16] chore: Fixup previous namespace-select-filter unit
tests
Signed-off-by: Sebastian Malton
---
.../namespace-select-filter.test.tsx.snap | 2014 +++++++++++++++++
.../namespace-select-filter/component.tsx | 3 +
.../namespace-select-filter.test.tsx | 54 +-
.../namespace-select-filter.test.tsx.snap | 1698 --------------
4 files changed, 2045 insertions(+), 1724 deletions(-)
create mode 100644 packages/core/src/renderer/components/namespace-select-filter/__snapshots__/namespace-select-filter.test.tsx.snap
delete mode 100644 packages/core/src/renderer/components/namespaces/__snapshots__/namespace-select-filter.test.tsx.snap
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..b8662b524769
--- /dev/null
+++ b/packages/core/src/renderer/components/namespace-select-filter/__snapshots__/namespace-select-filter.test.tsx.snap
@@ -0,0 +1,2014 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[` once the subscribe resolves renders 1`] = `
+
+
+
+`;
+
+exports[` once the subscribe resolves when clicked renders 1`] = `
+
+
+
+
+
+
+
+ -
+
+ All Namespaces
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-1
+
+
+ test-1
+
+
+
+ check
+
+
+
+ Remove test-1 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-10
+
+
+ test-10
+
+
+
+ check
+
+
+
+ Remove test-10 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-11
+
+
+ test-11
+
+
+
+ check
+
+
+
+ Remove test-11 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-12
+
+
+ test-12
+
+
+
+ check
+
+
+
+ Remove test-12 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-13
+
+
+ test-13
+
+
+
+ check
+
+
+
+ Remove test-13 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-2
+
+
+ test-2
+
+
+
+ check
+
+
+
+ Remove test-2 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-3
+
+
+ test-3
+
+
+
+ check
+
+
+
+ Remove test-3 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-4
+
+
+ test-4
+
+
+
+ check
+
+
+
+ Remove test-4 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-5
+
+
+ test-5
+
+
+
+ check
+
+
+
+ Remove test-5 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-6
+
+
+ test-6
+
+
+
+ check
+
+
+
+ Remove test-6 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-7
+
+
+ test-7
+
+
+
+ check
+
+
+
+ Remove test-7 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-8
+
+
+ test-8
+
+
+
+ check
+
+
+
+ Remove test-8 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-9
+
+
+ test-9
+
+
+
+ check
+
+
+
+ Remove test-9 from selection
+
+
+
+
+
+
+
+
+`;
+
+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`] = `
+
+
+
+
+
+
+
+ -
+
+ All Namespaces
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-2
+
+
+ test-2
+
+
+
+ check
+
+
+
+ Remove test-2 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-1
+
+
+ test-1
+
+
+
+ add_box
+
+
+
+ Add test-1 to selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-10
+
+
+ test-10
+
+
+
+ add_box
+
+
+
+ Add test-10 to selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-11
+
+
+ test-11
+
+
+
+ add_box
+
+
+
+ Add test-11 to selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-12
+
+
+ test-12
+
+
+
+ add_box
+
+
+
+ Add test-12 to selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-13
+
+
+ test-13
+
+
+
+ add_box
+
+
+
+ Add test-13 to selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-3
+
+
+ test-3
+
+
+
+ add_box
+
+
+
+ Add test-3 to selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-4
+
+
+ test-4
+
+
+
+ add_box
+
+
+
+ Add test-4 to selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-5
+
+
+ test-5
+
+
+
+ add_box
+
+
+
+ Add test-5 to selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-6
+
+
+ test-6
+
+
+
+ add_box
+
+
+
+ Add test-6 to selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-7
+
+
+ test-7
+
+
+
+ add_box
+
+
+
+ Add test-7 to selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-8
+
+
+ test-8
+
+
+
+ add_box
+
+
+
+ Add test-8 to selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-9
+
+
+ test-9
+
+
+
+ add_box
+
+
+
+ Add test-9 to selection
+
+
+
+
+
+
+
+
+`;
+
+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`] = `
+
+
+
+
+
+
+
+ -
+
+ All Namespaces
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-1
+
+
+ test-1
+
+
+
+ check
+
+
+
+ Remove test-1 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-2
+
+
+ test-2
+
+
+
+ add_box
+
+
+
+ Add test-2 to selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-10
+
+
+ test-10
+
+
+
+ add_box
+
+
+
+ Add test-10 to selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-11
+
+
+ test-11
+
+
+
+ add_box
+
+
+
+ Add test-11 to selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-12
+
+
+ test-12
+
+
+
+ add_box
+
+
+
+ Add test-12 to selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-13
+
+
+ test-13
+
+
+
+ add_box
+
+
+
+ Add test-13 to selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-3
+
+
+ test-3
+
+
+
+ check
+
+
+
+ Remove test-3 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-4
+
+
+ test-4
+
+
+
+ add_box
+
+
+
+ Add test-4 to selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-5
+
+
+ test-5
+
+
+
+ add_box
+
+
+
+ Add test-5 to selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-6
+
+
+ test-6
+
+
+
+ add_box
+
+
+
+ Add test-6 to selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-7
+
+
+ test-7
+
+
+
+ add_box
+
+
+
+ Add test-7 to selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-8
+
+
+ test-8
+
+
+
+ add_box
+
+
+
+ Add test-8 to selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-9
+
+
+ test-9
+
+
+
+ add_box
+
+
+
+ Add test-9 to selection
+
+
+
+
+
+
+
+
+`;
diff --git a/packages/core/src/renderer/components/namespace-select-filter/component.tsx b/packages/core/src/renderer/components/namespace-select-filter/component.tsx
index 56c45cea5a40..379232b581ed 100644
--- a/packages/core/src/renderer/components/namespace-select-filter/component.tsx
+++ b/packages/core/src/renderer/components/namespace-select-filter/component.tsx
@@ -43,6 +43,7 @@ const NamespaceSelectFilterMenu = observer(({ id, model }: Dependencies & Namesp
value={model.filterText.get()}
onChange={(event) => model.filterText.set(event.target.value)}
onClick={model.menu.open}
+ data-testid="namespace-select-filter-input"
/>
+
+
+
+
+
+
+ -
+
+ All Namespaces
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-1
+
+
+ test-1
+
+
+
+ check
+
+
+
+ Remove test-1 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-10
+
+
+ test-10
+
+
+
+ check
+
+
+
+ Remove test-10 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-11
+
+
+ test-11
+
+
+
+ check
+
+
+
+ Remove test-11 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-12
+
+
+ test-12
+
+
+
+ check
+
+
+
+ Remove test-12 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-13
+
+
+ test-13
+
+
+
+ check
+
+
+
+ Remove test-13 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-2
+
+
+ test-2
+
+
+
+ check
+
+
+
+ Remove test-2 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-3
+
+
+ test-3
+
+
+
+ check
+
+
+
+ Remove test-3 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-4
+
+
+ test-4
+
+
+
+ check
+
+
+
+ Remove test-4 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-5
+
+
+ test-5
+
+
+
+ check
+
+
+
+ Remove test-5 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-6
+
+
+ test-6
+
+
+
+ check
+
+
+
+ Remove test-6 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-7
+
+
+ test-7
+
+
+
+ check
+
+
+
+ Remove test-7 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-8
+
+
+ test-8
+
+
+
+ check
+
+
+
+ Remove test-8 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-9
+
+
+ test-9
+
+
+
+ check
+
+
+
+ Remove test-9 from selection
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+ All Namespaces
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-1
+
+
+ test-1
+
+
+
+ check
+
+
+
+ Remove test-1 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-10
+
+
+ test-10
+
+
+
+ check
+
+
+
+ Remove test-10 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-11
+
+
+ test-11
+
+
+
+ check
+
+
+
+ Remove test-11 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-12
+
+
+ test-12
+
+
+
+ check
+
+
+
+ Remove test-12 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-13
+
+
+ test-13
+
+
+
+ check
+
+
+
+ Remove test-13 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-2
+
+
+ test-2
+
+
+
+ add_box
+
+
+
+ Add test-2 to selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-3
+
+
+ test-3
+
+
+
+ check
+
+
+
+ Remove test-3 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-4
+
+
+ test-4
+
+
+
+ check
+
+
+
+ Remove test-4 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-5
+
+
+ test-5
+
+
+
+ check
+
+
+
+ Remove test-5 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-6
+
+
+ test-6
+
+
+
+ check
+
+
+
+ Remove test-6 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-7
+
+
+ test-7
+
+
+
+ check
+
+
+
+ Remove test-7 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-8
+
+
+ test-8
+
+
+
+ check
+
+
+
+ Remove test-8 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-9
+
+
+ test-9
+
+
+
+ check
+
+
+
+ Remove test-9 from selection
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+ All Namespaces
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-1
+
+
+ test-1
+
+
+
+ check
+
+
+
+ Remove test-1 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-10
+
+
+ test-10
+
+
+
+ check
+
+
+
+ Remove test-10 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-11
+
+
+ test-11
+
+
+
+ check
+
+
+
+ Remove test-11 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-12
+
+
+ test-12
+
+
+
+ check
+
+
+
+ Remove test-12 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-13
+
+
+ test-13
+
+
+
+ check
+
+
+
+ Remove test-13 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-2
+
+
+ test-2
+
+
+
+ add_box
+
+
+
+ Add test-2 to selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-3
+
+
+ test-3
+
+
+
+ add_box
+
+
+
+ Add test-3 to selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-4
+
+
+ test-4
+
+
+
+ check
+
+
+
+ Remove test-4 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-5
+
+
+ test-5
+
+
+
+ check
+
+
+
+ Remove test-5 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-6
+
+
+ test-6
+
+
+
+ check
+
+
+
+ Remove test-6 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-7
+
+
+ test-7
+
+
+
+ check
+
+
+
+ Remove test-7 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-8
+
+
+ test-8
+
+
+
+ check
+
+
+
+ Remove test-8 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-9
+
+
+ test-9
+
+
+
+ check
+
+
+
+ Remove test-9 from selection
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+ All Namespaces
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-1
+
+
+ test-1
+
+
+
+ check
+
+
+
+ Remove test-1 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-10
+
+
+ test-10
+
+
+
+ check
+
+
+
+ Remove test-10 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-11
+
+
+ test-11
+
+
+
+ check
+
+
+
+ Remove test-11 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-12
+
+
+ test-12
+
+
+
+ check
+
+
+
+ Remove test-12 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-13
+
+
+ test-13
+
+
+
+ check
+
+
+
+ Remove test-13 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-2
+
+
+ test-2
+
+
+
+ check
+
+
+
+ Remove test-2 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-3
+
+
+ test-3
+
+
+
+ add_box
+
+
+
+ Add test-3 to selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-4
+
+
+ test-4
+
+
+
+ check
+
+
+
+ Remove test-4 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-5
+
+
+ test-5
+
+
+
+ check
+
+
+
+ Remove test-5 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-6
+
+
+ test-6
+
+
+
+ check
+
+
+
+ Remove test-6 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-7
+
+
+ test-7
+
+
+
+ check
+
+
+
+ Remove test-7 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-8
+
+
+ test-8
+
+
+
+ check
+
+
+
+ Remove test-8 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-9
+
+
+ test-9
+
+
+
+ check
+
+
+
+ Remove test-9 from selection
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+ All Namespaces
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-1
+
+
+ test-1
+
+
+
+ check
+
+
+
+ Remove test-1 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-10
+
+
+ test-10
+
+
+
+ check
+
+
+
+ Remove test-10 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-11
+
+
+ test-11
+
+
+
+ check
+
+
+
+ Remove test-11 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-12
+
+
+ test-12
+
+
+
+ check
+
+
+
+ Remove test-12 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-13
+
+
+ test-13
+
+
+
+ check
+
+
+
+ Remove test-13 from selection
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+ All Namespaces
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-1
+
+
+ test-1
+
+
+
+ check
+
+
+
+ Remove test-1 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-10
+
+
+ test-10
+
+
+
+ check
+
+
+
+ Remove test-10 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-11
+
+
+ test-11
+
+
+
+ check
+
+
+
+ Remove test-11 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-12
+
+
+ test-12
+
+
+
+ check
+
+
+
+ Remove test-12 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-13
+
+
+ test-13
+
+
+
+ check
+
+
+
+ Remove test-13 from selection
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+ All Namespaces
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-1
+
+
+ test-1
+
+
+
+ check
+
+
+
+ Remove test-1 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-10
+
+
+ test-10
+
+
+
+ check
+
+
+
+ Remove test-10 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-11
+
+
+ test-11
+
+
+
+ check
+
+
+
+ Remove test-11 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-12
+
+
+ test-12
+
+
+
+ check
+
+
+
+ Remove test-12 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-13
+
+
+ test-13
+
+
+
+ check
+
+
+
+ Remove test-13 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-2
+
+
+ test-2
+
+
+
+ check
+
+
+
+ Remove test-2 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-3
+
+
+ test-3
+
+
+
+ check
+
+
+
+ Remove test-3 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-4
+
+
+ test-4
+
+
+
+ check
+
+
+
+ Remove test-4 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-5
+
+
+ test-5
+
+
+
+ check
+
+
+
+ Remove test-5 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-6
+
+
+ test-6
+
+
+
+ check
+
+
+
+ Remove test-6 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-7
+
+
+ test-7
+
+
+
+ check
+
+
+
+ Remove test-7 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-8
+
+
+ test-8
+
+
+
+ check
+
+
+
+ Remove test-8 from selection
+
+
+ -
+
+
+ layers
+
+
+
+ Select only test-9
+
+
+ test-9
+
+
+
+ check
+
+
+
+ Remove test-9 from selection
+
+
+
+
+
+
+
+
+
+