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

fix: namespace-store refactoring / saving selected-namespaces to external json-file

Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
Roman 2021-05-10 15:00:09 +03:00
parent f96b789280
commit 3357269c5d
11 changed files with 112 additions and 144 deletions

View File

@ -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(),
},
];

View File

@ -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(),
});
}

View File

@ -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<string[] | undefined>("selected_namespaces", undefined);
export const namespaceUrlParam = createPageParam<string[]>({
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<Namespace> {
api = namespacesApi;
@observable private contextNs = observable.set<string>();
private defaultNamespaces: string[] = [];
private storage = createStorage<string[]>("selected_namespaces", this.defaultNamespaces);
@computed get selectedNamespaces(): string[] {
return this.storage.get();
}
constructor() {
super();
@ -41,29 +24,19 @@ export class NamespaceStore extends KubeObjectStore<Namespace> {
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<Namespace> {
}
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<Namespace> {
}
@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<Namespace> {
@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<Namespace> {
@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}`
}
});
}

View File

@ -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(),
});
}

View File

@ -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(),
});
}

View File

@ -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(),
},
);

View File

@ -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(),
});
}

View File

@ -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<Props> {
if (crdStore.isLoading) {
return (
<div className="flex justify-center">
<Spinner />
<Spinner/>
</div>
);
}
@ -154,7 +153,6 @@ export class Sidebar extends React.Component<Props> {
render() {
const { toggle, compact, className } = this.props;
const query = namespaceUrlParam.toObjectParam();
return (
<div className={cssNames(Sidebar.displayName, "flex column", { compact }, className)}>
@ -193,7 +191,7 @@ export class Sidebar extends React.Component<Props> {
text="Workloads"
isActive={isActiveRoute(workloadsRoute)}
isHidden={Workloads.tabRoutes.length == 0}
url={workloadsURL({ query })}
url={workloadsURL()}
icon={<Icon svg="workloads"/>}
>
{this.renderTreeFromTabRoutes(Workloads.tabRoutes)}
@ -203,7 +201,7 @@ export class Sidebar extends React.Component<Props> {
text="Configuration"
isActive={isActiveRoute(configRoute)}
isHidden={Config.tabRoutes.length == 0}
url={configURL({ query })}
url={configURL()}
icon={<Icon material="list"/>}
>
{this.renderTreeFromTabRoutes(Config.tabRoutes)}
@ -213,7 +211,7 @@ export class Sidebar extends React.Component<Props> {
text="Network"
isActive={isActiveRoute(networkRoute)}
isHidden={Network.tabRoutes.length == 0}
url={networkURL({ query })}
url={networkURL()}
icon={<Icon material="device_hub"/>}
>
{this.renderTreeFromTabRoutes(Network.tabRoutes)}
@ -223,7 +221,7 @@ export class Sidebar extends React.Component<Props> {
text="Storage"
isActive={isActiveRoute(storageRoute)}
isHidden={Storage.tabRoutes.length == 0}
url={storageURL({ query })}
url={storageURL()}
icon={<Icon svg="storage"/>}
>
{this.renderTreeFromTabRoutes(Storage.tabRoutes)}
@ -241,14 +239,14 @@ export class Sidebar extends React.Component<Props> {
text="Events"
isActive={isActiveRoute(eventRoute)}
isHidden={!isAllowedResource("events")}
url={eventsURL({ query })}
url={eventsURL()}
icon={<Icon material="access_time"/>}
/>
<SidebarItem
id="apps"
text="Apps" // helm charts
isActive={isActiveRoute(appsRoute)}
url={appsURL({ query })}
url={appsURL()}
icon={<Icon material="apps"/>}
>
{this.renderTreeFromTabRoutes(Apps.tabRoutes)}
@ -257,7 +255,7 @@ export class Sidebar extends React.Component<Props> {
id="users"
text="Access Control"
isActive={isActiveRoute(usersManagementRoute)}
url={usersManagementURL({ query })}
url={usersManagementURL()}
icon={<Icon material="security"/>}
>
{this.renderTreeFromTabRoutes(UserManagement.tabRoutes)}

View File

@ -4,7 +4,7 @@ import { action, makeObservable } from "mobx";
export interface PageParamInit<V = any> {
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<V = any> {
return `${withPrefix ? "?" : ""}${searchParams}`;
}
toObjectParam(): Record<string, V> {
return {
[this.name]: this.get(),
};
}
}

View File

@ -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<string/*key*/, any>, // json-serializable
});
/**
@ -22,6 +20,7 @@ const storage = observable({
* @param defaultValue
*/
export function createStorage<T>(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<T>(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<T>(key: string, defaultValue: T) {
}
async function saveFile(state: Record<string, any> = {}) {
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);
}
}

View File

@ -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<T> {
}
export class StorageHelper<T> {
static readonly defaultOptions: Partial<StorageHelperOptions<any>> = {
autoInit: true,
};
static logPrefix = "[StorageHelper]:";
readonly storage: StorageAdapter<T>;
private data: IObservableValue<T>;
@observable initialized = false;
whenReady = when(() => this.initialized);
public readonly storage: StorageAdapter<T>;
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<T>) {
const { storage, defaultValue, autoInit = true } = options;
makeObservable(this);
this.storage = options.storage;
this.defaultValue = options.defaultValue;
this.data = observable.box<T>(this.defaultValue, {
autoBind: true,
deep: true,
equals: comparer.structural,
});
this.storage = storage;
this.data = observable.box<T>(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<T> {
};
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<T> {
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