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:
parent
37af2ebe79
commit
f935454875
@ -3,13 +3,6 @@
|
||||
* 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 "./app-version";
|
||||
export * from "./autobind";
|
||||
@ -27,6 +20,8 @@ export * from "./formatDuration";
|
||||
export * from "./getRandId";
|
||||
export * from "./hash-set";
|
||||
export * from "./n-fircate";
|
||||
export * from "./noop";
|
||||
export * from "./observable-crate/impl";
|
||||
export * from "./openBrowser";
|
||||
export * from "./paths";
|
||||
export * from "./promise-exec";
|
||||
|
||||
10
src/common/utils/noop.ts
Normal file
10
src/common/utils/noop.ts
Normal 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;
|
||||
}
|
||||
53
src/common/utils/observable-crate/impl.ts
Normal file
53
src/common/utils/observable-crate/impl.ts
Normal 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;
|
||||
116
src/common/utils/observable-crate/observable-crate.test.ts
Normal file
116
src/common/utils/observable-crate/observable-crate.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||
@ -2,15 +2,17 @@
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* 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 namespaceStoreInjectable from "../store.injectable";
|
||||
import isMultiSelectionKeyInjectable from "./is-selection-key.injectable";
|
||||
|
||||
const namespaceSelectFilterModelInjectable = getInjectable({
|
||||
id: "namespace-select-filter-model",
|
||||
|
||||
instantiate: (di) => new NamespaceSelectFilterModel({
|
||||
instantiate: (di) => namespaceSelectFilterModelFor({
|
||||
namespaceStore: di.inject(namespaceStoreInjectable),
|
||||
isMultiSelectionKey: di.inject(isMultiSelectionKeyInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@ -3,16 +3,18 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
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 { isMac } from "../../../../common/vars";
|
||||
import type { ActionMeta } from "react-select";
|
||||
import type { ActionMeta, MultiValue } from "react-select";
|
||||
import { Icon } from "../../icon";
|
||||
import type { SelectOption } from "../../select";
|
||||
import { autoBind } from "../../../utils";
|
||||
import { observableCrate } from "../../../utils";
|
||||
import type { IsMultiSelectionKey } from "./is-selection-key.injectable";
|
||||
|
||||
interface Dependencies {
|
||||
readonly namespaceStore: NamespaceStore;
|
||||
namespaceStore: NamespaceStore;
|
||||
isMultiSelectionKey: IsMultiSelectionKey;
|
||||
}
|
||||
|
||||
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 NamespaceSelectFilterOption = SelectOption<string | SelectAllNamespaces>;
|
||||
|
||||
export class NamespaceSelectFilterModel {
|
||||
constructor(private readonly dependencies: Dependencies) {
|
||||
makeObservable(this);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
readonly options = computed((): readonly NamespaceSelectFilterOption[] => {
|
||||
const baseOptions = this.dependencies.namespaceStore.items.map(ns => ns.getName());
|
||||
|
||||
baseOptions.sort((
|
||||
(left, right) =>
|
||||
+this.selectedNames.has(right)
|
||||
- +this.selectedNames.has(left)
|
||||
));
|
||||
|
||||
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();
|
||||
}
|
||||
export interface NamespaceSelectFilterModel {
|
||||
readonly options: IComputedValue<readonly NamespaceSelectFilterOption[]>;
|
||||
readonly menu: {
|
||||
open: () => void;
|
||||
close: () => void;
|
||||
readonly isOpen: IComputedValue<boolean>;
|
||||
};
|
||||
onChange: (newValue: MultiValue<NamespaceSelectFilterOption>, actionMeta: ActionMeta<NamespaceSelectFilterOption>) => void;
|
||||
onClick: () => void;
|
||||
onKeyDown: React.KeyboardEventHandler;
|
||||
onKeyUp: React.KeyboardEventHandler;
|
||||
reset: () => void;
|
||||
isOptionSelected: (option: NamespaceSelectFilterOption) => boolean;
|
||||
formatOptionLabel: (option: NamespaceSelectFilterOption) => JSX.Element;
|
||||
}
|
||||
|
||||
const isSelectionKey = (event: React.KeyboardEvent): boolean => {
|
||||
if (isMac) {
|
||||
return event.key === "Meta";
|
||||
}
|
||||
enum SelectMenuState {
|
||||
Close = "close",
|
||||
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;
|
||||
}
|
||||
|
||||
@ -4,14 +4,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
.NamespaceSelectFilterParent {
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.NamespaceSelectFilter {
|
||||
--gradientColor: var(--select-menu-bgc);
|
||||
|
||||
.Select {
|
||||
&__placeholder {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: scroll!important;
|
||||
overflow: scroll hidden!important;
|
||||
text-overflow: unset!important;
|
||||
margin-left: -8px;
|
||||
padding-left: 8px;
|
||||
@ -61,6 +64,10 @@
|
||||
word-break: break-all;
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
|
||||
&--is-selected:not(&--is-focused) {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -29,12 +29,14 @@ const NonInjectedNamespaceSelectFilter = observer(({ model, id }: Dependencies &
|
||||
onKeyUp={model.onKeyUp}
|
||||
onKeyDown={model.onKeyDown}
|
||||
onClick={model.onClick}
|
||||
className="NamespaceSelectFilterParent"
|
||||
data-testid="namespace-select-filter"
|
||||
>
|
||||
<Select<string | SelectAllNamespaces, NamespaceSelectFilterOption, true>
|
||||
id={id}
|
||||
isMulti={true}
|
||||
isClearable={false}
|
||||
menuIsOpen={model.menuIsOpen.get()}
|
||||
menuIsOpen={model.menu.isOpen.get()}
|
||||
components={{ Placeholder }}
|
||||
closeMenuOnSelect={false}
|
||||
controlShouldRenderValue={false}
|
||||
@ -43,7 +45,10 @@ const NonInjectedNamespaceSelectFilter = observer(({ model, id }: Dependencies &
|
||||
formatOptionLabel={model.formatOptionLabel}
|
||||
options={model.options.get()}
|
||||
className="NamespaceSelect NamespaceSelectFilter"
|
||||
menuClass="NamespaceSelectFilterMenu" />
|
||||
menuClass="NamespaceSelectFilterMenu"
|
||||
isOptionSelected={model.isOptionSelected}
|
||||
hideSelectedOptions={false}
|
||||
/>
|
||||
</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 {
|
||||
namespaceStore: NamespaceStore;
|
||||
|
||||
@ -25,6 +25,7 @@ export interface SelectOption<Value> {
|
||||
label: React.ReactNode;
|
||||
isDisabled?: boolean;
|
||||
isSelected?: boolean;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -1278,7 +1278,10 @@ exports[`<ClusterFrame /> given cluster without list nodes, but with namespaces
|
||||
>
|
||||
Overview
|
||||
</h5>
|
||||
<div>
|
||||
<div
|
||||
class="NamespaceSelectFilterParent"
|
||||
data-testid="namespace-select-filter"
|
||||
>
|
||||
<div
|
||||
class="Select theme-dark NamespaceSelect NamespaceSelectFilter css-b62m3t-container"
|
||||
>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user