mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
chore: Consolidate namespace-select-filter model into single file
Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
8fc9fc7a66
commit
b619eb56d8
@ -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<NamespaceSelectFilterOption[]>;
|
||||||
|
readonly filteredOptions: IComputedValue<NamespaceSelectFilterOption[]>;
|
||||||
|
readonly selectedOptions: IComputedValue<NamespaceSelectFilterOption[]>;
|
||||||
|
readonly menu: {
|
||||||
|
open: () => void;
|
||||||
|
close: () => void;
|
||||||
|
toggle: () => void;
|
||||||
|
readonly isOpen: IComputedValue<boolean>;
|
||||||
|
readonly hasSelectedAll: IComputedValue<boolean>;
|
||||||
|
onKeyDown: React.KeyboardEventHandler;
|
||||||
|
onKeyUp: React.KeyboardEventHandler;
|
||||||
|
};
|
||||||
|
onClick: (options: NamespaceSelectFilterOption) => void;
|
||||||
|
deselect: (namespace: string) => void;
|
||||||
|
select: (namespace: string) => void;
|
||||||
|
readonly filterText: IObservableValue<string>;
|
||||||
|
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;
|
||||||
@ -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;
|
|
||||||
@ -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<NamespaceSelectFilterOption[]>;
|
|
||||||
readonly filteredOptions: IComputedValue<NamespaceSelectFilterOption[]>;
|
|
||||||
readonly selectedOptions: IComputedValue<NamespaceSelectFilterOption[]>;
|
|
||||||
readonly menu: {
|
|
||||||
open: () => void;
|
|
||||||
close: () => void;
|
|
||||||
toggle: () => void;
|
|
||||||
readonly isOpen: IComputedValue<boolean>;
|
|
||||||
readonly hasSelectedAll: IComputedValue<boolean>;
|
|
||||||
onKeyDown: React.KeyboardEventHandler;
|
|
||||||
onKeyUp: React.KeyboardEventHandler;
|
|
||||||
};
|
|
||||||
onClick: (options: NamespaceSelectFilterOption) => void;
|
|
||||||
deselect: (namespace: string) => void;
|
|
||||||
select: (namespace: string) => void;
|
|
||||||
readonly filterText: IObservableValue<string>;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@ -8,9 +8,8 @@ import "./namespace-select-filter.scss";
|
|||||||
import React, { useEffect, useRef } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||||
import type { NamespaceSelectFilterModel, NamespaceSelectFilterOption } from "./namespace-select-filter-model";
|
import type { NamespaceSelectFilterModel, NamespaceSelectFilterOption } from "./model.injectable";
|
||||||
import { selectAllNamespaces } from "./namespace-select-filter-model";
|
import namespaceSelectFilterModelInjectable, { selectAllNamespaces } from "./model.injectable";
|
||||||
import namespaceSelectFilterModelInjectable from "./namespace-select-filter-model.injectable";
|
|
||||||
import { VariableSizeList } from "react-window";
|
import { VariableSizeList } from "react-window";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { cssNames, prevDefault } from "@k8slens/utilities";
|
import { cssNames, prevDefault } from "@k8slens/utilities";
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user