From 5a76c2f33150a15fffa4a0614ad1ae6cf22b1e97 Mon Sep 17 00:00:00 2001 From: Roman Date: Wed, 13 Jan 2021 18:21:58 +0200 Subject: [PATCH] fixes & refactoring Signed-off-by: Roman --- src/common/rbac.ts | 59 +++++++++++-------- .../components/+namespaces/namespace.store.ts | 59 ++++++++----------- .../role-bindings.store.ts | 6 +- .../+user-management-roles/roles.store.ts | 6 +- .../+workloads-overview/overview.tsx | 1 - .../item-object-list/item-list-layout.tsx | 18 ++---- src/renderer/item.store.ts | 2 +- src/renderer/kube-object.store.ts | 34 +++++------ 8 files changed, 87 insertions(+), 98 deletions(-) diff --git a/src/common/rbac.ts b/src/common/rbac.ts index bd003e87a1..8c0e6330a2 100644 --- a/src/common/rbac.ts +++ b/src/common/rbac.ts @@ -7,38 +7,49 @@ export type KubeResource = "endpoints" | "customresourcedefinitions" | "horizontalpodautoscalers" | "podsecuritypolicies" | "poddisruptionbudgets"; export interface KubeApiResource { + kind: string; // resource type resource: KubeResource; // valid resource name group?: string; // api-group } // TODO: auto-populate all resources dynamically (see: kubectl api-resources -o=wide -v=7) export const apiResources: KubeApiResource[] = [ - { resource: "configmaps" }, - { resource: "cronjobs", group: "batch" }, - { resource: "customresourcedefinitions", group: "apiextensions.k8s.io" }, - { resource: "daemonsets", group: "apps" }, - { resource: "deployments", group: "apps" }, - { resource: "endpoints" }, - { resource: "events" }, - { resource: "horizontalpodautoscalers" }, - { resource: "ingresses", group: "networking.k8s.io" }, - { resource: "jobs", group: "batch" }, - { resource: "namespaces" }, - { resource: "networkpolicies", group: "networking.k8s.io" }, - { resource: "nodes" }, - { resource: "persistentvolumes" }, - { resource: "persistentvolumeclaims" }, - { resource: "pods" }, - { resource: "poddisruptionbudgets" }, - { resource: "podsecuritypolicies" }, - { resource: "resourcequotas" }, - { resource: "replicasets", group: "apps" }, - { resource: "secrets" }, - { resource: "services" }, - { resource: "statefulsets", group: "apps" }, - { resource: "storageclasses", group: "storage.k8s.io" }, + { kind: "ConfigMap", resource: "configmaps" }, + { kind: "CronJob", resource: "cronjobs", group: "batch" }, + { kind: "CustomResourceDefinition", resource: "customresourcedefinitions", group: "apiextensions.k8s.io" }, + { kind: "DaemonSet", resource: "daemonsets", group: "apps" }, + { kind: "Deployment", resource: "deployments", group: "apps" }, + { kind: "Endpoint", resource: "endpoints" }, + { kind: "Event", resource: "events" }, + { kind: "HorizontalPodAutoscaler", resource: "horizontalpodautoscalers" }, + { kind: "Ingress", resource: "ingresses", group: "networking.k8s.io" }, + { kind: "Job", resource: "jobs", group: "batch" }, + { kind: "Namespace", resource: "namespaces" }, + { kind: "NetworkPolicy", resource: "networkpolicies", group: "networking.k8s.io" }, + { kind: "Node", resource: "nodes" }, + { kind: "PersistentVolume", resource: "persistentvolumes" }, + { kind: "PersistentVolumeClaim", resource: "persistentvolumeclaims" }, + { kind: "Pod", resource: "pods" }, + { kind: "PodDisruptionBudget", resource: "poddisruptionbudgets" }, + { kind: "PodSecurityPolicy", resource: "podsecuritypolicies" }, + { kind: "ResourceQuota", resource: "resourcequotas" }, + { kind: "ReplicaSet", resource: "replicasets", group: "apps" }, + { kind: "Secret", resource: "secrets" }, + { kind: "Service", resource: "services" }, + { kind: "StatefulSet", resource: "statefulsets", group: "apps" }, + { 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[]) { if (!Array.isArray(resources)) { resources = [resources]; diff --git a/src/renderer/components/+namespaces/namespace.store.ts b/src/renderer/components/+namespaces/namespace.store.ts index 009dc72e71..1834dc9957 100644 --- a/src/renderer/components/+namespaces/namespace.store.ts +++ b/src/renderer/components/+namespaces/namespace.store.ts @@ -1,3 +1,4 @@ +import { debounce } from "lodash"; import { action, comparer, IReactionDisposer, IReactionOptions, observable, reaction, toJS, when } from "mobx"; import { autobind, createStorage } from "../../utils"; import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store"; @@ -7,14 +8,14 @@ import { apiManager } from "../../api/api-manager"; import { isAllowedResource } from "../../../common/rbac"; import { clusterStore, getHostedCluster } from "../../../common/cluster-store"; -const storage = createStorage("context_namespaces", []); +const storage = createStorage("context_namespaces"); export const namespaceUrlParam = createPageParam({ name: "namespaces", isSystem: true, multiValues: true, 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 { await getHostedCluster().whenReady; // wait for cluster-state from main this.isReady = true; - this.setContext(this.initNamespaces); - this.onSelectedNamespacesChange(); - this.onAllowedNamespacesChange(); + this.setContext(this.initialNamespaces); + this.autoLoadAllowedNamespaces(); + this.autoUpdateUrlAndLocalStorage(); } public onContextChange(callback: (contextNamespaces: string[]) => void, opts: IReactionOptions = {}): IReactionDisposer { @@ -63,7 +64,7 @@ export class NamespaceStore extends KubeObjectStore { }); } - private onSelectedNamespacesChange(): IReactionDisposer { + private autoUpdateUrlAndLocalStorage(): IReactionDisposer { return this.onContextChange(namespaces => { storage.set(namespaces); // save to local-storage namespaceUrlParam.set(namespaces, { replaceHistory: true }); // update url @@ -73,7 +74,7 @@ export class NamespaceStore extends KubeObjectStore { }); } - private onAllowedNamespacesChange(): IReactionDisposer { + private autoLoadAllowedNamespaces(): IReactionDisposer { return reaction(() => this.allowedNamespaces, () => this.loadAll(), { fireImmediately: true, equals: comparer.identity, @@ -84,28 +85,19 @@ export class NamespaceStore extends KubeObjectStore { return toJS(getHostedCluster().allowedNamespaces); } - get initNamespaces() { - const allowedNamespaces = new Set(this.allowedNamespaces); - const lastUsedNamespaces = new Set(storage.get()); + private get initialNamespaces(): string[] { + const allowed = new Set(this.allowedNamespaces); + const prevSelected = storage.get(); - // remove previously saved, but currently disallowed namespaces - lastUsedNamespaces.forEach(namespace => { - if (!allowedNamespaces.has(namespace)) { - lastUsedNamespaces.delete(namespace); - } - }); - - // return previously saved and currently allowed namespaces - if (lastUsedNamespaces.size) { - return Array.from(lastUsedNamespaces); + if (Array.isArray(prevSelected)) { + return prevSelected.filter(namespace => allowed.has(namespace)); } + // otherwise select "default" or first allowed namespace - else { - if (allowedNamespaces.has("default")) { - return ["default"]; - } else if (allowedNamespaces.size) { - return [Array.from(allowedNamespaces)[0]]; - } + if (allowed.has("default")) { + return ["default"]; + } else if (allowed.size) { + return [Array.from(allowed)[0]]; } return []; @@ -132,17 +124,18 @@ export class NamespaceStore extends KubeObjectStore { return super.subscribe(apis); } - async loadAll() { - return super.loadAll({ + // prevent multiple loading from different sources (e.g. items-list-layout, namespace-select) + private loadAllLazy = debounce(() => { + super.loadAll({ namespaces: this.allowedNamespaces, }); + }, 250); + + async loadAll() { + this.loadAllLazy(); } - protected async loadItems({ isAdmin, namespaces }: KubeObjectStoreLoadingParams) { - if (isAdmin) { - return this.api.list(); - } - + protected async loadItems({ namespaces }: KubeObjectStoreLoadingParams) { if (!isAllowedResource("namespaces")) { return namespaces.map(getDummyNamespace); } diff --git a/src/renderer/components/+user-management-roles-bindings/role-bindings.store.ts b/src/renderer/components/+user-management-roles-bindings/role-bindings.store.ts index 64ecf0f921..71890acc44 100644 --- a/src/renderer/components/+user-management-roles-bindings/role-bindings.store.ts +++ b/src/renderer/components/+user-management-roles-bindings/role-bindings.store.ts @@ -26,10 +26,10 @@ export class RoleBindingsStore extends KubeObjectStore { return clusterRoleBindingApi.get(params); } - protected async loadItems({ isAdmin, namespaces }: KubeObjectStoreLoadingParams): Promise { + protected async loadItems(params: KubeObjectStoreLoadingParams): Promise { const items = await Promise.all([ - super.loadItems({ isAdmin, namespaces, api: clusterRoleBindingApi }), - super.loadItems({ isAdmin, namespaces, api: roleBindingApi }), + super.loadItems({ ...params, api: clusterRoleBindingApi }), + super.loadItems({ ...params, api: roleBindingApi }), ]); return items.flat(); diff --git a/src/renderer/components/+user-management-roles/roles.store.ts b/src/renderer/components/+user-management-roles/roles.store.ts index dc136091c0..56075a6a97 100644 --- a/src/renderer/components/+user-management-roles/roles.store.ts +++ b/src/renderer/components/+user-management-roles/roles.store.ts @@ -24,10 +24,10 @@ export class RolesStore extends KubeObjectStore { return clusterRoleApi.get(params); } - protected async loadItems({ isAdmin, namespaces }: KubeObjectStoreLoadingParams): Promise { + protected async loadItems(params: KubeObjectStoreLoadingParams): Promise { const items = await Promise.all([ - super.loadItems({ isAdmin, namespaces, api: clusterRoleApi }), - super.loadItems({ isAdmin, namespaces, api: roleApi }), + super.loadItems({ ...params, api: clusterRoleApi }), + super.loadItems({ ...params, api: roleApi }), ]); return items.flat(); diff --git a/src/renderer/components/+workloads-overview/overview.tsx b/src/renderer/components/+workloads-overview/overview.tsx index 4e474bf313..351b57462c 100644 --- a/src/renderer/components/+workloads-overview/overview.tsx +++ b/src/renderer/components/+workloads-overview/overview.tsx @@ -48,7 +48,6 @@ export class WorkloadsOverview extends React.Component { if (this.isUnmounting) break; try { - store.reset(); await store.loadAll(); unsubscribeMap.get(store)?.(); // unsubscribe previous watcher unsubscribeMap.set(store, store.subscribe()); diff --git a/src/renderer/components/item-object-list/item-list-layout.tsx b/src/renderer/components/item-object-list/item-list-layout.tsx index 77810f008e..377550c1e5 100644 --- a/src/renderer/components/item-object-list/item-list-layout.tsx +++ b/src/renderer/components/item-object-list/item-list-layout.tsx @@ -112,11 +112,11 @@ export class ItemListLayout extends React.Component { } async componentDidMount() { + this.loadStores(); + if (!this.props.isClusterScoped) { disposeOnUnmount(this, [ - namespaceStore.onContextChange(() => this.loadStores(), { - fireImmediately: true, - }) + namespaceStore.onContextChange(() => this.loadStores()) ]); } } @@ -148,7 +148,6 @@ export class ItemListLayout extends React.Component { } try { - store.reset(); await store.loadAll(); this.watchDisposers.push(store.subscribe()); } catch (error) { @@ -195,9 +194,7 @@ export class ItemListLayout extends React.Component { }; @computed get isReady() { - const { isReady, store } = this.props; - - return typeof isReady == "boolean" ? isReady : store.isLoaded; + return this.props.isReady ?? this.props.store.isLoaded; } @computed get filters() { @@ -330,12 +327,7 @@ export class ItemListLayout extends React.Component { } renderNoItems() { - const { allItems, items, filters } = this; - const allItemsCount = allItems.length; - const itemsCount = items.length; - const isFiltered = filters.length > 0 && allItemsCount > itemsCount; - - if (isFiltered) { + if (this.filters.length > 0) { return ( No items found. diff --git a/src/renderer/item.store.ts b/src/renderer/item.store.ts index 7c579d3a56..eccd2b52df 100644 --- a/src/renderer/item.store.ts +++ b/src/renderer/item.store.ts @@ -9,7 +9,7 @@ export interface ItemObject { @autobind() export abstract class ItemStore { - abstract loadAll(...args: any[]): Promise; + abstract loadAll(...args: any[]): Promise; protected defaultSorting = (item: T) => item.getName(); diff --git a/src/renderer/kube-object.store.ts b/src/renderer/kube-object.store.ts index 3ea6d3f3c3..b1e452b1c5 100644 --- a/src/renderer/kube-object.store.ts +++ b/src/renderer/kube-object.store.ts @@ -6,10 +6,9 @@ import { ItemStore } from "./item.store"; import { apiManager } from "./api/api-manager"; import { IKubeApiQueryParams, KubeApi } from "./api/kube-api"; import { KubeJsonApiData } from "./api/kube-json-api"; -import { getHostedCluster } from "../common/cluster-store"; +import { isAllowedResourceType } from "../common/rbac"; export interface KubeObjectStoreLoadingParams { - isAdmin: boolean; namespaces: string[]; api?: KubeApi; } @@ -77,16 +76,18 @@ export abstract class KubeObjectStore extends ItemSt } } - protected async loadItems({ isAdmin, namespaces, api }: KubeObjectStoreLoadingParams): Promise { - if (!api.isNamespaced) { - if (isAdmin) return api.list({}, this.query); + protected async loadItems({ namespaces, api }: KubeObjectStoreLoadingParams): Promise { + if (isAllowedResourceType(api.kind)) { + if (api.isNamespaced) { + return Promise + .all(namespaces.map(namespace => api.list({ namespace }))) + .then(items => items.flat()); + } - return []; + return api.list({}, this.query); } - return Promise - .all(namespaces.map(namespace => api.list({ namespace }))) - .then(items => items.flat()); + return []; } protected filterItemsOnLoad(items: T[]) { @@ -94,28 +95,21 @@ export abstract class KubeObjectStore extends ItemSt } @action - async loadAll(params: { namespaces?: string[] } = {}) { + async loadAll({ namespaces: contextNamespaces }: { namespaces?: string[] } = {}) { this.isLoading = true; - let items: T[]; try { - let contextNamespaces = params.namespaces; - - if (!params.namespaces) { + if (!contextNamespaces) { const { namespaceStore } = await import("./components/+namespaces/namespace.store"); - await namespaceStore.whenReady; contextNamespaces = namespaceStore.getContextNamespaces(); } - items = await this.loadItems({ - isAdmin: getHostedCluster().isAdmin, - namespaces: contextNamespaces, - api: this.api, - }); + let items = await this.loadItems({ namespaces: contextNamespaces, api: this.api }); items = this.filterItemsOnLoad(items); items = this.sortItems(items); + this.items.replace(items); this.isLoaded = true; } catch (error) {