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 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 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 { TabKind } from "../../../renderer/components/dock/dock/store";
|
||||
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", () => {
|
||||
let builder: ApplicationBuilder;
|
||||
let callForNamespaceMock: AsyncFnMock<CallForResource>;
|
||||
let storagesAreReady: () => Promise<void>;
|
||||
|
||||
beforeEach(() => {
|
||||
builder = getApplicationBuilder();
|
||||
@ -35,10 +32,6 @@ describe("cluster/namespaces - edit namespaces from previously opened tab", () =
|
||||
() => "/some-directory-for-lens-local-storage",
|
||||
);
|
||||
|
||||
windowDi.override(hostedClusterIdInjectable, () => "some-cluster-id");
|
||||
|
||||
storagesAreReady = controlWhenStoragesAreReady(windowDi);
|
||||
|
||||
windowDi.override(callForResourceInjectable, () => callForNamespaceMock);
|
||||
});
|
||||
|
||||
@ -83,8 +76,6 @@ describe("cluster/namespaces - edit namespaces from previously opened tab", () =
|
||||
});
|
||||
|
||||
rendered = await builder.render();
|
||||
|
||||
await storagesAreReady();
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
|
||||
@ -4,7 +4,7 @@ exports[`installing helm chart from previously opened tab given tab for installi
|
||||
<body>
|
||||
<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;"
|
||||
>
|
||||
<div
|
||||
@ -1109,9 +1109,222 @@ exports[`installing helm chart from previously opened tab given tab for installi
|
||||
style="flex-basis: 300px;"
|
||||
>
|
||||
<div
|
||||
class="Spinner singleColor center"
|
||||
data-testid="install-chart-tab-spinner"
|
||||
/>
|
||||
class="InstallChart flex column"
|
||||
>
|
||||
<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>
|
||||
|
||||
@ -9,13 +9,9 @@ import type { ApplicationBuilder } from "../../../renderer/components/test-utils
|
||||
import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
|
||||
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 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 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 { controlWhenStoragesAreReady } from "../../../renderer/utils/create-storage/storages-are-ready";
|
||||
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 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 requestHelmChartVersionsMock: AsyncFnMock<RequestHelmChartVersions>;
|
||||
let requestHelmChartValuesMock: AsyncFnMock<RequestHelmChartValues>;
|
||||
let storagesAreReady: () => Promise<void>;
|
||||
|
||||
beforeEach(() => {
|
||||
builder = getApplicationBuilder();
|
||||
@ -36,29 +31,15 @@ describe("installing helm chart from previously opened tab", () => {
|
||||
requestHelmChartVersionsMock = asyncFn();
|
||||
requestHelmChartValuesMock = asyncFn();
|
||||
|
||||
builder.beforeWindowStart((windowDi) => {
|
||||
storagesAreReady = controlWhenStoragesAreReady(windowDi);
|
||||
builder.namespaces.add("default");
|
||||
builder.namespaces.add("some-other-namespace");
|
||||
|
||||
builder.beforeWindowStart((windowDi) => {
|
||||
windowDi.override(directoryForLensLocalStorageInjectable, () => "/some-directory-for-lens-local-storage");
|
||||
windowDi.override(hostedClusterIdInjectable, () => "some-cluster-id");
|
||||
windowDi.override(requestHelmChartVersionsInjectable, () => requestHelmChartVersionsMock);
|
||||
windowDi.override(requestHelmChartValuesInjectable, () => requestHelmChartValuesMock);
|
||||
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, () =>
|
||||
jest
|
||||
.fn(() => "some-irrelevant-tab-id")
|
||||
@ -106,8 +87,6 @@ describe("installing helm chart from previously opened tab", () => {
|
||||
});
|
||||
|
||||
rendered = await builder.render();
|
||||
|
||||
await storagesAreReady();
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
import { action, observable, reaction } from "mobx";
|
||||
import type { StorageLayer } 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";
|
||||
|
||||
export interface DockTabStoreOptions {
|
||||
@ -33,10 +33,8 @@ export class DockTabStore<T> {
|
||||
if (autoInit && storageKey) {
|
||||
const storage = this.storage = this.dependencies.createStorage(storageKey, {});
|
||||
|
||||
storage.whenReady.then(() => {
|
||||
this.data.replace(storage.value);
|
||||
reaction(() => this.toJSON(), data => storage.set(data));
|
||||
});
|
||||
this.data.replace(storage.get());
|
||||
reaction(() => this.toJSON(), data => storage.set(data));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -108,19 +108,25 @@ export class DockStore implements DockStorageState {
|
||||
constructor(private readonly dependencies: Dependencies) {
|
||||
makeObservable(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;
|
||||
@observable fullSize = false;
|
||||
|
||||
get whenReady() {
|
||||
return this.dependencies.storage.whenReady;
|
||||
}
|
||||
|
||||
@computed
|
||||
get isOpen(): boolean {
|
||||
return this.dependencies.storage.value.isOpen;
|
||||
return this.dependencies.storage.get().isOpen;
|
||||
}
|
||||
|
||||
set isOpen(isOpen: boolean) {
|
||||
@ -129,7 +135,7 @@ export class DockStore implements DockStorageState {
|
||||
|
||||
@computed
|
||||
get height(): number {
|
||||
return this.dependencies.storage.value.height;
|
||||
return this.dependencies.storage.get().height;
|
||||
}
|
||||
|
||||
set height(height: number) {
|
||||
@ -140,7 +146,7 @@ export class DockStore implements DockStorageState {
|
||||
|
||||
@computed
|
||||
get tabs(): DockTab[] {
|
||||
return this.dependencies.storage.value.tabs;
|
||||
return this.dependencies.storage.get().tabs;
|
||||
}
|
||||
|
||||
set tabs(tabs: DockTab[]) {
|
||||
@ -149,7 +155,7 @@ export class DockStore implements DockStorageState {
|
||||
|
||||
@computed
|
||||
get selectedTabId(): TabId | undefined {
|
||||
const storageData = this.dependencies.storage.value;
|
||||
const storageData = this.dependencies.storage.get();
|
||||
|
||||
return (
|
||||
storageData.selectedTabId ||
|
||||
@ -171,21 +177,6 @@ export class DockStore implements DockStorageState {
|
||||
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() {
|
||||
const mainLayoutHeader = 40;
|
||||
const mainLayoutTabs = 33;
|
||||
|
||||
@ -4,9 +4,11 @@
|
||||
*/
|
||||
|
||||
import { observable, reaction } from "mobx";
|
||||
import { StorageHelper } from "../storageHelper";
|
||||
import { delay } from "../../../common/utils/delay";
|
||||
import { noop, toJS } from "../../../common/utils";
|
||||
import type { StorageHelper } from "../storage-helper";
|
||||
import { 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 {
|
||||
[prop: string]: any /*json-serializable*/;
|
||||
@ -15,27 +17,25 @@ interface StorageModel {
|
||||
}
|
||||
|
||||
describe("renderer/utils/StorageHelper", () => {
|
||||
let createStorageHelper: CreateStorageHelper;
|
||||
|
||||
beforeEach(() => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
createStorageHelper = di.inject(createStorageHelperInjectable);
|
||||
});
|
||||
|
||||
describe("Using custom StorageAdapter", () => {
|
||||
const storageKey = "ui-settings";
|
||||
const remoteStorageMock = observable.map<string, StorageModel>();
|
||||
let storageHelper: StorageHelper<StorageModel>;
|
||||
let storageHelperAsync: StorageHelper<StorageModel>;
|
||||
|
||||
beforeEach(() => {
|
||||
remoteStorageMock.set(storageKey, {
|
||||
message: "saved-before", // pretending as previously saved data
|
||||
});
|
||||
|
||||
storageHelper = new StorageHelper<StorageModel>({
|
||||
logger: {
|
||||
debug: noop,
|
||||
error: noop,
|
||||
info: noop,
|
||||
silly: noop,
|
||||
warn: noop,
|
||||
},
|
||||
}, storageKey, {
|
||||
autoInit: false,
|
||||
storageHelper = createStorageHelper<StorageModel>(storageKey, {
|
||||
defaultValue: {
|
||||
message: "blabla",
|
||||
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 () => {
|
||||
storageHelper.init();
|
||||
expect(storageHelper.key).toBe(storageKey);
|
||||
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", () => {
|
||||
storageHelper.init();
|
||||
storageHelper.set({ message: "msg" });
|
||||
storageHelper.get().description = "desc";
|
||||
expect(storageHelper.get().message).toBe("msg");
|
||||
@ -106,7 +74,6 @@ describe("renderer/utils/StorageHelper", () => {
|
||||
});
|
||||
|
||||
it("merge() does partial data tree updates", () => {
|
||||
storageHelper.init();
|
||||
storageHelper.merge({ message: "updated" });
|
||||
|
||||
expect(storageHelper.get()).toEqual({ ...storageHelper.defaultValue, message: "updated" });
|
||||
@ -134,16 +101,7 @@ describe("renderer/utils/StorageHelper", () => {
|
||||
beforeEach(() => {
|
||||
observedChanges.length = 0;
|
||||
|
||||
storageHelper = new StorageHelper<typeof defaultValue>({
|
||||
logger: {
|
||||
debug: noop,
|
||||
error: noop,
|
||||
info: noop,
|
||||
silly: noop,
|
||||
warn: noop,
|
||||
},
|
||||
}, "some-key", {
|
||||
autoInit: true,
|
||||
storageHelper = createStorageHelper<typeof defaultValue>("some-key", {
|
||||
defaultValue,
|
||||
storage: {
|
||||
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.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import directoryForLensLocalStorageInjectable from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
|
||||
import { createStorage } from "./create-storage";
|
||||
import readJsonFileInjectable from "../../../common/fs/read-json-file.injectable";
|
||||
import writeJsonFileInjectable from "../../../common/fs/write-json-file.injectable";
|
||||
import { observable } from "mobx";
|
||||
import loggerInjectable from "../../../common/logger.injectable";
|
||||
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";
|
||||
import { action } from "mobx";
|
||||
import lensLocalStorageStateInjectable from "./state.injectable";
|
||||
import createStorageHelperInjectable from "../create-storage-helper.injectable";
|
||||
import type { StorageLayer } from "../storage-helper";
|
||||
|
||||
export type CreateStorage = <T>(key: string, defaultValue: T) => StorageLayer<T>;
|
||||
|
||||
const createStorageInjectable = getInjectable({
|
||||
id: "create-storage",
|
||||
|
||||
instantiate: (di) => createStorage({
|
||||
storage: observable({
|
||||
initialized: false,
|
||||
loaded: false,
|
||||
data: {},
|
||||
}),
|
||||
readJsonFile: di.inject(readJsonFileInjectable),
|
||||
writeJsonFile: di.inject(writeJsonFileInjectable),
|
||||
logger: di.inject(loggerInjectable),
|
||||
directoryForLensLocalStorage: di.inject(directoryForLensLocalStorageInjectable),
|
||||
joinPaths: di.inject(joinPathsInjectable),
|
||||
hostedClusterId: di.inject(hostedClusterIdInjectable),
|
||||
saveDelay: di.inject(storageSaveDelayInjectable),
|
||||
}),
|
||||
instantiate: (di): CreateStorage => {
|
||||
const lensLocalStorageState = di.inject(lensLocalStorageStateInjectable);
|
||||
const createStorageHelper = di.inject(createStorageHelperInjectable);
|
||||
|
||||
return <T>(key: string, defaultValue: T) => createStorageHelper<T>(key, {
|
||||
defaultValue,
|
||||
storage: {
|
||||
getItem: (key) => lensLocalStorageState[key] as T,
|
||||
setItem: action((key, value) => lensLocalStorageState[key] = value),
|
||||
removeItem: action((key) => delete lensLocalStorageState[key]),
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
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 "./prevDefault";
|
||||
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.)
|
||||
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 { produce, isDraft } from "immer";
|
||||
import { isEqual, isPlainObject } from "lodash";
|
||||
@ -19,14 +19,13 @@ export interface StorageChange<T> {
|
||||
|
||||
export interface StorageAdapter<T> {
|
||||
[metadata: string]: unknown;
|
||||
getItem(key: string): T | Promise<T>;
|
||||
getItem(key: string): T;
|
||||
setItem(key: string, value: T): void;
|
||||
removeItem(key: string): void;
|
||||
onChange?(change: StorageChange<T>): void;
|
||||
}
|
||||
|
||||
export interface StorageHelperOptions<T> {
|
||||
readonly autoInit?: boolean; // start preloading data immediately, default: true
|
||||
readonly storage: StorageAdapter<T>;
|
||||
readonly defaultValue: T;
|
||||
}
|
||||
@ -34,8 +33,6 @@ export interface StorageHelperOptions<T> {
|
||||
export interface StorageLayer<T> {
|
||||
isDefaultValue(val: T): boolean;
|
||||
get(): T;
|
||||
readonly value: T;
|
||||
readonly whenReady: Promise<void>;
|
||||
set(value: T): void;
|
||||
reset(): 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]:";
|
||||
|
||||
interface Dependencies {
|
||||
export interface StorageHelperDependencies {
|
||||
readonly logger: Logger;
|
||||
}
|
||||
|
||||
export class StorageHelper<T> implements StorageLayer<T> {
|
||||
readonly storage: StorageAdapter<T>;
|
||||
|
||||
private readonly data = observable.box<T | undefined>(undefined, {
|
||||
private readonly data = observable.box<T>(undefined, {
|
||||
deep: true,
|
||||
equals: comparer.structural,
|
||||
});
|
||||
|
||||
@observable initialized = false;
|
||||
|
||||
get whenReady() {
|
||||
return when(() => this.initialized);
|
||||
}
|
||||
private readonly value = computed(() => this.data.get() ?? this.defaultValue);
|
||||
|
||||
get defaultValue(): T {
|
||||
// return as-is since options.defaultValue might be a getter too
|
||||
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);
|
||||
|
||||
const { storage, autoInit = true } = options;
|
||||
|
||||
this.storage = storage;
|
||||
this.storage = this.options.storage;
|
||||
|
||||
observe(this.data, (change) => {
|
||||
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 {
|
||||
const data = this.storage.getItem(this.key);
|
||||
const notEmpty = data != null;
|
||||
const notDefault = !this.isDefaultValue(data);
|
||||
|
||||
if (data instanceof Promise) {
|
||||
data.then(this.onData, this.onError);
|
||||
} else {
|
||||
this.onData(data);
|
||||
if (notEmpty && notDefault) {
|
||||
this.set(data);
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
|
||||
protected onChange(value: T | undefined, oldValue: T | undefined) {
|
||||
if (!this.initialized) return;
|
||||
|
||||
private onChange(value: T | undefined, oldValue: T | undefined) {
|
||||
try {
|
||||
if (value == null) {
|
||||
this.storage.removeItem(this.key);
|
||||
@ -137,18 +100,13 @@ export class StorageHelper<T> implements StorageLayer<T> {
|
||||
}
|
||||
|
||||
get(): T {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
@computed
|
||||
get value(): T {
|
||||
return this.data.get() ?? this.defaultValue;
|
||||
return this.value.get();
|
||||
}
|
||||
|
||||
@action
|
||||
set(value: T) {
|
||||
if (this.isDefaultValue(value)) {
|
||||
this.reset();
|
||||
this.data.set(undefined);
|
||||
} else {
|
||||
this.data.set(value);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user