diff --git a/src/renderer/components/+custom-resources/crd-resources.tsx b/src/renderer/components/+custom-resources/crd-resources.tsx index 2bae92b8d4..1503267c24 100644 --- a/src/renderer/components/+custom-resources/crd-resources.tsx +++ b/src/renderer/components/+custom-resources/crd-resources.tsx @@ -30,7 +30,7 @@ export class CrdResources extends React.Component { const { store } = this; if (store && !store.isLoading && !store.isLoaded) { - store.loadSelectedNamespaces(); + store.loadContextNamespaces(); } }) ]); diff --git a/src/renderer/components/+events/kube-event-details.tsx b/src/renderer/components/+events/kube-event-details.tsx index 60821d416d..b8b3058fb0 100644 --- a/src/renderer/components/+events/kube-event-details.tsx +++ b/src/renderer/components/+events/kube-event-details.tsx @@ -14,7 +14,7 @@ export interface KubeEventDetailsProps { @observer export class KubeEventDetails extends React.Component { async componentDidMount() { - eventStore.loadSelectedNamespaces(); + eventStore.loadContextNamespaces(); } render() { diff --git a/src/renderer/components/+namespaces/namespace-details.tsx b/src/renderer/components/+namespaces/namespace-details.tsx index e7397b6a5e..c7a4a0e845 100644 --- a/src/renderer/components/+namespaces/namespace-details.tsx +++ b/src/renderer/components/+namespaces/namespace-details.tsx @@ -32,8 +32,8 @@ export class NamespaceDetails extends React.Component { } componentDidMount() { - resourceQuotaStore.loadSelectedNamespaces(); - limitRangeStore.loadSelectedNamespaces(); + resourceQuotaStore.loadContextNamespaces(); + limitRangeStore.loadContextNamespaces(); } render() { diff --git a/src/renderer/components/+namespaces/namespace.store.ts b/src/renderer/components/+namespaces/namespace.store.ts index c56043766a..2b5cf3cbbc 100644 --- a/src/renderer/components/+namespaces/namespace.store.ts +++ b/src/renderer/components/+namespaces/namespace.store.ts @@ -103,8 +103,20 @@ export class NamespaceStore extends KubeObjectStore { return []; } - public getContextNamespaces(): string[] { - return Array.from(this.contextNs); + getContextNamespaces(): string[] { + const namespaces = Array.from(this.contextNs); + + // show all namespaces when nothing selected + if (!namespaces.length) { + // return actual namespaces list since "allowedNamespaces" updating every 30s in cluster and thus might be stale + if (this.isLoaded) { + return this.items.map(namespace => namespace.getName()); + } + + return this.allowedNamespaces; + } + + return namespaces; } getSubscribeApis() { @@ -139,6 +151,11 @@ export class NamespaceStore extends KubeObjectStore { this.contextNs.replace(namespaces); } + @action + resetContext(){ + this.contextNs.clear(); + } + hasContext(namespaces: string | string[]) { return [namespaces].flat().every(namespace => this.contextNs.has(namespace)); } diff --git a/src/renderer/components/+nodes/node-details.tsx b/src/renderer/components/+nodes/node-details.tsx index d4208c4545..fb5da8b20f 100644 --- a/src/renderer/components/+nodes/node-details.tsx +++ b/src/renderer/components/+nodes/node-details.tsx @@ -29,7 +29,7 @@ export class NodeDetails extends React.Component { }); async componentDidMount() { - podsStore.loadSelectedNamespaces(); + podsStore.loadContextNamespaces(); } componentWillUnmount() { diff --git a/src/renderer/components/+user-management-roles-bindings/add-role-binding-dialog.tsx b/src/renderer/components/+user-management-roles-bindings/add-role-binding-dialog.tsx index 808cef90d6..97e4f9d9e3 100644 --- a/src/renderer/components/+user-management-roles-bindings/add-role-binding-dialog.tsx +++ b/src/renderer/components/+user-management-roles-bindings/add-role-binding-dialog.tsx @@ -80,7 +80,7 @@ export class AddRoleBindingDialog extends React.Component { ]; this.isLoading = true; - await Promise.all(stores.map(store => store.loadSelectedNamespaces())); + await Promise.all(stores.map(store => store.loadContextNamespaces())); this.isLoading = false; } diff --git a/src/renderer/components/+workloads-cronjobs/cronjob-details.tsx b/src/renderer/components/+workloads-cronjobs/cronjob-details.tsx index 4daff4abed..c5c31ef3d2 100644 --- a/src/renderer/components/+workloads-cronjobs/cronjob-details.tsx +++ b/src/renderer/components/+workloads-cronjobs/cronjob-details.tsx @@ -20,7 +20,7 @@ interface Props extends KubeObjectDetailsProps { @observer export class CronJobDetails extends React.Component { async componentDidMount() { - jobStore.loadSelectedNamespaces(); + jobStore.loadContextNamespaces(); } render() { diff --git a/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx b/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx index ab3269ede5..4435f718ec 100644 --- a/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx +++ b/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx @@ -30,7 +30,7 @@ export class DaemonSetDetails extends React.Component { }); componentDidMount() { - podsStore.loadSelectedNamespaces(); + podsStore.loadContextNamespaces(); } componentWillUnmount() { diff --git a/src/renderer/components/+workloads-deployments/deployment-details.tsx b/src/renderer/components/+workloads-deployments/deployment-details.tsx index e22137ea67..72f2013c9b 100644 --- a/src/renderer/components/+workloads-deployments/deployment-details.tsx +++ b/src/renderer/components/+workloads-deployments/deployment-details.tsx @@ -31,7 +31,7 @@ export class DeploymentDetails extends React.Component { }); componentDidMount() { - podsStore.loadSelectedNamespaces(); + podsStore.loadContextNamespaces(); } componentWillUnmount() { diff --git a/src/renderer/components/+workloads-jobs/job-details.tsx b/src/renderer/components/+workloads-jobs/job-details.tsx index 4ce4a9bc61..1dce500fe6 100644 --- a/src/renderer/components/+workloads-jobs/job-details.tsx +++ b/src/renderer/components/+workloads-jobs/job-details.tsx @@ -25,7 +25,7 @@ interface Props extends KubeObjectDetailsProps { @observer export class JobDetails extends React.Component { async componentDidMount() { - podsStore.loadSelectedNamespaces(); + podsStore.loadContextNamespaces(); } render() { diff --git a/src/renderer/components/+workloads-replicasets/replicaset-details.tsx b/src/renderer/components/+workloads-replicasets/replicaset-details.tsx index f427d78e9d..6b68ffa02d 100644 --- a/src/renderer/components/+workloads-replicasets/replicaset-details.tsx +++ b/src/renderer/components/+workloads-replicasets/replicaset-details.tsx @@ -29,7 +29,7 @@ export class ReplicaSetDetails extends React.Component { }); async componentDidMount() { - podsStore.loadSelectedNamespaces(); + podsStore.loadContextNamespaces(); } componentWillUnmount() { diff --git a/src/renderer/components/+workloads-statefulsets/statefulset-details.tsx b/src/renderer/components/+workloads-statefulsets/statefulset-details.tsx index 780572eb96..3088516114 100644 --- a/src/renderer/components/+workloads-statefulsets/statefulset-details.tsx +++ b/src/renderer/components/+workloads-statefulsets/statefulset-details.tsx @@ -30,7 +30,7 @@ export class StatefulSetDetails extends React.Component { }); componentDidMount() { - podsStore.loadSelectedNamespaces(); + podsStore.loadContextNamespaces(); } componentWillUnmount() { diff --git a/src/renderer/components/layout/sidebar.tsx b/src/renderer/components/layout/sidebar.tsx index 20c401ab4f..c3fc715a7c 100644 --- a/src/renderer/components/layout/sidebar.tsx +++ b/src/renderer/components/layout/sidebar.tsx @@ -40,7 +40,7 @@ interface Props { @observer export class Sidebar extends React.Component { async componentDidMount() { - crdStore.loadSelectedNamespaces(); + crdStore.loadContextNamespaces(); } renderCustomResources() { diff --git a/src/renderer/item.store.ts b/src/renderer/item.store.ts index 40df2282fd..f7a4e0e94e 100644 --- a/src/renderer/item.store.ts +++ b/src/renderer/item.store.ts @@ -22,13 +22,17 @@ export abstract class ItemStore { return this.items.filter(item => this.selectedItemsIds.get(item.getId())); } + public getItems(): T[] { + return this.items.toJS(); + } + getByName(name: string, ...args: any[]): T; getByName(name: string): T { return this.items.find(item => item.getName() === name); } - getIndex(item: T): number { - return this.items.findIndex(i => i === item); + getIndexById(id: string): number { + return this.items.findIndex(item => item.getId() === id); } @action diff --git a/src/renderer/kube-object.store.ts b/src/renderer/kube-object.store.ts index 9fe51d6ab4..eb76d72d30 100644 --- a/src/renderer/kube-object.store.ts +++ b/src/renderer/kube-object.store.ts @@ -1,5 +1,7 @@ import type { Cluster } from "../main/cluster"; -import { action, observable, reaction } from "mobx"; +import type { NamespaceStore } from "./components/+namespaces/namespace.store"; + +import { action, computed, observable, reaction } from "mobx"; import { autobind } from "./utils"; import { KubeObject } from "./api/kube-object"; import { IKubeWatchEvent, IKubeWatchMessage, kubeWatchApi } from "./api/kube-watch-api"; @@ -24,6 +26,36 @@ export abstract class KubeObjectStore extends ItemSt this.bindWatchEventsUpdater(); } + // TODO: detach / remove circular dependency + @observable.ref private namespaceStore: NamespaceStore; + + private async resolveNamespaceStore(): Promise { + const { namespaceStore } = await import("./components/+namespaces/namespace.store"); + + await namespaceStore.whenReady; + this.namespaceStore = namespaceStore; + + return namespaceStore; + } + + // TODO: detach / inject dependency as in kube-watch-api + @observable.ref private cluster: Cluster; + + private async resolveCluster(): Promise { + const { getHostedCluster } = await import("../common/cluster-store"); + + this.cluster = getHostedCluster(); + + return this.cluster; + } + + // TODO: figure out how to transparently replace with this.items + @computed get contextItems(): T[] { + const contextNamespaces = this.namespaceStore?.getContextNamespaces() ?? []; // not loaded + + return this.items.filter((item: T) => !item.getNs() || contextNamespaces.includes(item.getId())); + } + get query(): IKubeApiQueryParams { const { limit } = this; @@ -79,12 +111,6 @@ export abstract class KubeObjectStore extends ItemSt } } - protected async resolveCluster(): Promise { - const { getHostedCluster } = await import("../common/cluster-store"); - - return getHostedCluster(); - } - protected async loadItems({ namespaces, api }: KubeObjectStoreLoadingParams): Promise { const cluster = await this.resolveCluster(); @@ -106,23 +132,19 @@ export abstract class KubeObjectStore extends ItemSt } @action - async loadAll(namespaces: string[] = []): Promise { + async loadAll(namespaces: string[] = [], { replace = true } = {}): Promise { this.isLoading = true; try { + // load all available namespaces by default if (!namespaces.length) { - const { namespaceStore } = await import("./components/+namespaces/namespace.store"); + const namespaceStore = await this.resolveNamespaceStore(); - // load all available namespaces by default - namespaces.push(...namespaceStore.allowedNamespaces); + namespaces = namespaceStore.getContextNamespaces(); } let items = await this.loadItems({ namespaces, api: this.api }); - - items = this.filterItemsOnLoad(items); - items = this.sortItems(items); - - this.items.replace(items); + this.mergeItems(items, { replace }); this.isLoaded = true; } catch (error) { console.error("Loading store items failed", { error, store: this }); @@ -132,10 +154,26 @@ export abstract class KubeObjectStore extends ItemSt } } - async loadSelectedNamespaces(): Promise { - const { namespaceStore } = await import("./components/+namespaces/namespace.store"); + @action + private mergeItems(partialItems: T[], { replace = false, sort = true, filter = true } = {}): T[] { + let items = this.items.toJS(); - return this.loadAll(namespaceStore.getContextNamespaces()); + partialItems.forEach(item => { + const index = items.findIndex(i => i.getId() === item.getId()); + + if (index < 0) items.push(item); // add + else items[index] = item; // update + }); + + if (filter) items = this.filterItemsOnLoad(items) + if (sort) items = this.sortItems(items); + if (replace) this.items.replace(items); + + return items; + } + + async loadContextNamespaces(): Promise { + return this.loadAll(this.namespaceStore?.getContextNamespaces()); } protected resetOnError(error: any) { @@ -224,14 +262,11 @@ export abstract class KubeObjectStore extends ItemSt @action protected updateFromEventsBuffer() { - const eventsBuffer = this.eventsBuffer.clear(); + const items = this.items.toJS(); - if (!eventsBuffer.length) { - return; - } - - for (const { type, object } of eventsBuffer) { - const item = this.getById(object.metadata?.uid); + for (const { type, object } of this.eventsBuffer.clear()) { + const index = items.findIndex(item => item.getId() === object.metadata?.uid); + const item = items[index]; const api = apiManager.getApiByKind(object.kind, object.apiVersion); switch (type) { @@ -240,22 +275,20 @@ export abstract class KubeObjectStore extends ItemSt const newItem = new api.objectConstructor(object); if (!item) { - this.items.push(newItem); + items.push(newItem); } else { - const index = this.getIndex(item); - - this.items.splice(index, 1, newItem); + items[index] = newItem; } break; case "DELETED": - this.items.remove(item); + if (item) { + items.splice(index, 1); + } break; } } - // sort and limit items to store's maximum - const newItems = this.sortItems(this.items.slice(-this.bufferSize)); - - this.items.replace(newItems); + // update items + this.items.replace(this.sortItems(items.slice(-this.bufferSize))); } }