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:
parent
2c99cd0429
commit
1b492f27ad
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user