1
0
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:
Sebastian Malton 2021-07-27 08:53:34 -04:00 committed by GitHub
parent 91b3bd10ee
commit 51b53b3349
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 84 additions and 22 deletions

View File

@ -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>
); );

View File

@ -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: "" });
} }

View File

@ -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;
} }