1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Allow to quick select/deselect all namespaces in NamespaceSelect (#2068)

* allow to quick select/deselect all namespaces in `NamespaceSelect`

Signed-off-by: Roman <ixrock@gmail.com>

* fixes & refactoring

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2021-02-03 19:01:45 +02:00 committed by GitHub
parent 2c99cd0429
commit 1b492f27ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 100 additions and 65 deletions

View File

@ -13,17 +13,14 @@ import { kubeWatchApi } from "../../api/kube-watch-api";
interface Props extends SelectProps { interface Props extends SelectProps {
showIcons?: boolean; showIcons?: boolean;
showClusterOption?: boolean; // show cluster option on the top (default: false) showClusterOption?: boolean; // show "Cluster" option on the top (default: false)
clusterOptionLabel?: React.ReactNode; // label for cluster option (default: "Cluster") showAllNamespacesOption?: boolean; // show "All namespaces" option on the top (default: false)
customizeOptions?(nsOptions: SelectOption[]): SelectOption[]; customizeOptions?(options: SelectOption[]): SelectOption[];
} }
const defaultProps: Partial<Props> = { const defaultProps: Partial<Props> = {
showIcons: true, showIcons: true,
showClusterOption: false, showClusterOption: false,
get clusterOptionLabel() {
return `Cluster`;
},
}; };
@observer @observer
@ -39,13 +36,17 @@ export class NamespaceSelect extends React.Component<Props> {
} }
@computed get options(): SelectOption[] { @computed get options(): SelectOption[] {
const { customizeOptions, showClusterOption, clusterOptionLabel } = this.props; const { customizeOptions, showClusterOption, showAllNamespacesOption } = this.props;
let options: SelectOption[] = namespaceStore.items.map(ns => ({ value: ns.getName() })); let options: SelectOption[] = namespaceStore.items.map(ns => ({ value: ns.getName() }));
options = customizeOptions ? customizeOptions(options) : options; if (showAllNamespacesOption) {
options.unshift({ label: "All Namespaces", value: "" });
} else if (showClusterOption) {
options.unshift({ label: "Cluster", value: "" });
}
if (showClusterOption) { if (customizeOptions) {
options.unshift({ value: null, label: clusterOptionLabel }); options = customizeOptions(options);
} }
return options; return options;
@ -64,7 +65,7 @@ export class NamespaceSelect extends React.Component<Props> {
}; };
render() { render() {
const { className, showIcons, showClusterOption, clusterOptionLabel, customizeOptions, ...selectProps } = this.props; const { className, showIcons, customizeOptions, ...selectProps } = this.props;
return ( return (
<Select <Select
@ -80,23 +81,24 @@ export class NamespaceSelect extends React.Component<Props> {
@observer @observer
export class NamespaceSelectFilter extends React.Component { export class NamespaceSelectFilter extends React.Component {
render() { @computed get placeholder(): React.ReactNode {
const { contextNs, hasContext, toggleContext } = namespaceStore; const namespaces = namespaceStore.getContextNamespaces();
let placeholder = <>All namespaces</>;
if (contextNs.length == 1) placeholder = <>Namespace: {contextNs[0]}</>; switch (namespaces.length) {
if (contextNs.length >= 2) placeholder = <>Namespaces: {contextNs.join(", ")}</>; case namespaceStore.allowedNamespaces.length:
return <>All namespaces</>;
case 0:
return <>Select a namespace</>;
case 1:
return <>Namespace: {namespaces[0]}</>;
default:
return <>Namespaces: {namespaces.join(", ")}</>;
}
}
return ( formatOptionLabel = ({ value: namespace, label }: SelectOption) => {
<NamespaceSelect if (namespace) {
placeholder={placeholder} const isSelected = namespaceStore.hasContext(namespace);
closeMenuOnSelect={false}
isOptionSelected={() => false}
controlShouldRenderValue={false}
isMulti
onChange={([{ value }]: SelectOption[]) => toggleContext(value)}
formatOptionLabel={({ value: namespace }: SelectOption) => {
const isSelected = hasContext(namespace);
return ( return (
<div className="flex gaps align-center"> <div className="flex gaps align-center">
@ -105,7 +107,30 @@ export class NamespaceSelectFilter extends React.Component {
{isSelected && <Icon small material="check" className="box right"/>} {isSelected && <Icon small material="check" className="box right"/>}
</div> </div>
); );
}} }
return label;
};
onChange = ([{ value: namespace }]: SelectOption[]) => {
if (namespace) {
namespaceStore.toggleContext(namespace);
} else {
namespaceStore.toggleAll(); // "All namespaces" option clicked
}
};
render() {
return (
<NamespaceSelect
isMulti={true}
showAllNamespacesOption={true}
closeMenuOnSelect={false}
isOptionSelected={() => false}
controlShouldRenderValue={false}
placeholder={this.placeholder}
onChange={this.onChange}
formatOptionLabel={this.formatOptionLabel}
/> />
); );
} }

View File

@ -1,4 +1,4 @@
import { action, comparer, IReactionDisposer, IReactionOptions, observable, reaction, toJS, when } from "mobx"; import { action, comparer, computed, IReactionDisposer, IReactionOptions, observable, reaction, toJS, when } from "mobx";
import { autobind, createStorage } from "../../utils"; import { autobind, createStorage } from "../../utils";
import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store"; import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store";
import { Namespace, namespacesApi } from "../../api/endpoints/namespaces.api"; import { Namespace, namespacesApi } from "../../api/endpoints/namespaces.api";
@ -6,7 +6,7 @@ import { createPageParam } from "../../navigation";
import { apiManager } from "../../api/api-manager"; import { apiManager } from "../../api/api-manager";
import { clusterStore, getHostedCluster } from "../../../common/cluster-store"; import { clusterStore, getHostedCluster } from "../../../common/cluster-store";
const storage = createStorage<string[]>("context_namespaces"); const storage = createStorage<string[]>("context_namespaces", []);
export const namespaceUrlParam = createPageParam<string[]>({ export const namespaceUrlParam = createPageParam<string[]>({
name: "namespaces", name: "namespaces",
@ -34,7 +34,7 @@ export function getDummyNamespace(name: string) {
export class NamespaceStore extends KubeObjectStore<Namespace> { export class NamespaceStore extends KubeObjectStore<Namespace> {
api = namespacesApi; api = namespacesApi;
@observable contextNs = observable.array<string>(); @observable private contextNs = observable.set<string>();
@observable isReady = false; @observable isReady = false;
whenReady = when(() => this.isReady); whenReady = when(() => this.isReady);
@ -57,7 +57,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
} }
public onContextChange(callback: (contextNamespaces: string[]) => void, opts: IReactionOptions = {}): IReactionDisposer { public onContextChange(callback: (contextNamespaces: string[]) => void, opts: IReactionOptions = {}): IReactionDisposer {
return reaction(() => this.contextNs.toJS(), callback, { return reaction(() => Array.from(this.contextNs), callback, {
equals: comparer.shallow, equals: comparer.shallow,
...opts, ...opts,
}); });
@ -79,42 +79,32 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
}); });
} }
get allowedNamespaces(): string[] { @computed get allowedNamespaces(): string[] {
return toJS(getHostedCluster().allowedNamespaces); return toJS(getHostedCluster().allowedNamespaces);
} }
@computed
private get initialNamespaces(): string[] { private get initialNamespaces(): string[] {
const allowed = new Set(this.allowedNamespaces); const namespaces = new Set(this.allowedNamespaces);
const prevSelected = storage.get(); const prevSelected = storage.get().filter(namespace => namespaces.has(namespace));
if (Array.isArray(prevSelected)) { // return previously saved namespaces from local-storage
return prevSelected.filter(namespace => allowed.has(namespace)); if (prevSelected.length > 0) {
return prevSelected;
} }
// otherwise select "default" or first allowed namespace // otherwise select "default" or first allowed namespace
if (allowed.has("default")) { if (namespaces.has("default")) {
return ["default"]; return ["default"];
} else if (allowed.size) { } else if (namespaces.size) {
return [Array.from(allowed)[0]]; return [Array.from(namespaces)[0]];
} }
return []; return [];
} }
getContextNamespaces(): string[] { public getContextNamespaces(): string[] {
const namespaces = this.contextNs.toJS(); return Array.from(this.contextNs);
// show all namespaces when nothing selected
if (!namespaces.length) {
if (this.isLoaded) {
// return actual namespaces list since "allowedNamespaces" updating every 30s in cluster and thus might be stale
return this.items.map(namespace => namespace.getName());
}
return this.allowedNamespaces;
}
return namespaces;
} }
getSubscribeApis() { getSubscribeApis() {
@ -143,26 +133,46 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
} }
@action @action
setContext(namespaces: string[]) { setContext(namespace: string | string[]) {
const namespaces = [namespace].flat();
this.contextNs.replace(namespaces); this.contextNs.replace(namespaces);
} }
hasContext(namespace: string | string[]) { hasContext(namespaces: string | string[]) {
const context = Array.isArray(namespace) ? namespace : [namespace]; return [namespaces].flat().every(namespace => this.contextNs.has(namespace));
}
return context.every(namespace => this.contextNs.includes(namespace)); @computed get hasAllContexts(): boolean {
return this.contextNs.size === this.allowedNamespaces.length;
} }
@action @action
toggleContext(namespace: string) { toggleContext(namespace: string) {
if (this.hasContext(namespace)) this.contextNs.remove(namespace); if (this.hasContext(namespace)) {
else this.contextNs.push(namespace); this.contextNs.delete(namespace);
} else {
this.contextNs.add(namespace);
}
}
@action
toggleAll(showAll?: boolean) {
if (typeof showAll === "boolean") {
if (showAll) {
this.setContext(this.allowedNamespaces);
} else {
this.contextNs.clear();
}
} else {
this.toggleAll(!this.hasAllContexts);
}
} }
@action @action
async remove(item: Namespace) { async remove(item: Namespace) {
await super.remove(item); await super.remove(item);
this.contextNs.remove(item.getName()); this.contextNs.delete(item.getName());
} }
} }

View File

@ -30,7 +30,7 @@ export class PageFiltersStore {
protected syncWithContextNamespace() { protected syncWithContextNamespace() {
const disposers = [ const disposers = [
reaction(() => this.getValues(FilterType.NAMESPACE), filteredNs => { reaction(() => this.getValues(FilterType.NAMESPACE), filteredNs => {
if (filteredNs.length !== namespaceStore.contextNs.length) { if (filteredNs.length !== namespaceStore.getContextNamespaces().length) {
namespaceStore.setContext(filteredNs); namespaceStore.setContext(filteredNs);
} }
}), }),