mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Fix install-helm-chart-from-previously-opened-tab tests
- Split out storage initialization to a runnable Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
f52e79fdaa
commit
0a8df39824
@ -11,8 +11,6 @@ import asyncFn from "@async-fn/jest";
|
|||||||
import type { CallForResource } from "../../../renderer/components/dock/edit-resource/edit-resource-model/call-for-resource/call-for-resource.injectable";
|
import type { CallForResource } from "../../../renderer/components/dock/edit-resource/edit-resource-model/call-for-resource/call-for-resource.injectable";
|
||||||
import callForResourceInjectable from "../../../renderer/components/dock/edit-resource/edit-resource-model/call-for-resource/call-for-resource.injectable";
|
import callForResourceInjectable from "../../../renderer/components/dock/edit-resource/edit-resource-model/call-for-resource/call-for-resource.injectable";
|
||||||
import directoryForLensLocalStorageInjectable from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
|
import directoryForLensLocalStorageInjectable from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
|
||||||
import hostedClusterIdInjectable from "../../../renderer/cluster-frame-context/hosted-cluster-id.injectable";
|
|
||||||
import { controlWhenStoragesAreReady } from "../../../renderer/utils/create-storage/storages-are-ready";
|
|
||||||
import writeJsonFileInjectable from "../../../common/fs/write-json-file.injectable";
|
import writeJsonFileInjectable from "../../../common/fs/write-json-file.injectable";
|
||||||
import { TabKind } from "../../../renderer/components/dock/dock/store";
|
import { TabKind } from "../../../renderer/components/dock/dock/store";
|
||||||
import { Namespace } from "../../../common/k8s-api/endpoints";
|
import { Namespace } from "../../../common/k8s-api/endpoints";
|
||||||
@ -20,7 +18,6 @@ import { Namespace } from "../../../common/k8s-api/endpoints";
|
|||||||
describe("cluster/namespaces - edit namespaces from previously opened tab", () => {
|
describe("cluster/namespaces - edit namespaces from previously opened tab", () => {
|
||||||
let builder: ApplicationBuilder;
|
let builder: ApplicationBuilder;
|
||||||
let callForNamespaceMock: AsyncFnMock<CallForResource>;
|
let callForNamespaceMock: AsyncFnMock<CallForResource>;
|
||||||
let storagesAreReady: () => Promise<void>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
builder = getApplicationBuilder();
|
builder = getApplicationBuilder();
|
||||||
@ -35,10 +32,6 @@ describe("cluster/namespaces - edit namespaces from previously opened tab", () =
|
|||||||
() => "/some-directory-for-lens-local-storage",
|
() => "/some-directory-for-lens-local-storage",
|
||||||
);
|
);
|
||||||
|
|
||||||
windowDi.override(hostedClusterIdInjectable, () => "some-cluster-id");
|
|
||||||
|
|
||||||
storagesAreReady = controlWhenStoragesAreReady(windowDi);
|
|
||||||
|
|
||||||
windowDi.override(callForResourceInjectable, () => callForNamespaceMock);
|
windowDi.override(callForResourceInjectable, () => callForNamespaceMock);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -83,8 +76,6 @@ describe("cluster/namespaces - edit namespaces from previously opened tab", () =
|
|||||||
});
|
});
|
||||||
|
|
||||||
rendered = await builder.render();
|
rendered = await builder.render();
|
||||||
|
|
||||||
await storagesAreReady();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders", () => {
|
it("renders", () => {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ exports[`installing helm chart from previously opened tab given tab for installi
|
|||||||
<body>
|
<body>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="Animate slide-right Drawer KubeObjectDetails flex column right enter leave"
|
class="Animate slide-right Drawer KubeObjectDetails flex column right enter"
|
||||||
style="--size: 725px; --enter-duration: 100ms; --leave-duration: 100ms;"
|
style="--size: 725px; --enter-duration: 100ms; --leave-duration: 100ms;"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@ -1109,9 +1109,222 @@ exports[`installing helm chart from previously opened tab given tab for installi
|
|||||||
style="flex-basis: 300px;"
|
style="flex-basis: 300px;"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="Spinner singleColor center"
|
class="InstallChart flex column"
|
||||||
data-testid="install-chart-tab-spinner"
|
>
|
||||||
/>
|
<div
|
||||||
|
class="InfoPanel flex gaps align-center"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="controls"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="install-controls flex gaps align-center"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Chart
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
class="badge"
|
||||||
|
title="Repo/Name"
|
||||||
|
>
|
||||||
|
some-repository/some-name
|
||||||
|
</div>
|
||||||
|
<span>
|
||||||
|
Version
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
class="Select theme-outlined chart-version css-b62m3t-container"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="css-1f43avz-a11yText-A11yText"
|
||||||
|
id="react-select-install-chart-version-select-for-some-first-tab-id-live-region"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
aria-atomic="false"
|
||||||
|
aria-live="polite"
|
||||||
|
aria-relevant="additions text"
|
||||||
|
class="css-1f43avz-a11yText-A11yText"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="Select__control css-13cymwt-control"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="Select__value-container Select__value-container--has-value css-1fdsijx-ValueContainer"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="Select__single-value css-1dimb5e-singleValue"
|
||||||
|
>
|
||||||
|
some-other-version
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="Select__input-container css-qbdosj-Input"
|
||||||
|
data-value=""
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
aria-autocomplete="list"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-haspopup="true"
|
||||||
|
autocapitalize="none"
|
||||||
|
autocomplete="off"
|
||||||
|
autocorrect="off"
|
||||||
|
class="Select__input"
|
||||||
|
id="install-chart-version-select-for-some-first-tab-id"
|
||||||
|
role="combobox"
|
||||||
|
spellcheck="false"
|
||||||
|
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
|
||||||
|
tabindex="0"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="Select__indicators css-1hb7zxy-IndicatorsContainer"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="Select__indicator-separator css-1u9des2-indicatorSeparator"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
class="Select__indicator Select__dropdown-indicator css-1xc3v61-indicatorContainer"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="css-tj5bde-Svg"
|
||||||
|
focusable="false"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
width="20"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span>
|
||||||
|
Namespace
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
class="Select theme-outlined NamespaceSelect css-b62m3t-container"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="css-1f43avz-a11yText-A11yText"
|
||||||
|
id="react-select-install-chart-namespace-select-for-some-first-tab-id-live-region"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
aria-atomic="false"
|
||||||
|
aria-live="polite"
|
||||||
|
aria-relevant="additions text"
|
||||||
|
class="css-1f43avz-a11yText-A11yText"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="Select__control css-13cymwt-control"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="Select__value-container Select__value-container--has-value css-1fdsijx-ValueContainer"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="Select__single-value css-1dimb5e-singleValue"
|
||||||
|
>
|
||||||
|
some-other-namespace
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="Select__input-container css-qbdosj-Input"
|
||||||
|
data-value=""
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
aria-autocomplete="list"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-haspopup="true"
|
||||||
|
autocapitalize="none"
|
||||||
|
autocomplete="off"
|
||||||
|
autocorrect="off"
|
||||||
|
class="Select__input"
|
||||||
|
id="install-chart-namespace-select-for-some-first-tab-id"
|
||||||
|
role="combobox"
|
||||||
|
spellcheck="false"
|
||||||
|
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
|
||||||
|
tabindex="0"
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="Select__indicators css-1hb7zxy-IndicatorsContainer"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="Select__indicator-separator css-1u9des2-indicatorSeparator"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
class="Select__indicator Select__dropdown-indicator css-1xc3v61-indicatorContainer"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="css-tj5bde-Svg"
|
||||||
|
focusable="false"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
width="20"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M4.516 7.548c0.436-0.446 1.043-0.481 1.576 0l3.908 3.747 3.908-3.747c0.533-0.481 1.141-0.446 1.574 0 0.436 0.445 0.408 1.197 0 1.615-0.406 0.418-4.695 4.502-4.695 4.502-0.217 0.223-0.502 0.335-0.787 0.335s-0.57-0.112-0.789-0.335c0 0-4.287-4.084-4.695-4.502s-0.436-1.17 0-1.615z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="Input"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="input-area flex gaps align-center"
|
||||||
|
id=""
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="input box grow"
|
||||||
|
data-testid="install-chart-custom-name-input-for-some-first-tab-id"
|
||||||
|
placeholder="Name (optional)"
|
||||||
|
spellcheck="false"
|
||||||
|
title="Release name"
|
||||||
|
value="some-stored-custom-name"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
class="input-info flex gaps"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex gaps align-center"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="Button plain"
|
||||||
|
data-testid="cancel-install-chart-from-tab-for-some-first-tab-id"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="Button primary active"
|
||||||
|
data-testid="install-chart-from-tab-for-some-first-tab-id"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Install
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
data-testid="monaco-editor-for-some-first-tab-id"
|
||||||
|
>
|
||||||
|
some-stored-configuration
|
||||||
|
</textarea>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -9,13 +9,9 @@ import type { ApplicationBuilder } from "../../../renderer/components/test-utils
|
|||||||
import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
|
import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
|
||||||
import { HelmChart } from "../../../common/k8s-api/endpoints/helm-charts.api";
|
import { HelmChart } from "../../../common/k8s-api/endpoints/helm-charts.api";
|
||||||
import getRandomInstallChartTabIdInjectable from "../../../renderer/components/dock/install-chart/get-random-install-chart-tab-id.injectable";
|
import getRandomInstallChartTabIdInjectable from "../../../renderer/components/dock/install-chart/get-random-install-chart-tab-id.injectable";
|
||||||
import namespaceStoreInjectable from "../../../renderer/components/+namespaces/store.injectable";
|
|
||||||
import type { NamespaceStore } from "../../../renderer/components/+namespaces/store";
|
|
||||||
import writeJsonFileInjectable from "../../../common/fs/write-json-file.injectable";
|
import writeJsonFileInjectable from "../../../common/fs/write-json-file.injectable";
|
||||||
import directoryForLensLocalStorageInjectable from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
|
import directoryForLensLocalStorageInjectable from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
|
||||||
import hostedClusterIdInjectable from "../../../renderer/cluster-frame-context/hosted-cluster-id.injectable";
|
|
||||||
import { TabKind } from "../../../renderer/components/dock/dock/store";
|
import { TabKind } from "../../../renderer/components/dock/dock/store";
|
||||||
import { controlWhenStoragesAreReady } from "../../../renderer/utils/create-storage/storages-are-ready";
|
|
||||||
import requestCreateHelmReleaseInjectable from "../../../common/k8s-api/endpoints/helm-releases.api/request-create.injectable";
|
import requestCreateHelmReleaseInjectable from "../../../common/k8s-api/endpoints/helm-releases.api/request-create.injectable";
|
||||||
import type { RequestHelmChartVersions } from "../../../common/k8s-api/endpoints/helm-charts.api/request-versions.injectable";
|
import type { RequestHelmChartVersions } from "../../../common/k8s-api/endpoints/helm-charts.api/request-versions.injectable";
|
||||||
import requestHelmChartVersionsInjectable from "../../../common/k8s-api/endpoints/helm-charts.api/request-versions.injectable";
|
import requestHelmChartVersionsInjectable from "../../../common/k8s-api/endpoints/helm-charts.api/request-versions.injectable";
|
||||||
@ -26,7 +22,6 @@ describe("installing helm chart from previously opened tab", () => {
|
|||||||
let builder: ApplicationBuilder;
|
let builder: ApplicationBuilder;
|
||||||
let requestHelmChartVersionsMock: AsyncFnMock<RequestHelmChartVersions>;
|
let requestHelmChartVersionsMock: AsyncFnMock<RequestHelmChartVersions>;
|
||||||
let requestHelmChartValuesMock: AsyncFnMock<RequestHelmChartValues>;
|
let requestHelmChartValuesMock: AsyncFnMock<RequestHelmChartValues>;
|
||||||
let storagesAreReady: () => Promise<void>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
builder = getApplicationBuilder();
|
builder = getApplicationBuilder();
|
||||||
@ -36,29 +31,15 @@ describe("installing helm chart from previously opened tab", () => {
|
|||||||
requestHelmChartVersionsMock = asyncFn();
|
requestHelmChartVersionsMock = asyncFn();
|
||||||
requestHelmChartValuesMock = asyncFn();
|
requestHelmChartValuesMock = asyncFn();
|
||||||
|
|
||||||
builder.beforeWindowStart((windowDi) => {
|
builder.namespaces.add("default");
|
||||||
storagesAreReady = controlWhenStoragesAreReady(windowDi);
|
builder.namespaces.add("some-other-namespace");
|
||||||
|
|
||||||
|
builder.beforeWindowStart((windowDi) => {
|
||||||
windowDi.override(directoryForLensLocalStorageInjectable, () => "/some-directory-for-lens-local-storage");
|
windowDi.override(directoryForLensLocalStorageInjectable, () => "/some-directory-for-lens-local-storage");
|
||||||
windowDi.override(hostedClusterIdInjectable, () => "some-cluster-id");
|
|
||||||
windowDi.override(requestHelmChartVersionsInjectable, () => requestHelmChartVersionsMock);
|
windowDi.override(requestHelmChartVersionsInjectable, () => requestHelmChartVersionsMock);
|
||||||
windowDi.override(requestHelmChartValuesInjectable, () => requestHelmChartValuesMock);
|
windowDi.override(requestHelmChartValuesInjectable, () => requestHelmChartValuesMock);
|
||||||
windowDi.override(requestCreateHelmReleaseInjectable, () => jest.fn());
|
windowDi.override(requestCreateHelmReleaseInjectable, () => jest.fn());
|
||||||
|
|
||||||
// TODO: Replace store mocking with mock for the actual side-effect (where the namespaces are coming from)
|
|
||||||
windowDi.override(
|
|
||||||
namespaceStoreInjectable,
|
|
||||||
() =>
|
|
||||||
({
|
|
||||||
contextNamespaces: [],
|
|
||||||
items: [
|
|
||||||
{ getName: () => "default" },
|
|
||||||
{ getName: () => "some-other-namespace" },
|
|
||||||
],
|
|
||||||
selectNamespaces: () => {},
|
|
||||||
} as unknown as NamespaceStore),
|
|
||||||
);
|
|
||||||
|
|
||||||
windowDi.override(getRandomInstallChartTabIdInjectable, () =>
|
windowDi.override(getRandomInstallChartTabIdInjectable, () =>
|
||||||
jest
|
jest
|
||||||
.fn(() => "some-irrelevant-tab-id")
|
.fn(() => "some-irrelevant-tab-id")
|
||||||
@ -106,8 +87,6 @@ describe("installing helm chart from previously opened tab", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
rendered = await builder.render();
|
rendered = await builder.render();
|
||||||
|
|
||||||
await storagesAreReady();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders", () => {
|
it("renders", () => {
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
import { action, observable, reaction } from "mobx";
|
import { action, observable, reaction } from "mobx";
|
||||||
import type { StorageLayer } from "../../../utils";
|
import type { StorageLayer } from "../../../utils";
|
||||||
import { autoBind, toJS } from "../../../utils";
|
import { autoBind, toJS } from "../../../utils";
|
||||||
import type { CreateStorage } from "../../../utils/create-storage/create-storage";
|
import type { CreateStorage } from "../../../utils/create-storage/create-storage.injectable";
|
||||||
import type { TabId } from "../dock/store";
|
import type { TabId } from "../dock/store";
|
||||||
|
|
||||||
export interface DockTabStoreOptions {
|
export interface DockTabStoreOptions {
|
||||||
@ -33,10 +33,8 @@ export class DockTabStore<T> {
|
|||||||
if (autoInit && storageKey) {
|
if (autoInit && storageKey) {
|
||||||
const storage = this.storage = this.dependencies.createStorage(storageKey, {});
|
const storage = this.storage = this.dependencies.createStorage(storageKey, {});
|
||||||
|
|
||||||
storage.whenReady.then(() => {
|
this.data.replace(storage.get());
|
||||||
this.data.replace(storage.value);
|
reaction(() => this.toJSON(), data => storage.set(data));
|
||||||
reaction(() => this.toJSON(), data => storage.set(data));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -108,19 +108,25 @@ export class DockStore implements DockStorageState {
|
|||||||
constructor(private readonly dependencies: Dependencies) {
|
constructor(private readonly dependencies: Dependencies) {
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
autoBind(this);
|
autoBind(this);
|
||||||
this.init();
|
|
||||||
|
// adjust terminal height if window size changes
|
||||||
|
window.addEventListener("resize", throttle(this.adjustHeight, 250));
|
||||||
|
|
||||||
|
for (const tab of this.tabs) {
|
||||||
|
const tabDataIsValid = this.dependencies.tabDataValidator[tab.kind] ?? (() => true);
|
||||||
|
|
||||||
|
if (!tabDataIsValid(tab.id)) {
|
||||||
|
this.closeTab(tab.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly minHeight = 100;
|
readonly minHeight = 100;
|
||||||
@observable fullSize = false;
|
@observable fullSize = false;
|
||||||
|
|
||||||
get whenReady() {
|
|
||||||
return this.dependencies.storage.whenReady;
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed
|
@computed
|
||||||
get isOpen(): boolean {
|
get isOpen(): boolean {
|
||||||
return this.dependencies.storage.value.isOpen;
|
return this.dependencies.storage.get().isOpen;
|
||||||
}
|
}
|
||||||
|
|
||||||
set isOpen(isOpen: boolean) {
|
set isOpen(isOpen: boolean) {
|
||||||
@ -129,7 +135,7 @@ export class DockStore implements DockStorageState {
|
|||||||
|
|
||||||
@computed
|
@computed
|
||||||
get height(): number {
|
get height(): number {
|
||||||
return this.dependencies.storage.value.height;
|
return this.dependencies.storage.get().height;
|
||||||
}
|
}
|
||||||
|
|
||||||
set height(height: number) {
|
set height(height: number) {
|
||||||
@ -140,7 +146,7 @@ export class DockStore implements DockStorageState {
|
|||||||
|
|
||||||
@computed
|
@computed
|
||||||
get tabs(): DockTab[] {
|
get tabs(): DockTab[] {
|
||||||
return this.dependencies.storage.value.tabs;
|
return this.dependencies.storage.get().tabs;
|
||||||
}
|
}
|
||||||
|
|
||||||
set tabs(tabs: DockTab[]) {
|
set tabs(tabs: DockTab[]) {
|
||||||
@ -149,7 +155,7 @@ export class DockStore implements DockStorageState {
|
|||||||
|
|
||||||
@computed
|
@computed
|
||||||
get selectedTabId(): TabId | undefined {
|
get selectedTabId(): TabId | undefined {
|
||||||
const storageData = this.dependencies.storage.value;
|
const storageData = this.dependencies.storage.get();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
storageData.selectedTabId ||
|
storageData.selectedTabId ||
|
||||||
@ -171,21 +177,6 @@ export class DockStore implements DockStorageState {
|
|||||||
return this.tabs.find(tab => tab.id === this.selectedTabId);
|
return this.tabs.find(tab => tab.id === this.selectedTabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private init() {
|
|
||||||
// adjust terminal height if window size changes
|
|
||||||
window.addEventListener("resize", throttle(this.adjustHeight, 250));
|
|
||||||
|
|
||||||
this.whenReady.then(action(() => {
|
|
||||||
for (const tab of this.tabs) {
|
|
||||||
const validator = this.dependencies.tabDataValidator[tab.kind];
|
|
||||||
|
|
||||||
if (validator && !validator(tab.id)) {
|
|
||||||
this.closeTab(tab.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
get maxHeight() {
|
get maxHeight() {
|
||||||
const mainLayoutHeader = 40;
|
const mainLayoutHeader = 40;
|
||||||
const mainLayoutTabs = 33;
|
const mainLayoutTabs = 33;
|
||||||
|
|||||||
@ -4,9 +4,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { observable, reaction } from "mobx";
|
import { observable, reaction } from "mobx";
|
||||||
import { StorageHelper } from "../storageHelper";
|
import type { StorageHelper } from "../storage-helper";
|
||||||
import { delay } from "../../../common/utils/delay";
|
import { toJS } from "../../../common/utils";
|
||||||
import { noop, toJS } from "../../../common/utils";
|
import type { CreateStorageHelper } from "../create-storage-helper.injectable";
|
||||||
|
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
|
||||||
|
import createStorageHelperInjectable from "../create-storage-helper.injectable";
|
||||||
|
|
||||||
interface StorageModel {
|
interface StorageModel {
|
||||||
[prop: string]: any /*json-serializable*/;
|
[prop: string]: any /*json-serializable*/;
|
||||||
@ -15,27 +17,25 @@ interface StorageModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("renderer/utils/StorageHelper", () => {
|
describe("renderer/utils/StorageHelper", () => {
|
||||||
|
let createStorageHelper: CreateStorageHelper;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||||
|
|
||||||
|
createStorageHelper = di.inject(createStorageHelperInjectable);
|
||||||
|
});
|
||||||
|
|
||||||
describe("Using custom StorageAdapter", () => {
|
describe("Using custom StorageAdapter", () => {
|
||||||
const storageKey = "ui-settings";
|
const storageKey = "ui-settings";
|
||||||
const remoteStorageMock = observable.map<string, StorageModel>();
|
const remoteStorageMock = observable.map<string, StorageModel>();
|
||||||
let storageHelper: StorageHelper<StorageModel>;
|
let storageHelper: StorageHelper<StorageModel>;
|
||||||
let storageHelperAsync: StorageHelper<StorageModel>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
remoteStorageMock.set(storageKey, {
|
remoteStorageMock.set(storageKey, {
|
||||||
message: "saved-before", // pretending as previously saved data
|
message: "saved-before", // pretending as previously saved data
|
||||||
});
|
});
|
||||||
|
|
||||||
storageHelper = new StorageHelper<StorageModel>({
|
storageHelper = createStorageHelper<StorageModel>(storageKey, {
|
||||||
logger: {
|
|
||||||
debug: noop,
|
|
||||||
error: noop,
|
|
||||||
info: noop,
|
|
||||||
silly: noop,
|
|
||||||
warn: noop,
|
|
||||||
},
|
|
||||||
}, storageKey, {
|
|
||||||
autoInit: false,
|
|
||||||
defaultValue: {
|
defaultValue: {
|
||||||
message: "blabla",
|
message: "blabla",
|
||||||
description: "default",
|
description: "default",
|
||||||
@ -55,46 +55,14 @@ describe("renderer/utils/StorageHelper", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
storageHelperAsync = new StorageHelper({
|
|
||||||
logger: {
|
|
||||||
debug: noop,
|
|
||||||
error: noop,
|
|
||||||
info: noop,
|
|
||||||
silly: noop,
|
|
||||||
warn: noop,
|
|
||||||
},
|
|
||||||
}, storageKey, {
|
|
||||||
autoInit: false,
|
|
||||||
defaultValue: storageHelper.defaultValue,
|
|
||||||
storage: {
|
|
||||||
...storageHelper.storage,
|
|
||||||
async getItem(key: string): Promise<StorageModel> {
|
|
||||||
await delay(500); // fake loading timeout
|
|
||||||
|
|
||||||
return storageHelper.storage.getItem(key);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("initialized with default value", async () => {
|
it("initialized with default value", async () => {
|
||||||
storageHelper.init();
|
|
||||||
expect(storageHelper.key).toBe(storageKey);
|
expect(storageHelper.key).toBe(storageKey);
|
||||||
expect(storageHelper.get()).toEqual(storageHelper.defaultValue);
|
expect(storageHelper.get()).toEqual(storageHelper.defaultValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("async loading from storage supported too", async () => {
|
|
||||||
expect(storageHelperAsync.initialized).toBeFalsy();
|
|
||||||
storageHelperAsync.init();
|
|
||||||
await delay(300);
|
|
||||||
expect(storageHelperAsync.get()).toEqual(storageHelper.defaultValue);
|
|
||||||
await delay(200);
|
|
||||||
expect(storageHelperAsync.get().message).toBe("saved-before");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("set() fully replaces data in storage", () => {
|
it("set() fully replaces data in storage", () => {
|
||||||
storageHelper.init();
|
|
||||||
storageHelper.set({ message: "msg" });
|
storageHelper.set({ message: "msg" });
|
||||||
storageHelper.get().description = "desc";
|
storageHelper.get().description = "desc";
|
||||||
expect(storageHelper.get().message).toBe("msg");
|
expect(storageHelper.get().message).toBe("msg");
|
||||||
@ -106,7 +74,6 @@ describe("renderer/utils/StorageHelper", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("merge() does partial data tree updates", () => {
|
it("merge() does partial data tree updates", () => {
|
||||||
storageHelper.init();
|
|
||||||
storageHelper.merge({ message: "updated" });
|
storageHelper.merge({ message: "updated" });
|
||||||
|
|
||||||
expect(storageHelper.get()).toEqual({ ...storageHelper.defaultValue, message: "updated" });
|
expect(storageHelper.get()).toEqual({ ...storageHelper.defaultValue, message: "updated" });
|
||||||
@ -134,16 +101,7 @@ describe("renderer/utils/StorageHelper", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
observedChanges.length = 0;
|
observedChanges.length = 0;
|
||||||
|
|
||||||
storageHelper = new StorageHelper<typeof defaultValue>({
|
storageHelper = createStorageHelper<typeof defaultValue>("some-key", {
|
||||||
logger: {
|
|
||||||
debug: noop,
|
|
||||||
error: noop,
|
|
||||||
info: noop,
|
|
||||||
silly: noop,
|
|
||||||
warn: noop,
|
|
||||||
},
|
|
||||||
}, "some-key", {
|
|
||||||
autoInit: true,
|
|
||||||
defaultValue,
|
defaultValue,
|
||||||
storage: {
|
storage: {
|
||||||
getItem: jest.fn(),
|
getItem: jest.fn(),
|
||||||
23
src/renderer/utils/create-storage-helper.injectable.ts
Normal file
23
src/renderer/utils/create-storage-helper.injectable.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import loggerInjectable from "../../common/logger.injectable";
|
||||||
|
import type { StorageHelperDependencies, StorageHelperOptions } from "./storage-helper";
|
||||||
|
import { StorageHelper } from "./storage-helper";
|
||||||
|
|
||||||
|
export type CreateStorageHelper = <T>(key: string, options: StorageHelperOptions<T>) => StorageHelper<T>;
|
||||||
|
|
||||||
|
const createStorageHelperInjectable = getInjectable({
|
||||||
|
id: "create-storage-helper",
|
||||||
|
instantiate: (di): CreateStorageHelper => {
|
||||||
|
const deps: StorageHelperDependencies = {
|
||||||
|
logger: di.inject(loggerInjectable),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (key, options) => new StorageHelper(deps, key, options);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default createStorageHelperInjectable;
|
||||||
@ -3,33 +3,29 @@
|
|||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
*/
|
*/
|
||||||
import { getInjectable } from "@ogre-tools/injectable";
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
import directoryForLensLocalStorageInjectable from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
|
import { action } from "mobx";
|
||||||
import { createStorage } from "./create-storage";
|
import lensLocalStorageStateInjectable from "./state.injectable";
|
||||||
import readJsonFileInjectable from "../../../common/fs/read-json-file.injectable";
|
import createStorageHelperInjectable from "../create-storage-helper.injectable";
|
||||||
import writeJsonFileInjectable from "../../../common/fs/write-json-file.injectable";
|
import type { StorageLayer } from "../storage-helper";
|
||||||
import { observable } from "mobx";
|
|
||||||
import loggerInjectable from "../../../common/logger.injectable";
|
export type CreateStorage = <T>(key: string, defaultValue: T) => StorageLayer<T>;
|
||||||
import hostedClusterIdInjectable from "../../cluster-frame-context/hosted-cluster-id.injectable";
|
|
||||||
import storageSaveDelayInjectable from "./storage-save-delay.injectable";
|
|
||||||
import joinPathsInjectable from "../../../common/path/join-paths.injectable";
|
|
||||||
|
|
||||||
const createStorageInjectable = getInjectable({
|
const createStorageInjectable = getInjectable({
|
||||||
id: "create-storage",
|
id: "create-storage",
|
||||||
|
|
||||||
instantiate: (di) => createStorage({
|
instantiate: (di): CreateStorage => {
|
||||||
storage: observable({
|
const lensLocalStorageState = di.inject(lensLocalStorageStateInjectable);
|
||||||
initialized: false,
|
const createStorageHelper = di.inject(createStorageHelperInjectable);
|
||||||
loaded: false,
|
|
||||||
data: {},
|
return <T>(key: string, defaultValue: T) => createStorageHelper<T>(key, {
|
||||||
}),
|
defaultValue,
|
||||||
readJsonFile: di.inject(readJsonFileInjectable),
|
storage: {
|
||||||
writeJsonFile: di.inject(writeJsonFileInjectable),
|
getItem: (key) => lensLocalStorageState[key] as T,
|
||||||
logger: di.inject(loggerInjectable),
|
setItem: action((key, value) => lensLocalStorageState[key] = value),
|
||||||
directoryForLensLocalStorage: di.inject(directoryForLensLocalStorageInjectable),
|
removeItem: action((key) => delete lensLocalStorageState[key]),
|
||||||
joinPaths: di.inject(joinPathsInjectable),
|
},
|
||||||
hostedClusterId: di.inject(hostedClusterIdInjectable),
|
});
|
||||||
saveDelay: di.inject(storageSaveDelayInjectable),
|
},
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default createStorageInjectable;
|
export default createStorageInjectable;
|
||||||
|
|||||||
@ -1,98 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Keeps window.localStorage state in external JSON-files.
|
|
||||||
// Because app creates random port between restarts => storage session wiped out each time.
|
|
||||||
import { comparer, reaction, toJS, when } from "mobx";
|
|
||||||
import type { StorageLayer } from "../storageHelper";
|
|
||||||
import { storageHelperLogPrefix, StorageHelper } from "../storageHelper";
|
|
||||||
import type { JsonObject } from "type-fest";
|
|
||||||
import type { Logger } from "../../../common/logger";
|
|
||||||
import type { JoinPaths } from "../../../common/path/join-paths.injectable";
|
|
||||||
import type { WriteJson } from "../../../common/fs/write-json-file.injectable";
|
|
||||||
import type { ReadJson } from "../../../common/fs/read-json-file.injectable";
|
|
||||||
|
|
||||||
interface Dependencies {
|
|
||||||
storage: { initialized: boolean; loaded: boolean; data: Partial<Record<string, unknown>> };
|
|
||||||
logger: Logger;
|
|
||||||
directoryForLensLocalStorage: string;
|
|
||||||
readJsonFile: ReadJson;
|
|
||||||
writeJsonFile: WriteJson;
|
|
||||||
joinPaths: JoinPaths;
|
|
||||||
hostedClusterId: string | undefined;
|
|
||||||
saveDelay: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CreateStorage = <T>(key: string, defaultValue: T) => StorageLayer<T>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a helper for saving data under the "key" intended for window.localStorage
|
|
||||||
*/
|
|
||||||
export const createStorage = ({
|
|
||||||
storage,
|
|
||||||
joinPaths,
|
|
||||||
logger,
|
|
||||||
directoryForLensLocalStorage,
|
|
||||||
readJsonFile,
|
|
||||||
writeJsonFile,
|
|
||||||
hostedClusterId,
|
|
||||||
saveDelay,
|
|
||||||
}: Dependencies): CreateStorage => <T>(key: string, defaultValue: T) => {
|
|
||||||
if (!storage.initialized) {
|
|
||||||
storage.initialized = true;
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
const filePath = joinPaths(directoryForLensLocalStorage, `${hostedClusterId || "app"}.json`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
storage.data = (await readJsonFile(filePath)) as JsonObject;
|
|
||||||
} catch {
|
|
||||||
// do nothing
|
|
||||||
} finally {
|
|
||||||
logger.info(`${storageHelperLogPrefix} loading finished for ${filePath}`);
|
|
||||||
storage.loaded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// bind auto-saving data changes to %storage-file.json
|
|
||||||
reaction(() => toJS(storage.data), saveFile, {
|
|
||||||
delay: saveDelay, // lazy, avoid excessive writes to fs
|
|
||||||
equals: comparer.structural, // save only when something really changed
|
|
||||||
});
|
|
||||||
|
|
||||||
async function saveFile(state: Record<string, any> = {}) {
|
|
||||||
logger.info(`${storageHelperLogPrefix} saving ${filePath}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await writeJsonFile(filePath, state);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`${storageHelperLogPrefix} saving failed: ${error}`, {
|
|
||||||
json: state, jsonFilePath: filePath,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
.catch(error => logger.error(`${storageHelperLogPrefix} Failed to initialize storage: ${error}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
return new StorageHelper({
|
|
||||||
logger,
|
|
||||||
}, key, {
|
|
||||||
autoInit: true,
|
|
||||||
defaultValue,
|
|
||||||
storage: {
|
|
||||||
async getItem(key: string) {
|
|
||||||
await when(() => storage.loaded);
|
|
||||||
|
|
||||||
return storage.data[key] as T;
|
|
||||||
},
|
|
||||||
setItem(key: string, value: T) {
|
|
||||||
storage.data[key] = value;
|
|
||||||
},
|
|
||||||
removeItem(key: string) {
|
|
||||||
delete storage.data[key];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import AwaitLock from "await-lock";
|
||||||
|
import { comparer, reaction, runInAction, toJS } from "mobx";
|
||||||
|
import directoryForLensLocalStorageInjectable from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
|
||||||
|
import readJsonFileInjectable from "../../../common/fs/read-json-file.injectable";
|
||||||
|
import writeJsonFileInjectable from "../../../common/fs/write-json-file.injectable";
|
||||||
|
import loggerInjectable from "../../../common/logger.injectable";
|
||||||
|
import joinPathsInjectable from "../../../common/path/join-paths.injectable";
|
||||||
|
import setupAppPathsInjectable from "../../app-paths/setup-app-paths.injectable";
|
||||||
|
import { beforeFrameStartsFirstInjectionToken } from "../../before-frame-starts/tokens";
|
||||||
|
import hostedClusterIdInjectable from "../../cluster-frame-context/hosted-cluster-id.injectable";
|
||||||
|
import { storageHelperLogPrefix } from "../storage-helper";
|
||||||
|
import lensLocalStorageStateInjectable from "./state.injectable";
|
||||||
|
import storageSaveDelayInjectable from "./storage-save-delay.injectable";
|
||||||
|
|
||||||
|
const initializeStateInjectable = getInjectable({
|
||||||
|
id: "initialize-lens-local-storage-state",
|
||||||
|
instantiate: (di) => ({
|
||||||
|
id: "initialize-lens-local-storage-state",
|
||||||
|
run: async () => {
|
||||||
|
const joinPaths = di.inject(joinPathsInjectable);
|
||||||
|
const directoryForLensLocalStorage = di.inject(directoryForLensLocalStorageInjectable);
|
||||||
|
const hostedClusterId = di.inject(hostedClusterIdInjectable);
|
||||||
|
const lensLocalStorageState = di.inject(lensLocalStorageStateInjectable);
|
||||||
|
const readJsonFile = di.inject(readJsonFileInjectable);
|
||||||
|
const writeJsonFile = di.inject(writeJsonFileInjectable);
|
||||||
|
const logger = di.inject(loggerInjectable);
|
||||||
|
const storageSaveDelay = di.inject(storageSaveDelayInjectable);
|
||||||
|
const lock = new AwaitLock();
|
||||||
|
|
||||||
|
const filePath = joinPaths(directoryForLensLocalStorage, `${hostedClusterId || "app"}.json`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const localFile = await readJsonFile(filePath);
|
||||||
|
|
||||||
|
if (typeof localFile === "object") {
|
||||||
|
runInAction(() => {
|
||||||
|
Object.assign(lensLocalStorageState, localFile);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// do nothing
|
||||||
|
} finally {
|
||||||
|
logger.info(`${storageHelperLogPrefix} loading finished for ${filePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
reaction(() => toJS(lensLocalStorageState), saveFile, {
|
||||||
|
delay: storageSaveDelay, // lazy, avoid excessive writes to fs
|
||||||
|
equals: comparer.structural, // save only when something really changed
|
||||||
|
});
|
||||||
|
|
||||||
|
async function saveFile(state: Record<string, unknown>) {
|
||||||
|
try {
|
||||||
|
await lock.acquireAsync();
|
||||||
|
logger.info(`${storageHelperLogPrefix} saving ${filePath}`);
|
||||||
|
await writeJsonFile(filePath, state);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`${storageHelperLogPrefix} saving failed: ${error}`, {
|
||||||
|
json: state, jsonFilePath: filePath,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
lock.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
runAfter: di.inject(setupAppPathsInjectable),
|
||||||
|
}),
|
||||||
|
injectionToken: beforeFrameStartsFirstInjectionToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default initializeStateInjectable;
|
||||||
14
src/renderer/utils/create-storage/state.injectable.ts
Normal file
14
src/renderer/utils/create-storage/state.injectable.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||||
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getInjectable } from "@ogre-tools/injectable";
|
||||||
|
import { observable } from "mobx";
|
||||||
|
|
||||||
|
const lensLocalStorageStateInjectable = getInjectable({
|
||||||
|
id: "lens-local-storage-state",
|
||||||
|
instantiate: () => observable.object({} as Record<string, unknown>),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default lensLocalStorageStateInjectable;
|
||||||
@ -1,31 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
||||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
||||||
*/
|
|
||||||
import type { DiContainer } from "@ogre-tools/injectable";
|
|
||||||
import { runInAction } from "mobx";
|
|
||||||
import type { CreateStorage } from "./create-storage";
|
|
||||||
import createStorageInjectable from "./create-storage.injectable";
|
|
||||||
|
|
||||||
export const controlWhenStoragesAreReady = (di: DiContainer) => {
|
|
||||||
const storagesAreReady: Promise<void>[] = [];
|
|
||||||
|
|
||||||
const decorated =
|
|
||||||
(toBeDecorated: CreateStorage) =>
|
|
||||||
(key: string, defaultValue: any) => {
|
|
||||||
const storage = toBeDecorated(key, defaultValue);
|
|
||||||
|
|
||||||
storagesAreReady.push(storage.whenReady);
|
|
||||||
|
|
||||||
return storage;
|
|
||||||
};
|
|
||||||
|
|
||||||
runInAction(() => {
|
|
||||||
// TODO: Remove when typing is added to the library
|
|
||||||
(di as any).decorateFunction(createStorageInjectable, decorated);
|
|
||||||
});
|
|
||||||
|
|
||||||
return async () => {
|
|
||||||
await Promise.all(storagesAreReady);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -17,4 +17,4 @@ export * from "./metricUnitsToNumber";
|
|||||||
export * from "./name-parts";
|
export * from "./name-parts";
|
||||||
export * from "./prevDefault";
|
export * from "./prevDefault";
|
||||||
export * from "./saveFile";
|
export * from "./saveFile";
|
||||||
export * from "./storageHelper";
|
export * from "./storage-helper";
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// 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, computed, makeObservable, observable, observe, toJS, when } from "mobx";
|
import { action, comparer, computed, makeObservable, observable, observe, toJS } from "mobx";
|
||||||
import type { Draft } from "immer";
|
import type { Draft } from "immer";
|
||||||
import { produce, isDraft } from "immer";
|
import { produce, isDraft } from "immer";
|
||||||
import { isEqual, isPlainObject } from "lodash";
|
import { isEqual, isPlainObject } from "lodash";
|
||||||
@ -19,14 +19,13 @@ export interface StorageChange<T> {
|
|||||||
|
|
||||||
export interface StorageAdapter<T> {
|
export interface StorageAdapter<T> {
|
||||||
[metadata: string]: unknown;
|
[metadata: string]: unknown;
|
||||||
getItem(key: string): T | Promise<T>;
|
getItem(key: string): T;
|
||||||
setItem(key: string, value: T): void;
|
setItem(key: string, value: T): void;
|
||||||
removeItem(key: string): void;
|
removeItem(key: string): void;
|
||||||
onChange?(change: StorageChange<T>): void;
|
onChange?(change: StorageChange<T>): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StorageHelperOptions<T> {
|
export interface StorageHelperOptions<T> {
|
||||||
readonly autoInit?: boolean; // start preloading data immediately, default: true
|
|
||||||
readonly storage: StorageAdapter<T>;
|
readonly storage: StorageAdapter<T>;
|
||||||
readonly defaultValue: T;
|
readonly defaultValue: T;
|
||||||
}
|
}
|
||||||
@ -34,8 +33,6 @@ export interface StorageHelperOptions<T> {
|
|||||||
export interface StorageLayer<T> {
|
export interface StorageLayer<T> {
|
||||||
isDefaultValue(val: T): boolean;
|
isDefaultValue(val: T): boolean;
|
||||||
get(): T;
|
get(): T;
|
||||||
readonly value: T;
|
|
||||||
readonly whenReady: Promise<void>;
|
|
||||||
set(value: T): void;
|
set(value: T): void;
|
||||||
reset(): void;
|
reset(): void;
|
||||||
merge(value: Partial<T> | ((draft: Draft<T>) => Partial<T> | void)): void;
|
merge(value: Partial<T> | ((draft: Draft<T>) => Partial<T> | void)): void;
|
||||||
@ -43,76 +40,44 @@ export interface StorageLayer<T> {
|
|||||||
|
|
||||||
export const storageHelperLogPrefix = "[STORAGE-HELPER]:";
|
export const storageHelperLogPrefix = "[STORAGE-HELPER]:";
|
||||||
|
|
||||||
interface Dependencies {
|
export interface StorageHelperDependencies {
|
||||||
readonly logger: Logger;
|
readonly logger: Logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StorageHelper<T> implements StorageLayer<T> {
|
export class StorageHelper<T> implements StorageLayer<T> {
|
||||||
readonly storage: StorageAdapter<T>;
|
readonly storage: StorageAdapter<T>;
|
||||||
|
|
||||||
private readonly data = observable.box<T | undefined>(undefined, {
|
private readonly data = observable.box<T>(undefined, {
|
||||||
deep: true,
|
deep: true,
|
||||||
equals: comparer.structural,
|
equals: comparer.structural,
|
||||||
});
|
});
|
||||||
|
|
||||||
@observable initialized = false;
|
private readonly value = computed(() => this.data.get() ?? this.defaultValue);
|
||||||
|
|
||||||
get whenReady() {
|
|
||||||
return when(() => this.initialized);
|
|
||||||
}
|
|
||||||
|
|
||||||
get defaultValue(): T {
|
get defaultValue(): T {
|
||||||
// return as-is since options.defaultValue might be a getter too
|
// return as-is since options.defaultValue might be a getter too
|
||||||
return this.options.defaultValue;
|
return this.options.defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(private readonly dependencies: Dependencies, readonly key: string, private readonly options: StorageHelperOptions<T>) {
|
constructor(private readonly dependencies: StorageHelperDependencies, readonly key: string, private readonly options: StorageHelperOptions<T>) {
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
|
|
||||||
const { storage, autoInit = true } = options;
|
this.storage = this.options.storage;
|
||||||
|
|
||||||
this.storage = storage;
|
|
||||||
|
|
||||||
observe(this.data, (change) => {
|
observe(this.data, (change) => {
|
||||||
this.onChange(change.newValue as T | undefined, change.oldValue as T | undefined);
|
this.onChange(change.newValue as T | undefined, change.oldValue as T | undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (autoInit) {
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onData = (data: T): void => {
|
|
||||||
const notEmpty = data != null;
|
|
||||||
const notDefault = !this.isDefaultValue(data);
|
|
||||||
|
|
||||||
if (notEmpty && notDefault) {
|
|
||||||
this.set(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.initialized = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
private onError = (error: any): void => {
|
|
||||||
this.dependencies.logger.error(`${storageHelperLogPrefix} loading error: ${error}`, this);
|
|
||||||
};
|
|
||||||
|
|
||||||
@action
|
|
||||||
init({ force = false } = {}) {
|
|
||||||
if (this.initialized && !force) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = this.storage.getItem(this.key);
|
const data = this.storage.getItem(this.key);
|
||||||
|
const notEmpty = data != null;
|
||||||
|
const notDefault = !this.isDefaultValue(data);
|
||||||
|
|
||||||
if (data instanceof Promise) {
|
if (notEmpty && notDefault) {
|
||||||
data.then(this.onData, this.onError);
|
this.set(data);
|
||||||
} else {
|
|
||||||
this.onData(data);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.onError(error);
|
this.dependencies.logger.error(`${storageHelperLogPrefix} loading error: ${error}`, this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,9 +85,7 @@ export class StorageHelper<T> implements StorageLayer<T> {
|
|||||||
return isEqual(value, this.defaultValue);
|
return isEqual(value, this.defaultValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected onChange(value: T | undefined, oldValue: T | undefined) {
|
private onChange(value: T | undefined, oldValue: T | undefined) {
|
||||||
if (!this.initialized) return;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
this.storage.removeItem(this.key);
|
this.storage.removeItem(this.key);
|
||||||
@ -137,18 +100,13 @@ export class StorageHelper<T> implements StorageLayer<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get(): T {
|
get(): T {
|
||||||
return this.value;
|
return this.value.get();
|
||||||
}
|
|
||||||
|
|
||||||
@computed
|
|
||||||
get value(): T {
|
|
||||||
return this.data.get() ?? this.defaultValue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
set(value: T) {
|
set(value: T) {
|
||||||
if (this.isDefaultValue(value)) {
|
if (this.isDefaultValue(value)) {
|
||||||
this.reset();
|
this.data.set(undefined);
|
||||||
} else {
|
} else {
|
||||||
this.data.set(value);
|
this.data.set(value);
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user