mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Improve <NamespaceSelectFilter>'s UX (#3490)
- Display previously selected namespaces at the top of the option list when first opened - Don't resort after every selection - Automatically close the option list if no longer in multi-select mode and have toggled at least one namespace Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
91b3bd10ee
commit
51b53b3349
@ -22,16 +22,16 @@
|
|||||||
import "./namespace-select-filter.scss";
|
import "./namespace-select-filter.scss";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import { components, PlaceholderProps } from "react-select";
|
import { components, PlaceholderProps } from "react-select";
|
||||||
|
import { action, computed, makeObservable, observable, reaction } from "mobx";
|
||||||
|
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { NamespaceSelect } from "./namespace-select";
|
import { NamespaceSelect } from "./namespace-select";
|
||||||
import { namespaceStore } from "./namespace.store";
|
import { namespaceStore } from "./namespace.store";
|
||||||
|
|
||||||
import type { SelectOption, SelectProps } from "../select";
|
import type { SelectOption, SelectProps } from "../select";
|
||||||
import { isLinux, isMac, isWindows } from "../../../common/vars";
|
import { isMac } from "../../../common/vars";
|
||||||
import { observable } from "mobx";
|
|
||||||
|
|
||||||
const Placeholder = observer((props: PlaceholderProps<any, boolean>) => {
|
const Placeholder = observer((props: PlaceholderProps<any, boolean>) => {
|
||||||
const getPlaceholder = (): React.ReactNode => {
|
const getPlaceholder = (): React.ReactNode => {
|
||||||
@ -60,6 +60,41 @@ export class NamespaceSelectFilter extends React.Component<SelectProps> {
|
|||||||
static isMultiSelection = observable.box(false);
|
static isMultiSelection = observable.box(false);
|
||||||
static isMenuOpen = observable.box(false);
|
static isMenuOpen = observable.box(false);
|
||||||
|
|
||||||
|
private selected = observable.set<string>();
|
||||||
|
private didToggle = false;
|
||||||
|
|
||||||
|
constructor(props: SelectProps) {
|
||||||
|
super(props);
|
||||||
|
makeObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get isMultiSelection() {
|
||||||
|
return NamespaceSelectFilter.isMultiSelection.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
set isMultiSelection(val: boolean) {
|
||||||
|
NamespaceSelectFilter.isMultiSelection.set(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get isMenuOpen() {
|
||||||
|
return NamespaceSelectFilter.isMenuOpen.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
set isMenuOpen(val: boolean) {
|
||||||
|
NamespaceSelectFilter.isMenuOpen.set(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
disposeOnUnmount(this, [
|
||||||
|
reaction(() => this.isMenuOpen, newVal => {
|
||||||
|
if (newVal) { // rising edge of selection
|
||||||
|
this.selected.replace(namespaceStore.selectedNamespaces);
|
||||||
|
this.didToggle = false;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
formatOptionLabel({ value: namespace, label }: SelectOption) {
|
formatOptionLabel({ value: namespace, label }: SelectOption) {
|
||||||
if (namespace) {
|
if (namespace) {
|
||||||
const isSelected = namespaceStore.hasContext(namespace);
|
const isSelected = namespaceStore.hasContext(namespace);
|
||||||
@ -76,37 +111,58 @@ export class NamespaceSelectFilter extends React.Component<SelectProps> {
|
|||||||
return label;
|
return label;
|
||||||
}
|
}
|
||||||
|
|
||||||
onChange([{ value: namespace }]: SelectOption[]) {
|
@action
|
||||||
if (NamespaceSelectFilter.isMultiSelection.get() && namespace) {
|
onChange = ([{ value: namespace }]: SelectOption[]) => {
|
||||||
|
if (namespace) {
|
||||||
|
if (this.isMultiSelection) {
|
||||||
|
this.didToggle = true;
|
||||||
namespaceStore.toggleContext(namespace);
|
namespaceStore.toggleContext(namespace);
|
||||||
} else if (!NamespaceSelectFilter.isMultiSelection.get() && namespace) {
|
} else {
|
||||||
namespaceStore.toggleSingle(namespace);
|
namespaceStore.toggleSingle(namespace);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
namespaceStore.toggleAll(true); // "All namespaces" clicked
|
namespaceStore.toggleAll(true); // "All namespaces" clicked
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private isSelectionKey(e: React.KeyboardEvent): boolean {
|
||||||
|
if (isMac) {
|
||||||
|
return e.key === "Meta";
|
||||||
}
|
}
|
||||||
|
|
||||||
onKeyDown = (e: React.KeyboardEvent<any>) => {
|
return e.key === "Control"; // windows or linux
|
||||||
if (isMac && e.metaKey || (isWindows || isLinux) && e.ctrlKey) {
|
}
|
||||||
NamespaceSelectFilter.isMultiSelection.set(true);
|
|
||||||
|
@action
|
||||||
|
onKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (this.isSelectionKey(e)) {
|
||||||
|
this.isMultiSelection = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onKeyUp = (e: React.KeyboardEvent<any>) => {
|
@action
|
||||||
if (isMac && e.key === "Meta" || (isWindows || isLinux) && e.key === "Control") {
|
onKeyUp = (e: React.KeyboardEvent) => {
|
||||||
NamespaceSelectFilter.isMultiSelection.set(false);
|
if (this.isSelectionKey(e)) {
|
||||||
|
this.isMultiSelection = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isMultiSelection && this.didToggle) {
|
||||||
|
this.isMenuOpen = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@action
|
||||||
onClick = () => {
|
onClick = () => {
|
||||||
if (!NamespaceSelectFilter.isMultiSelection.get()) {
|
if (!this.isMenuOpen) {
|
||||||
NamespaceSelectFilter.isMenuOpen.set(!NamespaceSelectFilter.isMenuOpen.get());
|
this.isMenuOpen = true;
|
||||||
|
} else if (!this.isMultiSelection) {
|
||||||
|
this.isMenuOpen = !this.isMenuOpen;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
reset = () => {
|
reset = () => {
|
||||||
NamespaceSelectFilter.isMultiSelection.set(false);
|
this.isMultiSelection = false;
|
||||||
NamespaceSelectFilter.isMenuOpen.set(false);
|
this.isMenuOpen = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -114,7 +170,7 @@ export class NamespaceSelectFilter extends React.Component<SelectProps> {
|
|||||||
<div onKeyUp={this.onKeyUp} onKeyDown={this.onKeyDown} onClick={this.onClick}>
|
<div onKeyUp={this.onKeyUp} onKeyDown={this.onKeyDown} onClick={this.onClick}>
|
||||||
<NamespaceSelect
|
<NamespaceSelect
|
||||||
isMulti={true}
|
isMulti={true}
|
||||||
menuIsOpen={NamespaceSelectFilter.isMenuOpen.get()}
|
menuIsOpen={this.isMenuOpen}
|
||||||
components={{ Placeholder }}
|
components={{ Placeholder }}
|
||||||
showAllNamespacesOption={true}
|
showAllNamespacesOption={true}
|
||||||
closeMenuOnSelect={false}
|
closeMenuOnSelect={false}
|
||||||
@ -124,6 +180,7 @@ export class NamespaceSelectFilter extends React.Component<SelectProps> {
|
|||||||
onBlur={this.reset}
|
onBlur={this.reset}
|
||||||
formatOptionLabel={this.formatOptionLabel}
|
formatOptionLabel={this.formatOptionLabel}
|
||||||
className="NamespaceSelectFilter"
|
className="NamespaceSelectFilter"
|
||||||
|
sort={(left, right) => +this.selected.has(right.value) - +this.selected.has(left.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -32,6 +32,7 @@ import { kubeWatchApi } from "../../api/kube-watch-api";
|
|||||||
|
|
||||||
interface Props extends SelectProps {
|
interface Props extends SelectProps {
|
||||||
showIcons?: boolean;
|
showIcons?: boolean;
|
||||||
|
sort?: (a: SelectOption<string>, b: SelectOption<string>) => number;
|
||||||
showAllNamespacesOption?: boolean; // show "All namespaces" option on the top (default: false)
|
showAllNamespacesOption?: boolean; // show "All namespaces" option on the top (default: false)
|
||||||
customizeOptions?(options: SelectOption[]): SelectOption[];
|
customizeOptions?(options: SelectOption[]): SelectOption[];
|
||||||
}
|
}
|
||||||
@ -59,9 +60,13 @@ export class NamespaceSelect extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@computed.struct get options(): SelectOption[] {
|
@computed.struct get options(): SelectOption[] {
|
||||||
const { customizeOptions, showAllNamespacesOption } = this.props;
|
const { customizeOptions, showAllNamespacesOption, sort } = this.props;
|
||||||
let options: SelectOption[] = namespaceStore.items.map(ns => ({ value: ns.getName() }));
|
let options: SelectOption[] = namespaceStore.items.map(ns => ({ value: ns.getName() }));
|
||||||
|
|
||||||
|
if (sort) {
|
||||||
|
options.sort(sort);
|
||||||
|
}
|
||||||
|
|
||||||
if (showAllNamespacesOption) {
|
if (showAllNamespacesOption) {
|
||||||
options.unshift({ label: "All Namespaces", value: "" });
|
options.unshift({ label: "All Namespaces", value: "" });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -115,8 +115,8 @@ export class Input extends React.Component<InputProps, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getValue(): string {
|
getValue(): string {
|
||||||
const { trim, value, defaultValue = "" } = this.props;
|
const { trim, value, defaultValue } = this.props;
|
||||||
const rawValue = value ?? this.input?.value ?? defaultValue;
|
const rawValue = value ?? this.input?.value ?? defaultValue ?? "";
|
||||||
|
|
||||||
return trim ? rawValue.trim() : rawValue;
|
return trim ? rawValue.trim() : rawValue;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user