1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

refactoring dock-tab stores -- part 3

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2021-10-05 12:11:26 +03:00
parent 5038267bbc
commit b421591931
8 changed files with 96 additions and 75 deletions

View File

@ -245,15 +245,18 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
} }
@action @action
async load(params: { name: string; namespace?: string }): Promise<T> { async load(params: { name: string; namespace?: string, save?: boolean }): Promise<T> {
const { name, namespace } = params; const { name, namespace, save = true } = params;
let item = this.getByName(name, namespace); let item = this.getByName(name, namespace);
if (!item) { if (!item) {
item = await this.loadItem(params); 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; return item;
@ -263,7 +266,10 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
async loadFromPath(resourcePath: string) { async loadFromPath(resourcePath: string) {
const { namespace, name } = parseKubeApi(resourcePath); 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<T>): Promise<T> { protected async createItem(params: { name: string; namespace?: string }, data?: Partial<T>): Promise<T> {

View File

@ -20,7 +20,7 @@
*/ */
import isEqual from "lodash/isEqual"; 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 { autoBind } from "../../utils";
import { createRelease, deleteRelease, HelmRelease, IReleaseCreatePayload, IReleaseUpdatePayload, listReleases, rollbackRelease, updateRelease } from "../../../common/k8s-api/endpoints/helm-releases.api"; import { createRelease, deleteRelease, HelmRelease, IReleaseCreatePayload, IReleaseUpdatePayload, listReleases, rollbackRelease, updateRelease } from "../../../common/k8s-api/endpoints/helm-releases.api";
import { ItemStore } from "../../../common/item.store"; import { ItemStore } from "../../../common/item.store";
@ -106,7 +106,10 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
} }
async loadFromContextNamespaces(): Promise<void> { async loadFromContextNamespaces(): Promise<void> {
return this.loadAll(namespaceStore.context.contextNamespaces); const { contextNamespaces } = namespaceStore.context ?? {};
if (!contextNamespaces) return;
await this.loadAll(contextNamespaces);
} }
async loadItems(namespaces: string[]) { async loadItems(namespaces: string[]) {

View File

@ -19,8 +19,8 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
import { autorun, computed, IReactionDisposer, IReactionOptions, makeObservable, observable, reaction } from "mobx"; import { comparer, makeObservable, observable, reaction } from "mobx";
import { autoBind, createStorage, Disposer, disposer, StorageHelper } from "../../utils"; import { autoBind, createStorage, disposer, StorageHelper } from "../../utils";
import { dockStore, TabId } from "./dock.store"; import { dockStore, TabId } from "./dock.store";
export interface DockTabStoreOptions { export interface DockTabStoreOptions {
@ -29,14 +29,13 @@ export interface DockTabStoreOptions {
} }
export class DockTabStore<T extends {}> { export class DockTabStore<T extends {}> {
@observable private _data: Record<TabId, T> = {};
@observable.ref private storage?: StorageHelper<Record<TabId, T>>; // available only with `options.storageKey` @observable.ref private storage?: StorageHelper<Record<TabId, T>>; // available only with `options.storageKey`
@observable private _data: Record<TabId, T> = {};
@observable dataReady = false; // dock-tab's data ready to interact, e.g. start editing resource @observable dataReady = false; // dock-tab's data ready to interact, e.g. start editing resource
protected watchers = observable.map<TabId, IReactionDisposer | Disposer>();
protected dispose = disposer(); protected dispose = disposer();
@computed get data() { get data() {
if (this.options.storageKey) { if (this.options.storageKey) {
return this.storage.get(); return this.storage.get();
} }
@ -63,6 +62,7 @@ export class DockTabStore<T extends {}> {
if (this.options.storageKey) { if (this.options.storageKey) {
this.storage = createStorage(this.options.storageKey, this._data); this.storage = createStorage(this.options.storageKey, this._data);
this.dispose.push(() => this.storage.dispose());
} }
if (this.options.autoInit) { if (this.options.autoInit) {
@ -78,48 +78,33 @@ export class DockTabStore<T extends {}> {
} }
protected init() { protected init() {
this.dataReady = true;
this.dispose.push( this.dispose.push(
autorun(() => { reaction(() => ({
const docTabIds = dockStore.tabs.map(tab => tab.id); dockTabs: dockStore.tabs.map(tab => tab.id) as TabId[],
const savedDataTabIds = Object.keys(this.data); dataTabs: Object.keys(this.data) as TabId[],
}), ({ dataTabs, dockTabs }) => {
savedDataTabIds.forEach(tabId => { // clear data for closed or non-existing dock tabs
const isTabClosed = !docTabIds.includes(tabId); if (dockTabs.length < dataTabs.length) {
const closedDockTabs = dataTabs.filter(id => !dockTabs.includes(id));
if (isTabClosed) { closedDockTabs.forEach(tabId => this.clearData(tabId));
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 fireImmediately: true,
} equals: comparer.structural,
}); })
}),
// clean up data watchers (if any)
() => this.disposeWatchers(),
); );
}
protected disposeWatchers() { this.dataReady = true;
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);
} }
getData(tabId: TabId): T { getData(tabId: TabId): T {
return this.data[tabId] ?? {} as T; return this.data[tabId];
} }
setData(tabId: TabId, data: T) { setData(tabId: TabId, data: T) {
this.data[tabId] = { ...data }; this.data[tabId] = data;
} }
clearData(tabId: TabId) { clearData(tabId: TabId) {
@ -128,12 +113,11 @@ export class DockTabStore<T extends {}> {
reset() { reset() {
this.data = {}; this.data = {};
this.dataReady = false;
} }
destroy() { destroy() {
this.dataReady = false;
this.dispose();
this.disposeWatchers();
this.reset(); this.reset();
this.dispose();
} }
} }

View File

@ -20,8 +20,8 @@
*/ */
import * as uuid from "uuid"; import * as uuid from "uuid";
import { action, computed, IReactionOptions, makeObservable, observable, reaction } from "mobx"; import { action, comparer, computed, IReactionOptions, makeObservable, observable, reaction } from "mobx";
import { autoBind, createStorage } from "../../utils"; import { autoBind, createStorage, Disposer } from "../../utils";
import throttle from "lodash/throttle"; import throttle from "lodash/throttle";
export type TabId = string; export type TabId = string;
@ -189,6 +189,27 @@ export class DockStore implements DockStorageState {
return reaction(() => [this.height, this.fullSize], callback, options); 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 = {}) { onTabChange(callback?: (evt: DockTabChangeEvent) => void, options: DockTabChangeEventOptions = {}) {
const { kind, dockIsVisible = true, ...reactionOpts } = options; const { kind, dockIsVisible = true, ...reactionOpts } = options;

View File

@ -26,6 +26,7 @@ import { dockStore, DockTab, DockTabCreateSpecific, TabId, TabKind } from "./doc
import type { KubeObject } from "../../../common/k8s-api/kube-object"; import type { KubeObject } from "../../../common/k8s-api/kube-object";
import { apiManager } from "../../../common/k8s-api/api-manager"; import { apiManager } from "../../../common/k8s-api/api-manager";
import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
import jsYaml from "js-yaml";
export interface EditingResource { export interface EditingResource {
resource: string; // resource path, e.g. /api/v1/namespaces/default resource: string; // resource path, e.g. /api/v1/namespaces/default
@ -42,12 +43,17 @@ export class EditResourceStore extends DockTabStore<EditingResource> {
} }
async loadResource(tabId: TabId) { async loadResource(tabId: TabId) {
const resource = this.getResource(tabId);
const store = this.getStore(tabId); const store = this.getStore(tabId);
const data = this.getData(tabId); const data = this.getData(tabId);
let resource = this.getResource(tabId);
if (!resource && store && data) { if (!store || !data) return;
await store.loadFromPath(data.resource); // preloading resource for the first time
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}`);
} }
} }

View File

@ -61,17 +61,7 @@ export class EditResource extends React.Component<Props> {
} }
get draft(): string { get draft(): string {
const { draft } = editResourceStore.getData(this.tabId); return editResourceStore.getData(this.tabId)?.draft ?? "";
if (typeof draft === "string") {
return draft; // get previously edited draft
}
if (this.resource) {
return jsYaml.safeDump(this.resource.toPlainObject()); // dump resource first time
}
return "";
} }
@action @action
@ -80,10 +70,7 @@ export class EditResource extends React.Component<Props> {
draft = draft ? jsYaml.safeDump(draft) : undefined; draft = draft ? jsYaml.safeDump(draft) : undefined;
} }
editResourceStore.setData(this.tabId, { editResourceStore.getData(this.tabId).draft = draft;
...editResourceStore.getData(this.tabId),
draft,
});
} }
onChange = (draft: string) => { onChange = (draft: string) => {

View File

@ -20,7 +20,7 @@
*/ */
import uniqueId from "lodash/uniqueId"; import uniqueId from "lodash/uniqueId";
import { reaction } from "mobx"; import { reaction, when } from "mobx";
import { podsStore } from "../+workloads-pods/pods.store"; import { podsStore } from "../+workloads-pods/pods.store";
import { IPodContainer, Pod } from "../../../common/k8s-api/endpoints"; import { IPodContainer, Pod } from "../../../common/k8s-api/endpoints";
@ -52,6 +52,13 @@ export class LogTabStore extends DockTabStore<LogTabData> {
}); });
} }
get whenReady(): Promise<any> {
return Promise.all([
super.whenReady,
when(() => podsStore.isLoaded),
]);
}
protected init() { protected init() {
super.init(); super.init();

View File

@ -21,10 +21,11 @@
// Helper for working with storages (e.g. window.localStorage, NodeJS/file-system, etc.) // 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 produce, { Draft } from "immer";
import { isEqual, isFunction, isPlainObject } from "lodash"; import { isEqual, isFunction, isPlainObject } from "lodash";
import logger from "../../main/logger"; import logger from "../../main/logger";
import { disposer } from "../../common/utils";
export interface StorageAdapter<T> { export interface StorageAdapter<T> {
[metadata: string]: any; [metadata: string]: any;
@ -36,6 +37,7 @@ export interface StorageAdapter<T> {
export interface StorageHelperOptions<T> { export interface StorageHelperOptions<T> {
autoInit?: boolean; // start preloading data immediately, default: true autoInit?: boolean; // start preloading data immediately, default: true
autoSave?: boolean; // bind auto-saving changes to storage-adapter, default: true
storage: StorageAdapter<T>; storage: StorageAdapter<T>;
defaultValue: T; defaultValue: T;
} }
@ -43,6 +45,7 @@ export interface StorageHelperOptions<T> {
export class StorageHelper<T> { export class StorageHelper<T> {
static logPrefix = "[StorageHelper]:"; static logPrefix = "[StorageHelper]:";
readonly storage: StorageAdapter<T>; readonly storage: StorageAdapter<T>;
public dispose = disposer();
private data = observable.box<T>(undefined, { private data = observable.box<T>(undefined, {
deep: true, deep: true,
@ -63,17 +66,21 @@ export class StorageHelper<T> {
constructor(readonly key: string, private options: StorageHelperOptions<T>) { constructor(readonly key: string, private options: StorageHelperOptions<T>) {
makeObservable(this); makeObservable(this);
const { storage, autoInit = true } = options; const { storage, autoInit = true, autoSave = true } = options;
this.storage = storage; this.storage = storage;
this.data.observe_(({ newValue, oldValue }) => {
this.onChange(newValue as T, oldValue as T);
});
if (autoInit) { if (autoInit) {
this.init(); 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 => { private onData = (data: T): void => {
@ -114,7 +121,7 @@ export class StorageHelper<T> {
return isEqual(value, this.defaultValue); return isEqual(value, this.defaultValue);
} }
protected onChange(value: T, oldValue?: T) { protected onChange = (value: T, oldValue?: T) => {
if (!this.initialized) return; if (!this.initialized) return;
try { try {
@ -128,7 +135,7 @@ export class StorageHelper<T> {
} catch (error) { } catch (error) {
logger.error(`${StorageHelper.logPrefix} updating storage: ${error}`, this, { value, oldValue }); logger.error(`${StorageHelper.logPrefix} updating storage: ${error}`, this, { value, oldValue });
} }
} };
get(): T { get(): T {
return this.data.get() ?? this.defaultValue; return this.data.get() ?? this.defaultValue;