diff --git a/src/common/k8s-api/kube-object.store.ts b/src/common/k8s-api/kube-object.store.ts index f90eaab8e5..916f8c84b5 100644 --- a/src/common/k8s-api/kube-object.store.ts +++ b/src/common/k8s-api/kube-object.store.ts @@ -245,15 +245,18 @@ export abstract class KubeObjectStore extends ItemStore } @action - async load(params: { name: string; namespace?: string }): Promise { - const { name, namespace } = params; + async load(params: { name: string; namespace?: string, save?: boolean }): Promise { + const { name, namespace, save = true } = params; let item = this.getByName(name, namespace); if (!item) { item = await this.loadItem(params); - const newItems = this.sortItems([...this.items, item]); - this.items.replace(newItems); + if (save) { + const newItems = this.sortItems([...this.items, item]); + + this.items.replace(newItems); + } } return item; @@ -263,7 +266,10 @@ export abstract class KubeObjectStore extends ItemStore async loadFromPath(resourcePath: string) { const { namespace, name } = parseKubeApi(resourcePath); - return this.load({ name, namespace }); + return this.load({ + name, namespace, + save: false, // don't save loaded resource to store + }); } protected async createItem(params: { name: string; namespace?: string }, data?: Partial): Promise { diff --git a/src/renderer/components/+apps-releases/release.store.ts b/src/renderer/components/+apps-releases/release.store.ts index 93b5cf5eef..4475953138 100644 --- a/src/renderer/components/+apps-releases/release.store.ts +++ b/src/renderer/components/+apps-releases/release.store.ts @@ -20,7 +20,7 @@ */ import isEqual from "lodash/isEqual"; -import { action, observable, reaction, when, makeObservable } from "mobx"; +import { action, makeObservable, observable, reaction, when } from "mobx"; import { autoBind } from "../../utils"; import { createRelease, deleteRelease, HelmRelease, IReleaseCreatePayload, IReleaseUpdatePayload, listReleases, rollbackRelease, updateRelease } from "../../../common/k8s-api/endpoints/helm-releases.api"; import { ItemStore } from "../../../common/item.store"; @@ -106,7 +106,10 @@ export class ReleaseStore extends ItemStore { } async loadFromContextNamespaces(): Promise { - return this.loadAll(namespaceStore.context.contextNamespaces); + const { contextNamespaces } = namespaceStore.context ?? {}; + + if (!contextNamespaces) return; + await this.loadAll(contextNamespaces); } async loadItems(namespaces: string[]) { diff --git a/src/renderer/components/dock/dock-tab.store.ts b/src/renderer/components/dock/dock-tab.store.ts index 01f62e7d5b..a19a93388e 100644 --- a/src/renderer/components/dock/dock-tab.store.ts +++ b/src/renderer/components/dock/dock-tab.store.ts @@ -19,8 +19,8 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { autorun, computed, IReactionDisposer, IReactionOptions, makeObservable, observable, reaction } from "mobx"; -import { autoBind, createStorage, Disposer, disposer, StorageHelper } from "../../utils"; +import { comparer, makeObservable, observable, reaction } from "mobx"; +import { autoBind, createStorage, disposer, StorageHelper } from "../../utils"; import { dockStore, TabId } from "./dock.store"; export interface DockTabStoreOptions { @@ -29,14 +29,13 @@ export interface DockTabStoreOptions { } export class DockTabStore { - @observable private _data: Record = {}; @observable.ref private storage?: StorageHelper>; // available only with `options.storageKey` + @observable private _data: Record = {}; @observable dataReady = false; // dock-tab's data ready to interact, e.g. start editing resource - protected watchers = observable.map(); protected dispose = disposer(); - @computed get data() { + get data() { if (this.options.storageKey) { return this.storage.get(); } @@ -63,6 +62,7 @@ export class DockTabStore { if (this.options.storageKey) { this.storage = createStorage(this.options.storageKey, this._data); + this.dispose.push(() => this.storage.dispose()); } if (this.options.autoInit) { @@ -78,48 +78,33 @@ export class DockTabStore { } protected init() { - this.dataReady = true; - this.dispose.push( - autorun(() => { - const docTabIds = dockStore.tabs.map(tab => tab.id); - const savedDataTabIds = Object.keys(this.data); + reaction(() => ({ + dockTabs: dockStore.tabs.map(tab => tab.id) as TabId[], + dataTabs: Object.keys(this.data) as TabId[], + }), ({ dataTabs, dockTabs }) => { - savedDataTabIds.forEach(tabId => { - const isTabClosed = !docTabIds.includes(tabId); + // clear data for closed or non-existing dock tabs + if (dockTabs.length < dataTabs.length) { + const closedDockTabs = dataTabs.filter(id => !dockTabs.includes(id)); - if (isTabClosed) { - this.clearData(tabId); // clear related tab's data - this.watchers.get(tabId)?.(); // dispose tab related data watcher - dockStore.closeTab(tabId); // make sure dock tab is closed - } - }); - }), - - // clean up data watchers (if any) - () => this.disposeWatchers(), + closedDockTabs.forEach(tabId => this.clearData(tabId)); + } + }, { + fireImmediately: true, + equals: comparer.structural, + }) ); - } - protected disposeWatchers() { - this.watchers.forEach(dispose => dispose()); - this.watchers.clear(); - } - - onDataChange(callback: (entries: [TabId, T][]) => void, opts?: IReactionOptions): IReactionDisposer { - return reaction(() => Object.entries(this.data), callback, opts); - } - - onTabDataChange(tabId: TabId, callback: (data: T, prevData?: T) => void, opts?: IReactionOptions): IReactionDisposer { - return reaction(() => this.data[tabId], callback, opts); + this.dataReady = true; } getData(tabId: TabId): T { - return this.data[tabId] ?? {} as T; + return this.data[tabId]; } setData(tabId: TabId, data: T) { - this.data[tabId] = { ...data }; + this.data[tabId] = data; } clearData(tabId: TabId) { @@ -128,12 +113,11 @@ export class DockTabStore { reset() { this.data = {}; + this.dataReady = false; } destroy() { - this.dataReady = false; - this.dispose(); - this.disposeWatchers(); this.reset(); + this.dispose(); } } diff --git a/src/renderer/components/dock/dock.store.ts b/src/renderer/components/dock/dock.store.ts index 264648e98f..feb8471ace 100644 --- a/src/renderer/components/dock/dock.store.ts +++ b/src/renderer/components/dock/dock.store.ts @@ -20,8 +20,8 @@ */ import * as uuid from "uuid"; -import { action, computed, IReactionOptions, makeObservable, observable, reaction } from "mobx"; -import { autoBind, createStorage } from "../../utils"; +import { action, comparer, computed, IReactionOptions, makeObservable, observable, reaction } from "mobx"; +import { autoBind, createStorage, Disposer } from "../../utils"; import throttle from "lodash/throttle"; export type TabId = string; @@ -189,6 +189,27 @@ export class DockStore implements DockStorageState { return reaction(() => [this.height, this.fullSize], callback, options); } + onTabClose(tabId: TabId, callback: () => void, options: IReactionOptions = {}): Disposer { + let disposed = false; + + const stopWatcher = reaction(() => this.getTabById(tabId), tab => { + if (!tab) { + disposed = true; + stopWatcher(); + callback(); + } + }, { + fireImmediately: true, + equals: comparer.shallow, + ...options, + }); + + return () => { + if (disposed) return; + stopWatcher(); + }; + } + onTabChange(callback?: (evt: DockTabChangeEvent) => void, options: DockTabChangeEventOptions = {}) { const { kind, dockIsVisible = true, ...reactionOpts } = options; diff --git a/src/renderer/components/dock/edit-resource.store.ts b/src/renderer/components/dock/edit-resource.store.ts index 3614995436..1851faee60 100644 --- a/src/renderer/components/dock/edit-resource.store.ts +++ b/src/renderer/components/dock/edit-resource.store.ts @@ -26,6 +26,7 @@ import { dockStore, DockTab, DockTabCreateSpecific, TabId, TabKind } from "./doc import type { KubeObject } from "../../../common/k8s-api/kube-object"; import { apiManager } from "../../../common/k8s-api/api-manager"; import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; +import jsYaml from "js-yaml"; export interface EditingResource { resource: string; // resource path, e.g. /api/v1/namespaces/default @@ -42,12 +43,17 @@ export class EditResourceStore extends DockTabStore { } async loadResource(tabId: TabId) { - const resource = this.getResource(tabId); const store = this.getStore(tabId); const data = this.getData(tabId); + let resource = this.getResource(tabId); - if (!resource && store && data) { - await store.loadFromPath(data.resource); // preloading resource for the first time + if (!store || !data) return; + + try { + resource ??= await store.loadFromPath(data.resource); + this.getData(tabId).draft = jsYaml.safeDump(resource.toPlainObject()); + } catch (error) { + console.error(`[DOCK]: dump of resource "${data.resource}" failed: ${error}`); } } diff --git a/src/renderer/components/dock/edit-resource.tsx b/src/renderer/components/dock/edit-resource.tsx index eaf06764e0..62ff0ce368 100644 --- a/src/renderer/components/dock/edit-resource.tsx +++ b/src/renderer/components/dock/edit-resource.tsx @@ -61,17 +61,7 @@ export class EditResource extends React.Component { } get draft(): string { - const { draft } = editResourceStore.getData(this.tabId); - - if (typeof draft === "string") { - return draft; // get previously edited draft - } - - if (this.resource) { - return jsYaml.safeDump(this.resource.toPlainObject()); // dump resource first time - } - - return ""; + return editResourceStore.getData(this.tabId)?.draft ?? ""; } @action @@ -80,10 +70,7 @@ export class EditResource extends React.Component { draft = draft ? jsYaml.safeDump(draft) : undefined; } - editResourceStore.setData(this.tabId, { - ...editResourceStore.getData(this.tabId), - draft, - }); + editResourceStore.getData(this.tabId).draft = draft; } onChange = (draft: string) => { diff --git a/src/renderer/components/dock/log-tab.store.ts b/src/renderer/components/dock/log-tab.store.ts index e9614c740f..7872d95a6d 100644 --- a/src/renderer/components/dock/log-tab.store.ts +++ b/src/renderer/components/dock/log-tab.store.ts @@ -20,7 +20,7 @@ */ import uniqueId from "lodash/uniqueId"; -import { reaction } from "mobx"; +import { reaction, when } from "mobx"; import { podsStore } from "../+workloads-pods/pods.store"; import { IPodContainer, Pod } from "../../../common/k8s-api/endpoints"; @@ -52,6 +52,13 @@ export class LogTabStore extends DockTabStore { }); } + get whenReady(): Promise { + return Promise.all([ + super.whenReady, + when(() => podsStore.isLoaded), + ]); + } + protected init() { super.init(); diff --git a/src/renderer/utils/storageHelper.ts b/src/renderer/utils/storageHelper.ts index a7931ae41e..eba8402d71 100755 --- a/src/renderer/utils/storageHelper.ts +++ b/src/renderer/utils/storageHelper.ts @@ -21,10 +21,11 @@ // Helper for working with storages (e.g. window.localStorage, NodeJS/file-system, etc.) -import { action, comparer, makeObservable, observable, toJS, when, } from "mobx"; +import { action, comparer, IReactionDisposer, IReactionOptions, makeObservable, observable, reaction, toJS, when, } from "mobx"; import produce, { Draft } from "immer"; import { isEqual, isFunction, isPlainObject } from "lodash"; import logger from "../../main/logger"; +import { disposer } from "../../common/utils"; export interface StorageAdapter { [metadata: string]: any; @@ -36,6 +37,7 @@ export interface StorageAdapter { export interface StorageHelperOptions { autoInit?: boolean; // start preloading data immediately, default: true + autoSave?: boolean; // bind auto-saving changes to storage-adapter, default: true storage: StorageAdapter; defaultValue: T; } @@ -43,6 +45,7 @@ export interface StorageHelperOptions { export class StorageHelper { static logPrefix = "[StorageHelper]:"; readonly storage: StorageAdapter; + public dispose = disposer(); private data = observable.box(undefined, { deep: true, @@ -63,17 +66,21 @@ export class StorageHelper { constructor(readonly key: string, private options: StorageHelperOptions) { makeObservable(this); - const { storage, autoInit = true } = options; + const { storage, autoInit = true, autoSave = true } = options; this.storage = storage; - this.data.observe_(({ newValue, oldValue }) => { - this.onChange(newValue as T, oldValue as T); - }); - if (autoInit) { this.init(); } + + if (autoSave) { + this.dispose.push(this.bindAutoSave()); + } + } + + protected bindAutoSave(options: IReactionOptions = {}): IReactionDisposer { + return reaction(() => this.toJSON(), this.onChange, options); } private onData = (data: T): void => { @@ -114,7 +121,7 @@ export class StorageHelper { return isEqual(value, this.defaultValue); } - protected onChange(value: T, oldValue?: T) { + protected onChange = (value: T, oldValue?: T) => { if (!this.initialized) return; try { @@ -128,7 +135,7 @@ export class StorageHelper { } catch (error) { logger.error(`${StorageHelper.logPrefix} updating storage: ${error}`, this, { value, oldValue }); } - } + }; get(): T { return this.data.get() ?? this.defaultValue;