1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Fix check on NamespaceSelectFilter not updating (#5691)

This commit is contained in:
Sebastian Malton 2022-07-07 08:59:33 -07:00 committed by GitHub
parent 37af2ebe79
commit f935454875
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 2341 additions and 155 deletions

View File

@ -3,13 +3,6 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
/**
* A function that does nothing
*/
export function noop<T extends any[]>(...args: T): void {
return void args;
}
export * from "./abort-controller"; export * from "./abort-controller";
export * from "./app-version"; export * from "./app-version";
export * from "./autobind"; export * from "./autobind";
@ -27,6 +20,8 @@ export * from "./formatDuration";
export * from "./getRandId"; export * from "./getRandId";
export * from "./hash-set"; export * from "./hash-set";
export * from "./n-fircate"; export * from "./n-fircate";
export * from "./noop";
export * from "./observable-crate/impl";
export * from "./openBrowser"; export * from "./openBrowser";
export * from "./paths"; export * from "./paths";
export * from "./promise-exec"; export * from "./promise-exec";

10
src/common/utils/noop.ts Normal file
View File

@ -0,0 +1,10 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
/**
* A function that does nothing
*/
export function noop<T extends any[]>(...args: T): void {
return void args;
}

View File

@ -0,0 +1,53 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { observable, runInAction } from "mobx";
import { getOrInsertMap } from "../collection-functions";
import { noop } from "../noop";
export interface ObservableCrate<T> {
get(): T;
set(value: T): void;
}
export interface ObservableCrateFactory {
<T>(initialValue: T, transitionHandlers?: ObservableCrateTransitionHandlers<T>): ObservableCrate<T>;
}
export interface ObservableCrateTransitionHandler<T> {
from: T;
to: T;
onTransition: () => void;
}
export type ObservableCrateTransitionHandlers<T> = ObservableCrateTransitionHandler<T>[];
function convertToHandlersMap<T>(handlers: ObservableCrateTransitionHandlers<T>): Map<T, Map<T, () => void>> {
const res: ReturnType<typeof convertToHandlersMap<T>> = new Map();
for (const { from, to, onTransition } of handlers) {
getOrInsertMap(res, from).set(to, onTransition);
}
return res;
}
export const observableCrate = ((initialValue, transitionHandlers = []) => {
const crate = observable.box(initialValue);
const handlers = convertToHandlersMap(transitionHandlers);
return {
get() {
return crate.get();
},
set(value) {
const onTransition = handlers.get(crate.get())?.get(value) ?? noop;
runInAction(() => {
crate.set(value);
onTransition();
});
},
};
}) as ObservableCrateFactory;

View File

@ -0,0 +1,116 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ObservableCrate } from "./impl";
import { observableCrate } from "./impl";
describe("observable-crate", () => {
it("can be constructed with initial value", () => {
expect(() => observableCrate(0)).not.toThrow();
});
it("has a definite type if the initial value is provided", () => {
expect (() => {
const res: ObservableCrate<number> = observableCrate(0);
void res;
}).not.toThrow();
});
it("accepts an array of transitionHandlers", () => {
expect(() => observableCrate(0, [])).not.toThrow();
});
describe("with a crate over an enum, and some transition handlers", () => {
enum Test {
Start,
T1,
End,
}
let crate: ObservableCrate<Test>;
let correctHandler: jest.MockedFunction<() => void>;
let incorrectHandler: jest.MockedFunction<() => void>;
beforeEach(() => {
correctHandler = jest.fn();
incorrectHandler = jest.fn();
crate = observableCrate(Test.Start, [
{
from: Test.Start,
to: Test.Start,
onTransition: incorrectHandler,
},
{
from: Test.Start,
to: Test.T1,
onTransition: correctHandler,
},
{
from: Test.Start,
to: Test.End,
onTransition: incorrectHandler,
},
{
from: Test.T1,
to: Test.Start,
onTransition: incorrectHandler,
},
{
from: Test.T1,
to: Test.T1,
onTransition: incorrectHandler,
},
{
from: Test.T1,
to: Test.End,
onTransition: incorrectHandler,
},
{
from: Test.End,
to: Test.Start,
onTransition: incorrectHandler,
},
{
from: Test.End,
to: Test.T1,
onTransition: incorrectHandler,
},
{
from: Test.End,
to: Test.End,
onTransition: incorrectHandler,
},
]);
});
it("initial value is available", () => {
expect(crate.get()).toBe(Test.Start);
});
it("does not call any transition handler", () => {
expect(correctHandler).not.toBeCalled();
expect(incorrectHandler).not.toBeCalled();
});
describe("when setting a new value", () => {
beforeEach(() => {
crate.set(Test.T1);
});
it("calls the associated transition handler", () => {
expect(correctHandler).toBeCalled();
});
it("does not call any other transition handler", () => {
expect(incorrectHandler).not.toBeCalled();
});
it("new value is available", () => {
expect(crate.get()).toBe(Test.T1);
});
});
});
});

View File

@ -0,0 +1,22 @@
/**
* 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 type React from "react";
import isMacInjectable from "../../../../common/vars/is-mac.injectable";
export type IsMultiSelectionKey = (event: React.KeyboardEvent) => boolean;
const isMultiSelectionKeyInjectable = getInjectable({
id: "is-multi-selection-key",
instantiate: (di): IsMultiSelectionKey => {
const isMac = di.inject(isMacInjectable);
return isMac
? ({ key }) => key === "Meta"
: ({ key }) => key === "Control";
},
});
export default isMultiSelectionKeyInjectable;

View File

@ -2,15 +2,17 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { NamespaceSelectFilterModel } from "./namespace-select-filter-model"; import { namespaceSelectFilterModelFor } from "./namespace-select-filter-model";
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import namespaceStoreInjectable from "../store.injectable"; import namespaceStoreInjectable from "../store.injectable";
import isMultiSelectionKeyInjectable from "./is-selection-key.injectable";
const namespaceSelectFilterModelInjectable = getInjectable({ const namespaceSelectFilterModelInjectable = getInjectable({
id: "namespace-select-filter-model", id: "namespace-select-filter-model",
instantiate: (di) => new NamespaceSelectFilterModel({ instantiate: (di) => namespaceSelectFilterModelFor({
namespaceStore: di.inject(namespaceStoreInjectable), namespaceStore: di.inject(namespaceStoreInjectable),
isMultiSelectionKey: di.inject(isMultiSelectionKeyInjectable),
}), }),
}); });

View File

@ -3,16 +3,18 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import React from "react"; import React from "react";
import { observable, action, untracked, computed, makeObservable } from "mobx"; import type { IComputedValue } from "mobx";
import { observable, action, computed, comparer } from "mobx";
import type { NamespaceStore } from "../store"; import type { NamespaceStore } from "../store";
import { isMac } from "../../../../common/vars"; import type { ActionMeta, MultiValue } from "react-select";
import type { ActionMeta } from "react-select";
import { Icon } from "../../icon"; import { Icon } from "../../icon";
import type { SelectOption } from "../../select"; import type { SelectOption } from "../../select";
import { autoBind } from "../../../utils"; import { observableCrate } from "../../../utils";
import type { IsMultiSelectionKey } from "./is-selection-key.injectable";
interface Dependencies { interface Dependencies {
readonly namespaceStore: NamespaceStore; namespaceStore: NamespaceStore;
isMultiSelectionKey: IsMultiSelectionKey;
} }
export const selectAllNamespaces = Symbol("all-namespaces-selected"); export const selectAllNamespaces = Symbol("all-namespaces-selected");
@ -20,140 +22,160 @@ export const selectAllNamespaces = Symbol("all-namespaces-selected");
export type SelectAllNamespaces = typeof selectAllNamespaces; export type SelectAllNamespaces = typeof selectAllNamespaces;
export type NamespaceSelectFilterOption = SelectOption<string | SelectAllNamespaces>; export type NamespaceSelectFilterOption = SelectOption<string | SelectAllNamespaces>;
export class NamespaceSelectFilterModel { export interface NamespaceSelectFilterModel {
constructor(private readonly dependencies: Dependencies) { readonly options: IComputedValue<readonly NamespaceSelectFilterOption[]>;
makeObservable(this); readonly menu: {
autoBind(this); open: () => void;
} close: () => void;
readonly isOpen: IComputedValue<boolean>;
readonly options = computed((): readonly NamespaceSelectFilterOption[] => { };
const baseOptions = this.dependencies.namespaceStore.items.map(ns => ns.getName()); onChange: (newValue: MultiValue<NamespaceSelectFilterOption>, actionMeta: ActionMeta<NamespaceSelectFilterOption>) => void;
onClick: () => void;
baseOptions.sort(( onKeyDown: React.KeyboardEventHandler;
(left, right) => onKeyUp: React.KeyboardEventHandler;
+this.selectedNames.has(right) reset: () => void;
- +this.selectedNames.has(left) isOptionSelected: (option: NamespaceSelectFilterOption) => boolean;
)); formatOptionLabel: (option: NamespaceSelectFilterOption) => JSX.Element;
return [
{
value: selectAllNamespaces,
label: "All Namespaces",
isSelected: false,
},
...baseOptions.map(namespace => ({
value: namespace,
label: namespace,
isSelected: this.selectedNames.has(namespace),
})),
];
});
formatOptionLabel({ value, isSelected }: NamespaceSelectFilterOption) {
if (value === selectAllNamespaces) {
return "All Namespaces";
}
return (
<div className="flex gaps align-center">
<Icon small material="layers" />
<span>{value}</span>
{isSelected && (
<Icon
small
material="check"
className="box right"
/>
)}
</div>
);
}
readonly menuIsOpen = observable.box(false);
@action
closeMenu() {
this.menuIsOpen.set(false);
}
@action
openMenu(){
this.menuIsOpen.set(true);
}
get selectedNames() {
return untracked(() => this.dependencies.namespaceStore.selectedNames);
}
isSelected(namespace: string | string[]) {
return this.dependencies.namespaceStore.hasContext(namespace);
}
selectSingle(namespace: string) {
this.dependencies.namespaceStore.selectSingle(namespace);
}
selectAll() {
this.dependencies.namespaceStore.selectAll();
}
onChange(namespace: unknown, action: ActionMeta<NamespaceSelectFilterOption>) {
switch (action.action) {
case "clear":
this.dependencies.namespaceStore.selectAll();
break;
case "deselect-option":
if (typeof action.option === "string") {
this.dependencies.namespaceStore.toggleSingle(action.option);
}
break;
case "select-option":
if (action.option?.value === selectAllNamespaces) {
this.dependencies.namespaceStore.selectAll();
} else if (action.option) {
if (this.isMultiSelection) {
this.dependencies.namespaceStore.toggleSingle(action.option.value);
} else {
this.dependencies.namespaceStore.selectSingle(action.option.value);
}
}
break;
}
}
onClick() {
if (!this.menuIsOpen.get()) {
this.openMenu();
} else if (!this.isMultiSelection) {
this.closeMenu();
}
}
private isMultiSelection = false;
onKeyDown(event: React.KeyboardEvent) {
if (isSelectionKey(event)) {
this.isMultiSelection = true;
}
}
onKeyUp(event: React.KeyboardEvent) {
if (isSelectionKey(event)) {
this.isMultiSelection = false;
}
}
@action
reset() {
this.isMultiSelection = false;
this.closeMenu();
}
} }
const isSelectionKey = (event: React.KeyboardEvent): boolean => { enum SelectMenuState {
if (isMac) { Close = "close",
return event.key === "Meta"; Open = "open",
} }
return event.key === "Control"; // windows or linux export function namespaceSelectFilterModelFor(dependencies: Dependencies): NamespaceSelectFilterModel {
}; const { isMultiSelectionKey, namespaceStore } = 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(namespaceStore.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",
},
...namespaceStore
.items
.map(ns => ns.getName())
.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 (
<div className="flex gaps align-center">
<Icon small material="layers" />
<span>{option.value}</span>
{isOptionSelected(option) && (
<Icon
small
material="check"
className="box right"
data-testid={`namespace-select-filter-option-${option.value}-selected`}
/>
)}
</div>
);
},
};
return model;
}

View File

@ -4,14 +4,17 @@
} }
} }
.NamespaceSelectFilterParent {
max-width: 300px;
}
.NamespaceSelectFilter { .NamespaceSelectFilter {
--gradientColor: var(--select-menu-bgc); --gradientColor: var(--select-menu-bgc);
.Select { .Select {
&__placeholder { &__placeholder {
width: 100%;
white-space: nowrap; white-space: nowrap;
overflow: scroll!important; overflow: scroll hidden!important;
text-overflow: unset!important; text-overflow: unset!important;
margin-left: -8px; margin-left: -8px;
padding-left: 8px; padding-left: 8px;
@ -61,10 +64,14 @@
word-break: break-all; word-break: break-all;
padding: 4px 8px; padding: 4px 8px;
border-radius: 3px; border-radius: 3px;
&--is-selected:not(&--is-focused) {
background: transparent;
}
} }
} }
.Icon { .Icon {
margin-right: $margin * 0.5; margin-right: $margin * 0.5;
} }
} }

View File

@ -0,0 +1,252 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
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 directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import { Namespace } from "../../../common/k8s-api/endpoints";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable";
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("<NamespaceSelectFilter />", () => {
let di: DiContainer;
let namespaceStore: NamespaceStore;
let result: RenderResult;
beforeEach(() => {
di = getDiForUnitTesting({ doGeneralOverrides: true });
di.override(directoryForUserDataInjectable, () => "/some-directory");
di.override(storesAndApisCanBeCreatedInjectable, () => true);
namespaceStore = di.inject(namespaceStoreInjectable);
const render = renderFor(di);
namespaceStore.items.replace([
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"),
]);
result = render((
<NamespaceSelectFilter id="namespace-select-filter" />
));
});
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.nextSibling).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");
});
});
});
});
});
});
});

View File

@ -29,12 +29,14 @@ const NonInjectedNamespaceSelectFilter = observer(({ model, id }: Dependencies &
onKeyUp={model.onKeyUp} onKeyUp={model.onKeyUp}
onKeyDown={model.onKeyDown} onKeyDown={model.onKeyDown}
onClick={model.onClick} onClick={model.onClick}
className="NamespaceSelectFilterParent"
data-testid="namespace-select-filter"
> >
<Select<string | SelectAllNamespaces, NamespaceSelectFilterOption, true> <Select<string | SelectAllNamespaces, NamespaceSelectFilterOption, true>
id={id} id={id}
isMulti={true} isMulti={true}
isClearable={false} isClearable={false}
menuIsOpen={model.menuIsOpen.get()} menuIsOpen={model.menu.isOpen.get()}
components={{ Placeholder }} components={{ Placeholder }}
closeMenuOnSelect={false} closeMenuOnSelect={false}
controlShouldRenderValue={false} controlShouldRenderValue={false}
@ -43,7 +45,10 @@ const NonInjectedNamespaceSelectFilter = observer(({ model, id }: Dependencies &
formatOptionLabel={model.formatOptionLabel} formatOptionLabel={model.formatOptionLabel}
options={model.options.get()} options={model.options.get()}
className="NamespaceSelect NamespaceSelectFilter" className="NamespaceSelect NamespaceSelectFilter"
menuClass="NamespaceSelectFilterMenu" /> menuClass="NamespaceSelectFilterMenu"
isOptionSelected={model.isOptionSelected}
hideSelectedOptions={false}
/>
</div> </div>
)); ));
@ -54,7 +59,7 @@ export const NamespaceSelectFilter = withInjectables<Dependencies, NamespaceSele
}), }),
}); });
export interface CustomPlaceholderProps extends PlaceholderProps<NamespaceSelectFilterOption, boolean> {} export interface CustomPlaceholderProps extends PlaceholderProps<NamespaceSelectFilterOption, true> {}
interface PlaceholderDependencies { interface PlaceholderDependencies {
namespaceStore: NamespaceStore; namespaceStore: NamespaceStore;

View File

@ -25,6 +25,7 @@ export interface SelectOption<Value> {
label: React.ReactNode; label: React.ReactNode;
isDisabled?: boolean; isDisabled?: boolean;
isSelected?: boolean; isSelected?: boolean;
id?: string;
} }
/** /**

View File

@ -1278,7 +1278,10 @@ exports[`<ClusterFrame /> given cluster without list nodes, but with namespaces
> >
Overview Overview
</h5> </h5>
<div> <div
class="NamespaceSelectFilterParent"
data-testid="namespace-select-filter"
>
<div <div
class="Select theme-dark NamespaceSelect NamespaceSelectFilter css-b62m3t-container" class="Select theme-dark NamespaceSelect NamespaceSelectFilter css-b62m3t-container"
> >