mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
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 <sebastian@malton.name>
This commit is contained in:
parent
21914db35b
commit
151a393fb5
@ -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<string | SelectAllNamespaces>;
|
||||
export interface NamespaceSelectFilterOption {
|
||||
value: string | SelectAllNamespaces;
|
||||
label: string;
|
||||
id: string | SelectAllNamespaces;
|
||||
}
|
||||
|
||||
export interface NamespaceSelectFilterModel {
|
||||
readonly options: IComputedValue<readonly NamespaceSelectFilterOption[]>;
|
||||
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;
|
||||
};
|
||||
onChange: (newValue: MultiValue<NamespaceSelectFilterOption>, actionMeta: ActionMeta<NamespaceSelectFilterOption>) => void;
|
||||
onClick: () => void;
|
||||
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;
|
||||
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);
|
||||
}
|
||||
}
|
||||
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) {
|
||||
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,
|
||||
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,75 +4,86 @@
|
||||
}
|
||||
}
|
||||
|
||||
.NamespaceSelectFilterParent {
|
||||
max-width: 300px;
|
||||
}
|
||||
.namespace-select-filter {
|
||||
width: 300px;
|
||||
position: relative;
|
||||
|
||||
.NamespaceSelectFilter {
|
||||
--gradientColor: var(--select-menu-bgc);
|
||||
.list-container {
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
z-index: 10;
|
||||
background: var(--mainBackground);
|
||||
border-radius: var(--border-radius);
|
||||
|
||||
.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;
|
||||
li.option {
|
||||
cursor: pointer;
|
||||
padding: calc(var(--padding) / 2) var(--padding);
|
||||
list-style: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
&:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.selected-icon:hover {
|
||||
color: var(--buttonAccentBackground);
|
||||
}
|
||||
|
||||
.add-selection-icon:hover {
|
||||
color: var(--colorSuccess);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__value-container {
|
||||
.menu {
|
||||
width: 300px;
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid;
|
||||
border-color: var(--halfGray);
|
||||
padding: calc(var(--padding) / 2) var(--padding);
|
||||
display: flex;
|
||||
|
||||
.non-icon {
|
||||
width: -webkit-fill-available;
|
||||
position: relative;
|
||||
|
||||
&::before, &::after {
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
display: block;
|
||||
width: 8px;
|
||||
height: var(--font-size);
|
||||
}
|
||||
|
||||
&::before {
|
||||
left: 0;
|
||||
background: linear-gradient(to right, var(--gradientColor) 0px, transparent);
|
||||
}
|
||||
|
||||
&::after {
|
||||
right: 0;
|
||||
background: linear-gradient(to left, var(--gradientColor) 0px, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.NamespaceSelectFilterMenu {
|
||||
right: 0;
|
||||
|
||||
.Select {
|
||||
&__menu-list {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
&__option {
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
|
||||
&--is-selected:not(&--is-focused) {
|
||||
input {
|
||||
width: 100%;
|
||||
background: 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;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.gradient {
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
top: 0;
|
||||
height: 20px;
|
||||
z-index: 21;
|
||||
|
||||
&.left {
|
||||
left: -7px;
|
||||
background: linear-gradient(to right, var(--contentColor) 0%, rgba(255, 255, 255, 0) 100%);
|
||||
}
|
||||
|
||||
&.right {
|
||||
right: 2px;
|
||||
background: linear-gradient(to left, var(--contentColor) 0%, rgba(255, 255, 255, 0) 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Icon {
|
||||
margin-right: $margin * 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,33 +25,171 @@ interface Dependencies {
|
||||
model: NamespaceSelectFilterModel;
|
||||
}
|
||||
|
||||
const NonInjectedNamespaceSelectFilter = observer(({ model, id }: Dependencies & NamespaceSelectFilterProps) => (
|
||||
<div
|
||||
onKeyUp={model.onKeyUp}
|
||||
onKeyDown={model.onKeyDown}
|
||||
onClick={model.onClick}
|
||||
className="NamespaceSelectFilterParent"
|
||||
data-testid="namespace-select-filter"
|
||||
>
|
||||
<Select<string | SelectAllNamespaces, NamespaceSelectFilterOption, true>
|
||||
const Gradient = ({ type }: { type: "left" | "right" }) => (
|
||||
<div className={cssNames("gradient", type)} />
|
||||
);
|
||||
|
||||
const NamespaceSelectFilterMenu = observer(({ id, model }: Dependencies & NamespaceSelectFilterProps) => {
|
||||
const selectedOptions = model.selectedOptions.get();
|
||||
const prefix = selectedOptions.length === 1
|
||||
? "Namespace"
|
||||
: "Namespaces";
|
||||
|
||||
return (
|
||||
<div className="menu">
|
||||
<div className="non-icon">
|
||||
<input
|
||||
type="text"
|
||||
id={`${id}-filter`}
|
||||
value={model.filterText.get()}
|
||||
onChange={(event) => model.filterText.set(event.target.value)}
|
||||
onClick={model.menu.open}
|
||||
/>
|
||||
<Gradient type="left" />
|
||||
<label htmlFor={`${id}-filter`}>
|
||||
{(
|
||||
model.filterText.get() !== ""
|
||||
? model.filterText.get()
|
||||
: (
|
||||
model.menu.hasSelectedAll.get()
|
||||
? "All namespaces"
|
||||
: `${prefix}: ${selectedOptions.map(option => option.value).join(", ")}`
|
||||
)
|
||||
)}
|
||||
</label>
|
||||
<Gradient type="right" />
|
||||
</div>
|
||||
<Icon
|
||||
className="expand-icon"
|
||||
material={model.menu.isOpen.get() ? "expand_less" : "expand_more"}
|
||||
onClick={model.menu.toggle}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const rowHeight = 29;
|
||||
|
||||
const NonInjectedNamespaceSelectFilter = observer(({ model, id }: Dependencies & NamespaceSelectFilterProps) => {
|
||||
const divRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
return addWindowEventListener("click", (event) => {
|
||||
if (!model.menu.isOpen.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (divRef.current?.contains(event.target as Node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
model.menu.close();
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
onKeyUp={model.menu.onKeyUp}
|
||||
onKeyDown={model.menu.onKeyDown}
|
||||
className="namespace-select-filter"
|
||||
data-testid="namespace-select-filter"
|
||||
id={id}
|
||||
isMulti={true}
|
||||
isClearable={false}
|
||||
menuIsOpen={model.menu.isOpen.get()}
|
||||
components={{ Placeholder }}
|
||||
closeMenuOnSelect={false}
|
||||
controlShouldRenderValue={false}
|
||||
onChange={model.onChange}
|
||||
onBlur={model.reset}
|
||||
formatOptionLabel={model.formatOptionLabel}
|
||||
options={model.options.get()}
|
||||
className="NamespaceSelect NamespaceSelectFilter"
|
||||
menuClass="NamespaceSelectFilterMenu"
|
||||
isOptionSelected={model.isOptionSelected}
|
||||
hideSelectedOptions={false}
|
||||
ref={divRef}
|
||||
tabIndex={1}
|
||||
>
|
||||
<NamespaceSelectFilterMenu model={model} id={id} />
|
||||
{model.menu.isOpen.get() && (
|
||||
<div
|
||||
className="list-container"
|
||||
>
|
||||
<VariableSizeList
|
||||
className="list"
|
||||
width={300}
|
||||
height={Math.min(model.filteredOptions.get().length * rowHeight, 300)}
|
||||
itemSize={() => rowHeight}
|
||||
itemCount={model.filteredOptions.get().length}
|
||||
itemData={{
|
||||
items: model.filteredOptions.get(),
|
||||
model,
|
||||
}}
|
||||
overscanCount={5}
|
||||
innerElementType={"ul"}
|
||||
>
|
||||
{NamespaceSelectFilterRow}
|
||||
</VariableSizeList>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
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 (
|
||||
<Icon
|
||||
small
|
||||
material="check"
|
||||
className="selected-icon box right"
|
||||
data-testid={`namespace-select-filter-option-${namespace}-selected`}
|
||||
tooltip={`Remove ${namespace} from selection`}
|
||||
onClick={prevDefault(() => model.deselect(namespace))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Icon
|
||||
small
|
||||
material="add_box"
|
||||
className="add-selection-icon box right"
|
||||
data-testid={`namespace-select-filter-option-${namespace}-add-to-selection`}
|
||||
tooltip={`Add ${namespace} to selection`}
|
||||
onClick={prevDefault(() => model.select(namespace))}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
);
|
||||
};
|
||||
|
||||
const NamespaceSelectFilterRow = observer(({ index, style, data: { model, items }}: FilterRowProps) => {
|
||||
const option = items[index];
|
||||
|
||||
return (
|
||||
<li
|
||||
style={style}
|
||||
className={cssNames("option", "flex gaps align-center", {
|
||||
"all-namespaces": option.value === selectAllNamespaces,
|
||||
"single-namespace": option.value !== selectAllNamespaces,
|
||||
})}
|
||||
onClick={() => model.onClick(option)}
|
||||
>
|
||||
{option.value === selectAllNamespaces
|
||||
? <span className="data">All Namespaces</span>
|
||||
: (
|
||||
<>
|
||||
<Icon
|
||||
small
|
||||
material="layers"
|
||||
onClick={prevDefault(() => model.onClick(option))}
|
||||
tooltip={{
|
||||
preferredPositions: TooltipPosition.LEFT,
|
||||
children: `Select only ${option.value}`,
|
||||
}}
|
||||
/>
|
||||
<span className="data">{option.value}</span>
|
||||
{renderSingleOptionIcons(option.value, option, model)}
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
});
|
||||
|
||||
export const NamespaceSelectFilter = withInjectables<Dependencies, NamespaceSelectFilterProps>(NonInjectedNamespaceSelectFilter, {
|
||||
getProps: (di, props) => ({
|
||||
@ -58,38 +197,3 @@ export const NamespaceSelectFilter = withInjectables<Dependencies, NamespaceSele
|
||||
...props,
|
||||
}),
|
||||
});
|
||||
|
||||
export interface CustomPlaceholderProps extends PlaceholderProps<NamespaceSelectFilterOption, true> {}
|
||||
|
||||
interface PlaceholderDependencies {
|
||||
namespaceStore: NamespaceStore;
|
||||
}
|
||||
|
||||
const NonInjectedPlaceholder = observer(({ namespaceStore, ...props }: CustomPlaceholderProps & PlaceholderDependencies) => {
|
||||
const getPlaceholder = () => {
|
||||
const namespaces = namespaceStore.contextNamespaces;
|
||||
|
||||
if (namespaceStore.areAllSelectedImplicitly || namespaces.length === 0) {
|
||||
return "All namespaces";
|
||||
}
|
||||
|
||||
const prefix = namespaces.length === 1
|
||||
? "Namespace"
|
||||
: "Namespaces";
|
||||
|
||||
return `${prefix}: ${namespaces.join(", ")}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<components.Placeholder {...props}>
|
||||
{getPlaceholder()}
|
||||
</components.Placeholder>
|
||||
);
|
||||
});
|
||||
|
||||
const Placeholder = withInjectables<PlaceholderDependencies, CustomPlaceholderProps>( NonInjectedPlaceholder, {
|
||||
getProps: (di, props) => ({
|
||||
namespaceStore: di.inject(namespaceStoreInjectable),
|
||||
...props,
|
||||
}),
|
||||
});
|
||||
|
||||
@ -171,6 +171,20 @@ export class NamespaceStore extends KubeObjectStore<Namespace, NamespaceApi> {
|
||||
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
|
||||
*/
|
||||
|
||||
@ -9,10 +9,10 @@ import type { Disposer } from "@k8slens/utilities";
|
||||
export type AddWindowEventListener = typeof addWindowEventListener;
|
||||
export type WindowEventListener<K extends keyof WindowEventMap> = (this: Window, ev: WindowEventMap[K]) => any;
|
||||
|
||||
function addWindowEventListener<K extends keyof WindowEventMap>(type: K, listener: WindowEventListener<K>, options?: boolean | AddEventListenerOptions): Disposer {
|
||||
export function addWindowEventListener<K extends keyof WindowEventMap>(type: K, listener: WindowEventListener<K>, 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({
|
||||
|
||||
@ -123,7 +123,8 @@ export function isDefined<T>(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<T>(val: T): val is Extract<T, Function> extends never ? ((...args: unknown[]) => unknown) : Extract<T, Function> {
|
||||
return typeof val === "function";
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user