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

fixes & refactoring

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2021-01-13 18:21:58 +02:00
parent 4a8079debc
commit 5a76c2f331
8 changed files with 87 additions and 98 deletions

View File

@ -7,38 +7,49 @@ export type KubeResource =
"endpoints" | "customresourcedefinitions" | "horizontalpodautoscalers" | "podsecuritypolicies" | "poddisruptionbudgets"; "endpoints" | "customresourcedefinitions" | "horizontalpodautoscalers" | "podsecuritypolicies" | "poddisruptionbudgets";
export interface KubeApiResource { export interface KubeApiResource {
kind: string; // resource type
resource: KubeResource; // valid resource name resource: KubeResource; // valid resource name
group?: string; // api-group group?: string; // api-group
} }
// TODO: auto-populate all resources dynamically (see: kubectl api-resources -o=wide -v=7) // TODO: auto-populate all resources dynamically (see: kubectl api-resources -o=wide -v=7)
export const apiResources: KubeApiResource[] = [ export const apiResources: KubeApiResource[] = [
{ resource: "configmaps" }, { kind: "ConfigMap", resource: "configmaps" },
{ resource: "cronjobs", group: "batch" }, { kind: "CronJob", resource: "cronjobs", group: "batch" },
{ resource: "customresourcedefinitions", group: "apiextensions.k8s.io" }, { kind: "CustomResourceDefinition", resource: "customresourcedefinitions", group: "apiextensions.k8s.io" },
{ resource: "daemonsets", group: "apps" }, { kind: "DaemonSet", resource: "daemonsets", group: "apps" },
{ resource: "deployments", group: "apps" }, { kind: "Deployment", resource: "deployments", group: "apps" },
{ resource: "endpoints" }, { kind: "Endpoint", resource: "endpoints" },
{ resource: "events" }, { kind: "Event", resource: "events" },
{ resource: "horizontalpodautoscalers" }, { kind: "HorizontalPodAutoscaler", resource: "horizontalpodautoscalers" },
{ resource: "ingresses", group: "networking.k8s.io" }, { kind: "Ingress", resource: "ingresses", group: "networking.k8s.io" },
{ resource: "jobs", group: "batch" }, { kind: "Job", resource: "jobs", group: "batch" },
{ resource: "namespaces" }, { kind: "Namespace", resource: "namespaces" },
{ resource: "networkpolicies", group: "networking.k8s.io" }, { kind: "NetworkPolicy", resource: "networkpolicies", group: "networking.k8s.io" },
{ resource: "nodes" }, { kind: "Node", resource: "nodes" },
{ resource: "persistentvolumes" }, { kind: "PersistentVolume", resource: "persistentvolumes" },
{ resource: "persistentvolumeclaims" }, { kind: "PersistentVolumeClaim", resource: "persistentvolumeclaims" },
{ resource: "pods" }, { kind: "Pod", resource: "pods" },
{ resource: "poddisruptionbudgets" }, { kind: "PodDisruptionBudget", resource: "poddisruptionbudgets" },
{ resource: "podsecuritypolicies" }, { kind: "PodSecurityPolicy", resource: "podsecuritypolicies" },
{ resource: "resourcequotas" }, { kind: "ResourceQuota", resource: "resourcequotas" },
{ resource: "replicasets", group: "apps" }, { kind: "ReplicaSet", resource: "replicasets", group: "apps" },
{ resource: "secrets" }, { kind: "Secret", resource: "secrets" },
{ resource: "services" }, { kind: "Service", resource: "services" },
{ resource: "statefulsets", group: "apps" }, { kind: "StatefulSet", resource: "statefulsets", group: "apps" },
{ resource: "storageclasses", group: "storage.k8s.io" }, { kind: "StorageClass", resource: "storageclasses", group: "storage.k8s.io" },
]; ];
export function isAllowedResourceType(kind: string): boolean {
const apiResource = apiResources.find(resource => resource.kind === kind);
if (apiResource) {
return getHostedCluster().allowedResources.includes(apiResource.resource);
}
return true; // allowed by default for other resources
}
export function isAllowedResource(resources: KubeResource | KubeResource[]) { export function isAllowedResource(resources: KubeResource | KubeResource[]) {
if (!Array.isArray(resources)) { if (!Array.isArray(resources)) {
resources = [resources]; resources = [resources];

View File

@ -1,3 +1,4 @@
import { debounce } from "lodash";
import { action, comparer, IReactionDisposer, IReactionOptions, observable, reaction, toJS, when } from "mobx"; import { action, comparer, 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";
@ -7,14 +8,14 @@ import { apiManager } from "../../api/api-manager";
import { isAllowedResource } from "../../../common/rbac"; import { isAllowedResource } from "../../../common/rbac";
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",
isSystem: true, isSystem: true,
multiValues: true, multiValues: true,
get defaultValue() { get defaultValue() {
return storage.get(); // initial namespaces coming from URL or local-storage (default) return storage.get() ?? []; // initial namespaces coming from URL or local-storage (default)
} }
}); });
@ -51,9 +52,9 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
await getHostedCluster().whenReady; // wait for cluster-state from main await getHostedCluster().whenReady; // wait for cluster-state from main
this.isReady = true; this.isReady = true;
this.setContext(this.initNamespaces); this.setContext(this.initialNamespaces);
this.onSelectedNamespacesChange(); this.autoLoadAllowedNamespaces();
this.onAllowedNamespacesChange(); this.autoUpdateUrlAndLocalStorage();
} }
public onContextChange(callback: (contextNamespaces: string[]) => void, opts: IReactionOptions = {}): IReactionDisposer { public onContextChange(callback: (contextNamespaces: string[]) => void, opts: IReactionOptions = {}): IReactionDisposer {
@ -63,7 +64,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
}); });
} }
private onSelectedNamespacesChange(): IReactionDisposer { private autoUpdateUrlAndLocalStorage(): IReactionDisposer {
return this.onContextChange(namespaces => { return this.onContextChange(namespaces => {
storage.set(namespaces); // save to local-storage storage.set(namespaces); // save to local-storage
namespaceUrlParam.set(namespaces, { replaceHistory: true }); // update url namespaceUrlParam.set(namespaces, { replaceHistory: true }); // update url
@ -73,7 +74,7 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
}); });
} }
private onAllowedNamespacesChange(): IReactionDisposer { private autoLoadAllowedNamespaces(): IReactionDisposer {
return reaction(() => this.allowedNamespaces, () => this.loadAll(), { return reaction(() => this.allowedNamespaces, () => this.loadAll(), {
fireImmediately: true, fireImmediately: true,
equals: comparer.identity, equals: comparer.identity,
@ -84,28 +85,19 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
return toJS(getHostedCluster().allowedNamespaces); return toJS(getHostedCluster().allowedNamespaces);
} }
get initNamespaces() { private get initialNamespaces(): string[] {
const allowedNamespaces = new Set(this.allowedNamespaces); const allowed = new Set(this.allowedNamespaces);
const lastUsedNamespaces = new Set(storage.get()); const prevSelected = storage.get();
// remove previously saved, but currently disallowed namespaces if (Array.isArray(prevSelected)) {
lastUsedNamespaces.forEach(namespace => { return prevSelected.filter(namespace => allowed.has(namespace));
if (!allowedNamespaces.has(namespace)) {
lastUsedNamespaces.delete(namespace);
}
});
// return previously saved and currently allowed namespaces
if (lastUsedNamespaces.size) {
return Array.from(lastUsedNamespaces);
} }
// otherwise select "default" or first allowed namespace // otherwise select "default" or first allowed namespace
else { if (allowed.has("default")) {
if (allowedNamespaces.has("default")) { return ["default"];
return ["default"]; } else if (allowed.size) {
} else if (allowedNamespaces.size) { return [Array.from(allowed)[0]];
return [Array.from(allowedNamespaces)[0]];
}
} }
return []; return [];
@ -132,17 +124,18 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
return super.subscribe(apis); return super.subscribe(apis);
} }
async loadAll() { // prevent multiple loading from different sources (e.g. items-list-layout, namespace-select)
return super.loadAll({ private loadAllLazy = debounce(() => {
super.loadAll({
namespaces: this.allowedNamespaces, namespaces: this.allowedNamespaces,
}); });
}, 250);
async loadAll() {
this.loadAllLazy();
} }
protected async loadItems({ isAdmin, namespaces }: KubeObjectStoreLoadingParams) { protected async loadItems({ namespaces }: KubeObjectStoreLoadingParams) {
if (isAdmin) {
return this.api.list();
}
if (!isAllowedResource("namespaces")) { if (!isAllowedResource("namespaces")) {
return namespaces.map(getDummyNamespace); return namespaces.map(getDummyNamespace);
} }

View File

@ -26,10 +26,10 @@ export class RoleBindingsStore extends KubeObjectStore<RoleBinding> {
return clusterRoleBindingApi.get(params); return clusterRoleBindingApi.get(params);
} }
protected async loadItems({ isAdmin, namespaces }: KubeObjectStoreLoadingParams): Promise<RoleBinding[]> { protected async loadItems(params: KubeObjectStoreLoadingParams): Promise<RoleBinding[]> {
const items = await Promise.all([ const items = await Promise.all([
super.loadItems({ isAdmin, namespaces, api: clusterRoleBindingApi }), super.loadItems({ ...params, api: clusterRoleBindingApi }),
super.loadItems({ isAdmin, namespaces, api: roleBindingApi }), super.loadItems({ ...params, api: roleBindingApi }),
]); ]);
return items.flat(); return items.flat();

View File

@ -24,10 +24,10 @@ export class RolesStore extends KubeObjectStore<Role> {
return clusterRoleApi.get(params); return clusterRoleApi.get(params);
} }
protected async loadItems({ isAdmin, namespaces }: KubeObjectStoreLoadingParams): Promise<Role[]> { protected async loadItems(params: KubeObjectStoreLoadingParams): Promise<Role[]> {
const items = await Promise.all([ const items = await Promise.all([
super.loadItems({ isAdmin, namespaces, api: clusterRoleApi }), super.loadItems({ ...params, api: clusterRoleApi }),
super.loadItems({ isAdmin, namespaces, api: roleApi }), super.loadItems({ ...params, api: roleApi }),
]); ]);
return items.flat(); return items.flat();

View File

@ -48,7 +48,6 @@ export class WorkloadsOverview extends React.Component<Props> {
if (this.isUnmounting) break; if (this.isUnmounting) break;
try { try {
store.reset();
await store.loadAll(); await store.loadAll();
unsubscribeMap.get(store)?.(); // unsubscribe previous watcher unsubscribeMap.get(store)?.(); // unsubscribe previous watcher
unsubscribeMap.set(store, store.subscribe()); unsubscribeMap.set(store, store.subscribe());

View File

@ -112,11 +112,11 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
} }
async componentDidMount() { async componentDidMount() {
this.loadStores();
if (!this.props.isClusterScoped) { if (!this.props.isClusterScoped) {
disposeOnUnmount(this, [ disposeOnUnmount(this, [
namespaceStore.onContextChange(() => this.loadStores(), { namespaceStore.onContextChange(() => this.loadStores())
fireImmediately: true,
})
]); ]);
} }
} }
@ -148,7 +148,6 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
} }
try { try {
store.reset();
await store.loadAll(); await store.loadAll();
this.watchDisposers.push(store.subscribe()); this.watchDisposers.push(store.subscribe());
} catch (error) { } catch (error) {
@ -195,9 +194,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
}; };
@computed get isReady() { @computed get isReady() {
const { isReady, store } = this.props; return this.props.isReady ?? this.props.store.isLoaded;
return typeof isReady == "boolean" ? isReady : store.isLoaded;
} }
@computed get filters() { @computed get filters() {
@ -330,12 +327,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
} }
renderNoItems() { renderNoItems() {
const { allItems, items, filters } = this; if (this.filters.length > 0) {
const allItemsCount = allItems.length;
const itemsCount = items.length;
const isFiltered = filters.length > 0 && allItemsCount > itemsCount;
if (isFiltered) {
return ( return (
<NoItems> <NoItems>
No items found. No items found.

View File

@ -9,7 +9,7 @@ export interface ItemObject {
@autobind() @autobind()
export abstract class ItemStore<T extends ItemObject = ItemObject> { export abstract class ItemStore<T extends ItemObject = ItemObject> {
abstract loadAll(...args: any[]): Promise<any>; abstract loadAll(...args: any[]): Promise<void>;
protected defaultSorting = (item: T) => item.getName(); protected defaultSorting = (item: T) => item.getName();

View File

@ -6,10 +6,9 @@ import { ItemStore } from "./item.store";
import { apiManager } from "./api/api-manager"; import { apiManager } from "./api/api-manager";
import { IKubeApiQueryParams, KubeApi } from "./api/kube-api"; import { IKubeApiQueryParams, KubeApi } from "./api/kube-api";
import { KubeJsonApiData } from "./api/kube-json-api"; import { KubeJsonApiData } from "./api/kube-json-api";
import { getHostedCluster } from "../common/cluster-store"; import { isAllowedResourceType } from "../common/rbac";
export interface KubeObjectStoreLoadingParams { export interface KubeObjectStoreLoadingParams {
isAdmin: boolean;
namespaces: string[]; namespaces: string[];
api?: KubeApi; api?: KubeApi;
} }
@ -77,16 +76,18 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
} }
} }
protected async loadItems({ isAdmin, namespaces, api }: KubeObjectStoreLoadingParams): Promise<T[]> { protected async loadItems({ namespaces, api }: KubeObjectStoreLoadingParams): Promise<T[]> {
if (!api.isNamespaced) { if (isAllowedResourceType(api.kind)) {
if (isAdmin) return api.list({}, this.query); if (api.isNamespaced) {
return Promise
.all(namespaces.map(namespace => api.list({ namespace })))
.then(items => items.flat());
}
return []; return api.list({}, this.query);
} }
return Promise return [];
.all(namespaces.map(namespace => api.list({ namespace })))
.then(items => items.flat());
} }
protected filterItemsOnLoad(items: T[]) { protected filterItemsOnLoad(items: T[]) {
@ -94,28 +95,21 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
} }
@action @action
async loadAll(params: { namespaces?: string[] } = {}) { async loadAll({ namespaces: contextNamespaces }: { namespaces?: string[] } = {}) {
this.isLoading = true; this.isLoading = true;
let items: T[];
try { try {
let contextNamespaces = params.namespaces; if (!contextNamespaces) {
if (!params.namespaces) {
const { namespaceStore } = await import("./components/+namespaces/namespace.store"); const { namespaceStore } = await import("./components/+namespaces/namespace.store");
await namespaceStore.whenReady;
contextNamespaces = namespaceStore.getContextNamespaces(); contextNamespaces = namespaceStore.getContextNamespaces();
} }
items = await this.loadItems({ let items = await this.loadItems({ namespaces: contextNamespaces, api: this.api });
isAdmin: getHostedCluster().isAdmin,
namespaces: contextNamespaces,
api: this.api,
});
items = this.filterItemsOnLoad(items); items = this.filterItemsOnLoad(items);
items = this.sortItems(items); items = this.sortItems(items);
this.items.replace(items); this.items.replace(items);
this.isLoaded = true; this.isLoaded = true;
} catch (error) { } catch (error) {