diff --git a/src/common/k8s-api/cluster-context.ts b/src/common/k8s-api/cluster-context.ts index 596e658bde..af892a2ce3 100644 --- a/src/common/k8s-api/cluster-context.ts +++ b/src/common/k8s-api/cluster-context.ts @@ -25,4 +25,5 @@ export interface ClusterContext { cluster?: Cluster; allNamespaces: string[]; // available / allowed namespaces from cluster.ts contextNamespaces: string[]; // selected by user (see: namespace-select.tsx) + hasSelectedAll: boolean; } diff --git a/src/common/k8s-api/kube-object.store.ts b/src/common/k8s-api/kube-object.store.ts index 16f9c6457f..035f21d2d5 100644 --- a/src/common/k8s-api/kube-object.store.ts +++ b/src/common/k8s-api/kube-object.store.ts @@ -33,9 +33,8 @@ import type { RequestInit } from "node-fetch"; import AbortController from "abort-controller"; import type { Patch } from "rfc6902"; -export interface KubeObjectStoreLoadingParams { +export interface KubeObjectStoreLoadingParams { namespaces: string[]; - api?: KubeApi; reqInit?: RequestInit; /** @@ -63,6 +62,11 @@ export interface KubeObjectStoreSubscribeParams { * being rejected with */ onLoadFailure?: (err: any) => void; + + /** + * An optional parent abort controller + */ + abortController?: AbortController; } export abstract class KubeObjectStore extends ItemStore { @@ -167,8 +171,8 @@ export abstract class KubeObjectStore extends ItemStore } } - protected async loadItems({ namespaces, api, reqInit, onLoadFailure }: KubeObjectStoreLoadingParams): Promise { - if (!this.context?.cluster.isAllowedResource(api.kind)) { + protected async loadItems({ namespaces, reqInit, onLoadFailure }: KubeObjectStoreLoadingParams): Promise { + if (!this.context?.cluster.isAllowedResource(this.api.kind)) { return []; } @@ -176,12 +180,12 @@ export abstract class KubeObjectStore extends ItemStore && this.context.cluster.accessibleNamespaces.length === 0 && this.context.allNamespaces.every(ns => namespaces.includes(ns)); - if (!api.isNamespaced || isLoadingAll) { - if (api.isNamespaced) { + if (!this.api.isNamespaced || isLoadingAll) { + if (this.api.isNamespaced) { this.loadedNamespaces = []; } - const res = api.list({ reqInit }, this.query); + const res = this.api.list({ reqInit }, this.query); if (onLoadFailure) { try { @@ -203,7 +207,7 @@ export abstract class KubeObjectStore extends ItemStore this.loadedNamespaces = namespaces; const results = await Promise.allSettled( - namespaces.map(namespace => api.list({ namespace, reqInit }, this.query)), + namespaces.map(namespace => this.api.list({ namespace, reqInit }, this.query)), ); const res: T[] = []; @@ -231,24 +235,14 @@ export abstract class KubeObjectStore extends ItemStore } @action - async loadAll(options: KubeObjectStoreLoadAllParams = {}): Promise { + async loadAll({ namespaces = this.context.contextNamespaces, merge = true, reqInit, onLoadFailure }: KubeObjectStoreLoadAllParams = {}): Promise { await this.contextReady; this.isLoading = true; - const { - namespaces = this.context.allNamespaces, // load all namespaces by default - merge = true, // merge loaded items or return as result - reqInit, - onLoadFailure, - } = options; try { - const items = await this.loadItems({ namespaces, api: this.api, reqInit, onLoadFailure }); + const items = await this.loadItems({ namespaces, reqInit, onLoadFailure }); - if (merge) { - this.mergeItems(items, { replace: false }); - } else { - this.mergeItems(items, { replace: true }); - } + this.mergeItems(items, { merge }); this.isLoaded = true; this.failedLoading = false; @@ -275,11 +269,11 @@ export abstract class KubeObjectStore extends ItemStore } @action - protected mergeItems(partialItems: T[], { replace = false, updateStore = true, sort = true, filter = true } = {}): T[] { + protected mergeItems(partialItems: T[], { merge = true, updateStore = true, sort = true, filter = true } = {}): T[] { let items = partialItems; // update existing items - if (!replace) { + if (merge) { const namespaces = partialItems.map(item => item.getNs()); items = [ @@ -394,23 +388,21 @@ export abstract class KubeObjectStore extends ItemStore }); } - subscribe(opts: KubeObjectStoreSubscribeParams = {}) { - const abortController = new AbortController(); - + subscribe({ onLoadFailure, abortController = new AbortController() }: KubeObjectStoreSubscribeParams = {}) { if (this.api.isNamespaced) { Promise.race([rejectPromiseBy(abortController.signal), Promise.all([this.contextReady, this.namespacesReady])]) .then(() => { if (this.context.cluster.isGlobalWatchEnabled && this.loadedNamespaces.length === 0) { - return this.watchNamespace("", abortController, opts); + return this.watchNamespace("", abortController, { onLoadFailure }); } for (const namespace of this.loadedNamespaces) { - this.watchNamespace(namespace, abortController, opts); + this.watchNamespace(namespace, abortController, { onLoadFailure }); } }) .catch(noop); // ignore DOMExceptions } else { - this.watchNamespace("", abortController, opts); + this.watchNamespace("", abortController, { onLoadFailure }); } return () => abortController.abort(); diff --git a/src/common/k8s-api/kube-object.ts b/src/common/k8s-api/kube-object.ts index 63e50f9cc5..62d1840453 100644 --- a/src/common/k8s-api/kube-object.ts +++ b/src/common/k8s-api/kube-object.ts @@ -105,8 +105,9 @@ export class KubeCreationError extends Error { } export class KubeObject implements ItemObject { - static readonly kind: string; - static readonly namespaced: boolean; + static readonly kind?: string; + static readonly namespaced?: boolean; + static readonly apiBase?: string; apiVersion: string; kind: string; @@ -215,7 +216,7 @@ export class KubeObject): Promise { diff --git a/src/common/k8s-api/kube-watch-api.ts b/src/common/k8s-api/kube-watch-api.ts index 116e8777be..63636eddc3 100644 --- a/src/common/k8s-api/kube-watch-api.ts +++ b/src/common/k8s-api/kube-watch-api.ts @@ -25,32 +25,36 @@ import type { KubeObjectStore } from "./kube-object.store"; import type { ClusterContext } from "./cluster-context"; -import plimit from "p-limit"; -import { comparer, observable, reaction, makeObservable } from "mobx"; -import { autoBind, disposer, Disposer, noop } from "../utils"; -import type { KubeApi } from "./kube-api"; +import { comparer, reaction } from "mobx"; +import { disposer, Disposer, noop } from "../utils"; import type { KubeJsonApiData } from "./kube-json-api"; -import { isDebugging, isProduction } from "../vars"; import type { KubeObject } from "./kube-object"; +import AbortController from "abort-controller"; +import { once } from "lodash"; +import logger from "../logger"; + +class WrappedAbortController extends AbortController { + constructor(protected parent: AbortController) { + super(); + + parent.signal.addEventListener("abort", () => { + this.abort(); + }); + } +} export interface IKubeWatchEvent { type: "ADDED" | "MODIFIED" | "DELETED" | "ERROR"; object?: T; } -interface KubeWatchPreloadOptions { +export interface KubeWatchSubscribeStoreOptions { /** * The namespaces to watch - * @default all-accessible + * @default all selected namespaces */ namespaces?: string[]; - /** - * Whether to skip loading if the store is already loaded - * @default false - */ - loadOnce?: boolean; - /** * A function that is called when listing fails. If set then blocks errors * being rejected with @@ -58,123 +62,148 @@ interface KubeWatchPreloadOptions { onLoadFailure?: (err: any) => void; } -export interface KubeWatchSubscribeStoreOptions extends KubeWatchPreloadOptions { - /** - * Whether to subscribe only after loading all stores - * @default true - */ - waitUntilLoaded?: boolean; - - /** - * Whether to preload the stores before watching - * @default true - */ - preload?: boolean; -} - export interface IKubeWatchLog { message: string | string[] | Error; meta?: object; cssStyle?: string; } +interface SubscribeStoreParams { + store: KubeObjectStore; + parent: AbortController; + watchChanges: boolean; + namespaces: string[]; + onLoadFailure?: (err: any) => void; +} + +class WatchCount { + #data = new Map, number>(); + + public inc(store: KubeObjectStore): number { + if (!this.#data.has(store)) { + this.#data.set(store, 0); + } + + const newCount = this.#data.get(store) + 1; + + logger.info(`[KUBE-WATCH-API]: inc() count for ${store.api.objectConstructor.apiBase} is now ${newCount}`); + this.#data.set(store, newCount); + + return newCount; + } + + public dec(store: KubeObjectStore): number { + if (!this.#data.has(store)) { + throw new Error(`Cannot dec count for store that has never been inc: ${store.api.objectConstructor.kind}`); + } + + const newCount = this.#data.get(store) - 1; + + if (newCount < 0) { + throw new Error(`Cannot dec count more times than it has been inc: ${store.api.objectConstructor.kind}`); + } + + logger.debug(`[KUBE-WATCH-API]: dec() count for ${store.api.objectConstructor.apiBase} is now ${newCount}`); + this.#data.set(store, newCount); + + return newCount; + } +} + export class KubeWatchApi { - @observable context: ClusterContext = null; + static context: ClusterContext = null; - constructor() { - makeObservable(this); - autoBind(this); - } + #watch = new WatchCount(); - isAllowedApi(api: KubeApi): boolean { - return Boolean(this.context?.cluster.isAllowedResource(api.kind)); - } - - preloadStores(stores: KubeObjectStore[], { loadOnce, namespaces, onLoadFailure }: KubeWatchPreloadOptions = {}) { - const limitRequests = plimit(1); // load stores one by one to allow quick skipping when fast clicking btw pages - const preloading: Promise[] = []; - - for (const store of stores) { - preloading.push(limitRequests(async () => { - if (store.isLoaded && loadOnce) return; // skip - - return store.loadAll({ namespaces, onLoadFailure }); - })); + private subscribeStore({ store, parent, watchChanges, namespaces, onLoadFailure }: SubscribeStoreParams): Disposer { + if (this.#watch.inc(store) > 1) { + // don't load or subscribe to a store more than once + return () => this.#watch.dec(store); } - return { - loading: Promise.allSettled(preloading), - cancelLoading: () => limitRequests.clearQueue(), - }; - } + let childController = new WrappedAbortController(parent); + const unsubscribe = disposer(); - subscribeStores(stores: KubeObjectStore[], opts: KubeWatchSubscribeStoreOptions = {}): Disposer { - const { preload = true, waitUntilLoaded = true, loadOnce = false, onLoadFailure } = opts; - const subscribingNamespaces = opts.namespaces ?? this.context?.allNamespaces ?? []; - const unsubscribeStores = disposer(); - let isUnsubscribed = false; - - const load = (namespaces = subscribingNamespaces) => this.preloadStores(stores, { namespaces, loadOnce, onLoadFailure }); - let preloading = preload && load(); - let cancelReloading: Disposer = noop; - - const subscribe = () => { - if (isUnsubscribed) { - return; - } - - unsubscribeStores.push(...stores.map(store => store.subscribe({ onLoadFailure }))); - }; - - if (preloading) { - if (waitUntilLoaded) { - preloading.loading.then(subscribe, error => { - this.log({ - message: new Error("Loading stores has failed"), - meta: { stores, error, options: opts }, + const loadThenSubscribe = async (namespaces: string[]) => { + try { + await store.loadAll({ namespaces, reqInit: { signal: childController.signal }, onLoadFailure }); + unsubscribe.push(store.subscribe({ onLoadFailure, abortController: childController })); + } catch (error) { + if (!(error instanceof DOMException)) { + this.log(Object.assign(new Error("Loading stores has failed"), { cause: error }), { + meta: { store, namespaces }, }); - }); - } else { - subscribe(); + } } + }; - // reload stores only for context namespaces change - cancelReloading = reaction(() => this.context?.contextNamespaces, namespaces => { - preloading?.cancelLoading(); - unsubscribeStores(); - preloading = load(namespaces); - preloading.loading.then(subscribe); - }, { - equals: comparer.shallow, - }); - } + /** + * We don't want to wait because we want to start reacting to namespace + * selection changes ASAP + */ + loadThenSubscribe(namespaces).catch(noop); + + const cancelReloading = watchChanges + ? reaction( + // Note: must slice because reaction won't fire if it isn't there + () => [KubeWatchApi.context.contextNamespaces.slice(), KubeWatchApi.context.hasSelectedAll] as const, + ([namespaces, curSelectedAll], [prevNamespaces, prevSelectedAll]) => { + if (curSelectedAll && prevSelectedAll) { + const action = namespaces.length > prevNamespaces.length ? "created" : "deleted"; + + return console.debug(`[KUBE-WATCH-API]: Not changing watch for ${store.api.apiBase} because a new namespace was ${action} but all namespaces are selected`); + } + + console.log(`[KUBE-WATCH-API]: changing watch ${store.api.apiBase}`, namespaces); + childController.abort(); + unsubscribe(); + childController = new WrappedAbortController(parent); + loadThenSubscribe(namespaces).catch(noop); + }, + { + equals: comparer.shallow, + }, + ) + : noop; // don't watch namespaces if namespaces were provided + + return () => { + if (this.#watch.dec(store) === 0) { + // only stop the subcribe if this is the last one + cancelReloading(); + childController.abort(); + unsubscribe(); + } + }; + } + + subscribeStores(stores: KubeObjectStore[], { namespaces, onLoadFailure }: KubeWatchSubscribeStoreOptions = {}): Disposer { + const parent = new AbortController(); + const unsubscribe = disposer( + ...stores.map(store => this.subscribeStore({ + store, + parent, + watchChanges: !namespaces && store.api.isNamespaced, + namespaces: namespaces ?? KubeWatchApi.context?.contextNamespaces ?? [], + onLoadFailure, + })), + ); // unsubscribe - return () => { - if (isUnsubscribed) return; - isUnsubscribed = true; - cancelReloading(); - preloading?.cancelLoading(); - unsubscribeStores(); - }; + return once(() => { + parent.abort(); + unsubscribe(); + }); } - protected log({ message, cssStyle = "", meta = {}}: IKubeWatchLog) { - if (isProduction && !isDebugging) { - return; - } + protected log(message: any, meta: any) { + const log = message instanceof Error + ? console.error + : console.debug; - const logInfo = [`%c[KUBE-WATCH-API]:`, `font-weight: bold; ${cssStyle}`, message].flat().map(String); - const logMeta = { + log("[KUBE-WATCH-API]:", message, { time: new Date().toLocaleString(), ...meta, - }; - - if (message instanceof Error) { - console.error(...logInfo, logMeta); - } else { - console.info(...logInfo, logMeta); - } + }); } } diff --git a/src/renderer/cluster-frame.tsx b/src/renderer/cluster-frame.tsx index 0786ca18a5..734e6c8486 100755 --- a/src/renderer/cluster-frame.tsx +++ b/src/renderer/cluster-frame.tsx @@ -42,11 +42,11 @@ import whatInput from "what-input"; import { clusterSetFrameIdHandler } from "../common/cluster-ipc"; import { ClusterPageMenuRegistration, ClusterPageMenuRegistry } from "../extensions/registries"; import { StatefulSetScaleDialog } from "./components/+workloads-statefulsets/statefulset-scale-dialog"; -import { kubeWatchApi } from "../common/k8s-api/kube-watch-api"; +import { KubeWatchApi, kubeWatchApi } from "../common/k8s-api/kube-watch-api"; import { ReplicaSetScaleDialog } from "./components/+workloads-replicasets/replicaset-scale-dialog"; import { CommandContainer } from "./components/command-palette/command-container"; import { KubeObjectStore } from "../common/k8s-api/kube-object.store"; -import { clusterContext } from "./components/context"; +import { FrameContext } from "./components/context"; import * as routes from "../common/routes"; import { TabLayout, TabLayoutRoute } from "./components/layout/tab-layout"; import { ErrorBoundary } from "./components/error-boundary"; @@ -73,6 +73,8 @@ import { watchHistoryState } from "./remote-helpers/history-updater"; import { unmountComponentAtNode } from "react-dom"; import { PortForwardDialog } from "./port-forward"; import { DeleteClusterDialog } from "./components/delete-cluster-dialog"; +import { WorkloadsOverview } from "./components/+workloads-overview/overview"; +import { KubeObjectListLayout } from "./components/kube-object-list-layout"; @observer export class ClusterFrame extends React.Component { @@ -91,10 +93,12 @@ export class ClusterFrame extends React.Component { ClusterFrame.clusterId = getHostedClusterId(); + const cluster = ClusterStore.getInstance().getById(ClusterFrame.clusterId); + logger.info(`${ClusterFrame.logPrefix} Init dashboard, clusterId=${ClusterFrame.clusterId}, frameId=${frameId}`); await Terminal.preloadFonts(); await requestMain(clusterSetFrameIdHandler, ClusterFrame.clusterId); - await ClusterStore.getInstance().getById(ClusterFrame.clusterId).whenReady; // cluster.activate() is done at this point + await cluster.whenReady; // cluster.activate() is done at this point catalogEntityRegistry.activeEntity = ClusterFrame.clusterId; @@ -120,16 +124,21 @@ export class ClusterFrame extends React.Component { whatInput.ask(); // Start to monitor user input device + const clusterContext = new FrameContext(cluster); + // Setup hosted cluster context KubeObjectStore.defaultContext.set(clusterContext); - kubeWatchApi.context = clusterContext; + WorkloadsOverview.clusterContext + = KubeObjectListLayout.clusterContext + = KubeWatchApi.context + = clusterContext; } componentDidMount() { disposeOnUnmount(this, [ - kubeWatchApi.subscribeStores([namespaceStore], { - preload: true, - }), + kubeWatchApi.subscribeStores([ + namespaceStore, + ]), watchHistoryState(), ]); diff --git a/src/renderer/components/+cluster/cluster-overview.tsx b/src/renderer/components/+cluster/cluster-overview.tsx index 1815fee001..d2e3e3ee9a 100644 --- a/src/renderer/components/+cluster/cluster-overview.tsx +++ b/src/renderer/components/+cluster/cluster-overview.tsx @@ -55,9 +55,11 @@ export class ClusterOverview extends React.Component { this.metricPoller.start(true); disposeOnUnmount(this, [ - kubeWatchApi.subscribeStores([podsStore, eventStore, nodesStore], { - preload: true, - }), + kubeWatchApi.subscribeStores([ + podsStore, + eventStore, + nodesStore, + ]), reaction( () => clusterOverviewStore.metricNodeRole, // Toggle Master/Worker node switcher () => this.metricPoller.restart(true), diff --git a/src/renderer/components/+events/kube-event-details.tsx b/src/renderer/components/+events/kube-event-details.tsx index a06911e08a..e573f97303 100644 --- a/src/renderer/components/+events/kube-event-details.tsx +++ b/src/renderer/components/+events/kube-event-details.tsx @@ -22,13 +22,14 @@ import "./kube-event-details.scss"; import React from "react"; -import { observer } from "mobx-react"; +import { disposeOnUnmount, observer } from "mobx-react"; import { KubeObject } from "../../../common/k8s-api/kube-object"; import { DrawerItem, DrawerTitle } from "../drawer"; import { cssNames } from "../../utils"; import { LocaleDate } from "../locale-date"; import { eventStore } from "./event.store"; import logger from "../../../common/logger"; +import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api"; export interface KubeEventDetailsProps { object: KubeObject; @@ -36,8 +37,12 @@ export interface KubeEventDetailsProps { @observer export class KubeEventDetails extends React.Component { - async componentDidMount() { - eventStore.reloadAll(); + componentDidMount() { + disposeOnUnmount(this, [ + kubeWatchApi.subscribeStores([ + eventStore, + ]), + ]); } render() { diff --git a/src/renderer/components/+namespaces/namespace-details.tsx b/src/renderer/components/+namespaces/namespace-details.tsx index 8267637de3..68136499ed 100644 --- a/src/renderer/components/+namespaces/namespace-details.tsx +++ b/src/renderer/components/+namespaces/namespace-details.tsx @@ -39,6 +39,7 @@ import { ClusterMetricsResourceType } from "../../../common/cluster-types"; import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; import { getDetailsUrl } from "../kube-detail-params"; import logger from "../../../common/logger"; +import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api"; interface Props extends KubeObjectDetailsProps { } @@ -52,14 +53,16 @@ export class NamespaceDetails extends React.Component { makeObservable(this); } - @disposeOnUnmount - clean = reaction(() => this.props.object, () => { - this.metrics = null; - }); - componentDidMount() { - resourceQuotaStore.reloadAll(); - limitRangeStore.reloadAll(); + disposeOnUnmount(this, [ + reaction(() => this.props.object, () => { + this.metrics = null; + }), + kubeWatchApi.subscribeStores([ + resourceQuotaStore, + limitRangeStore, + ]), + ]); } @computed get quotas() { diff --git a/src/renderer/components/+namespaces/namespace-select.tsx b/src/renderer/components/+namespaces/namespace-select.tsx index aadfa601a2..7974b25f79 100644 --- a/src/renderer/components/+namespaces/namespace-select.tsx +++ b/src/renderer/components/+namespaces/namespace-select.tsx @@ -23,12 +23,11 @@ import "./namespace-select.scss"; import React from "react"; import { computed, makeObservable } from "mobx"; -import { disposeOnUnmount, observer } from "mobx-react"; +import { observer } from "mobx-react"; import { Select, SelectOption, SelectProps } from "../select"; import { cssNames } from "../../utils"; import { Icon } from "../icon"; import { namespaceStore } from "./namespace.store"; -import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api"; interface Props extends SelectProps { showIcons?: boolean; @@ -50,14 +49,7 @@ export class NamespaceSelect extends React.Component { makeObservable(this); } - componentDidMount() { - disposeOnUnmount(this, [ - kubeWatchApi.subscribeStores([namespaceStore], { - preload: true, - loadOnce: true, // skip reloading namespaces on every render / page visit - }), - ]); - } + // No subscribe here because the subscribe is in (the cluster frame root component) @computed.struct get options(): SelectOption[] { const { customizeOptions, showAllNamespacesOption, sort } = this.props; diff --git a/src/renderer/components/+namespaces/namespace.store.ts b/src/renderer/components/+namespaces/namespace.store.ts index 1fc9100935..34e4348488 100644 --- a/src/renderer/components/+namespaces/namespace.store.ts +++ b/src/renderer/components/+namespaces/namespace.store.ts @@ -133,7 +133,7 @@ export class NamespaceStore extends KubeObjectStore { return super.subscribe(); } - protected async loadItems(params: KubeObjectStoreLoadingParams): Promise { + protected async loadItems(params: KubeObjectStoreLoadingParams): Promise { const { allowedNamespaces } = this; let namespaces = await super.loadItems(params).catch(() => []); diff --git a/src/renderer/components/+network-ingresses/ingress-details.tsx b/src/renderer/components/+network-ingresses/ingress-details.tsx index a0ec3ce8ca..632f0d5b60 100644 --- a/src/renderer/components/+network-ingresses/ingress-details.tsx +++ b/src/renderer/components/+network-ingresses/ingress-details.tsx @@ -49,10 +49,13 @@ export class IngressDetails extends React.Component { makeObservable(this); } - @disposeOnUnmount - clean = reaction(() => this.props.object, () => { - this.metrics = null; - }); + componentDidMount() { + disposeOnUnmount(this, [ + reaction(() => this.props.object, () => { + this.metrics = null; + }), + ]); + } @boundMethod async loadMetrics() { diff --git a/src/renderer/components/+network-services/service-details.tsx b/src/renderer/components/+network-services/service-details.tsx index d247e7908c..36dc9dee8a 100644 --- a/src/renderer/components/+network-services/service-details.tsx +++ b/src/renderer/components/+network-services/service-details.tsx @@ -44,8 +44,9 @@ export class ServiceDetails extends React.Component { const { object: service } = this.props; disposeOnUnmount(this, [ - kubeWatchApi.subscribeStores([endpointStore], { - preload: true, + kubeWatchApi.subscribeStores([ + endpointStore, + ], { namespaces: [service.getNs()], }), portForwardStore.watch(), diff --git a/src/renderer/components/+nodes/node-details.tsx b/src/renderer/components/+nodes/node-details.tsx index 350ba563b8..c576397903 100644 --- a/src/renderer/components/+nodes/node-details.tsx +++ b/src/renderer/components/+nodes/node-details.tsx @@ -41,6 +41,7 @@ import { NodeDetailsResources } from "./node-details-resources"; import { DrawerTitle } from "../drawer/drawer-title"; import { boundMethod } from "../../utils"; import logger from "../../../common/logger"; +import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api"; interface Props extends KubeObjectDetailsProps { } @@ -54,13 +55,15 @@ export class NodeDetails extends React.Component { makeObservable(this); } - @disposeOnUnmount - clean = reaction(() => this.props.object.getName(), () => { - this.metrics = null; - }); - - async componentDidMount() { - podsStore.reloadAll(); + componentDidMount() { + disposeOnUnmount(this, [ + reaction(() => this.props.object.getName(), () => { + this.metrics = null; + }), + kubeWatchApi.subscribeStores([ + podsStore, + ]), + ]); } @boundMethod diff --git a/src/renderer/components/+storage-classes/storage-class-details.tsx b/src/renderer/components/+storage-classes/storage-class-details.tsx index 0ce2827014..30e0df6a6b 100644 --- a/src/renderer/components/+storage-classes/storage-class-details.tsx +++ b/src/renderer/components/+storage-classes/storage-class-details.tsx @@ -25,7 +25,7 @@ import React from "react"; import startCase from "lodash/startCase"; import { DrawerItem, DrawerTitle } from "../drawer"; import { Badge } from "../badge"; -import { observer } from "mobx-react"; +import { disposeOnUnmount, observer } from "mobx-react"; import type { KubeObjectDetailsProps } from "../kube-object-details"; import { StorageClass } from "../../../common/k8s-api/endpoints"; import { KubeObjectMeta } from "../kube-object-meta"; @@ -33,14 +33,19 @@ import { storageClassStore } from "./storage-class.store"; import { VolumeDetailsList } from "../+storage-volumes/volume-details-list"; import { volumesStore } from "../+storage-volumes/volumes.store"; import logger from "../../../common/logger"; +import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api"; interface Props extends KubeObjectDetailsProps { } @observer export class StorageClassDetails extends React.Component { - async componentDidMount() { - volumesStore.reloadAll(); + componentDidMount() { + disposeOnUnmount(this, [ + kubeWatchApi.subscribeStores([ + volumesStore, + ]), + ]); } render() { diff --git a/src/renderer/components/+storage-volume-claims/volume-claim-details.tsx b/src/renderer/components/+storage-volume-claims/volume-claim-details.tsx index 35ca4989e1..6018af72e1 100644 --- a/src/renderer/components/+storage-volume-claims/volume-claim-details.tsx +++ b/src/renderer/components/+storage-volume-claims/volume-claim-details.tsx @@ -51,10 +51,13 @@ export class PersistentVolumeClaimDetails extends React.Component { makeObservable(this); } - @disposeOnUnmount - clean = reaction(() => this.props.object, () => { - this.metrics = null; - }); + componentDidMount() { + disposeOnUnmount(this, [ + reaction(() => this.props.object, () => { + this.metrics = null; + }), + ]); + } @boundMethod async loadMetrics() { diff --git a/src/renderer/components/+workloads-cronjobs/cronjob-details.tsx b/src/renderer/components/+workloads-cronjobs/cronjob-details.tsx index 78e7a6ba7c..a4ba0cbeea 100644 --- a/src/renderer/components/+workloads-cronjobs/cronjob-details.tsx +++ b/src/renderer/components/+workloads-cronjobs/cronjob-details.tsx @@ -23,7 +23,7 @@ import "./cronjob-details.scss"; import React from "react"; import kebabCase from "lodash/kebabCase"; -import { observer } from "mobx-react"; +import { disposeOnUnmount, observer } from "mobx-react"; import { DrawerItem, DrawerTitle } from "../drawer"; import { Badge } from "../badge/badge"; import { jobStore } from "../+workloads-jobs/job.store"; @@ -34,14 +34,19 @@ import { getDetailsUrl } from "../kube-detail-params"; import { CronJob, Job } from "../../../common/k8s-api/endpoints"; import { KubeObjectMeta } from "../kube-object-meta"; import logger from "../../../common/logger"; +import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api"; interface Props extends KubeObjectDetailsProps { } @observer export class CronJobDetails extends React.Component { - async componentDidMount() { - jobStore.reloadAll(); + componentDidMount() { + disposeOnUnmount(this, [ + kubeWatchApi.subscribeStores([ + jobStore, + ]), + ]); } render() { diff --git a/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx b/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx index efd67c1fae..3e431986c3 100644 --- a/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx +++ b/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx @@ -41,6 +41,7 @@ import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; import { ClusterMetricsResourceType } from "../../../common/cluster-types"; import { boundMethod } from "../../utils"; import logger from "../../../common/logger"; +import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api"; interface Props extends KubeObjectDetailsProps { } @@ -54,13 +55,15 @@ export class DaemonSetDetails extends React.Component { makeObservable(this); } - @disposeOnUnmount - clean = reaction(() => this.props.object, () => { - this.metrics = null; - }); - componentDidMount() { - podsStore.reloadAll(); + disposeOnUnmount(this, [ + reaction(() => this.props.object, () => { + this.metrics = null; + }), + kubeWatchApi.subscribeStores([ + podsStore, + ]), + ]); } @boundMethod diff --git a/src/renderer/components/+workloads-deployments/deployment-details.tsx b/src/renderer/components/+workloads-deployments/deployment-details.tsx index b60f8cb5fc..f0ae0c1d6e 100644 --- a/src/renderer/components/+workloads-deployments/deployment-details.tsx +++ b/src/renderer/components/+workloads-deployments/deployment-details.tsx @@ -43,6 +43,7 @@ import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; import { ClusterMetricsResourceType } from "../../../common/cluster-types"; import { boundMethod } from "../../utils"; import logger from "../../../common/logger"; +import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api"; interface Props extends KubeObjectDetailsProps { } @@ -56,14 +57,16 @@ export class DeploymentDetails extends React.Component { makeObservable(this); } - @disposeOnUnmount - clean = reaction(() => this.props.object, () => { - this.metrics = null; - }); - componentDidMount() { - podsStore.reloadAll(); - replicaSetStore.reloadAll(); + disposeOnUnmount(this, [ + reaction(() => this.props.object, () => { + this.metrics = null; + }), + kubeWatchApi.subscribeStores([ + podsStore, + replicaSetStore, + ]), + ]); } @boundMethod diff --git a/src/renderer/components/+workloads-jobs/job-details.tsx b/src/renderer/components/+workloads-jobs/job-details.tsx index 88dcaf7547..a0445874d8 100644 --- a/src/renderer/components/+workloads-jobs/job-details.tsx +++ b/src/renderer/components/+workloads-jobs/job-details.tsx @@ -23,7 +23,7 @@ import "./job-details.scss"; import React from "react"; import kebabCase from "lodash/kebabCase"; -import { observer } from "mobx-react"; +import { disposeOnUnmount, observer } from "mobx-react"; import { DrawerItem } from "../drawer"; import { Badge } from "../badge"; import { PodDetailsStatuses } from "../+workloads-pods/pod-details-statuses"; @@ -36,7 +36,7 @@ import type { KubeObjectDetailsProps } from "../kube-object-details"; import { getMetricsForJobs, IPodMetrics, Job } from "../../../common/k8s-api/endpoints"; import { PodDetailsList } from "../+workloads-pods/pod-details-list"; import { KubeObjectMeta } from "../kube-object-meta"; -import { makeObservable, observable } from "mobx"; +import { makeObservable, observable, reaction } from "mobx"; import { podMetricTabs, PodCharts } from "../+workloads-pods/pod-charts"; import { ClusterMetricsResourceType } from "../../../common/cluster-types"; import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; @@ -45,6 +45,7 @@ import { boundMethod } from "autobind-decorator"; import { getDetailsUrl } from "../kube-detail-params"; import { apiManager } from "../../../common/k8s-api/api-manager"; import logger from "../../../common/logger"; +import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api"; interface Props extends KubeObjectDetailsProps { } @@ -58,8 +59,15 @@ export class JobDetails extends React.Component { makeObservable(this); } - async componentDidMount() { - podsStore.reloadAll(); + componentDidMount() { + disposeOnUnmount(this, [ + reaction(() => this.props.object, () => { + this.metrics = null; + }), + kubeWatchApi.subscribeStores([ + podsStore, + ]), + ]); } @boundMethod diff --git a/src/renderer/components/+workloads-overview/overview.tsx b/src/renderer/components/+workloads-overview/overview.tsx index a684a645aa..fa7ad590b0 100644 --- a/src/renderer/components/+workloads-overview/overview.tsx +++ b/src/renderer/components/+workloads-overview/overview.tsx @@ -36,16 +36,18 @@ import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api"; import { WorkloadsOverviewDetailRegistry } from "../../../extensions/registries"; import type { WorkloadsOverviewRouteParams } from "../../../common/routes"; import { makeObservable, observable, reaction } from "mobx"; -import { clusterContext } from "../context"; import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter"; import { Icon } from "../icon"; import { TooltipPosition } from "../tooltip"; +import type { ClusterContext } from "../../../common/k8s-api/cluster-context"; interface Props extends RouteComponentProps { } @observer export class WorkloadsOverview extends React.Component { + static clusterContext: ClusterContext; + @observable loadErrors: string[] = []; constructor(props: Props) { @@ -56,12 +58,18 @@ export class WorkloadsOverview extends React.Component { componentDidMount() { disposeOnUnmount(this, [ kubeWatchApi.subscribeStores([ - podsStore, deploymentStore, daemonSetStore, statefulSetStore, replicaSetStore, - jobStore, cronJobStore, eventStore, + cronJobStore, + daemonSetStore, + deploymentStore, + eventStore, + jobStore, + podsStore, + replicaSetStore, + statefulSetStore, ], { onLoadFailure: error => this.loadErrors.push(String(error)), }), - reaction(() => clusterContext.contextNamespaces.slice(), () => { + reaction(() => WorkloadsOverview.clusterContext.contextNamespaces.slice(), () => { // clear load errors this.loadErrors.length = 0; }), diff --git a/src/renderer/components/+workloads-replicasets/replicaset-details.tsx b/src/renderer/components/+workloads-replicasets/replicaset-details.tsx index e8f77a34ce..4e38ae9e1e 100644 --- a/src/renderer/components/+workloads-replicasets/replicaset-details.tsx +++ b/src/renderer/components/+workloads-replicasets/replicaset-details.tsx @@ -40,6 +40,7 @@ import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; import { ClusterMetricsResourceType } from "../../../common/cluster-types"; import { boundMethod } from "../../utils"; import logger from "../../../common/logger"; +import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api"; interface Props extends KubeObjectDetailsProps { } @@ -53,13 +54,15 @@ export class ReplicaSetDetails extends React.Component { makeObservable(this); } - @disposeOnUnmount - clean = reaction(() => this.props.object, () => { - this.metrics = null; - }); - - async componentDidMount() { - podsStore.reloadAll(); + componentDidMount() { + disposeOnUnmount(this, [ + reaction(() => this.props.object, () => { + this.metrics = null; + }), + kubeWatchApi.subscribeStores([ + podsStore, + ]), + ]); } @boundMethod diff --git a/src/renderer/components/+workloads-statefulsets/statefulset-details.tsx b/src/renderer/components/+workloads-statefulsets/statefulset-details.tsx index 5351ec53f0..7d2d66568c 100644 --- a/src/renderer/components/+workloads-statefulsets/statefulset-details.tsx +++ b/src/renderer/components/+workloads-statefulsets/statefulset-details.tsx @@ -41,6 +41,7 @@ import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; import { ClusterMetricsResourceType } from "../../../common/cluster-types"; import { boundMethod } from "../../utils"; import logger from "../../../common/logger"; +import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api"; interface Props extends KubeObjectDetailsProps { } @@ -54,13 +55,15 @@ export class StatefulSetDetails extends React.Component { makeObservable(this); } - @disposeOnUnmount - clean = reaction(() => this.props.object, () => { - this.metrics = null; - }); - componentDidMount() { - podsStore.reloadAll(); + disposeOnUnmount(this, [ + reaction(() => this.props.object, () => { + this.metrics = null; + }), + kubeWatchApi.subscribeStores([ + podsStore, + ]), + ]); } @boundMethod diff --git a/src/renderer/components/context.ts b/src/renderer/components/context.ts index 6e37aabdce..fc494f5f37 100755 --- a/src/renderer/components/context.ts +++ b/src/renderer/components/context.ts @@ -19,24 +19,19 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { ClusterStore } from "../../common/cluster-store"; import type { Cluster } from "../../main/cluster"; -import { getHostedClusterId } from "../utils"; import { namespaceStore } from "./+namespaces/namespace.store"; import type { ClusterContext } from "../../common/k8s-api/cluster-context"; +import { computed, makeObservable } from "mobx"; -export const clusterContext: ClusterContext = { - get cluster(): Cluster | null { - return ClusterStore.getInstance().getById(getHostedClusterId()); - }, - - get allNamespaces(): string[] { - if (!this.cluster) { - return []; - } +export class FrameContext implements ClusterContext { + constructor(public cluster: Cluster) { + makeObservable(this); + } + @computed get allNamespaces(): string[] { // user given list of namespaces - if (this.cluster?.accessibleNamespaces.length) { + if (this.cluster.accessibleNamespaces.length) { return this.cluster.accessibleNamespaces; } @@ -47,9 +42,17 @@ export const clusterContext: ClusterContext = { // fallback to cluster resolved namespaces because we could not load list return this.cluster.allowedNamespaces || []; } - }, + } - get contextNamespaces(): string[] { - return namespaceStore.contextNamespaces ?? []; - }, -}; + @computed get contextNamespaces(): string[] { + return namespaceStore.contextNamespaces; + } + + @computed get hasSelectedAll(): boolean { + const namespaces = new Set(this.contextNamespaces); + + return this.allNamespaces?.length > 1 + && this.cluster.accessibleNamespaces.length === 0 + && this.allNamespaces.every(ns => namespaces.has(ns)); + } +} diff --git a/src/renderer/components/kube-object-list-layout/kube-object-list-layout.tsx b/src/renderer/components/kube-object-list-layout/kube-object-list-layout.tsx index 4bfe34461e..f29a6da6e7 100644 --- a/src/renderer/components/kube-object-list-layout/kube-object-list-layout.tsx +++ b/src/renderer/components/kube-object-list-layout/kube-object-list-layout.tsx @@ -30,12 +30,12 @@ import { ItemListLayout, ItemListLayoutProps } from "../item-object-list/item-li import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; import { KubeObjectMenu } from "../kube-object-menu"; import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api"; -import { clusterContext } from "../context"; import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter"; import { ResourceKindMap, ResourceNames } from "../../utils/rbac"; import { kubeSelectedUrlParam, toggleDetails } from "../kube-detail-params"; import { Icon } from "../icon"; import { TooltipPosition } from "../tooltip"; +import type { ClusterContext } from "../../../common/k8s-api/cluster-context"; export interface KubeObjectListLayoutProps extends ItemListLayoutProps { store: KubeObjectStore; @@ -51,6 +51,7 @@ const defaultProps: Partial> = { @observer export class KubeObjectListLayout extends React.Component> { static defaultProps = defaultProps as object; + static clusterContext: ClusterContext; constructor(props: KubeObjectListLayoutProps) { super(props); @@ -67,7 +68,7 @@ export class KubeObjectListLayout extends React.Component< const { store, dependentStores = [], subscribeStores } = this.props; const stores = Array.from(new Set([store, ...dependentStores])); const reactions: Disposer[] = [ - reaction(() => clusterContext.contextNamespaces.slice(), () => { + reaction(() => KubeObjectListLayout.clusterContext.contextNamespaces.slice(), () => { // clear load errors this.loadErrors.length = 0; }), @@ -76,8 +77,6 @@ export class KubeObjectListLayout extends React.Component< if (subscribeStores) { reactions.push( kubeWatchApi.subscribeStores(stores, { - preload: true, - namespaces: clusterContext.contextNamespaces, onLoadFailure: error => this.loadErrors.push(String(error)), }), ); diff --git a/src/renderer/components/layout/sidebar.tsx b/src/renderer/components/layout/sidebar.tsx index 09471b34f0..bd4c4ec661 100644 --- a/src/renderer/components/layout/sidebar.tsx +++ b/src/renderer/components/layout/sidebar.tsx @@ -54,7 +54,9 @@ export class Sidebar extends React.Component { componentDidMount() { disposeOnUnmount(this, [ - kubeWatchApi.subscribeStores([crdStore]), + kubeWatchApi.subscribeStores([ + crdStore, + ]), ]); }