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 { 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";
|
||||
|
||||
Loading…
Reference in New Issue
Block a user