From 51bdfc131b95042238107d205fe64321484dd143 Mon Sep 17 00:00:00 2001 From: Roman Date: Fri, 8 Jan 2021 13:11:52 +0200 Subject: [PATCH] loading k8s resources into stores per selected namespaces -- part 3 Signed-off-by: Roman --- src/renderer/api/kube-watch-api.ts | 3 +- .../components/+namespaces/namespace.store.ts | 47 ++++++++--- .../+workloads-overview/overview.tsx | 80 +++++++++---------- .../item-object-list/item-list-layout.tsx | 78 +++++++++++------- src/renderer/item.store.ts | 3 +- 5 files changed, 127 insertions(+), 84 deletions(-) diff --git a/src/renderer/api/kube-watch-api.ts b/src/renderer/api/kube-watch-api.ts index 3b7b795580..196d2775b0 100644 --- a/src/renderer/api/kube-watch-api.ts +++ b/src/renderer/api/kube-watch-api.ts @@ -169,8 +169,7 @@ export class KubeWatchApi { addListener(store: KubeObjectStore, callback: (evt: IKubeWatchEvent) => void) { const listener = (evt: IKubeWatchEvent) => { if (evt.type === "ERROR") { - // console.error(evt.object); - return; // fixme: too old resource version (e.g. reproduce: quickly jump btw pages) + return; // e.g. evt.object.message == "too old resource version" } const { namespace, resourceVersion } = evt.object.metadata; diff --git a/src/renderer/components/+namespaces/namespace.store.ts b/src/renderer/components/+namespaces/namespace.store.ts index a7c5ed4cc2..e70bb34a74 100644 --- a/src/renderer/components/+namespaces/namespace.store.ts +++ b/src/renderer/components/+namespaces/namespace.store.ts @@ -1,4 +1,4 @@ -import { action, comparer, observable, reaction, when } from "mobx"; +import { action, comparer, IReactionDisposer, IReactionOptions, observable, reaction, when } from "mobx"; import { autobind, createStorage } from "../../utils"; import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store"; import { Namespace, namespacesApi } from "../../api/endpoints/namespaces.api"; @@ -32,30 +32,52 @@ export class NamespaceStore extends KubeObjectStore { this.init(); } + onContextChange(callback: (contextNamespaces: string[]) => void, opts: IReactionOptions = {}): IReactionDisposer { + return reaction(() => this.contextNs.toJS(), callback, { + equals: comparer.identity, + ...opts, + }) + } + private async init() { await clusterStore.whenLoaded; if (!getHostedCluster()) return; - await getHostedCluster().whenReady; // wait for cluster-state from main - await this.loadAll(); // auto-load allowed namespaces this.isReady = true; this.setContext(this.initNamespaces); - return reaction(() => this.contextNs.toJS(), namespaces => { - storage.set(namespaces); // save to local-storage - namespaceUrlParam.set(namespaces, { replaceHistory: true }); // update url - }, { - fireImmediately: true, - equals: comparer.identity, - }); + const disposers: IReactionDisposer[] = []; + + // save selected namespaces to local-storage and update URL + disposers.push( + this.onContextChange(namespaces => { + storage.set(namespaces); + namespaceUrlParam.set(namespaces, { replaceHistory: true }); + }, { + fireImmediately: true, + equals: comparer.identity, + }) + ); + + // auto-load allowed namespaces + disposers.push( + reaction(() => this.allowedNamespaces, () => { + this.loadAll(); + this.setContext(this.initNamespaces) + }, { + fireImmediately: true, + equals: comparer.identity, + }) + ); + + return disposers; } get allowedNamespaces(): string[] { return getHostedCluster().allowedNamespaces; } - // FIXME: page/app reload doesn't restore previously selected namespaces get initNamespaces() { const allowedNamespaces = new Set(this.allowedNamespaces); const lastUsedNamespaces = new Set(storage.get()); @@ -118,6 +140,9 @@ export class NamespaceStore extends KubeObjectStore { } protected async loadItems({ isAdmin, namespaces }: KubeObjectStoreLoadingParams) { + if (isAdmin) { + return this.api.list(); + } if (!isAllowedResource("namespaces")) { return namespaces.map(this.getDummyNamespace); } diff --git a/src/renderer/components/+workloads-overview/overview.tsx b/src/renderer/components/+workloads-overview/overview.tsx index 318ad53f77..93d29df631 100644 --- a/src/renderer/components/+workloads-overview/overview.tsx +++ b/src/renderer/components/+workloads-overview/overview.tsx @@ -17,58 +17,56 @@ import { cronJobStore } from "../+workloads-cronjobs/cronjob.store"; import { Events } from "../+events"; import { KubeObjectStore } from "../../kube-object.store"; import { isAllowedResource } from "../../../common/rbac"; +import { namespaceStore } from "../+namespaces/namespace.store"; interface Props extends RouteComponentProps { } @observer export class WorkloadsOverview extends React.Component { + @observable isLoading = false; @observable isUnmounting = false; async componentDidMount() { - const stores: KubeObjectStore[] = []; + const stores: KubeObjectStore[] = [ + isAllowedResource("pods") && podsStore, + isAllowedResource("deployments") && deploymentStore, + isAllowedResource("daemonsets") && daemonSetStore, + isAllowedResource("statefulsets") && statefulSetStore, + isAllowedResource("statefulsets") && statefulSetStore, + isAllowedResource("replicasets") && replicaSetStore, + isAllowedResource("jobs") && jobStore, + isAllowedResource("cronjobs") && cronJobStore, + isAllowedResource("events") && eventStore, + ].filter(Boolean); - if (isAllowedResource("pods")) { - stores.push(podsStore); + const unsubscribeMap = new Map( + stores.map(store => [store, Function]) + ); + + const loadStores = async () => { + this.isLoading = true; + for (const store of stores) { + if (this.isUnmounting) break; + try { + store.reset(); + await store.loadAll(); + unsubscribeMap.get(store)(); // unsubscribe previous watcher + unsubscribeMap.set(store, store.subscribe()); + } catch (error) { + console.error("loading store error", error); + } + } + this.isLoading = false; } - if (isAllowedResource("deployments")) { - stores.push(deploymentStore); - } + namespaceStore.onContextChange(loadStores, { + fireImmediately: true, + }); - if (isAllowedResource("daemonsets")) { - stores.push(daemonSetStore); - } - - if (isAllowedResource("statefulsets")) { - stores.push(statefulSetStore); - } - - if (isAllowedResource("replicasets")) { - stores.push(replicaSetStore); - } - - if (isAllowedResource("jobs")) { - stores.push(jobStore); - } - - if (isAllowedResource("cronjobs")) { - stores.push(cronJobStore); - } - - if (isAllowedResource("events")) { - stores.push(eventStore); - } - - const unsubscribeList: Array<() => void> = []; - - for (const store of stores) { - await store.loadAll(); - unsubscribeList.push(store.subscribe()); - } - - await when(() => this.isUnmounting); - unsubscribeList.forEach(dispose => dispose()); + await when(() => this.isUnmounting && !this.isLoading); + unsubscribeMap.forEach(dispose => dispose()); + unsubscribeMap.clear(); } componentWillUnmount() { @@ -79,11 +77,11 @@ export class WorkloadsOverview extends React.Component { return ( <> - { isAllowedResource("events") && } + />} ); } 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 ae702b8d74..28af7baddf 100644 --- a/src/renderer/components/item-object-list/item-list-layout.tsx +++ b/src/renderer/components/item-object-list/item-list-layout.tsx @@ -2,10 +2,10 @@ import "./item-list-layout.scss"; import groupBy from "lodash/groupBy"; import React, { ReactNode } from "react"; -import { computed, observable, reaction, toJS, when } from "mobx"; +import { computed, IReactionDisposer, observable, reaction, toJS, when } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; import { ConfirmDialog, ConfirmDialogParams } from "../confirm-dialog"; -import { TableSortCallback, Table, TableCell, TableCellProps, TableHead, TableProps, TableRow, TableRowProps } from "../table"; +import { Table, TableCell, TableCellProps, TableHead, TableProps, TableRow, TableRowProps, TableSortCallback } from "../table"; import { autobind, createStorage, cssNames, IClassName, isReactNode, noop, prevDefault, stopPropagation } from "../../utils"; import { AddRemoveButtons, AddRemoveButtonsProps } from "../add-remove-buttons"; import { NoItems } from "../no-items"; @@ -90,6 +90,9 @@ interface ItemListLayoutUserSettings { export class ItemListLayout extends React.Component { static defaultProps = defaultProps as object; + private watchDisposers: IReactionDisposer[] = []; + + @observable isLoaded = false; @observable isUnmounting = false; // default user settings (ui show-hide tweaks mostly) @@ -110,30 +113,49 @@ export class ItemListLayout extends React.Component { ]); } - // FIXME: reload and re-subscribe stores when context namespaces changed async componentDidMount() { - const { store, dependentStores, isClusterScoped } = this.props; - const stores = [store, ...dependentStores]; - - if (!isClusterScoped) stores.push(namespaceStore); - - try { - stores.map(store => store.reset()); - await Promise.all(stores.map(store => store.loadAll())); - const subscriptions = stores.map(store => store.subscribe()); - - await when(() => this.isUnmounting); - subscriptions.forEach(dispose => dispose()); // unsubscribe all - } catch (error) { - console.log("catched", error); - } + disposeOnUnmount(this, [ + namespaceStore.onContextChange(() => this.loadStores(), { + fireImmediately: true, + }) + ]); } - componentWillUnmount() { + async componentWillUnmount() { this.isUnmounting = true; - const { store, isSelectable } = this.props; + await when(() => this.isLoaded); + this.unsubscribeStores(); + } - if (isSelectable) store.resetSelection(); + async loadStores() { + const { store, dependentStores, isClusterScoped } = this.props; + const stores = new Set([store, ...dependentStores]); + + if (!isClusterScoped) { + stores.add(namespaceStore); + } + + // reset + this.isLoaded = false; + this.unsubscribeStores(); + + // load + for (let store of stores) { + if (this.isUnmounting) break; + try { + store.reset(); + await store.loadAll(); + this.watchDisposers.push(store.subscribe()); + } catch (error) { + console.error("loading store error", error); + } + } + this.isLoaded = true; + } + + unsubscribeStores() { + this.watchDisposers.forEach(dispose => dispose()); + this.watchDisposers.length = 0; } private filterCallbacks: { [type: string]: ItemsFilter } = { @@ -300,7 +322,7 @@ export class ItemListLayout extends React.Component { return; } - return ; + return ; } renderNoItems() { @@ -322,7 +344,7 @@ export class ItemListLayout extends React.Component { ); } - return ; + return ; } renderHeaderContent(placeholders: IHeaderPlaceholders): ReactNode { @@ -366,12 +388,12 @@ export class ItemListLayout extends React.Component { title:
{title}
, info: this.renderInfo(), filters: <> - {!isClusterScoped && } + {!isClusterScoped && } + }}/> , - search: , + search: , }; let header = this.renderHeaderContent(placeholders); @@ -407,7 +429,7 @@ export class ItemListLayout extends React.Component { return (
{!isReady && ( - + )} {isReady && ( { /> )} {renderTableHeader.map((cellProps, index) => )} - {renderItemMenu && } + {renderItemMenu && } )} { diff --git a/src/renderer/item.store.ts b/src/renderer/item.store.ts index 94f93c1958..7c579d3a56 100644 --- a/src/renderer/item.store.ts +++ b/src/renderer/item.store.ts @@ -9,8 +9,7 @@ export interface ItemObject { @autobind() export abstract class ItemStore { - abstract loadAll(...args: any[]): any; - abstract loadAll(): Promise; + abstract loadAll(...args: any[]): Promise; protected defaultSorting = (item: T) => item.getName();