diff --git a/src/renderer/components/+apps/apps.tsx b/src/renderer/components/+apps/apps.tsx index f20da99fcc..0616b0e399 100644 --- a/src/renderer/components/+apps/apps.tsx +++ b/src/renderer/components/+apps/apps.tsx @@ -3,12 +3,10 @@ import { observer } from "mobx-react"; import { TabLayout, TabLayoutRoute } from "../layout/tab-layout"; import { HelmCharts, helmChartsRoute, helmChartsURL } from "../+apps-helm-charts"; import { HelmReleases, releaseRoute, releaseURL } from "../+apps-releases"; -import { namespaceUrlParam } from "../+namespaces/namespace.store"; @observer export class Apps extends React.Component { static get tabRoutes(): TabLayoutRoute[] { - const query = namespaceUrlParam.toObjectParam(); return [ { @@ -20,7 +18,7 @@ export class Apps extends React.Component { { title: "Releases", component: HelmReleases, - url: releaseURL({ query }), + url: releaseURL(), routePath: releaseRoute.path.toString(), }, ]; diff --git a/src/renderer/components/+config/config.tsx b/src/renderer/components/+config/config.tsx index e3158459ba..a283b67cc6 100644 --- a/src/renderer/components/+config/config.tsx +++ b/src/renderer/components/+config/config.tsx @@ -3,7 +3,6 @@ import { observer } from "mobx-react"; import { TabLayout, TabLayoutRoute } from "../layout/tab-layout"; import { ConfigMaps, configMapsRoute, configMapsURL } from "../+config-maps"; import { Secrets, secretsRoute, secretsURL } from "../+config-secrets"; -import { namespaceUrlParam } from "../+namespaces/namespace.store"; import { resourceQuotaRoute, ResourceQuotas, resourceQuotaURL } from "../+config-resource-quotas"; import { pdbRoute, pdbURL, PodDisruptionBudgets } from "../+config-pod-disruption-budgets"; import { HorizontalPodAutoscalers, hpaRoute, hpaURL } from "../+config-autoscalers"; @@ -13,14 +12,13 @@ import { LimitRanges, limitRangesRoute, limitRangeURL } from "../+config-limit-r @observer export class Config extends React.Component { static get tabRoutes(): TabLayoutRoute[] { - const query = namespaceUrlParam.toObjectParam(); const routes: TabLayoutRoute[] = []; if (isAllowedResource("configmaps")) { routes.push({ title: "ConfigMaps", component: ConfigMaps, - url: configMapsURL({ query }), + url: configMapsURL(), routePath: configMapsRoute.path.toString(), }); } @@ -29,7 +27,7 @@ export class Config extends React.Component { routes.push({ title: "Secrets", component: Secrets, - url: secretsURL({ query }), + url: secretsURL(), routePath: secretsRoute.path.toString(), }); } @@ -38,7 +36,7 @@ export class Config extends React.Component { routes.push({ title: "Resource Quotas", component: ResourceQuotas, - url: resourceQuotaURL({ query }), + url: resourceQuotaURL(), routePath: resourceQuotaRoute.path.toString(), }); } @@ -47,7 +45,7 @@ export class Config extends React.Component { routes.push({ title: "Limit Ranges", component: LimitRanges, - url: limitRangeURL({ query }), + url: limitRangeURL(), routePath: limitRangesRoute.path.toString(), }); } @@ -56,7 +54,7 @@ export class Config extends React.Component { routes.push({ title: "HPA", component: HorizontalPodAutoscalers, - url: hpaURL({ query }), + url: hpaURL(), routePath: hpaRoute.path.toString(), }); } @@ -65,7 +63,7 @@ export class Config extends React.Component { routes.push({ title: "Pod Disruption Budgets", component: PodDisruptionBudgets, - url: pdbURL({ query }), + url: pdbURL(), routePath: pdbRoute.path.toString(), }); } diff --git a/src/renderer/components/+namespaces/namespace.store.ts b/src/renderer/components/+namespaces/namespace.store.ts index f03b6c641f..4a2d3eb0ec 100644 --- a/src/renderer/components/+namespaces/namespace.store.ts +++ b/src/renderer/components/+namespaces/namespace.store.ts @@ -1,35 +1,18 @@ -import { action, comparer, computed, IReactionDisposer, IReactionOptions, makeObservable, observable, reaction, } from "mobx"; +import { action, comparer, computed, IReactionDisposer, IReactionOptions, makeObservable, reaction, when, } from "mobx"; import { autoBind, createStorage } from "../../utils"; import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../kube-object.store"; import { Namespace, namespacesApi } from "../../api/endpoints/namespaces.api"; -import { createPageParam } from "../../navigation"; import { apiManager } from "../../api/api-manager"; -// FIXME: something fishy with sync selected-namespaces with URL -const selectedNamespaces = createStorage("selected_namespaces", undefined); - -export const namespaceUrlParam = createPageParam({ - name: "namespaces", - defaultValue: [], -}); - -export function getDummyNamespace(name: string) { - return new Namespace({ - kind: Namespace.kind, - apiVersion: "v1", - metadata: { - name, - uid: "", - resourceVersion: "", - selfLink: `/api/v1/namespaces/${name}` - } - }); -} - export class NamespaceStore extends KubeObjectStore { api = namespacesApi; - @observable private contextNs = observable.set(); + private defaultNamespaces: string[] = []; + private storage = createStorage("selected_namespaces", this.defaultNamespaces); + + @computed get selectedNamespaces(): string[] { + return this.storage.get(); + } constructor() { super(); @@ -41,29 +24,19 @@ export class NamespaceStore extends KubeObjectStore { private async init() { await this.contextReady; - await selectedNamespaces.whenReady; + await when(() => this.storage.initialized); this.setContext(this.initialNamespaces); this.autoLoadAllowedNamespaces(); - this.autoUpdateUrlAndLocalStorage(); } - public onContextChange(callback: (contextNamespaces: string[]) => void, opts: IReactionOptions = {}): IReactionDisposer { - return reaction(() => Array.from(this.contextNs), callback, { + public onContextChange(callback: (namespaces: string[]) => void, opts: IReactionOptions = {}): IReactionDisposer { + return reaction(() => Array.from(this.selectedNamespaces), callback, { equals: comparer.shallow, ...opts, }); } - private autoUpdateUrlAndLocalStorage(): IReactionDisposer { - return this.onContextChange(namespaces => { - selectedNamespaces.set(namespaces); // save to local-storage - namespaceUrlParam.set(namespaces, { replaceHistory: true }); // update url - }, { - fireImmediately: true, - }); - } - private autoLoadAllowedNamespaces(): IReactionDisposer { return reaction(() => this.allowedNamespaces, namespaces => this.loadAll({ namespaces }), { fireImmediately: true, @@ -72,19 +45,18 @@ export class NamespaceStore extends KubeObjectStore { } private get initialNamespaces(): string[] { - const namespaces = new Set(this.allowedNamespaces); - const prevSelectedNamespaces = selectedNamespaces.get(); + const { allowedNamespaces, selectedNamespaces, defaultNamespaces } = this; // return previously saved namespaces from local-storage (if any) - if (prevSelectedNamespaces) { - return prevSelectedNamespaces.filter(namespace => namespaces.has(namespace)); + if (selectedNamespaces !== defaultNamespaces) { + return selectedNamespaces.filter(namespace => allowedNamespaces.includes(namespace)); } // otherwise select "default" or first allowed namespace - if (namespaces.has("default")) { + if (allowedNamespaces.includes("default")) { return ["default"]; - } else if (namespaces.size) { - return [Array.from(namespaces)[0]]; + } else if (allowedNamespaces.length) { + return [allowedNamespaces[0]]; } return []; @@ -98,13 +70,11 @@ export class NamespaceStore extends KubeObjectStore { } @computed get contextNamespaces(): string[] { - const namespaces = Array.from(this.contextNs); - - if (!namespaces.length) { + if (!this.selectedNamespaces.length) { return this.allowedNamespaces; // show all namespaces when nothing selected } - return namespaces; + return this.selectedNamespaces; } getSubscribeApis() { @@ -132,30 +102,39 @@ export class NamespaceStore extends KubeObjectStore { @action setContext(namespace: string | string[]) { - const namespaces = [namespace].flat(); + const namespaces = Array.from(new Set([namespace].flat())); - this.contextNs.replace(namespaces); + this.storage.set(namespaces); } @action - resetContext() { - this.contextNs.clear(); + resetContext(namespaces?: string | string[]) { + if (namespaces) { + const resettingNamespaces = [namespaces].flat(); + const newNamespaces = this.storage.get().filter(ns => !resettingNamespaces.includes(ns)); + + this.storage.set(newNamespaces); + } else { + this.storage.reset(); + } } - hasContext(namespaces: string | string[]) { - return [namespaces].flat().every(namespace => this.contextNs.has(namespace)); + hasContext(namespaces: string | string[]): boolean { + return [namespaces] + .flat() + .every(namespace => this.selectedNamespaces.includes(namespace)); } @computed get hasAllContexts(): boolean { - return this.contextNs.size === this.allowedNamespaces.length; + return this.selectedNamespaces.length === this.allowedNamespaces.length; } @action - toggleContext(namespace: string) { - if (this.hasContext(namespace)) { - this.contextNs.delete(namespace); + toggleContext(namespaces: string | string[]) { + if (this.hasContext(namespaces)) { + this.resetContext(namespaces); } else { - this.contextNs.add(namespace); + this.setContext([this.selectedNamespaces, namespaces].flat()); } } @@ -175,9 +154,22 @@ export class NamespaceStore extends KubeObjectStore { @action async remove(item: Namespace) { await super.remove(item); - this.contextNs.delete(item.getName()); + this.resetContext(item.getName()); } } export const namespaceStore = new NamespaceStore(); apiManager.registerStore(namespaceStore); + +export function getDummyNamespace(name: string) { + return new Namespace({ + kind: Namespace.kind, + apiVersion: "v1", + metadata: { + name, + uid: "", + resourceVersion: "", + selfLink: `/api/v1/namespaces/${name}` + } + }); +} diff --git a/src/renderer/components/+network/network.tsx b/src/renderer/components/+network/network.tsx index 638e2b3f11..f10aa6bfd6 100644 --- a/src/renderer/components/+network/network.tsx +++ b/src/renderer/components/+network/network.tsx @@ -7,20 +7,18 @@ import { Services, servicesRoute, servicesURL } from "../+network-services"; import { endpointRoute, Endpoints, endpointURL } from "../+network-endpoints"; import { Ingresses, ingressRoute, ingressURL } from "../+network-ingresses"; import { NetworkPolicies, networkPoliciesRoute, networkPoliciesURL } from "../+network-policies"; -import { namespaceUrlParam } from "../+namespaces/namespace.store"; import { isAllowedResource } from "../../../common/rbac"; @observer export class Network extends React.Component { static get tabRoutes(): TabLayoutRoute[] { - const query = namespaceUrlParam.toObjectParam(); const routes: TabLayoutRoute[] = []; if (isAllowedResource("services")) { routes.push({ title: "Services", component: Services, - url: servicesURL({ query }), + url: servicesURL(), routePath: servicesRoute.path.toString(), }); } @@ -29,7 +27,7 @@ export class Network extends React.Component { routes.push({ title: "Endpoints", component: Endpoints, - url: endpointURL({ query }), + url: endpointURL(), routePath: endpointRoute.path.toString(), }); } @@ -38,7 +36,7 @@ export class Network extends React.Component { routes.push({ title: "Ingresses", component: Ingresses, - url: ingressURL({ query }), + url: ingressURL(), routePath: ingressRoute.path.toString(), }); } @@ -47,7 +45,7 @@ export class Network extends React.Component { routes.push({ title: "Network Policies", component: NetworkPolicies, - url: networkPoliciesURL({ query }), + url: networkPoliciesURL(), routePath: networkPoliciesRoute.path.toString(), }); } diff --git a/src/renderer/components/+storage/storage.tsx b/src/renderer/components/+storage/storage.tsx index 83f0bf255a..8801cce422 100644 --- a/src/renderer/components/+storage/storage.tsx +++ b/src/renderer/components/+storage/storage.tsx @@ -6,20 +6,18 @@ import { TabLayout, TabLayoutRoute } from "../layout/tab-layout"; import { PersistentVolumes, volumesRoute, volumesURL } from "../+storage-volumes"; import { StorageClasses, storageClassesRoute, storageClassesURL } from "../+storage-classes"; import { PersistentVolumeClaims, volumeClaimsRoute, volumeClaimsURL } from "../+storage-volume-claims"; -import { namespaceUrlParam } from "../+namespaces/namespace.store"; import { isAllowedResource } from "../../../common/rbac"; @observer export class Storage extends React.Component { static get tabRoutes() { const tabRoutes: TabLayoutRoute[] = []; - const query = namespaceUrlParam.toObjectParam(); if (isAllowedResource("persistentvolumeclaims")) { tabRoutes.push({ title: "Persistent Volume Claims", component: PersistentVolumeClaims, - url: volumeClaimsURL({ query }), + url: volumeClaimsURL(), routePath: volumeClaimsRoute.path.toString(), }); } diff --git a/src/renderer/components/+user-management/user-management.tsx b/src/renderer/components/+user-management/user-management.tsx index 480808d6a1..83fede82ae 100644 --- a/src/renderer/components/+user-management/user-management.tsx +++ b/src/renderer/components/+user-management/user-management.tsx @@ -6,7 +6,6 @@ import { Roles } from "../+user-management-roles"; import { RoleBindings } from "../+user-management-roles-bindings"; import { ServiceAccounts } from "../+user-management-service-accounts"; import { podSecurityPoliciesRoute, podSecurityPoliciesURL, roleBindingsRoute, roleBindingsURL, rolesRoute, rolesURL, serviceAccountsRoute, serviceAccountsURL } from "./user-management.route"; -import { namespaceUrlParam } from "../+namespaces/namespace.store"; import { PodSecurityPolicies } from "../+pod-security-policies"; import { isAllowedResource } from "../../../common/rbac"; @@ -14,25 +13,24 @@ import { isAllowedResource } from "../../../common/rbac"; export class UserManagement extends React.Component { static get tabRoutes() { const tabRoutes: TabLayoutRoute[] = []; - const query = namespaceUrlParam.toObjectParam(); tabRoutes.push( { title: "Service Accounts", component: ServiceAccounts, - url: serviceAccountsURL({ query }), + url: serviceAccountsURL(), routePath: serviceAccountsRoute.path.toString(), }, { title: "Role Bindings", component: RoleBindings, - url: roleBindingsURL({ query }), + url: roleBindingsURL(), routePath: roleBindingsRoute.path.toString(), }, { title: "Roles", component: Roles, - url: rolesURL({ query }), + url: rolesURL(), routePath: rolesRoute.path.toString(), }, ); diff --git a/src/renderer/components/+workloads/workloads.tsx b/src/renderer/components/+workloads/workloads.tsx index ac896ff281..c84226a59f 100644 --- a/src/renderer/components/+workloads/workloads.tsx +++ b/src/renderer/components/+workloads/workloads.tsx @@ -5,7 +5,6 @@ import { observer } from "mobx-react"; import { TabLayout, TabLayoutRoute } from "../layout/tab-layout"; import { WorkloadsOverview } from "../+workloads-overview/overview"; import { cronJobsRoute, cronJobsURL, daemonSetsRoute, daemonSetsURL, deploymentsRoute, deploymentsURL, jobsRoute, jobsURL, overviewRoute, overviewURL, podsRoute, podsURL, replicaSetsRoute, replicaSetsURL, statefulSetsRoute, statefulSetsURL } from "./workloads.route"; -import { namespaceUrlParam } from "../+namespaces/namespace.store"; import { Pods } from "../+workloads-pods"; import { Deployments } from "../+workloads-deployments"; import { DaemonSets } from "../+workloads-daemonsets"; @@ -18,12 +17,11 @@ import { ReplicaSets } from "../+workloads-replicasets"; @observer export class Workloads extends React.Component { static get tabRoutes(): TabLayoutRoute[] { - const query = namespaceUrlParam.toObjectParam(); const routes: TabLayoutRoute[] = [ { title: "Overview", component: WorkloadsOverview, - url: overviewURL({ query }), + url: overviewURL(), routePath: overviewRoute.path.toString() } ]; @@ -32,7 +30,7 @@ export class Workloads extends React.Component { routes.push({ title: "Pods", component: Pods, - url: podsURL({ query }), + url: podsURL(), routePath: podsRoute.path.toString() }); } @@ -41,7 +39,7 @@ export class Workloads extends React.Component { routes.push({ title: "Deployments", component: Deployments, - url: deploymentsURL({ query }), + url: deploymentsURL(), routePath: deploymentsRoute.path.toString(), }); } @@ -50,7 +48,7 @@ export class Workloads extends React.Component { routes.push({ title: "DaemonSets", component: DaemonSets, - url: daemonSetsURL({ query }), + url: daemonSetsURL(), routePath: daemonSetsRoute.path.toString(), }); } @@ -59,7 +57,7 @@ export class Workloads extends React.Component { routes.push({ title: "StatefulSets", component: StatefulSets, - url: statefulSetsURL({ query }), + url: statefulSetsURL(), routePath: statefulSetsRoute.path.toString(), }); } @@ -68,7 +66,7 @@ export class Workloads extends React.Component { routes.push({ title: "ReplicaSets", component: ReplicaSets, - url: replicaSetsURL({ query }), + url: replicaSetsURL(), routePath: replicaSetsRoute.path.toString(), }); } @@ -77,7 +75,7 @@ export class Workloads extends React.Component { routes.push({ title: "Jobs", component: Jobs, - url: jobsURL({ query }), + url: jobsURL(), routePath: jobsRoute.path.toString(), }); } @@ -86,7 +84,7 @@ export class Workloads extends React.Component { routes.push({ title: "CronJobs", component: CronJobs, - url: cronJobsURL({ query }), + url: cronJobsURL(), routePath: cronJobsRoute.path.toString(), }); } diff --git a/src/renderer/components/layout/sidebar.tsx b/src/renderer/components/layout/sidebar.tsx index be8753642f..a5ff91ced2 100644 --- a/src/renderer/components/layout/sidebar.tsx +++ b/src/renderer/components/layout/sidebar.tsx @@ -16,7 +16,6 @@ import { clusterRoute, clusterURL } from "../+cluster"; import { Config, configRoute, configURL } from "../+config"; import { eventRoute, eventsURL } from "../+events"; import { Apps, appsRoute, appsURL } from "../+apps"; -import { namespaceUrlParam } from "../+namespaces/namespace.store"; import { Workloads } from "../+workloads"; import { UserManagement } from "../+user-management"; import { Storage } from "../+storage"; @@ -48,7 +47,7 @@ export class Sidebar extends React.Component { if (crdStore.isLoading) { return (
- +
); } @@ -154,7 +153,6 @@ export class Sidebar extends React.Component { render() { const { toggle, compact, className } = this.props; - const query = namespaceUrlParam.toObjectParam(); return (
@@ -193,7 +191,7 @@ export class Sidebar extends React.Component { text="Workloads" isActive={isActiveRoute(workloadsRoute)} isHidden={Workloads.tabRoutes.length == 0} - url={workloadsURL({ query })} + url={workloadsURL()} icon={} > {this.renderTreeFromTabRoutes(Workloads.tabRoutes)} @@ -203,7 +201,7 @@ export class Sidebar extends React.Component { text="Configuration" isActive={isActiveRoute(configRoute)} isHidden={Config.tabRoutes.length == 0} - url={configURL({ query })} + url={configURL()} icon={} > {this.renderTreeFromTabRoutes(Config.tabRoutes)} @@ -213,7 +211,7 @@ export class Sidebar extends React.Component { text="Network" isActive={isActiveRoute(networkRoute)} isHidden={Network.tabRoutes.length == 0} - url={networkURL({ query })} + url={networkURL()} icon={} > {this.renderTreeFromTabRoutes(Network.tabRoutes)} @@ -223,7 +221,7 @@ export class Sidebar extends React.Component { text="Storage" isActive={isActiveRoute(storageRoute)} isHidden={Storage.tabRoutes.length == 0} - url={storageURL({ query })} + url={storageURL()} icon={} > {this.renderTreeFromTabRoutes(Storage.tabRoutes)} @@ -241,14 +239,14 @@ export class Sidebar extends React.Component { text="Events" isActive={isActiveRoute(eventRoute)} isHidden={!isAllowedResource("events")} - url={eventsURL({ query })} + url={eventsURL()} icon={} /> } > {this.renderTreeFromTabRoutes(Apps.tabRoutes)} @@ -257,7 +255,7 @@ export class Sidebar extends React.Component { id="users" text="Access Control" isActive={isActiveRoute(usersManagementRoute)} - url={usersManagementURL({ query })} + url={usersManagementURL()} icon={} > {this.renderTreeFromTabRoutes(UserManagement.tabRoutes)} diff --git a/src/renderer/navigation/page-param.ts b/src/renderer/navigation/page-param.ts index 7d99496004..6e62ec1bd5 100644 --- a/src/renderer/navigation/page-param.ts +++ b/src/renderer/navigation/page-param.ts @@ -4,7 +4,7 @@ import { action, makeObservable } from "mobx"; export interface PageParamInit { name: string; - defaultValue?: V; + defaultValue?: V; // multi-values param must be defined with array-value, e.g. [] prefix?: string; // name prefix, for extensions it's `${extension.id}:` parse?(value: string | string[]): V; // from URL stringify?(value: V): string | string[]; // to URL @@ -106,10 +106,4 @@ export class PageParam { return `${withPrefix ? "?" : ""}${searchParams}`; } - - toObjectParam(): Record { - return { - [this.name]: this.get(), - }; - } } diff --git a/src/renderer/utils/createStorage.ts b/src/renderer/utils/createStorage.ts index f04d9ae6f9..7466e1056d 100755 --- a/src/renderer/utils/createStorage.ts +++ b/src/renderer/utils/createStorage.ts @@ -8,12 +8,10 @@ import { StorageHelper } from "./storageHelper"; import { ClusterStore, getHostedClusterId } from "../../common/cluster-store"; import logger from "../../main/logger"; -const LOG_PREFIX = "[LocalStorageHelper]:"; - const storage = observable({ initialized: false, loaded: false, - data: {} as { [key: string]: any }, // json-compatible state + data: {} as Record, // json-serializable }); /** @@ -22,6 +20,7 @@ const storage = observable({ * @param defaultValue */ export function createStorage(key: string, defaultValue: T) { + const { logPrefix } = StorageHelper; const clusterId = getHostedClusterId(); const folder = path.resolve((app || remote.app).getPath("userData"), "lens-local-storage"); const fileName = `${clusterId ?? "app"}.json`; @@ -39,7 +38,7 @@ export function createStorage(key: string, defaultValue: T) { .then(data => storage.data = data) .catch(() => null) // ignore empty / non-existing / invalid json files .finally(() => { - logger.info(`${LOG_PREFIX} loaded local-storage file "${filePath}"`); + logger.info(`${logPrefix} loading finished for ${filePath}`); storage.loaded = true; }); @@ -55,20 +54,20 @@ export function createStorage(key: string, defaultValue: T) { } async function saveFile(state: Record = {}) { - logger.info(`${LOG_PREFIX} saving ${filePath}`); + logger.info(`${logPrefix} saving ${filePath}`); try { await fse.ensureDir(folder, { mode: 0o755 }); await fse.writeJson(filePath, state, { spaces: 2 }); } catch (error) { - logger.error(`${LOG_PREFIX} saving failed: ${error}`, { + logger.error(`${logPrefix} saving failed: ${error}`, { json: state, jsonFilePath: filePath }); } } function removeFile() { - logger.debug(`${LOG_PREFIX} removing ${filePath}`); + logger.debug(`${logPrefix} removing ${filePath}`); fse.unlink(filePath).catch(Function); } } diff --git a/src/renderer/utils/storageHelper.ts b/src/renderer/utils/storageHelper.ts index 5c73112052..6fd54b2e36 100755 --- a/src/renderer/utils/storageHelper.ts +++ b/src/renderer/utils/storageHelper.ts @@ -1,6 +1,6 @@ // Helper for working with storages (e.g. window.localStorage, NodeJS/file-system, etc.) -import { action, comparer, IObservableValue, makeObservable, observable, toJS, when, } from "mobx"; +import { action, IObservableValue, makeObservable, observable, toJS, when, } from "mobx"; import produce, { Draft } from "immer"; import { isEqual, isFunction, isPlainObject } from "lodash"; import logger from "../../main/logger"; @@ -20,33 +20,30 @@ export interface StorageHelperOptions { } export class StorageHelper { - static readonly defaultOptions: Partial> = { - autoInit: true, - }; + static logPrefix = "[StorageHelper]:"; + readonly storage: StorageAdapter; private data: IObservableValue; - @observable initialized = false; - whenReady = when(() => this.initialized); - public readonly storage: StorageAdapter; - public readonly defaultValue: T; + @observable initialized = false; + whenReady = when(() => this.initialized); // FIXME: invalid, use when() at the place of usage + + get defaultValue(): T { + // return as-is since options.defaultValue might be a getter too + return this.options.defaultValue; + } constructor(readonly key: string, private options: StorageHelperOptions) { + const { storage, defaultValue, autoInit = true } = options; makeObservable(this); - this.storage = options.storage; - this.defaultValue = options.defaultValue; - this.data = observable.box(this.defaultValue, { - autoBind: true, - deep: true, - equals: comparer.structural, - }); - + this.storage = storage; + this.data = observable.box(defaultValue); this.data.observe_(({ newValue, oldValue }) => { this.onChange(newValue as T, oldValue as T); }); - if (this.options.autoInit) { + if (autoInit) { this.init(); } } @@ -63,7 +60,7 @@ export class StorageHelper { }; private onError = (error: any): void => { - logger.error(`[load]: ${error}`, this); + logger.error(`${StorageHelper.logPrefix} loading error: ${error}`, this); }; @action @@ -101,21 +98,21 @@ export class StorageHelper { this.storage.onChange?.({ value, oldValue, key: this.key }); } catch (error) { - logger.error(`[change]: ${error}`, this, { value, oldValue }); + logger.error(`${StorageHelper.logPrefix} updating storage: ${error}`, this, { value, oldValue }); } } get(): T { - return this.data.get(); + const value = this.data.get(); + + // return real default-value (not a copy from observable when it's an object, e.g. []) + // this will allow to compare values by link with == operator + return this.isDefaultValue(value) ? this.defaultValue : value; } @action set(value: T) { - if (value == null) { - this.reset(); - } else { - this.data.set(value); - } + this.data.set(value); } @action