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

Make base store non Singleton (#6690)

* Remove Singleton from BaseStore to remove global shared state

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove more usages of Singleton

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Replace use of legacy global execHelm with injectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove last use of legacy global execHelm

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Extract BaseStore deps into constructor argument

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Introduce method to make store migrations injectable
- Use it for ClusterStore

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Switch HotbarStore to injectable migrations

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Switch UserStore to injectable migrations

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Move migration utils into common/utils/

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Switch WeblinkStore to injectable migrations

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove dead code

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix type error in base-store tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove tests that reference lastSeenVersion
- That value is not used anywhere in code

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove usage of legacy global .getInstance

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove usage of legacy global ClusterStore.getInstance

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add simple migrations dependency for stores without any preexisting migrations

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix messed up import

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add typing to transient injectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Cleanup formatting

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix typing in tests to satisfy requirement to have cacheFile

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* More consistent use of BaseStore.displayName

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add catching of error while starting main application

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Move initializing sentry to runnable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove unneeded appPathsInjectionToken
- Only had once impl, which was in common anyway

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add support for multiple "runAfter" runnables
- Needed so that several dependencies can be declared

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Use multiple runAfter support to fix crash on renderer

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove traces

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add global override to fix tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix base store tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix runManyFor tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix hotbar store tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix user store tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add global override for getConfigurationFileModel to fix tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove overrides for configuration stores

- Now that there is an override for getConfiguration

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Overhaul FS fakes with full in-memory filesystem
- This increases our confidence in fs related logic

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove use of global shared Electron.App

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add fake access support

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Handle copy as part of fake FS

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add ensureDir/Sync support to fake FS

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix type error

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Use pathExistsSync instead of fsInjectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add createReadStream to fake FS

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add stat to fake FS

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove dead code

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix test failures due to incomplete overrides

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fully injectable-ize BaseStore so that ApplicationBuilder tests work

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Consolidate more bootstrapping into startFrame

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Move initializing CatalogCategories to runnable in bootstrap

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Convert contextMenuOpen initializers into runnables

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Convert navigateForExtension init to runnable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Make cluster state sync fully injectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Move init hotbar store into runnables

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Make LensTheme fully injectable and runnable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Cleanup old code from missed from previous commit

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Manually split out terminal color names and fully type LensTheme

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix old imports

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove unnecessart awaits

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove dead code

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fully cherry pick injectablizing custom monaco themes

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix duplicate mock warning

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix incorrectly fully cherry picking new runnable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Complete cherry-pick of current cluster injcetablization

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix override file name

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix injecting before app paths are set up

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix injecting before app paths are set up

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix ordering of runnable and order of injection

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Convert all renderer runnables to late-inject style
- To help fix issues around injection time

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix react-beautiful-dnd mocks

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Update and fix WriteJson(Sync) to fix error in tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix HotbarStore.load being called twice is being buggy

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Update listing-active-helm-repositories-in-preferences snapshots

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix sidebar-and-tab-navigation-tests
- Move enabling extensions in tests to a proper location
- Fix flushing promises

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove props from dnd mock to make snapshot diffs smaller

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix import

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Update snapshots

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix tests by overriding things that are no longer overriden by default
- NOTE: They are overridden when using ApplicationBuilder

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix hotbar store tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix cluster store tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix extension-loader tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix extension-discovery tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix cluster-role-dialog tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix user store tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix kubeconfig sync tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix sidebar and tab tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove unused code

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix pick paths import type error and simplify signature

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix type error in legacy ipc registration

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove another use of legacy requestOpenPathPicker

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Replace use of legacy global PathPicker.Pick

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix usage in light of changed prop names

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix catalog tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix more type errors

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix test flakiness by removing side effects from userStore preferences

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Update snapshots

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix loading

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix type error

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix crash

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Cherry pick updated startFrameInjectable

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add tests to verify runMany behaviour in new possible incorrect configuration

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix init ordering during start frame

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix cluster state sync

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Update snapshots after removing side-effects

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add override for technical test

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Correctly mark currentlyInClusterFrame as causedSideEffects

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Better formatting

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix behaviour regression

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add better logging

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix BaseStore sync

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix tests

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Update last snapshot

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add global override for randomBytes

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Make startMainApplication not an injection time side effect

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Choose better names for start-frame runnable tokens

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Remove duplication of code in RunManyFor

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add unit tests and fix handling empty runAfter array

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Replace use of mobx from runManyFor with custom barrier

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add missing test

Signed-off-by: Sebastian Malton <sebastian@malton.name>

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2022-12-15 07:07:19 -08:00 committed by GitHub
parent 7c5faaaf1d
commit 25f37ac1d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
364 changed files with 10039 additions and 5250 deletions

View File

@ -1,14 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export default {};
export const Uri = {
file: (path: string) => path,
};
export const editor = {
getModel: () => ({}),
create: () => ({}),
};

View File

@ -7,9 +7,49 @@ import React from "react";
import type { import type {
DragDropContextProps, DragDropContextProps,
DraggableProps, DraggableProps,
DraggableProvidedDraggableProps,
DroppableProps, DroppableProps,
DroppableProvidedProps,
} from "react-beautiful-dnd"; } from "react-beautiful-dnd";
export const DragDropContext = ({ children }: DragDropContextProps) => <>{ children }</>; export const DragDropContext = ({ children }: DragDropContextProps) => <>{ children }</>;
export const Draggable = ({ children }: DraggableProps) => <>{ children({} as any, {} as any, {} as any) }</>; export const Draggable = ({ children }: DraggableProps) => (
export const Droppable = ({ children }: DroppableProps) => <>{ children({} as any, {} as any) }</>; <>
{
children(
{
draggableProps: {} as DraggableProvidedDraggableProps,
innerRef: () => {},
},
{
isDragging: false,
isDropAnimating: false,
},
{
draggableId: "some-mock-draggable-id",
mode: "FLUID",
source: {
droppableId: "some-mock-droppable-id",
index: 0,
},
},
)
}
</>
);
export const Droppable = ({ children }: DroppableProps) => (
<>
{
children(
{
droppableProps: {} as DroppableProvidedProps,
innerRef: () => {},
},
{
isDraggingOver: false,
isUsingPlaceholder: false,
},
)
}
</>
);

View File

@ -1,31 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import fs from "fs-extra";
import path from "path";
import defaultBaseLensTheme from "../src/renderer/themes/lens-dark";
const outputCssFile = path.resolve("src/renderer/themes/theme-vars.css");
const banner = `/*
Generated Lens theme CSS-variables, don't edit manually.
To refresh file run $: yarn run ts-node build/${path.basename(__filename)}
*/`;
const themeCssVars = Object.entries(defaultBaseLensTheme.colors)
.map(([varName, value]) => `--${varName}: ${value};`);
const content = `
${banner}
:root {
${themeCssVars.join("\n")}
}
`;
// Run
console.info(`"Saving default Lens theme css-variables to "${outputCssFile}""`);
fs.ensureFileSync(outputCssFile);
fs.writeFile(outputCssFile, content);

View File

@ -228,6 +228,7 @@
"grapheme-splitter": "^1.0.4", "grapheme-splitter": "^1.0.4",
"handlebars": "^4.7.7", "handlebars": "^4.7.7",
"history": "^4.10.1", "history": "^4.10.1",
"hpagent": "^1.2.0",
"http-proxy": "^1.18.1", "http-proxy": "^1.18.1",
"immer": "^9.0.16", "immer": "^9.0.16",
"joi": "^17.7.0", "joi": "^17.7.0",

View File

@ -1,148 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import mockFs from "mock-fs";
import { BaseStore } from "../base-store";
import { action, comparer, makeObservable, observable, toJS } from "mobx";
import { readFileSync } from "fs";
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable";
jest.mock("electron", () => ({
ipcMain: {
on: jest.fn(),
off: jest.fn(),
},
}));
interface TestStoreModel {
a: string;
b: string;
c: string;
}
class TestStore extends BaseStore<TestStoreModel> {
@observable a = "";
@observable b = "";
@observable c = "";
constructor() {
super({
configName: "test-store",
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
syncOptions: {
equals: comparer.structural,
},
});
makeObservable(this);
this.load();
}
@action updateAll(data: TestStoreModel) {
this.a = data.a;
this.b = data.b;
this.c = data.c;
}
@action fromStore(data: Partial<TestStoreModel> = {}) {
this.a = data.a || "";
this.b = data.b || "";
this.c = data.c || "";
}
onSync(data: TestStoreModel) {
super.onSync(data);
}
async saveToFile(model: TestStoreModel) {
return super.saveToFile(model);
}
toJSON(): TestStoreModel {
const data: TestStoreModel = {
a: this.a,
b: this.b,
c: this.c,
};
return toJS(data);
}
}
describe("BaseStore", () => {
let store: TestStore;
beforeEach(() => {
const mainDi = getDiForUnitTesting({ doGeneralOverrides: true });
mainDi.override(directoryForUserDataInjectable, () => "some-user-data-directory");
mainDi.permitSideEffects(getConfigurationFileModelInjectable);
TestStore.resetInstance();
const mockOpts = {
"some-user-data-directory": {
"test-store.json": JSON.stringify({}),
},
};
mockFs(mockOpts);
store = TestStore.createInstance();
});
afterEach(() => {
mockFs.restore();
store.disableSync();
TestStore.resetInstance();
});
describe("persistence", () => {
it("persists changes to the filesystem", () => {
store.updateAll({
a: "foo", b: "bar", c: "hello",
});
const data = JSON.parse(readFileSync("some-user-data-directory/test-store.json").toString());
expect(data).toEqual({ a: "foo", b: "bar", c: "hello" });
});
it("persists transaction only once", () => {
const fileSpy = jest.spyOn(store, "saveToFile");
store.updateAll({
a: "foo", b: "bar", c: "hello",
});
expect(fileSpy).toHaveBeenCalledTimes(1);
});
it("persists changes one-by-one without transaction", () => {
const fileSpy = jest.spyOn(store, "saveToFile");
store.a = "a";
store.b = "b";
expect(fileSpy).toHaveBeenCalledTimes(2);
const data = JSON.parse(readFileSync("some-user-data-directory/test-store.json").toString());
expect(data).toEqual({ a: "a", b: "b", c: "" });
});
it("persists changes coming via onSync (sync from different process)", () => {
const fileSpy = jest.spyOn(store, "saveToFile");
store.onSync({ a: "foo", b: "", c: "bar" });
expect(store.toJSON()).toEqual({ a: "foo", b: "", c: "bar" });
expect(fileSpy).toHaveBeenCalledTimes(1);
});
});
});

View File

@ -93,10 +93,9 @@ describe("cluster-store", () => {
mainDi.override(normalizedPlatformInjectable, () => "darwin"); mainDi.override(normalizedPlatformInjectable, () => "darwin");
mainDi.permitSideEffects(getConfigurationFileModelInjectable); mainDi.permitSideEffects(getConfigurationFileModelInjectable);
mainDi.permitSideEffects(clusterStoreInjectable); mainDi.unoverride(getConfigurationFileModelInjectable);
mainDi.permitSideEffects(fsInjectable);
mainDi.unoverride(clusterStoreInjectable); mainDi.permitSideEffects(fsInjectable);
}); });
afterEach(() => { afterEach(() => {
@ -107,23 +106,19 @@ describe("cluster-store", () => {
let getCustomKubeConfigDirectory: (directoryName: string) => string; let getCustomKubeConfigDirectory: (directoryName: string) => string;
beforeEach(async () => { beforeEach(async () => {
getCustomKubeConfigDirectory = mainDi.inject( getCustomKubeConfigDirectory = mainDi.inject(getCustomKubeConfigDirectoryInjectable);
getCustomKubeConfigDirectoryInjectable,
);
const mockOpts = { mockFs({
"some-directory-for-user-data": { "some-directory-for-user-data": {
"lens-cluster-store.json": JSON.stringify({}), "lens-cluster-store.json": JSON.stringify({}),
}, },
}; });
mockFs(mockOpts);
createCluster = mainDi.inject(createClusterInjectionToken); createCluster = mainDi.inject(createClusterInjectionToken);
clusterStore = mainDi.inject(clusterStoreInjectable); clusterStore = mainDi.inject(clusterStoreInjectable);
clusterStore.unregisterIpcListener(); clusterStore.load();
}); });
afterEach(() => { afterEach(() => {
@ -207,7 +202,7 @@ describe("cluster-store", () => {
describe("config with existing clusters", () => { describe("config with existing clusters", () => {
beforeEach(() => { beforeEach(() => {
const mockOpts = { mockFs({
"temp-kube-config": kubeconfig, "temp-kube-config": kubeconfig,
"some-directory-for-user-data": { "some-directory-for-user-data": {
"lens-cluster-store.json": JSON.stringify({ "lens-cluster-store.json": JSON.stringify({
@ -241,13 +236,12 @@ describe("cluster-store", () => {
], ],
}), }),
}, },
}; });
mockFs(mockOpts);
createCluster = mainDi.inject(createClusterInjectionToken); createCluster = mainDi.inject(createClusterInjectionToken);
clusterStore = mainDi.inject(clusterStoreInjectable); clusterStore = mainDi.inject(clusterStoreInjectable);
clusterStore.load();
}); });
afterEach(() => { afterEach(() => {
@ -297,7 +291,7 @@ users:
token: kubeconfig-user-q4lm4:xxxyyyy token: kubeconfig-user-q4lm4:xxxyyyy
`; `;
const mockOpts = { mockFs({
"invalid-kube-config": invalidKubeconfig, "invalid-kube-config": invalidKubeconfig,
"valid-kube-config": kubeconfig, "valid-kube-config": kubeconfig,
"some-directory-for-user-data": { "some-directory-for-user-data": {
@ -325,13 +319,12 @@ users:
], ],
}), }),
}, },
}; });
mockFs(mockOpts);
createCluster = mainDi.inject(createClusterInjectionToken); createCluster = mainDi.inject(createClusterInjectionToken);
clusterStore = mainDi.inject(clusterStoreInjectable); clusterStore = mainDi.inject(clusterStoreInjectable);
clusterStore.load();
}); });
afterEach(() => { afterEach(() => {
@ -347,7 +340,7 @@ users:
describe("pre 3.6.0-beta.1 config with an existing cluster", () => { describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
beforeEach(() => { beforeEach(() => {
const mockOpts = { mockFs({
"some-directory-for-user-data": { "some-directory-for-user-data": {
"lens-cluster-store.json": JSON.stringify({ "lens-cluster-store.json": JSON.stringify({
__internal__: { __internal__: {
@ -368,15 +361,14 @@ users:
}), }),
icon_path: testDataIcon, icon_path: testDataIcon,
}, },
}; });
mockFs(mockOpts);
mainDi.override(storeMigrationVersionInjectable, () => "3.6.0"); mainDi.override(storeMigrationVersionInjectable, () => "3.6.0");
createCluster = mainDi.inject(createClusterInjectionToken); createCluster = mainDi.inject(createClusterInjectionToken);
clusterStore = mainDi.inject(clusterStoreInjectable); clusterStore = mainDi.inject(clusterStoreInjectable);
clusterStore.load();
}); });
afterEach(() => { afterEach(() => {

View File

@ -19,6 +19,7 @@ import loggerInjectable from "../logger.injectable";
import type { Logger } from "../logger"; import type { Logger } from "../logger";
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
import storeMigrationVersionInjectable from "../vars/store-migration-version.injectable"; import storeMigrationVersionInjectable from "../vars/store-migration-version.injectable";
import fsInjectable from "../fs/fs.injectable";
function getMockCatalogEntity(data: Partial<CatalogEntityData> & CatalogEntityKindData): CatalogEntity { function getMockCatalogEntity(data: Partial<CatalogEntityData> & CatalogEntityKindData): CatalogEntity {
return { return {
@ -46,7 +47,7 @@ describe("HotbarStore", () => {
beforeEach(async () => { beforeEach(async () => {
di = getDiForUnitTesting({ doGeneralOverrides: true }); di = getDiForUnitTesting({ doGeneralOverrides: true });
(di as any).unoverride(hotbarStoreInjectable); di.unoverride(hotbarStoreInjectable);
testCluster = getMockCatalogEntity({ testCluster = getMockCatalogEntity({
apiVersion: "v1", apiVersion: "v1",
@ -112,8 +113,9 @@ describe("HotbarStore", () => {
catalogCatalogEntity, catalogCatalogEntity,
])); ]));
di.permitSideEffects(fsInjectable);
di.permitSideEffects(getConfigurationFileModelInjectable); di.permitSideEffects(getConfigurationFileModelInjectable);
di.permitSideEffects(hotbarStoreInjectable); di.unoverride(getConfigurationFileModelInjectable);
}); });
afterEach(() => { afterEach(() => {
@ -255,22 +257,12 @@ describe("HotbarStore", () => {
}); });
it("throws if invalid arguments provided", () => { it("throws if invalid arguments provided", () => {
// Prevent writing to stderr during this render.
const { error, warn } = console;
console.error = jest.fn();
console.warn = jest.fn();
hotbarStore.addToHotbar(testCluster); hotbarStore.addToHotbar(testCluster);
expect(() => hotbarStore.restackItems(-5, 0)).toThrow(); expect(() => hotbarStore.restackItems(-5, 0)).toThrow();
expect(() => hotbarStore.restackItems(2, -1)).toThrow(); expect(() => hotbarStore.restackItems(2, -1)).toThrow();
expect(() => hotbarStore.restackItems(14, 1)).toThrow(); expect(() => hotbarStore.restackItems(14, 1)).toThrow();
expect(() => hotbarStore.restackItems(11, 112)).toThrow(); expect(() => hotbarStore.restackItems(11, 112)).toThrow();
// Restore writing to stderr.
console.error = error;
console.warn = warn;
}); });
it("checks if entity already pinned to hotbar", () => { it("checks if entity already pinned to hotbar", () => {
@ -284,7 +276,7 @@ describe("HotbarStore", () => {
describe("given data from 5.0.0-beta.3 and version being 5.0.0-beta.10", () => { describe("given data from 5.0.0-beta.3 and version being 5.0.0-beta.10", () => {
beforeEach(() => { beforeEach(() => {
const configurationToBeMigrated = { mockFs({
"some-directory-for-user-data": { "some-directory-for-user-data": {
"lens-hotbar-store.json": JSON.stringify({ "lens-hotbar-store.json": JSON.stringify({
__internal__: { __internal__: {
@ -344,9 +336,7 @@ describe("HotbarStore", () => {
], ],
}), }),
}, },
}; });
mockFs(configurationToBeMigrated);
di.override(storeMigrationVersionInjectable, () => "5.0.0-beta.10"); di.override(storeMigrationVersionInjectable, () => "5.0.0-beta.10");

View File

@ -35,6 +35,7 @@ import getConfigurationFileModelInjectable from "../get-configuration-file-model
import storeMigrationVersionInjectable from "../vars/store-migration-version.injectable"; import storeMigrationVersionInjectable from "../vars/store-migration-version.injectable";
import releaseChannelInjectable from "../vars/release-channel.injectable"; import releaseChannelInjectable from "../vars/release-channel.injectable";
import defaultUpdateChannelInjectable from "../../features/application-update/common/selected-update-channel/default-update-channel.injectable"; import defaultUpdateChannelInjectable from "../../features/application-update/common/selected-update-channel/default-update-channel.injectable";
import fsInjectable from "../fs/fs.injectable";
console = new Console(stdout, stderr); console = new Console(stdout, stderr);
@ -49,8 +50,10 @@ describe("user store tests", () => {
di.override(writeFileInjectable, () => () => Promise.resolve()); di.override(writeFileInjectable, () => () => Promise.resolve());
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
di.permitSideEffects(getConfigurationFileModelInjectable); di.permitSideEffects(getConfigurationFileModelInjectable);
di.permitSideEffects(userStoreInjectable); di.unoverride(getConfigurationFileModelInjectable);
di.permitSideEffects(fsInjectable);
di.override(releaseChannelInjectable, () => ({ di.override(releaseChannelInjectable, () => ({
get: () => "latest" as const, get: () => "latest" as const,
@ -58,7 +61,7 @@ describe("user store tests", () => {
})); }));
await di.inject(defaultUpdateChannelInjectable).init(); await di.inject(defaultUpdateChannelInjectable).init();
di.unoverride(userStoreInjectable); userStore = di.inject(userStoreInjectable);
}); });
afterEach(() => { afterEach(() => {
@ -67,17 +70,11 @@ describe("user store tests", () => {
describe("for an empty config", () => { describe("for an empty config", () => {
beforeEach(() => { beforeEach(() => {
mockFs({ "some-directory-for-user-data": { "config.json": "{}", "kube_config": "{}" }}); mockFs({ "some-directory-for-user-data": { "lens-user-store.json": "{}", "kube_config": "{}" }});
userStore = di.inject(userStoreInjectable);
userStore.load(); userStore.load();
}); });
it("allows setting and retrieving lastSeenAppVersion", () => {
userStore.lastSeenAppVersion = "1.2.3";
expect(userStore.lastSeenAppVersion).toBe("1.2.3");
});
it("allows setting and getting preferences", () => { it("allows setting and getting preferences", () => {
userStore.httpsProxy = "abcd://defg"; userStore.httpsProxy = "abcd://defg";
@ -99,10 +96,8 @@ describe("user store tests", () => {
beforeEach(() => { beforeEach(() => {
mockFs({ mockFs({
"some-directory-for-user-data": { "some-directory-for-user-data": {
"config.json": JSON.stringify({ "lens-user-store.json": JSON.stringify({
user: { username: "foobar" },
preferences: { colorTheme: "light" }, preferences: { colorTheme: "light" },
lastSeenAppVersion: "1.2.3",
}), }),
"lens-cluster-store.json": JSON.stringify({ "lens-cluster-store.json": JSON.stringify({
clusters: [ clusters: [
@ -127,17 +122,16 @@ describe("user store tests", () => {
di.override(storeMigrationVersionInjectable, () => "10.0.0"); di.override(storeMigrationVersionInjectable, () => "10.0.0");
userStore = di.inject(userStoreInjectable);
userStore.load(); userStore.load();
}); });
it("sets last seen app version to 0.0.0", () => { it("skips clusters for adding to kube-sync with files under extension_data/", () => {
expect(userStore.lastSeenAppVersion).toBe("0.0.0");
});
it.only("skips clusters for adding to kube-sync with files under extension_data/", () => {
expect(userStore.syncKubeconfigEntries.has("some-directory-for-user-data/extension_data/foo/bar")).toBe(false); expect(userStore.syncKubeconfigEntries.has("some-directory-for-user-data/extension_data/foo/bar")).toBe(false);
expect(userStore.syncKubeconfigEntries.has("some/other/path")).toBe(true); expect(userStore.syncKubeconfigEntries.has("some/other/path")).toBe(true);
}); });
it("allows access to the colorTheme preference", () => {
expect(userStore.colorTheme).toBe("light");
});
}); });
}); });

View File

@ -2,11 +2,6 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* 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 { getInjectionToken } from "@ogre-tools/injectable";
import type { PathName } from "./app-path-names"; import type { PathName } from "./app-path-names";
export type AppPaths = Record<PathName, string>; export type AppPaths = Record<PathName, string>;
export const appPathsInjectionToken = getInjectionToken<AppPaths>({ id: "app-paths-token" });

View File

@ -3,13 +3,11 @@
* 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 { appPathsInjectionToken } from "./app-path-injection-token";
import appPathsStateInjectable from "./app-paths-state.injectable"; import appPathsStateInjectable from "./app-paths-state.injectable";
const appPathsInjectable = getInjectable({ const appPathsInjectable = getInjectable({
id: "app-paths", id: "app-paths",
instantiate: (di) => di.inject(appPathsStateInjectable).get(), instantiate: (di) => di.inject(appPathsStateInjectable).get(),
injectionToken: appPathsInjectionToken,
}); });
export default appPathsInjectable; export default appPathsInjectable;

View File

@ -3,7 +3,6 @@
* 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 type { AppPaths } from "./app-path-injection-token"; import type { AppPaths } from "./app-path-injection-token";
import { appPathsInjectionToken } from "./app-path-injection-token";
import getElectronAppPathInjectable from "../../main/app-paths/get-electron-app-path/get-electron-app-path.injectable"; import getElectronAppPathInjectable from "../../main/app-paths/get-electron-app-path/get-electron-app-path.injectable";
import type { PathName } from "./app-path-names"; import type { PathName } from "./app-path-names";
import setElectronAppPathInjectable from "../../main/app-paths/set-electron-app-path/set-electron-app-path.injectable"; import setElectronAppPathInjectable from "../../main/app-paths/set-electron-app-path/set-electron-app-path.injectable";
@ -11,6 +10,7 @@ import directoryForIntegrationTestingInjectable from "../../main/app-paths/direc
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import type { DiContainer } from "@ogre-tools/injectable"; import type { DiContainer } from "@ogre-tools/injectable";
import appPathsInjectable from "./app-paths.injectable";
describe("app-paths", () => { describe("app-paths", () => {
let builder: ApplicationBuilder; let builder: ApplicationBuilder;
@ -68,7 +68,7 @@ describe("app-paths", () => {
}); });
it("given in renderer, when injecting app paths, returns application specific app paths", () => { it("given in renderer, when injecting app paths, returns application specific app paths", () => {
const actual = windowDi.inject(appPathsInjectionToken); const actual = windowDi.inject(appPathsInjectable);
expect(actual).toEqual({ expect(actual).toEqual({
currentApp: "some-current-app", currentApp: "some-current-app",
@ -92,7 +92,7 @@ describe("app-paths", () => {
}); });
it("given in main, when injecting app paths, returns application specific app paths", () => { it("given in main, when injecting app paths, returns application specific app paths", () => {
const actual = mainDi.inject(appPathsInjectionToken); const actual = mainDi.inject(appPathsInjectable);
expect(actual).toEqual({ expect(actual).toEqual({
currentApp: "some-current-app", currentApp: "some-current-app",
@ -133,7 +133,7 @@ describe("app-paths", () => {
}); });
it("given in renderer, when injecting path for app data, has integration specific app data path", () => { it("given in renderer, when injecting path for app data, has integration specific app data path", () => {
const { appData, userData } = windowDi.inject(appPathsInjectionToken); const { appData, userData } = windowDi.inject(appPathsInjectable);
expect({ appData, userData }).toEqual({ expect({ appData, userData }).toEqual({
appData: "some-integration-testing-app-data", appData: "some-integration-testing-app-data",
@ -142,7 +142,7 @@ describe("app-paths", () => {
}); });
it("given in main, when injecting path for app data, has integration specific app data path", () => { it("given in main, when injecting path for app data, has integration specific app data path", () => {
const { appData, userData } = windowDi.inject(appPathsInjectionToken); const { appData, userData } = windowDi.inject(appPathsInjectable);
expect({ appData, userData }).toEqual({ expect({ appData, userData }).toEqual({
appData: "some-integration-testing-app-data", appData: "some-integration-testing-app-data",

View File

@ -3,11 +3,11 @@
* 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 { appPathsInjectionToken } from "../app-path-injection-token"; import appPathsInjectable from "../app-paths.injectable";
const directoryForDownloadsInjectable = getInjectable({ const directoryForDownloadsInjectable = getInjectable({
id: "directory-for-downloads", id: "directory-for-downloads",
instantiate: (di) => di.inject(appPathsInjectionToken).downloads, instantiate: (di) => di.inject(appPathsInjectable).downloads,
}); });
export default directoryForDownloadsInjectable; export default directoryForDownloadsInjectable;

View File

@ -3,11 +3,11 @@
* 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 { appPathsInjectionToken } from "../app-path-injection-token"; import appPathsInjectable from "../app-paths.injectable";
const directoryForExesInjectable = getInjectable({ const directoryForExesInjectable = getInjectable({
id: "directory-for-exes", id: "directory-for-exes",
instantiate: (di) => di.inject(appPathsInjectionToken).exe, instantiate: (di) => di.inject(appPathsInjectable).exe,
}); });
export default directoryForExesInjectable; export default directoryForExesInjectable;

View File

@ -3,11 +3,11 @@
* 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 { appPathsInjectionToken } from "../app-path-injection-token"; import appPathsInjectable from "../app-paths.injectable";
const directoryForTempInjectable = getInjectable({ const directoryForTempInjectable = getInjectable({
id: "directory-for-temp", id: "directory-for-temp",
instantiate: (di) => di.inject(appPathsInjectionToken).temp, instantiate: (di) => di.inject(appPathsInjectable).temp,
}); });
export default directoryForTempInjectable; export default directoryForTempInjectable;

View File

@ -3,11 +3,11 @@
* 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 { appPathsInjectionToken } from "../app-path-injection-token"; import appPathsInjectable from "../app-paths.injectable";
const directoryForUserDataInjectable = getInjectable({ const directoryForUserDataInjectable = getInjectable({
id: "directory-for-user-data", id: "directory-for-user-data",
instantiate: (di) => di.inject(appPathsInjectionToken).userData, instantiate: (di) => di.inject(appPathsInjectable).userData,
}); });
export default directoryForUserDataInjectable; export default directoryForUserDataInjectable;

View File

@ -1,194 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import path from "path";
import type Config from "conf";
import type { Options as ConfOptions } from "conf/dist/source/types";
import { ipcMain, ipcRenderer } from "electron";
import type { IEqualsComparer } from "mobx";
import { makeObservable, reaction, runInAction } from "mobx";
import type { Disposer } from "./utils";
import { Singleton, toJS } from "./utils";
import logger from "../main/logger";
import { broadcastMessage, ipcMainOn, ipcRendererOn } from "./ipc";
import isEqual from "lodash/isEqual";
import { isTestEnv } from "./vars";
import { kebabCase } from "lodash";
import { getLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
import directoryForUserDataInjectable from "./app-paths/directory-for-user-data/directory-for-user-data.injectable";
import getConfigurationFileModelInjectable from "./get-configuration-file-model/get-configuration-file-model.injectable";
import storeMigrationVersionInjectable from "./vars/store-migration-version.injectable";
export interface BaseStoreParams<T> extends ConfOptions<T> {
syncOptions?: {
fireImmediately?: boolean;
equals?: IEqualsComparer<T>;
};
}
/**
* Note: T should only contain base JSON serializable types.
*/
export abstract class BaseStore<T extends object> extends Singleton {
protected storeConfig?: Config<T>;
protected syncDisposers: Disposer[] = [];
readonly displayName: string = this.constructor.name;
protected constructor(protected params: BaseStoreParams<T>) {
super();
makeObservable(this);
if (ipcRenderer) {
params.migrations = undefined; // don't run migrations on renderer
}
}
/**
* This must be called after the last child's constructor is finished (or just before it finishes)
*/
load() {
if (!isTestEnv) {
logger.info(`[${kebabCase(this.displayName).toUpperCase()}]: LOADING from ${this.path} ...`);
}
const di = getLegacyGlobalDiForExtensionApi();
const getConfigurationFileModel = di.inject(getConfigurationFileModelInjectable);
this.storeConfig = getConfigurationFileModel({
projectName: "lens",
projectVersion: di.inject(storeMigrationVersionInjectable),
cwd: this.cwd(),
...this.params,
});
const res: any = this.fromStore(this.storeConfig.store);
if (res instanceof Promise || (typeof res === "object" && res && typeof res.then === "function")) {
console.error(`${this.displayName} extends BaseStore<T>'s fromStore method returns a Promise or promise-like object. This is an error and must be fixed.`);
}
this.enableSync();
if (!isTestEnv) {
logger.info(`[${kebabCase(this.displayName).toUpperCase()}]: LOADED from ${this.path}`);
}
}
get name() {
return path.basename(this.path);
}
protected get syncRendererChannel() {
return `store-sync-renderer:${this.path}`;
}
protected get syncMainChannel() {
return `store-sync-main:${this.path}`;
}
get path() {
return this.storeConfig?.path || "";
}
protected cwd() {
const di = getLegacyGlobalDiForExtensionApi();
return di.inject(directoryForUserDataInjectable);
}
protected saveToFile(model: T) {
logger.info(`[STORE]: SAVING ${this.path}`);
// todo: update when fixed https://github.com/sindresorhus/conf/issues/114
if (this.storeConfig) {
for (const [key, value] of Object.entries(model)) {
this.storeConfig.set(key, value);
}
}
}
enableSync() {
this.syncDisposers.push(
reaction(
() => toJS(this.toJSON()), // unwrap possible observables and react to everything
model => this.onModelChange(model),
this.params.syncOptions,
),
);
if (ipcMain) {
this.syncDisposers.push(ipcMainOn(this.syncMainChannel, (event, model: T) => {
logger.silly(`[STORE]: SYNC ${this.name} from renderer`, { model });
this.onSync(model);
}));
}
if (ipcRenderer) {
this.syncDisposers.push(ipcRendererOn(this.syncRendererChannel, (event, model: T) => {
logger.silly(`[STORE]: SYNC ${this.name} from main`, { model });
this.onSyncFromMain(model);
}));
}
}
protected onSyncFromMain(model: T) {
this.applyWithoutSync(() => {
this.onSync(model);
});
}
unregisterIpcListener() {
ipcRenderer?.removeAllListeners(this.syncMainChannel);
ipcRenderer?.removeAllListeners(this.syncRendererChannel);
}
disableSync() {
this.syncDisposers.forEach(dispose => dispose());
this.syncDisposers.length = 0;
}
protected applyWithoutSync(callback: () => void) {
this.disableSync();
runInAction(callback);
this.enableSync();
}
protected onSync(model: T) {
// todo: use "resourceVersion" if merge required (to avoid equality checks => better performance)
if (!isEqual(this.toJSON(), model)) {
this.fromStore(model);
}
}
protected onModelChange(model: T) {
if (ipcMain) {
this.saveToFile(model); // save config file
broadcastMessage(this.syncRendererChannel, model);
} else {
broadcastMessage(this.syncMainChannel, model);
}
}
/**
* fromStore is called internally when a child class syncs with the file
* system.
*
* Note: This function **must** be synchronous.
*
* @param data the parsed information read from the stored JSON file
*/
protected abstract fromStore(data: T): void;
/**
* toJSON is called when syncing the store to the filesystem. It should
* produce a JSON serializable object representation of the current state.
*
* It is recommended that a round trip is valid. Namely, calling
* `this.fromStore(this.toJSON())` shouldn't change the state.
*/
abstract toJSON(): T;
}

View File

@ -0,0 +1,148 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type Config from "conf";
import type { Migrations, Options as ConfOptions } from "conf/dist/source/types";
import type { IEqualsComparer } from "mobx";
import { makeObservable, reaction } from "mobx";
import { disposer, isPromiseLike, toJS } from "../utils";
import { broadcastMessage } from "../ipc";
import isEqual from "lodash/isEqual";
import { kebabCase } from "lodash";
import type { GetConfigurationFileModel } from "../get-configuration-file-model/get-configuration-file-model.injectable";
import type { Logger } from "../logger";
import type { PersistStateToConfig } from "./save-to-file";
import type { GetBasenameOfPath } from "../path/get-basename.injectable";
import type { EnlistMessageChannelListener } from "../utils/channel/enlist-message-channel-listener-injection-token";
export interface BaseStoreParams<T> extends Omit<ConfOptions<T>, "migrations"> {
syncOptions?: {
fireImmediately?: boolean;
equals?: IEqualsComparer<T>;
};
configName: string;
}
export interface IpcChannelPrefixes {
local: string;
remote: string;
}
export interface BaseStoreDependencies {
readonly logger: Logger;
readonly storeMigrationVersion: string;
readonly directoryForUserData: string;
readonly migrations: Migrations<Record<string, unknown>>;
readonly ipcChannelPrefixes: IpcChannelPrefixes;
readonly shouldDisableSyncInListener: boolean;
getConfigurationFileModel: GetConfigurationFileModel;
persistStateToConfig: PersistStateToConfig;
getBasenameOfPath: GetBasenameOfPath;
enlistMessageChannelListener: EnlistMessageChannelListener;
}
/**
* Note: T should only contain base JSON serializable types.
*/
export abstract class BaseStore<T extends object> {
private readonly syncDisposers = disposer();
readonly displayName = kebabCase(this.params.configName).toUpperCase();
protected constructor(
protected readonly dependencies: BaseStoreDependencies,
protected readonly params: BaseStoreParams<T>,
) {
makeObservable(this);
}
/**
* This must be called after the last child's constructor is finished (or just before it finishes)
*/
load() {
this.dependencies.logger.info(`[${this.displayName}]: LOADING ...`);
const config = this.dependencies.getConfigurationFileModel({
projectName: "lens",
projectVersion: this.dependencies.storeMigrationVersion,
cwd: this.cwd(),
...this.params,
migrations: this.dependencies.migrations as Migrations<T>,
});
const res = this.fromStore(config.store);
if (isPromiseLike(res)) {
this.dependencies.logger.error(`${this.displayName} extends BaseStore<T>'s fromStore method returns a Promise or promise-like object. This is an error and must be fixed.`);
}
this.startSyncing(config);
this.dependencies.logger.info(`[${this.displayName}]: LOADED from ${config.path}`);
}
protected cwd() {
return this.dependencies.directoryForUserData;
}
private startSyncing(config: Config<T>) {
const name = this.dependencies.getBasenameOfPath(config.path);
const disableSync = () => this.syncDisposers();
const enableSync = () => {
this.syncDisposers.push(
reaction(
() => toJS(this.toJSON()), // unwrap possible observables and react to everything
model => {
this.dependencies.persistStateToConfig(config, model);
broadcastMessage(`${this.dependencies.ipcChannelPrefixes.remote}:${config.path}`, model);
},
this.params.syncOptions,
),
this.dependencies.enlistMessageChannelListener({
channel: {
id: `${this.dependencies.ipcChannelPrefixes.local}:${config.path}`,
},
handler: (model) => {
this.dependencies.logger.silly(`[${this.displayName}]: syncing ${name}`, { model });
if (this.dependencies.shouldDisableSyncInListener) {
disableSync();
}
// todo: use "resourceVersion" if merge required (to avoid equality checks => better performance)
if (!isEqual(this.toJSON(), model)) {
this.fromStore(model as T);
}
if (this.dependencies.shouldDisableSyncInListener) {
enableSync();
}
},
}),
);
};
enableSync();
}
/**
* fromStore is called internally when a child class syncs with the file
* system.
*
* Note: This function **must** be synchronous.
*
* @param data the parsed information read from the stored JSON file
*/
protected abstract fromStore(data: T): void;
/**
* toJSON is called when syncing the store to the filesystem. It should
* produce a JSON serializable object representation of the current state.
*
* It is recommended that a round trip is valid. Namely, calling
* `this.fromStore(this.toJSON())` shouldn't change the state.
*/
abstract toJSON(): T;
}

View File

@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type { IpcChannelPrefixes } from "./base-store";
export const baseStoreIpcChannelPrefixesInjectionToken = getInjectionToken<IpcChannelPrefixes>({
id: "base-store-ipc-channel-prefix-token",
});

View File

@ -2,9 +2,9 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* 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 { getInjectionToken } from "@ogre-tools/injectable";
import type { Runnable } from "../../common/runnable/run-many-for";
export const beforeFrameStartsInjectionToken = getInjectionToken<Runnable>({ import { getInjectionToken } from "@ogre-tools/injectable";
id: "before-frame-starts",
export const shouldBaseStoreDisableSyncInIpcListenerInjectionToken = getInjectionToken<boolean>({
id: "should-base-store-disable-sync-in-ipc-listener-token",
}); });

View File

@ -0,0 +1,46 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { InjectionToken } from "@ogre-tools/injectable";
import { lifecycleEnum, getInjectable } from "@ogre-tools/injectable";
import type Conf from "conf/dist/source";
import type { Migrations } from "conf/dist/source/types";
import loggerInjectable from "../logger.injectable";
import { getOrInsert, iter } from "../utils";
export interface MigrationDeclaration {
version: string;
run(store: Conf<Partial<Record<string, unknown>>>): void;
}
const storeMigrationsInjectable = getInjectable({
id: "store-migrations",
instantiate: (di, token): Migrations<Record<string, unknown>> => {
const logger = di.inject(loggerInjectable);
const declarations = di.injectMany(token);
const migrations = new Map<string, MigrationDeclaration["run"][]>();
for (const decl of declarations) {
getOrInsert(migrations, decl.version, []).push(decl.run);
}
return Object.fromEntries(
iter.map(
migrations,
([v, fns]) => [v, (store) => {
logger.info(`Running ${v} migration for ${store.path}`);
for (const fn of fns) {
fn(store);
}
}],
),
);
},
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, token: InjectionToken<MigrationDeclaration, void>) => token.id,
}),
});
export default storeMigrationsInjectable;

View File

@ -0,0 +1,12 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type Config from "conf";
export type PersistStateToConfig = <T extends object>(config: Config<T>, state: T) => void;
export const persistStateToConfigInjectionToken = getInjectionToken<PersistStateToConfig>({
id: "persist-state-to-config-token",
});

View File

@ -5,13 +5,14 @@
import type { CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus, CatalogCategorySpec } from "../catalog"; import type { CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus, CatalogCategorySpec } from "../catalog";
import { CatalogEntity, CatalogCategory, categoryVersion } from "../catalog/catalog-entity"; import { CatalogEntity, CatalogCategory, categoryVersion } from "../catalog/catalog-entity";
import { ClusterStore } from "../cluster-store/cluster-store";
import { broadcastMessage } from "../ipc"; import { broadcastMessage } from "../ipc";
import { app } from "electron"; import { app } from "electron";
import type { CatalogEntityConstructor, CatalogEntitySpec } from "../catalog/catalog-entity"; import type { CatalogEntityConstructor, CatalogEntitySpec } from "../catalog/catalog-entity";
import { IpcRendererNavigationEvents } from "../../renderer/navigation/events"; import { IpcRendererNavigationEvents } from "../../renderer/navigation/events";
import { requestClusterActivation, requestClusterDisconnection } from "../../renderer/ipc"; import { requestClusterActivation, requestClusterDisconnection } from "../../renderer/ipc";
import KubeClusterCategoryIcon from "./icons/kubernetes.svg"; import KubeClusterCategoryIcon from "./icons/kubernetes.svg";
import { asLegacyGlobalFunctionForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/as-legacy-global-function-for-extension-api";
import getClusterByIdInjectable from "../cluster-store/get-by-id.injectable";
export interface KubernetesClusterPrometheusMetrics { export interface KubernetesClusterPrometheusMetrics {
address?: { address?: {
@ -63,6 +64,8 @@ export function isKubernetesCluster(item: unknown): item is KubernetesCluster {
return item instanceof KubernetesCluster; return item instanceof KubernetesCluster;
} }
const getClusterById = asLegacyGlobalFunctionForExtensionApi(getClusterByIdInjectable);
export class KubernetesCluster< export class KubernetesCluster<
Metadata extends KubernetesClusterMetadata = KubernetesClusterMetadata, Metadata extends KubernetesClusterMetadata = KubernetesClusterMetadata,
Status extends KubernetesClusterStatus = KubernetesClusterStatus, Status extends KubernetesClusterStatus = KubernetesClusterStatus,
@ -76,7 +79,7 @@ export class KubernetesCluster<
async connect(): Promise<void> { async connect(): Promise<void> {
if (app) { if (app) {
await ClusterStore.getInstance().getById(this.getId())?.activate(); await getClusterById(this.getId())?.activate();
} else { } else {
await requestClusterActivation(this.getId(), false); await requestClusterActivation(this.getId(), false);
} }
@ -84,7 +87,7 @@ export class KubernetesCluster<
async disconnect(): Promise<void> { async disconnect(): Promise<void> {
if (app) { if (app) {
ClusterStore.getInstance().getById(this.getId())?.disconnect(); getClusterById(this.getId())?.disconnect();
} else { } else {
await requestClusterDisconnection(this.getId(), false); await requestClusterDisconnection(this.getId(), false);
} }

View File

@ -7,7 +7,7 @@ import { Environments, getEnvironmentSpecificLegacyGlobalDiForExtensionApi } fro
import type { CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog"; import type { CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
import { CatalogCategory, CatalogEntity, categoryVersion } from "../catalog/catalog-entity"; import { CatalogCategory, CatalogEntity, categoryVersion } from "../catalog/catalog-entity";
import productNameInjectable from "../vars/product-name.injectable"; import productNameInjectable from "../vars/product-name.injectable";
import weblinkStoreInjectable from "../weblink-store.injectable"; import weblinkStoreInjectable from "../weblinks-store/weblink-store.injectable";
export type WebLinkStatusPhase = "available" | "unavailable"; export type WebLinkStatusPhase = "available" | "unavailable";
@ -34,12 +34,13 @@ export class WebLink extends CatalogEntity<CatalogEntityMetadata, WebLinkStatus,
// NOTE: this is safe because `onContextMenuOpen` is only supposed to be called in the renderer // NOTE: this is safe because `onContextMenuOpen` is only supposed to be called in the renderer
const di = getEnvironmentSpecificLegacyGlobalDiForExtensionApi(Environments.renderer); const di = getEnvironmentSpecificLegacyGlobalDiForExtensionApi(Environments.renderer);
const productName = di.inject(productNameInjectable); const productName = di.inject(productNameInjectable);
const weblinkStore = di.inject(weblinkStoreInjectable);
if (this.metadata.source === "local") { if (this.metadata.source === "local") {
context.menuItems.push({ context.menuItems.push({
title: "Delete", title: "Delete",
icon: "delete", icon: "delete",
onClick: async () => di.inject(weblinkStoreInjectable).removeById(this.getId()), onClick: async () => weblinkStore.removeById(this.getId()),
confirm: { confirm: {
message: `Remove Web Link "${this.getName()}" from ${productName}?`, message: `Remove Web Link "${this.getName()}" from ${productName}?`,
}, },

View File

@ -1,12 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { asLegacyGlobalForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
import catalogCategoryRegistryInjectable from "./category-registry.injectable";
/**
* @deprecated use `di.inject(catalogCategoryRegistryInjectable)` instead
*/
export const catalogCategoryRegistry = asLegacyGlobalForExtensionApi(catalogCategoryRegistryInjectable);

View File

@ -3,6 +3,5 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
export * from "./catalog-category-registry";
export * from "./category-registry"; export * from "./category-registry";
export * from "./catalog-entity"; export * from "./catalog-entity";

View File

@ -1,17 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "../test-utils/get-global-override";
import clusterStoreInjectable from "./cluster-store.injectable";
import type { Cluster } from "../cluster/cluster";
import type { ClusterStore } from "./cluster-store";
export default getGlobalOverride(
clusterStoreInjectable,
() =>
({
provideInitialFromMain: () => {},
getById: (id) => (void id, {}) as Cluster,
} as ClusterStore),
);

View File

@ -7,21 +7,36 @@ import { ClusterStore } from "./cluster-store";
import { createClusterInjectionToken } from "../cluster/create-cluster-injection-token"; import { createClusterInjectionToken } from "../cluster/create-cluster-injection-token";
import readClusterConfigSyncInjectable from "./read-cluster-config.injectable"; import readClusterConfigSyncInjectable from "./read-cluster-config.injectable";
import emitAppEventInjectable from "../app-event-bus/emit-event.injectable"; import emitAppEventInjectable from "../app-event-bus/emit-event.injectable";
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable";
import loggerInjectable from "../logger.injectable";
import storeMigrationVersionInjectable from "../vars/store-migration-version.injectable";
import storeMigrationsInjectable from "../base-store/migrations.injectable";
import { clusterStoreMigrationInjectionToken } from "./migration-token";
import { baseStoreIpcChannelPrefixesInjectionToken } from "../base-store/channel-prefix";
import { shouldBaseStoreDisableSyncInIpcListenerInjectionToken } from "../base-store/disable-sync";
import { persistStateToConfigInjectionToken } from "../base-store/save-to-file";
import getBasenameOfPathInjectable from "../path/get-basename.injectable";
import { enlistMessageChannelListenerInjectionToken } from "../utils/channel/enlist-message-channel-listener-injection-token";
const clusterStoreInjectable = getInjectable({ const clusterStoreInjectable = getInjectable({
id: "cluster-store", id: "cluster-store",
instantiate: (di) => { instantiate: (di) => new ClusterStore({
ClusterStore.resetInstance(); createCluster: di.inject(createClusterInjectionToken),
readClusterConfigSync: di.inject(readClusterConfigSyncInjectable),
return ClusterStore.createInstance({ emitAppEvent: di.inject(emitAppEventInjectable),
createCluster: di.inject(createClusterInjectionToken), directoryForUserData: di.inject(directoryForUserDataInjectable),
readClusterConfigSync: di.inject(readClusterConfigSyncInjectable), getConfigurationFileModel: di.inject(getConfigurationFileModelInjectable),
emitAppEvent: di.inject(emitAppEventInjectable), logger: di.inject(loggerInjectable),
}); storeMigrationVersion: di.inject(storeMigrationVersionInjectable),
}, migrations: di.inject(storeMigrationsInjectable, clusterStoreMigrationInjectionToken),
getBasenameOfPath: di.inject(getBasenameOfPathInjectable),
causesSideEffects: true, ipcChannelPrefixes: di.inject(baseStoreIpcChannelPrefixesInjectionToken),
persistStateToConfig: di.inject(persistStateToConfigInjectionToken),
enlistMessageChannelListener: di.inject(enlistMessageChannelListenerInjectionToken),
shouldDisableSyncInListener: di.inject(shouldBaseStoreDisableSyncInIpcListenerInjectionToken),
}),
}); });
export default clusterStoreInjectable; export default clusterStoreInjectable;

View File

@ -4,17 +4,12 @@
*/ */
import { ipcMain, ipcRenderer, webFrame } from "electron"; import { action, comparer, computed, makeObservable, observable } from "mobx";
import { action, comparer, computed, makeObservable, observable, reaction } from "mobx"; import type { BaseStoreDependencies } from "../base-store/base-store";
import { BaseStore } from "../base-store"; import { BaseStore } from "../base-store/base-store";
import { Cluster } from "../cluster/cluster"; import { Cluster } from "../cluster/cluster";
import migrations from "../../migrations/cluster-store"; import { toJS } from "../utils";
import logger from "../../main/logger"; import type { ClusterModel, ClusterId } from "../cluster-types";
import { ipcMainHandle } from "../ipc";
import { disposer, toJS } from "../utils";
import type { ClusterModel, ClusterId, ClusterState } from "../cluster-types";
import { requestInitialClusterStates } from "../../renderer/ipc";
import { clusterStates } from "../ipc/cluster";
import type { CreateCluster } from "../cluster/create-cluster-injection-token"; import type { CreateCluster } from "../cluster/create-cluster-injection-token";
import type { ReadClusterConfigSync } from "./read-cluster-config.injectable"; import type { ReadClusterConfigSync } from "./read-cluster-config.injectable";
import type { EmitAppEvent } from "../app-event-bus/emit-event.injectable"; import type { EmitAppEvent } from "../app-event-bus/emit-event.injectable";
@ -23,76 +18,25 @@ export interface ClusterStoreModel {
clusters?: ClusterModel[]; clusters?: ClusterModel[];
} }
interface Dependencies { interface Dependencies extends BaseStoreDependencies {
createCluster: CreateCluster; createCluster: CreateCluster;
readClusterConfigSync: ReadClusterConfigSync; readClusterConfigSync: ReadClusterConfigSync;
emitAppEvent: EmitAppEvent; emitAppEvent: EmitAppEvent;
} }
export class ClusterStore extends BaseStore<ClusterStoreModel> { export class ClusterStore extends BaseStore<ClusterStoreModel> {
readonly displayName = "ClusterStore"; readonly clusters = observable.map<ClusterId, Cluster>();
clusters = observable.map<ClusterId, Cluster>();
protected disposer = disposer(); constructor(protected readonly dependencies: Dependencies) {
super(dependencies, {
constructor(private readonly dependencies: Dependencies) {
super({
configName: "lens-cluster-store", configName: "lens-cluster-store",
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
syncOptions: { syncOptions: {
equals: comparer.structural, equals: comparer.structural,
}, },
migrations,
}); });
makeObservable(this); makeObservable(this);
this.load();
this.pushStateToViewsAutomatically();
}
async loadInitialOnRenderer() {
logger.info("[CLUSTER-STORE] requesting initial state sync");
for (const { id, state } of await requestInitialClusterStates()) {
this.getById(id)?.setState(state);
}
}
provideInitialFromMain() {
ipcMainHandle(clusterStates, () => (
this.clustersList.map(cluster => ({
id: cluster.id,
state: cluster.getState(),
}))
));
}
protected pushStateToViewsAutomatically() {
if (ipcMain) {
this.disposer.push(
reaction(() => this.connectedClustersList, () => this.pushState()),
);
}
}
registerIpcListener() {
logger.info(`[CLUSTER-STORE] start to listen (${webFrame.routingId})`);
const ipc = ipcMain ?? ipcRenderer;
ipc?.on("cluster:state", (event, clusterId: ClusterId, state: ClusterState) => {
this.getById(clusterId)?.setState(state);
});
}
unregisterIpcListener() {
super.unregisterIpcListener();
this.disposer();
}
pushState() {
this.clusters.forEach((c) => {
c.pushState();
});
} }
@computed get clustersList(): Cluster[] { @computed get clustersList(): Cluster[] {
@ -150,7 +94,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
} }
newClusters.set(clusterModel.id, cluster); newClusters.set(clusterModel.id, cluster);
} catch (error) { } catch (error) {
logger.warn(`[CLUSTER-STORE]: Failed to update/create a cluster: ${error}`); this.dependencies.logger.warn(`[CLUSTER-STORE]: Failed to update/create a cluster: ${error}`);
} }
} }

View File

@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type { MigrationDeclaration } from "../base-store/migrations.injectable";
export const clusterStoreMigrationInjectionToken = getInjectionToken<MigrationDeclaration>({
id: "cluster-store-migration",
});

View File

@ -316,7 +316,6 @@ export class Cluster implements ClusterModel, ClusterState {
const refreshMetadataTimer = setInterval(() => this.available && this.refreshAccessibilityAndMetadata(), 900000); // every 15 minutes const refreshMetadataTimer = setInterval(() => this.available && this.refreshAccessibilityAndMetadata(), 900000); // every 15 minutes
this.eventsDisposer.push( this.eventsDisposer.push(
reaction(() => this.getState(), state => this.pushState(state)),
reaction( reaction(
() => this.prometheusPreferences, () => this.prometheusPreferences,
prefs => this.contextHandler.setupPrometheus(prefs), prefs => this.contextHandler.setupPrometheus(prefs),
@ -349,7 +348,7 @@ export class Cluster implements ClusterModel, ClusterState {
@action @action
async activate(force = false) { async activate(force = false) {
if (this.activated && !force) { if (this.activated && !force) {
return this.pushState(); return;
} }
this.dependencies.logger.info(`[CLUSTER]: activate`, this.getMeta()); this.dependencies.logger.info(`[CLUSTER]: activate`, this.getMeta());
@ -395,7 +394,6 @@ export class Cluster implements ClusterModel, ClusterState {
} }
this.activated = true; this.activated = true;
this.pushState();
} }
/** /**
@ -437,7 +435,6 @@ export class Cluster implements ClusterModel, ClusterState {
this.activated = false; this.activated = false;
this.allowedNamespaces = []; this.allowedNamespaces = [];
this.resourceAccessStatuses.clear(); this.resourceAccessStatuses.clear();
this.pushState();
this.dependencies.logger.info(`[CLUSTER]: disconnected`, { id: this.id }); this.dependencies.logger.info(`[CLUSTER]: disconnected`, { id: this.id });
} }
@ -448,7 +445,6 @@ export class Cluster implements ClusterModel, ClusterState {
async refresh() { async refresh() {
this.dependencies.logger.info(`[CLUSTER]: refresh`, this.getMeta()); this.dependencies.logger.info(`[CLUSTER]: refresh`, this.getMeta());
await this.refreshConnectionStatus(); await this.refreshConnectionStatus();
this.pushState();
} }
/** /**
@ -614,16 +610,15 @@ export class Cluster implements ClusterModel, ClusterState {
* @param state cluster state * @param state cluster state
*/ */
@action setState(state: ClusterState) { @action setState(state: ClusterState) {
Object.assign(this, state); this.accessible = state.accessible;
} this.allowedNamespaces = state.allowedNamespaces;
this.allowedResources = state.allowedResources;
/** this.apiUrl = state.apiUrl;
* @internal this.disconnected = state.disconnected;
* @param state cluster state this.isAdmin = state.isAdmin;
*/ this.isGlobalWatchEnabled = state.isGlobalWatchEnabled;
pushState(state = this.getState()) { this.online = state.online;
this.dependencies.logger.silly(`[CLUSTER]: push-state`, state); this.ready = state.ready;
this.dependencies.broadcastMessage("cluster:state", this.id, state);
} }
// get cluster system meta, e.g. use in "logger" // get cluster system meta, e.g. use in "logger"

View File

@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ClusterId } from "../cluster-types";
import type { MessageChannel } from "../utils/channel/message-channel-listener-injection-token";
export const currentClusterMessageChannel: MessageChannel<ClusterId> = {
id: "current-visible-cluster",
};

View File

@ -3,7 +3,9 @@
* 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 { HttpsProxyAgent } from "hpagent";
import type * as FetchModule from "node-fetch"; import type * as FetchModule from "node-fetch";
import userStoreInjectable from "../user-store/user-store.injectable";
const { NodeFetch: { default: fetch }} = require("../../../build/webpack/node-fetch.bundle") as { NodeFetch: typeof FetchModule }; const { NodeFetch: { default: fetch }} = require("../../../build/webpack/node-fetch.bundle") as { NodeFetch: typeof FetchModule };
@ -14,7 +16,20 @@ export type Fetch = (url: string, init?: RequestInit) => Promise<Response>;
const fetchInjectable = getInjectable({ const fetchInjectable = getInjectable({
id: "fetch", id: "fetch",
instantiate: (): Fetch => fetch, instantiate: (di): Fetch => {
const { httpsProxy, allowUntrustedCAs } = di.inject(userStoreInjectable);
const agent = httpsProxy
? new HttpsProxyAgent({
proxy: httpsProxy,
rejectUnauthorized: !allowUntrustedCAs,
})
: undefined;
return (url, init = {}) => fetch(url, {
agent,
...init,
});
},
causesSideEffects: true, causesSideEffects: true,
}); });

View File

@ -1,11 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "../test-utils/get-global-override";
import deleteFileInjectable from "./delete-file.injectable";
export default getGlobalOverride(deleteFileInjectable, () => async () => {
throw new Error("tried to delete file without override");
});

View File

@ -0,0 +1,9 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverrideForFunction } from "../test-utils/get-global-override-for-function";
import execFileInjectable from "./exec-file.injectable";
export default getGlobalOverrideForFunction(execFileInjectable);

View File

@ -3,11 +3,61 @@
* 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 type { ReadOptions } from "fs-extra";
import fse from "fs-extra"; import fse from "fs-extra";
/**
* NOTE: Add corrisponding a corrisponding override of this injecable in `src/test-utils/override-fs-with-fakes.ts`
*/
const fsInjectable = getInjectable({ const fsInjectable = getInjectable({
id: "fs", id: "fs",
instantiate: () => fse, instantiate: () => {
const {
promises: {
readFile,
writeFile,
readdir,
lstat,
rm,
access,
stat,
},
ensureDir,
ensureDirSync,
readFileSync,
readJson,
writeJson,
readJsonSync,
writeFileSync,
writeJsonSync,
pathExistsSync,
pathExists,
copy,
createReadStream,
} = fse;
return {
readFile,
readJson: readJson as (file: string, options?: ReadOptions | BufferEncoding) => Promise<any>,
writeFile,
writeJson,
pathExists,
readdir,
readFileSync,
readJsonSync,
writeFileSync,
writeJsonSync,
pathExistsSync,
lstat,
rm,
access,
copy: copy as (src: string, dest: string, options?: fse.CopyOptions) => Promise<void>,
ensureDir: ensureDir as (path: string, options?: number | fse.EnsureOptions ) => Promise<void>,
ensureDirSync,
createReadStream,
stat,
};
},
causesSideEffects: true, causesSideEffects: true,
}); });

View File

@ -1,16 +0,0 @@
/**
* 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 type { MoveOptions } from "fs-extra";
import fsInjectable from "./fs.injectable";
export type Move = (src: string, dest: string, options?: MoveOptions) => Promise<void>;
const moveInjectable = getInjectable({
id: "move",
instantiate: (di): Move => di.inject(fsInjectable).move,
});
export default moveInjectable;

View File

@ -5,11 +5,9 @@
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import fsInjectable from "./fs.injectable"; import fsInjectable from "./fs.injectable";
export type DeleteFile = (filePath: string) => Promise<void>; const pathExistsSyncInjectable = getInjectable({
id: "path-exists-sync",
const deleteFileInjectable = getInjectable({ instantiate: (di) => di.inject(fsInjectable).pathExistsSync,
id: "delete-file",
instantiate: (di): DeleteFile => di.inject(fsInjectable).unlink,
}); });
export default deleteFileInjectable; export default pathExistsSyncInjectable;

View File

@ -1,11 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "../test-utils/get-global-override";
import pathExistsInjectable from "./path-exists.injectable";
export default getGlobalOverride(pathExistsInjectable, () => async () => {
throw new Error("Tried to check if a path exists without override");
});

View File

@ -14,18 +14,16 @@ export interface ReadDirectory {
( (
path: string, path: string,
options?: options?:
| { encoding: BufferEncoding | string | null; withFileTypes?: false | undefined } | { encoding: BufferEncoding; withFileTypes?: false | undefined }
| BufferEncoding | BufferEncoding
| string
| null,
): Promise<string[]>; ): Promise<string[]>;
( (
path: string, path: string,
options?: { encoding?: BufferEncoding | string | null | undefined; withFileTypes?: false | undefined }, options?: { encoding?: BufferEncoding; withFileTypes?: false | undefined },
): Promise<string[] | Buffer[]>; ): Promise<string[] | Buffer[]>;
( (
path: string, path: string,
options: { encoding?: BufferEncoding | string | null | undefined; withFileTypes: true }, options: { encoding?: BufferEncoding; withFileTypes: true },
): Promise<Dirent[]>; ): Promise<Dirent[]>;
} }

View File

@ -0,0 +1,19 @@
/**
* 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 fsInjectable from "./fs.injectable";
export type ReadFileBufferSync = (filePath: string) => Buffer;
const readFileBufferSyncInjectable = getInjectable({
id: "read-file-buffer-sync",
instantiate: (di): ReadFileBufferSync => {
const { readFileSync } = di.inject(fsInjectable);
return (filePath) => readFileSync(filePath);
},
});
export default readFileBufferSyncInjectable;

View File

@ -0,0 +1,13 @@
/**
* 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 fsInjectable from "./fs.injectable";
const readJsonSyncInjectable = getInjectable({
id: "read-json-sync",
instantiate: (di) => di.inject(fsInjectable).readJsonSync,
});
export default readJsonSyncInjectable;

View File

@ -4,8 +4,8 @@
*/ */
import { getGlobalOverride } from "../test-utils/get-global-override"; import { getGlobalOverride } from "../test-utils/get-global-override";
import removePathInjectable from "./remove-path.injectable"; import removePathInjectable from "./remove.injectable";
export default getGlobalOverride(removePathInjectable, () => async () => { export default getGlobalOverride(removePathInjectable, () => async () => {
throw new Error("tried to remove a path without override"); throw new Error("tried to remove path without override");
}); });

View File

@ -5,11 +5,15 @@
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import fsInjectable from "./fs.injectable"; import fsInjectable from "./fs.injectable";
export type RemovePath = (path: string) => Promise<void>; export type RemovePath = (filePath: string) => Promise<void>;
const removePathInjectable = getInjectable({ const removePathInjectable = getInjectable({
id: "remove-path", id: "remove-path",
instantiate: (di): RemovePath => di.inject(fsInjectable).remove, instantiate: (di): RemovePath => {
const { rm } = di.inject(fsInjectable);
return (filePath) => rm(filePath, { force: true });
},
}); });
export default removePathInjectable; export default removePathInjectable;

View File

@ -4,7 +4,7 @@
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import type { Stats } from "fs"; import type { Stats } from "fs";
import fsInjectable from "../fs.injectable"; import fsInjectable from "./fs.injectable";
export type Stat = (path: string) => Promise<Stats>; export type Stat = (path: string) => Promise<Stats>;

View File

@ -1,10 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import statInjectable from "./stat.injectable";
import { getGlobalOverride } from "../../test-utils/get-global-override";
export default getGlobalOverride(statInjectable, () => () => {
throw new Error("Tried to call stat without explicit override");
});

View File

@ -7,7 +7,7 @@ import type { AsyncResult } from "../utils/async-result";
import { isErrnoException } from "../utils"; import { isErrnoException } from "../utils";
import type { Stats } from "fs-extra"; import type { Stats } from "fs-extra";
import { lowerFirst } from "lodash/fp"; import { lowerFirst } from "lodash/fp";
import statInjectable from "./stat/stat.injectable"; import statInjectable from "./stat.injectable";
export type ValidateDirectory = (path: string) => Promise<AsyncResult<undefined>>; export type ValidateDirectory = (path: string) => Promise<AsyncResult<undefined>>;

View File

@ -0,0 +1,29 @@
/**
* 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 getDirnameOfPathInjectable from "../path/get-dirname.injectable";
import fsInjectable from "./fs.injectable";
export type WriteFileSync = (filePath: string, contents: string) => void;
const writeFileSyncInjectable = getInjectable({
id: "write-file-sync",
instantiate: (di): WriteFileSync => {
const {
writeFileSync,
ensureDirSync,
} = di.inject(fsInjectable);
const getDirnameOfPath = di.inject(getDirnameOfPathInjectable);
return (filePath, contents) => {
ensureDirSync(getDirnameOfPath(filePath), {
mode: 0o755,
});
writeFileSync(filePath, contents);
};
},
});
export default writeFileSyncInjectable;

View File

@ -16,15 +16,17 @@ const writeFileInjectable = getInjectable({
const { writeFile, ensureDir } = di.inject(fsInjectable); const { writeFile, ensureDir } = di.inject(fsInjectable);
const getDirnameOfPath = di.inject(getDirnameOfPathInjectable); const getDirnameOfPath = di.inject(getDirnameOfPathInjectable);
return async (filePath, content, opts) => { return async (filePath, content, opts = {}) => {
await ensureDir(getDirnameOfPath(filePath), { await ensureDir(getDirnameOfPath(filePath), {
mode: 0o755, mode: 0o755,
...(opts ?? {}), ...opts,
}); });
const { encoding = "utf-8", ...options } = opts;
await writeFile(filePath, content, { await writeFile(filePath, content, {
encoding: "utf-8", encoding: encoding as BufferEncoding,
...(opts ?? {}), ...options,
}); });
}; };
}, },

View File

@ -3,11 +3,10 @@
* 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 type { JsonValue } from "type-fest";
import getDirnameOfPathInjectable from "../path/get-dirname.injectable"; import getDirnameOfPathInjectable from "../path/get-dirname.injectable";
import fsInjectable from "./fs.injectable"; import fsInjectable from "./fs.injectable";
export type WriteJson = (filePath: string, contents: JsonValue) => Promise<void>; export type WriteJson = (filePath: string, contents: unknown) => Promise<void>;
const writeJsonFileInjectable = getInjectable({ const writeJsonFileInjectable = getInjectable({
id: "write-json-file", id: "write-json-file",

View File

@ -0,0 +1,31 @@
/**
* 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 getDirnameOfPathInjectable from "../path/get-dirname.injectable";
import fsInjectable from "./fs.injectable";
export type WriteJsonSync = (filePath: string, contents: unknown) => void;
const writeJsonSyncInjectable = getInjectable({
id: "write-json-sync",
instantiate: (di): WriteJsonSync => {
const {
writeJsonSync,
ensureDirSync,
} = di.inject(fsInjectable);
const getDirnameOfPath = di.inject(getDirnameOfPathInjectable);
return (filePath, content) => {
ensureDirSync(getDirnameOfPath(filePath), { mode: 0o755 });
writeJsonSync(filePath, content, {
encoding: "utf-8",
spaces: 2,
});
};
},
});
export default writeJsonSyncInjectable;

View File

@ -0,0 +1,53 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import assert from "assert";
import path from "path";
import { getGlobalOverride } from "../test-utils/get-global-override";
import getConfigurationFileModelInjectable from "./get-configuration-file-model.injectable";
import type Config from "conf";
import readJsonSyncInjectable from "../fs/read-json-sync.injectable";
import writeJsonSyncInjectable from "../fs/write-json-sync.injectable";
export default getGlobalOverride(getConfigurationFileModelInjectable, (di) => {
const readJsonSync = di.inject(readJsonSyncInjectable);
const writeJsonSync = di.inject(writeJsonSyncInjectable);
return (options) => {
assert(options.cwd, "Missing options.cwd");
assert(options.configName, "Missing options.configName");
const configFilePath = path.posix.join(options.cwd, `${options.configName}.json`);
let store: object = {};
try {
store = readJsonSync(configFilePath);
} catch {
// ignore
}
return {
get store() {
return store;
},
path: configFilePath,
set: (key: string, value: unknown) => {
let currentState: object;
try {
currentState = readJsonSync(configFilePath);
} catch {
currentState = {};
}
writeJsonSync(configFilePath, {
...currentState,
[key]: value,
});
store = readJsonSync(configFilePath);
},
} as Partial<Config> as Config<any>;
};
});

View File

@ -4,11 +4,13 @@
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import Config from "conf"; import Config from "conf";
import type { BaseStoreParams } from "../base-store"; import type { Options as ConfOptions } from "conf/dist/source/types";
export type GetConfigurationFileModel = <T extends object>(content: ConfOptions<T>) => Config<T>;
const getConfigurationFileModelInjectable = getInjectable({ const getConfigurationFileModelInjectable = getInjectable({
id: "get-configuration-file-model", id: "get-configuration-file-model",
instantiate: () => <T extends object>(content: BaseStoreParams<T>) => new Config(content), instantiate: (): GetConfigurationFileModel => (content) => new Config(content),
causesSideEffects: true, causesSideEffects: true,
}); });

View File

@ -2,15 +2,15 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type HelmRepo = { export interface HelmRepo {
name: string; name: string;
url: string; url: string;
cacheFilePath?: string; cacheFilePath: string;
caFile?: string; caFile?: string;
certFile?: string; certFile?: string;
insecureSkipTlsVerify?: boolean; insecureSkipTlsVerify?: boolean;
keyFile?: string; keyFile?: string;
username?: string; username?: string;
password?: string; password?: string;
}; }

View File

@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type { MigrationDeclaration } from "../base-store/migrations.injectable";
export const hotbarStoreMigrationInjectionToken = getInjectionToken<MigrationDeclaration>({
id: "hotbar-store-migration-token",
});

View File

@ -6,20 +6,33 @@ import { getInjectable } from "@ogre-tools/injectable";
import catalogCatalogEntityInjectable from "../catalog-entities/general-catalog-entities/implementations/catalog-catalog-entity.injectable"; import catalogCatalogEntityInjectable from "../catalog-entities/general-catalog-entities/implementations/catalog-catalog-entity.injectable";
import { HotbarStore } from "./store"; import { HotbarStore } from "./store";
import loggerInjectable from "../logger.injectable"; import loggerInjectable from "../logger.injectable";
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable";
import storeMigrationVersionInjectable from "../vars/store-migration-version.injectable";
import storeMigrationsInjectable from "../base-store/migrations.injectable";
import { hotbarStoreMigrationInjectionToken } from "./migrations-token";
import getBasenameOfPathInjectable from "../path/get-basename.injectable";
import { baseStoreIpcChannelPrefixesInjectionToken } from "../base-store/channel-prefix";
import { persistStateToConfigInjectionToken } from "../base-store/save-to-file";
import { enlistMessageChannelListenerInjectionToken } from "../utils/channel/enlist-message-channel-listener-injection-token";
import { shouldBaseStoreDisableSyncInIpcListenerInjectionToken } from "../base-store/disable-sync";
const hotbarStoreInjectable = getInjectable({ const hotbarStoreInjectable = getInjectable({
id: "hotbar-store", id: "hotbar-store",
instantiate: (di) => { instantiate: (di) => new HotbarStore({
HotbarStore.resetInstance(); catalogCatalogEntity: di.inject(catalogCatalogEntityInjectable),
logger: di.inject(loggerInjectable),
return HotbarStore.createInstance({ directoryForUserData: di.inject(directoryForUserDataInjectable),
catalogCatalogEntity: di.inject(catalogCatalogEntityInjectable), getConfigurationFileModel: di.inject(getConfigurationFileModelInjectable),
logger: di.inject(loggerInjectable), storeMigrationVersion: di.inject(storeMigrationVersionInjectable),
}); migrations: di.inject(storeMigrationsInjectable, hotbarStoreMigrationInjectionToken),
}, getBasenameOfPath: di.inject(getBasenameOfPathInjectable),
ipcChannelPrefixes: di.inject(baseStoreIpcChannelPrefixesInjectionToken),
causesSideEffects: true, persistStateToConfig: di.inject(persistStateToConfigInjectionToken),
enlistMessageChannelListener: di.inject(enlistMessageChannelListenerInjectionToken),
shouldDisableSyncInListener: di.inject(shouldBaseStoreDisableSyncInIpcListenerInjectionToken),
}),
}); });
export default hotbarStoreInjectable; export default hotbarStoreInjectable;

View File

@ -4,8 +4,8 @@
*/ */
import { action, comparer, observable, makeObservable, computed } from "mobx"; import { action, comparer, observable, makeObservable, computed } from "mobx";
import { BaseStore } from "../base-store"; import type { BaseStoreDependencies } from "../base-store/base-store";
import migrations from "../../migrations/hotbar-store"; import { BaseStore } from "../base-store/base-store";
import { toJS } from "../utils"; import { toJS } from "../utils";
import type { CatalogEntity } from "../catalog"; import type { CatalogEntity } from "../catalog";
import { broadcastMessage } from "../ipc"; import { broadcastMessage } from "../ipc";
@ -21,26 +21,23 @@ export interface HotbarStoreModel {
activeHotbarId: string; activeHotbarId: string;
} }
interface Dependencies { interface Dependencies extends BaseStoreDependencies {
readonly catalogCatalogEntity: GeneralEntity; readonly catalogCatalogEntity: GeneralEntity;
readonly logger: Logger; readonly logger: Logger;
} }
export class HotbarStore extends BaseStore<HotbarStoreModel> { export class HotbarStore extends BaseStore<HotbarStoreModel> {
readonly displayName = "HotbarStore";
@observable hotbars: Hotbar[] = []; @observable hotbars: Hotbar[] = [];
@observable private _activeHotbarId!: string; @observable private _activeHotbarId!: string;
constructor(private readonly dependencies: Dependencies) { constructor(protected readonly dependencies: Dependencies) {
super({ super(dependencies, {
configName: "lens-hotbar-store", configName: "lens-hotbar-store",
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
syncOptions: { syncOptions: {
equals: comparer.structural, equals: comparer.structural,
}, },
migrations,
}); });
makeObservable(this); makeObservable(this);
} }
@ -99,21 +96,19 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
this.hotbars.forEach(ensureExactHotbarItemLength); this.hotbars.forEach(ensureExactHotbarItemLength);
if (data.activeHotbarId) { if (data.activeHotbarId) {
this.setActiveHotbar(data.activeHotbarId); this._activeHotbarId = data.activeHotbarId;
} }
if (!this.activeHotbarId) { if (!this._activeHotbarId) {
this.setActiveHotbar(0); this._activeHotbarId = this.hotbars[0].id;
} }
} }
toJSON(): HotbarStoreModel { toJSON(): HotbarStoreModel {
const model: HotbarStoreModel = { return toJS({
hotbars: this.hotbars, hotbars: this.hotbars,
activeHotbarId: this.activeHotbarId, activeHotbarId: this.activeHotbarId,
}; });
return toJS(model);
} }
getActive(): Hotbar { getActive(): Hotbar {
@ -148,7 +143,7 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
const index = this.hotbars.findIndex((hotbar) => hotbar.id === id); const index = this.hotbars.findIndex((hotbar) => hotbar.id === id);
if (index < 0) { if (index < 0) {
return void console.warn( return this.dependencies.logger.warn(
`[HOTBAR-STORE]: cannot setHotbarName: unknown id`, `[HOTBAR-STORE]: cannot setHotbarName: unknown id`,
{ id }, { id },
); );

View File

@ -1,6 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export const openFilePickingDialogChannel = "dialog:open:file-picking";

View File

@ -1,8 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export const setNativeThemeChannel = "theme:set-native-theme";
export const getNativeThemeChannel = "theme:get-native-theme";

View File

@ -1,34 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import request from "request";
import requestPromise from "request-promise-native";
import { UserStore } from "./user-store";
// todo: get rid of "request" (deprecated)
// https://github.com/lensapp/lens/issues/459
function getDefaultRequestOpts(): Partial<request.Options> {
const { httpsProxy, allowUntrustedCAs } = UserStore.getInstance();
return {
proxy: httpsProxy || undefined,
rejectUnauthorized: !allowUntrustedCAs,
};
}
/**
* @deprecated
*/
export function customRequest(opts: request.Options) {
return request.defaults(getDefaultRequestOpts())(opts);
}
/**
* @deprecated
*/
export function customRequestPromise(opts: requestPromise.Options) {
return requestPromise.defaults(getDefaultRequestOpts())(opts);
}

View File

@ -8,6 +8,8 @@ import { createContainer, getInjectable, getInjectionToken } from "@ogre-tools/i
import type { Runnable } from "./run-many-for"; import type { Runnable } from "./run-many-for";
import { runManyFor } from "./run-many-for"; import { runManyFor } from "./run-many-for";
import { getPromiseStatus } from "../test-utils/get-promise-status"; import { getPromiseStatus } from "../test-utils/get-promise-status";
import { runInAction } from "mobx";
import { flushPromises } from "../test-utils/flush-promises";
describe("runManyFor", () => { describe("runManyFor", () => {
describe("given no hierarchy, when running many", () => { describe("given no hierarchy, when running many", () => {
@ -223,7 +225,68 @@ describe("runManyFor", () => {
); );
return expect(() => runMany()).rejects.toThrow( return expect(() => runMany()).rejects.toThrow(
/Tried to get a composite but encountered missing parent ids: "some-runnable-2".\n\nAvailable parent ids are:\n"[0-9a-z-]+",\n"some-runnable-1"/, /Runnable "some-runnable-1" is unreachable for injection token "some-injection-token": run afters "some-runnable-2" are a part of different injection tokens./,
);
});
it("given partially incorrect hierarchy, when running runnables, throws", () => {
const rootDi = createContainer("irrelevant");
const runMock = asyncFn<(...args: unknown[]) => void>();
const someInjectionToken = getInjectionToken<Runnable>({
id: "some-injection-token",
});
const someOtherInjectionToken = getInjectionToken<Runnable>({
id: "some-other-injection-token",
});
const someInjectable = getInjectable({
id: "some-runnable-1",
instantiate: (di) => ({
id: "some-runnable-1",
run: () => runMock("some-runnable-1"),
runAfter: [
di.inject(someOtherInjectable),
di.inject(someSecondInjectable),
],
}),
injectionToken: someInjectionToken,
});
const someSecondInjectable = getInjectable({
id: "some-runnable-2",
instantiate: () => ({
id: "some-runnable-2",
run: () => runMock("some-runnable-2"),
}),
injectionToken: someInjectionToken,
});
const someOtherInjectable = getInjectable({
id: "some-runnable-3",
instantiate: () => ({
id: "some-runnable-3",
run: () => runMock("some-runnable-3"),
}),
injectionToken: someOtherInjectionToken,
});
rootDi.register(someInjectable, someOtherInjectable, someSecondInjectable);
const runMany = runManyFor(rootDi)(
someInjectionToken,
);
return expect(() => runMany()).rejects.toThrow(
/Runnable "some-runnable-3" is not part of the injection token "some-injection-token"/,
); );
}); });
@ -279,4 +342,319 @@ describe("runManyFor", () => {
]); ]);
}); });
}); });
describe("given multiple runAfters", () => {
let runMock: AsyncFnMock<(...args: unknown[]) => void>;
let finishingPromise: Promise<void>;
beforeEach(async () => {
const rootDi = createContainer("irrelevant");
runMock = asyncFn<(...args: unknown[]) => void>();
const someInjectionToken = getInjectionToken<Runnable>({
id: "some-injection-token",
});
const runnableOneInjectable = getInjectable({
id: "runnable-1",
instantiate: () => ({
id: "runnable-1",
run: () => runMock("runnable-1"),
}),
injectionToken: someInjectionToken,
});
const runnableTwoInjectable = getInjectable({
id: "runnable-2",
instantiate: () => ({
id: "runnable-2",
run: () => runMock("runnable-2"),
runAfter: [], // shouldn't block being called
}),
injectionToken: someInjectionToken,
});
const runnableThreeInjectable = getInjectable({
id: "runnable-3",
instantiate: (di) => ({
id: "runnable-3",
run: () => runMock("runnable-3"),
runAfter: di.inject(runnableOneInjectable),
}),
injectionToken: someInjectionToken,
});
const runnableFourInjectable = getInjectable({
id: "runnable-4",
instantiate: (di) => ({
id: "runnable-4",
run: () => runMock("runnable-4"),
runAfter: [di.inject(runnableThreeInjectable)], // should be the same as an single item
}),
injectionToken: someInjectionToken,
});
const runnableFiveInjectable = getInjectable({
id: "runnable-5",
instantiate: (di) => ({
id: "runnable-5",
run: () => runMock("runnable-5"),
runAfter: di.inject(runnableThreeInjectable),
}),
injectionToken: someInjectionToken,
});
const runnableSixInjectable = getInjectable({
id: "runnable-6",
instantiate: (di) => ({
id: "runnable-6",
run: () => runMock("runnable-6"),
runAfter: [
di.inject(runnableFourInjectable),
di.inject(runnableFiveInjectable),
],
}),
injectionToken: someInjectionToken,
});
const runnableSevenInjectable = getInjectable({
id: "runnable-7",
instantiate: (di) => ({
id: "runnable-7",
run: () => runMock("runnable-7"),
runAfter: [
di.inject(runnableFiveInjectable),
di.inject(runnableSixInjectable),
],
}),
injectionToken: someInjectionToken,
});
runInAction(() => {
rootDi.register(
runnableOneInjectable,
runnableTwoInjectable,
runnableThreeInjectable,
runnableFourInjectable,
runnableFiveInjectable,
runnableSixInjectable,
runnableSevenInjectable,
);
});
const runMany = runManyFor(rootDi);
const runSome = runMany(someInjectionToken);
finishingPromise = runSome();
await flushPromises();
});
it("should run 'runnable-1'", () => {
expect(runMock).toBeCalledWith("runnable-1");
});
it("should run 'runnable-2'", () => {
expect(runMock).toBeCalledWith("runnable-2");
});
it("should not run 'runnable-3'", () => {
expect(runMock).not.toBeCalledWith("runnable-3");
});
describe("when 'runnable-1' resolves", () => {
beforeEach(async () => {
await runMock.resolveSpecific(["runnable-1"]);
});
it("should run 'runnable-3'", () => {
expect(runMock).toBeCalledWith("runnable-3");
});
describe("when 'runnable-2' resolves", () => {
beforeEach(async () => {
await runMock.resolveSpecific(["runnable-2"]);
});
it("shouldn't call any more runnables", () => {
expect(runMock).toBeCalledTimes(3);
});
});
describe("when 'runnable-3' resolves", () => {
beforeEach(async () => {
await runMock.resolveSpecific(["runnable-3"]);
});
it("should run 'runnable-4'", () => {
expect(runMock).toBeCalledWith("runnable-4");
});
it("should run 'runnable-5'", () => {
expect(runMock).toBeCalledWith("runnable-5");
});
describe("when 'runnable-2' resolves", () => {
beforeEach(async () => {
await runMock.resolveSpecific(["runnable-2"]);
});
it("shouldn't call any more runnables", () => {
expect(runMock).toBeCalledTimes(5);
});
});
describe("when 'runnable-4' resolves", () => {
beforeEach(async () => {
await runMock.resolveSpecific(["runnable-4"]);
});
it("shouldn't call any more runnables", () => {
expect(runMock).toBeCalledTimes(5);
});
describe("when 'runnable-2' resolves", () => {
beforeEach(async () => {
await runMock.resolveSpecific(["runnable-2"]);
});
it("shouldn't call any more runnables", () => {
expect(runMock).toBeCalledTimes(5);
});
});
describe("when 'runnable-5' resolves", () => {
beforeEach(async () => {
await runMock.resolveSpecific(["runnable-5"]);
});
it("should run 'runnable-6'", () => {
expect(runMock).toBeCalledWith("runnable-6");
});
describe("when 'runnable-2' resolves", () => {
beforeEach(async () => {
await runMock.resolveSpecific(["runnable-2"]);
});
it("shouldn't call any more runnables", () => {
expect(runMock).toBeCalledTimes(6);
});
});
describe("when 'runnable-6' resolves", () => {
beforeEach(async () => {
await runMock.resolveSpecific(["runnable-6"]);
});
it("should run 'runnable-7'", () => {
expect(runMock).toBeCalledWith("runnable-7");
});
describe("when 'runnable-2' resolves", () => {
beforeEach(async () => {
await runMock.resolveSpecific(["runnable-2"]);
});
it("shouldn't call any more runnables", () => {
expect(runMock).toBeCalledTimes(7);
});
describe("when 'runnable-7' resolves", () => {
beforeEach(async () => {
await runMock.resolveSpecific(["runnable-7"]);
});
it("should resolve the runMany promise call", async () => {
await finishingPromise;
});
});
});
});
});
});
describe("when 'runnable-5' resolves", () => {
beforeEach(async () => {
await runMock.resolveSpecific(["runnable-5"]);
});
it("shouldn't call any more runnables", () => {
expect(runMock).toBeCalledTimes(5);
});
describe("when 'runnable-2' resolves", () => {
beforeEach(async () => {
await runMock.resolveSpecific(["runnable-2"]);
});
it("shouldn't call any more runnables", () => {
expect(runMock).toBeCalledTimes(5);
});
});
describe("when 'runnable-4' resolves", () => {
beforeEach(async () => {
await runMock.resolveSpecific(["runnable-4"]);
});
it("should run 'runnable-6'", () => {
expect(runMock).toBeCalledWith("runnable-6");
});
describe("when 'runnable-2' resolves", () => {
beforeEach(async () => {
await runMock.resolveSpecific(["runnable-2"]);
});
it("shouldn't call any more runnables", () => {
expect(runMock).toBeCalledTimes(6);
});
});
describe("when 'runnable-6' resolves", () => {
beforeEach(async () => {
await runMock.resolveSpecific(["runnable-6"]);
});
it("should run 'runnable-7'", () => {
expect(runMock).toBeCalledWith("runnable-7");
});
describe("when 'runnable-2' resolves", () => {
beforeEach(async () => {
await runMock.resolveSpecific(["runnable-2"]);
});
it("shouldn't call any more runnables", () => {
expect(runMock).toBeCalledTimes(7);
});
describe("when 'runnable-7' resolves", () => {
beforeEach(async () => {
await runMock.resolveSpecific(["runnable-7"]);
});
it("should resolve the runMany promise call", async () => {
await finishingPromise;
});
});
});
});
});
});
});
});
describe("when 'runnable-2' resolves", () => {
beforeEach(async () => {
await runMock.resolveSpecific(["runnable-2"]);
});
it("shouldn't call any more runnables", () => {
expect(runMock).toBeCalledTimes(2);
});
});
});
}); });

View File

@ -3,46 +3,138 @@
* 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 type { DiContainerForInjection, InjectionToken } from "@ogre-tools/injectable"; import type { DiContainerForInjection, InjectionToken } from "@ogre-tools/injectable";
import type { Composite } from "../utils/composite/get-composite/get-composite"; import type { SingleOrMany } from "../utils";
import { getCompositeFor } from "../utils/composite/get-composite/get-composite"; import { getOrInsert, getOrInsertSetFor, isDefined } from "../utils";
import * as uuid from "uuid"; import * as uuid from "uuid";
import assert from "assert";
import type { Asyncify } from "type-fest";
import type TypedEventEmitter from "typed-emitter";
import EventEmitter from "events";
export interface Runnable<TParameter = void> { export interface Runnable<TParameter = void> {
id: string; id: string;
run: Run<TParameter>; run: Run<TParameter>;
runAfter?: Runnable<TParameter>; runAfter?: SingleOrMany<Runnable<TParameter>>;
} }
type Run<Param> = (parameter: Param) => Promise<void> | void; type Run<Param> = (parameter: Param) => Promise<void> | void;
export type RunMany = <Param>(injectionToken: InjectionToken<Runnable<Param>, void>) => Run<Param>; export type RunMany = <Param>(injectionToken: InjectionToken<Runnable<Param>, void>) => Asyncify<Run<Param>>;
async function runCompositeRunnables<Param>(param: Param, composite: Composite<Runnable<Param>>) { const computedNextEdge = (traversed: string[], graph: Map<string, Set<string>>, currentId: string, seenIds: Set<string>) => {
await composite.value.run(param); seenIds.add(currentId);
await Promise.all(composite.children.map(composite => runCompositeRunnables(param, composite))); const currentNode = graph.get(currentId);
assert(currentNode, `Runnable graph does not contain node with id="${currentId}"`);
for (const nextId of currentNode.values()) {
if (traversed.includes(nextId)) {
throw new Error(`Cycle in runnable graph: "${traversed.join(`" -> "`)}" -> "${nextId}"`);
}
computedNextEdge([...traversed, nextId], graph, nextId, seenIds);
}
};
const verifyRunnablesAreDAG = <Param>(injectionToken: InjectionToken<Runnable<Param>, void>, runnables: Runnable<Param>[]) => {
const rootId = uuid.v4();
const runnableGraph = new Map<string, Set<string>>();
const seenIds = new Set<string>();
const addRunnableId = getOrInsertSetFor(runnableGraph);
// Build the Directed graph
for (const runnable of runnables) {
addRunnableId(runnable.id);
if (!runnable.runAfter || (Array.isArray(runnable.runAfter) && runnable.runAfter.length === 0)) {
addRunnableId(rootId).add(runnable.id);
} else if (Array.isArray(runnable.runAfter)) {
for (const parentRunnable of runnable.runAfter) {
addRunnableId(parentRunnable.id).add(runnable.id);
}
} else {
addRunnableId(runnable.runAfter.id).add(runnable.id);
}
}
addRunnableId(rootId);
// Do a DFS to find any cycles
computedNextEdge([], runnableGraph, rootId, seenIds);
for (const id of runnableGraph.keys()) {
if (!seenIds.has(id)) {
const runnable = runnables.find(runnable => runnable.id === id);
if (!runnable) {
throw new Error(`Runnable "${id}" is not part of the injection token "${injectionToken.id}"`);
}
const runAfters = [runnable.runAfter]
.flat()
.filter(isDefined)
.map(runnable => runnable.id)
.join('", "');
throw new Error(`Runnable "${id}" is unreachable for injection token "${injectionToken.id}": run afters "${runAfters}" are a part of different injection tokens.`);
}
}
};
interface BarrierEvent {
finish: (id: string) => void;
} }
class DynamicBarrier {
private readonly finishedIds = new Map<string, Promise<void>>();
private readonly events: TypedEventEmitter<BarrierEvent> = new EventEmitter();
private initFinishingPromise(id: string): Promise<void> {
return getOrInsert(this.finishedIds, id, new Promise(resolve => {
const handler = (finishedId: string) => {
if (finishedId === id) {
resolve();
this.events.removeListener("finish", handler);
}
};
this.events.addListener("finish", handler);
}));
}
setFinished(id: string): void {
void this.initFinishingPromise(id);
this.events.emit("finish", id);
}
async blockOn(id: string): Promise<void> {
await this.initFinishingPromise(id);
}
}
const executeRunnableWith = <Param>(param: Param) => {
const barrier = new DynamicBarrier();
return async (runnable: Runnable<Param>): Promise<void> => {
const parentRunnables = [runnable.runAfter].flat().filter(isDefined);
for (const parentRunnable of parentRunnables) {
await barrier.blockOn(parentRunnable.id);
}
await runnable.run(param);
barrier.setFinished(runnable.id);
};
};
export function runManyFor(di: DiContainerForInjection): RunMany { export function runManyFor(di: DiContainerForInjection): RunMany {
return <Param>(injectionToken: InjectionToken<Runnable<Param>, void>) => async (param: Param) => { return <Param>(injectionToken: InjectionToken<Runnable<Param>, void>) => async (param: Param) => {
const executeRunnable = executeRunnableWith(param);
const allRunnables = di.injectMany(injectionToken); const allRunnables = di.injectMany(injectionToken);
const rootId = uuid.v4();
const getCompositeRunnables = getCompositeFor<Runnable<Param>>({
getId: (runnable) => runnable.id,
getParentId: (runnable) => (
runnable.id === rootId
? undefined
: runnable.runAfter?.id ?? rootId
),
});
const composite = getCompositeRunnables([
// This is a dummy runnable to conform to the requirements of `getCompositeFor` to only have one root
{
id: rootId,
run: () => {},
},
...allRunnables,
]);
await runCompositeRunnables(param, composite); verifyRunnablesAreDAG(injectionToken, allRunnables);
await Promise.all(allRunnables.map(executeRunnable));
}; };
} }

View File

@ -4,8 +4,6 @@
*/ */
import { getGlobalOverride } from "../test-utils/get-global-override"; import { getGlobalOverride } from "../test-utils/get-global-override";
import moveInjectable from "./move.injectable"; import currentTimezoneInjectable from "./current-timezone.injectable";
export default getGlobalOverride(moveInjectable, () => async () => { export default getGlobalOverride(currentTimezoneInjectable, () => "Etc/GMT");
throw new Error("tried to move without override");
});

View 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 moment from "moment-timezone";
const currentTimezoneInjectable = getInjectable({
id: "current-timezone",
instantiate: () => moment.tz.guess(true),
causesSideEffects: true,
});
export default currentTimezoneInjectable;

View File

@ -0,0 +1,18 @@
/**
* 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 { computed } from "mobx";
import userStoreInjectable from "./user-store.injectable";
const httpsProxyConfigurationInjectable = getInjectable({
id: "https-proxy-configuration",
instantiate: (di) => {
const userStore = di.inject(userStoreInjectable);
return computed(() => userStore.httpsProxy);
},
});
export default httpsProxyConfigurationInjectable;

View File

@ -7,11 +7,7 @@ import userStoreInjectable from "./user-store.injectable";
const kubeconfigSyncsInjectable = getInjectable({ const kubeconfigSyncsInjectable = getInjectable({
id: "kubeconfig-syncs", id: "kubeconfig-syncs",
instantiate: (di) => { instantiate: (di) => di.inject(userStoreInjectable).syncKubeconfigEntries,
const store = di.inject(userStoreInjectable);
return store.syncKubeconfigEntries;
},
}); });
export default kubeconfigSyncsInjectable; export default kubeconfigSyncsInjectable;

View File

@ -0,0 +1,37 @@
/**
* 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 { computed } from "mobx";
import userStoreInjectable from "./user-store.injectable";
export type LensColorThemePreference = {
useSystemTheme: true;
} | {
useSystemTheme: false;
lensThemeId: string;
};
const lensColorThemePreferenceInjectable = getInjectable({
id: "lens-color-theme-preference",
instantiate: (di) => {
const userStore = di.inject(userStoreInjectable);
return computed((): LensColorThemePreference => {
// TODO: remove magic strings
if (userStore.colorTheme === "system") {
return {
useSystemTheme: true,
};
}
return {
useSystemTheme: false,
lensThemeId: userStore.colorTheme,
};
});
},
});
export default lensColorThemePreferenceInjectable;

View File

@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type { MigrationDeclaration } from "../base-store/migrations.injectable";
export const userStoreMigrationInjectionToken = getInjectionToken<MigrationDeclaration>({
id: "user-store-migration-token",
});

View File

@ -0,0 +1,143 @@
/**
* 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 { merge } from "lodash";
import type { ObservableMap } from "mobx";
import { observable } from "mobx";
import homeDirectoryPathInjectable from "../os/home-directory-path.injectable";
import joinPathsInjectable from "../path/join-paths.injectable";
import { defaultThemeId } from "../vars";
import currentTimezoneInjectable from "./current-timezone.injectable";
import type { EditorConfiguration, ExtensionRegistry, KubeconfigSyncEntry, KubeconfigSyncValue, TerminalConfig } from "./preferences-helpers";
import { defaultExtensionRegistryUrlLocation, defaultEditorConfig, defaultTerminalConfig, defaultPackageMirror, getPreferenceDescriptor, packageMirrors } from "./preferences-helpers";
export type PreferenceDescriptors = ReturnType<typeof userStorePreferenceDescriptorsInjectable["instantiate"]>;
const userStorePreferenceDescriptorsInjectable = getInjectable({
id: "user-store-preference-descriptors",
instantiate: (di) => {
const currentTimezone = di.inject(currentTimezoneInjectable);
const joinPaths = di.inject(joinPathsInjectable);
const homeDirectoryPath = di.inject(homeDirectoryPathInjectable);
const mainKubeFolderPath = joinPaths(homeDirectoryPath, ".kube");
return ({
httpsProxy: getPreferenceDescriptor<string | undefined>({
fromStore: val => val,
toStore: val => val || undefined,
}),
shell: getPreferenceDescriptor<string | undefined>({
fromStore: val => val,
toStore: val => val || undefined,
}),
colorTheme: getPreferenceDescriptor<string>({
fromStore: val => val || defaultThemeId,
toStore: val => !val || val === defaultThemeId
? undefined
: val,
}),
terminalTheme: getPreferenceDescriptor<string>({
fromStore: val => val || "",
toStore: val => val || undefined,
}),
localeTimezone: getPreferenceDescriptor<string>({
fromStore: val => val || currentTimezone,
toStore: val => !val || val === currentTimezone
? undefined
: val,
}),
allowUntrustedCAs: getPreferenceDescriptor<boolean>({
fromStore: val => val ?? false,
toStore: val => !val
? undefined
: val,
}),
allowErrorReporting: getPreferenceDescriptor<boolean>({
fromStore: val => val ?? true,
toStore: val => val
? undefined
: val,
}),
downloadMirror: getPreferenceDescriptor<string>({
fromStore: val => !val || !packageMirrors.has(val)
? defaultPackageMirror
: val,
toStore: val => val === defaultPackageMirror
? undefined
: val,
}),
downloadKubectlBinaries: getPreferenceDescriptor<boolean>({
fromStore: val => val ?? true,
toStore: val => val
? undefined
: val,
}),
downloadBinariesPath: getPreferenceDescriptor<string | undefined>({
fromStore: val => val,
toStore: val => val || undefined,
}),
kubectlBinariesPath: getPreferenceDescriptor<string | undefined>({
fromStore: val => val,
toStore: val => val || undefined,
}),
openAtLogin: getPreferenceDescriptor<boolean>({
fromStore: val => val ?? false,
toStore: val => !val
? undefined
: val,
}),
terminalCopyOnSelect: getPreferenceDescriptor<boolean>({
fromStore: val => val ?? false,
toStore: val => !val
? undefined
: val,
}),
hiddenTableColumns: getPreferenceDescriptor<[string, string[]][], Map<string, Set<string>>>({
fromStore: (val = []) => new Map(
val.map(([tableId, columnIds]) => [tableId, new Set(columnIds)]),
),
toStore: (val) => {
const res: [string, string[]][] = [];
for (const [table, columns] of val) {
if (columns.size) {
res.push([table, Array.from(columns)]);
}
}
return res.length ? res : undefined;
},
}),
syncKubeconfigEntries: getPreferenceDescriptor<KubeconfigSyncEntry[], ObservableMap<string, KubeconfigSyncValue>>({
fromStore: val => observable.map(
val?.map(({ filePath, ...rest }) => [filePath, rest])
?? [[mainKubeFolderPath, {}]],
),
toStore: val => val.size === 1 && val.has(mainKubeFolderPath)
? undefined
: Array.from(val, ([filePath, rest]) => ({ filePath, ...rest })),
}),
editorConfiguration: getPreferenceDescriptor<Partial<EditorConfiguration>, EditorConfiguration>({
fromStore: val => merge(defaultEditorConfig, val),
toStore: val => val,
}),
terminalConfig: getPreferenceDescriptor<Partial<TerminalConfig>, TerminalConfig>({
fromStore: val => merge(defaultTerminalConfig, val),
toStore: val => val,
}),
extensionRegistryUrl: getPreferenceDescriptor<ExtensionRegistry>({
fromStore: val => val ?? {
location: defaultExtensionRegistryUrlLocation,
},
toStore: val => val.location === defaultExtensionRegistryUrlLocation
? undefined
: val,
}),
}) as const;
},
});
export default userStorePreferenceDescriptorsInjectable;

View File

@ -3,14 +3,9 @@
* 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 moment from "moment-timezone";
import path from "path";
import os from "os";
import type { editor } from "monaco-editor"; import type { editor } from "monaco-editor";
import merge from "lodash/merge"; import { defaultEditorFontFamily, defaultFontSize, defaultTerminalFontFamily } from "../vars";
import { defaultThemeId, defaultEditorFontFamily, defaultFontSize, defaultTerminalFontFamily } from "../vars"; import type { PreferenceDescriptors } from "./preference-descriptors.injectable";
import type { ObservableMap } from "mobx";
import { observable } from "mobx";
export interface KubeconfigSyncEntry extends KubeconfigSyncValue { export interface KubeconfigSyncEntry extends KubeconfigSyncValue {
filePath: string; filePath: string;
@ -54,86 +49,8 @@ export interface PreferenceDescription<T, R = T> {
toStore(val: R): T | undefined; toStore(val: R): T | undefined;
} }
const httpsProxy: PreferenceDescription<string | undefined> = { export const getPreferenceDescriptor = <T, R = T>(desc: PreferenceDescription<T, R>) => desc;
fromStore(val) {
return val;
},
toStore(val) {
return val || undefined;
},
};
const shell: PreferenceDescription<string | undefined> = {
fromStore(val) {
return val;
},
toStore(val) {
return val || undefined;
},
};
const colorTheme: PreferenceDescription<string> = {
fromStore(val) {
return val || defaultThemeId;
},
toStore(val) {
if (!val || val === defaultThemeId) {
return undefined;
}
return val;
},
};
const terminalTheme: PreferenceDescription<string> = {
fromStore(val) {
return val || "";
},
toStore(val) {
return val || undefined;
},
};
export const defaultLocaleTimezone = "UTC";
const localeTimezone: PreferenceDescription<string> = {
fromStore(val) {
return val || moment.tz.guess(true) || defaultLocaleTimezone;
},
toStore(val) {
if (!val || val === moment.tz.guess(true) || val === defaultLocaleTimezone) {
return undefined;
}
return val;
},
};
const allowUntrustedCAs: PreferenceDescription<boolean> = {
fromStore(val) {
return val ?? false;
},
toStore(val) {
if (!val) {
return undefined;
}
return val;
},
};
const allowErrorReporting: PreferenceDescription<boolean> = {
fromStore(val) {
return val ?? true;
},
toStore(val) {
if (val === true) {
return undefined;
}
return val;
},
};
export interface DownloadMirror { export interface DownloadMirror {
url: string; url: string;
@ -157,142 +74,6 @@ export const packageMirrors = new Map<string, DownloadMirror>([
}], }],
]); ]);
const downloadMirror: PreferenceDescription<string> = {
fromStore(val) {
return !val || !packageMirrors.has(val)
? defaultPackageMirror
: val;
},
toStore(val) {
if (!val || val === defaultPackageMirror) {
return undefined;
}
return val;
},
};
const downloadKubectlBinaries: PreferenceDescription<boolean> = {
fromStore(val) {
return val ?? true;
},
toStore(val) {
if (val === true) {
return undefined;
}
return val;
},
};
const downloadBinariesPath: PreferenceDescription<string | undefined> = {
fromStore(val) {
return val;
},
toStore(val) {
if (!val) {
return undefined;
}
return val;
},
};
const kubectlBinariesPath: PreferenceDescription<string | undefined> = {
fromStore(val) {
return val;
},
toStore(val) {
if (!val) {
return undefined;
}
return val;
},
};
const openAtLogin: PreferenceDescription<boolean> = {
fromStore(val) {
return val ?? false;
},
toStore(val) {
if (!val) {
return undefined;
}
return val;
},
};
const terminalCopyOnSelect: PreferenceDescription<boolean> = {
fromStore(val) {
return val ?? false;
},
toStore(val) {
if (!val) {
return undefined;
}
return val;
},
};
const hiddenTableColumns: PreferenceDescription<[string, string[]][], Map<string, Set<string>>> = {
fromStore(val) {
return new Map(
(val ?? []).map(([tableId, columnIds]) => [tableId, new Set(columnIds)]),
);
},
toStore(val) {
const res: [string, string[]][] = [];
for (const [table, columns] of val) {
if (columns.size) {
res.push([table, Array.from(columns)]);
}
}
return res.length ? res : undefined;
},
};
const mainKubeFolder = path.join(os.homedir(), ".kube");
const syncKubeconfigEntries: PreferenceDescription<KubeconfigSyncEntry[], ObservableMap<string, KubeconfigSyncValue>> = {
fromStore(val) {
return observable.map(
val
?.map(({ filePath, ...rest }) => [filePath, rest])
?? [[mainKubeFolder, {}]],
);
},
toStore(val) {
if (val.size === 1 && val.has(mainKubeFolder)) {
return undefined;
}
return Array.from(val, ([filePath, rest]) => ({ filePath, ...rest }));
},
};
const editorConfiguration: PreferenceDescription<Partial<EditorConfiguration> | undefined, EditorConfiguration> = {
fromStore(val) {
return merge(defaultEditorConfig, val);
},
toStore(val) {
return val;
},
};
const terminalConfig: PreferenceDescription<TerminalConfig, TerminalConfig> = {
fromStore(val) {
return merge(defaultTerminalConfig, val);
},
toStore(val) {
return val;
},
};
export type ExtensionRegistryLocation = "default" | "npmrc" | "custom"; export type ExtensionRegistryLocation = "default" | "npmrc" | "custom";
export type ExtensionRegistry = { export type ExtensionRegistry = {
@ -306,49 +87,13 @@ export type ExtensionRegistry = {
export const defaultExtensionRegistryUrlLocation = "default"; export const defaultExtensionRegistryUrlLocation = "default";
export const defaultExtensionRegistryUrl = "https://registry.npmjs.org"; export const defaultExtensionRegistryUrl = "https://registry.npmjs.org";
const extensionRegistryUrl: PreferenceDescription<ExtensionRegistry> = { type PreferencesModelType<field extends keyof PreferenceDescriptors> = PreferenceDescriptors[field] extends PreferenceDescription<infer T, any> ? T : never;
fromStore(val) { type UserStoreModelType<field extends keyof PreferenceDescriptors> = PreferenceDescriptors[field] extends PreferenceDescription<any, infer T> ? T : never;
return val ?? {
location: defaultExtensionRegistryUrlLocation,
};
},
toStore(val) {
if (val.location === defaultExtensionRegistryUrlLocation) {
return undefined;
}
return val;
},
};
type PreferencesModelType<field extends keyof typeof DESCRIPTORS> = typeof DESCRIPTORS[field] extends PreferenceDescription<infer T, any> ? T : never;
type UserStoreModelType<field extends keyof typeof DESCRIPTORS> = typeof DESCRIPTORS[field] extends PreferenceDescription<any, infer T> ? T : never;
export type UserStoreFlatModel = { export type UserStoreFlatModel = {
[field in keyof typeof DESCRIPTORS]: UserStoreModelType<field>; [field in keyof PreferenceDescriptors]: UserStoreModelType<field>;
}; };
export type UserPreferencesModel = { export type UserPreferencesModel = {
[field in keyof typeof DESCRIPTORS]: PreferencesModelType<field>; [field in keyof PreferenceDescriptors]: PreferencesModelType<field>;
} & { updateChannel: string }; } & { updateChannel: string };
export const DESCRIPTORS = {
httpsProxy,
shell,
colorTheme,
terminalTheme,
localeTimezone,
allowUntrustedCAs,
allowErrorReporting,
downloadMirror,
downloadKubectlBinaries,
downloadBinariesPath,
kubectlBinariesPath,
openAtLogin,
hiddenTableColumns,
syncKubeconfigEntries,
editorConfiguration,
terminalCopyOnSelect,
terminalConfig,
extensionRegistryUrl,
};

View File

@ -0,0 +1,37 @@
/**
* 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 { computed } from "mobx";
import userStoreInjectable from "./user-store.injectable";
export type TerminalThemePreference = {
matchLensTheme: true;
} | {
matchLensTheme: false;
themeId: string;
};
const terminalThemePreferenceInjectable = getInjectable({
id: "terminal-theme-preference",
instantiate: (di) => {
const userStore = di.inject(userStoreInjectable);
return computed((): TerminalThemePreference => {
// NOTE: remove use of magic strings
if (!userStore.terminalTheme) {
return {
matchLensTheme: true,
};
}
return {
matchLensTheme: false,
themeId: userStore.terminalTheme,
};
});
},
});
export default terminalThemePreferenceInjectable;

View File

@ -6,20 +6,37 @@ import { getInjectable } from "@ogre-tools/injectable";
import { UserStore } from "./user-store"; import { UserStore } from "./user-store";
import selectedUpdateChannelInjectable from "../../features/application-update/common/selected-update-channel/selected-update-channel.injectable"; import selectedUpdateChannelInjectable from "../../features/application-update/common/selected-update-channel/selected-update-channel.injectable";
import emitAppEventInjectable from "../app-event-bus/emit-event.injectable"; import emitAppEventInjectable from "../app-event-bus/emit-event.injectable";
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable";
import loggerInjectable from "../logger.injectable";
import storeMigrationVersionInjectable from "../vars/store-migration-version.injectable";
import storeMigrationsInjectable from "../base-store/migrations.injectable";
import { userStoreMigrationInjectionToken } from "./migrations-token";
import { baseStoreIpcChannelPrefixesInjectionToken } from "../base-store/channel-prefix";
import { shouldBaseStoreDisableSyncInIpcListenerInjectionToken } from "../base-store/disable-sync";
import { persistStateToConfigInjectionToken } from "../base-store/save-to-file";
import getBasenameOfPathInjectable from "../path/get-basename.injectable";
import { enlistMessageChannelListenerInjectionToken } from "../utils/channel/enlist-message-channel-listener-injection-token";
import userStorePreferenceDescriptorsInjectable from "./preference-descriptors.injectable";
const userStoreInjectable = getInjectable({ const userStoreInjectable = getInjectable({
id: "user-store", id: "user-store",
instantiate: (di) => { instantiate: (di) => new UserStore({
UserStore.resetInstance(); selectedUpdateChannel: di.inject(selectedUpdateChannelInjectable),
emitAppEvent: di.inject(emitAppEventInjectable),
return UserStore.createInstance({ directoryForUserData: di.inject(directoryForUserDataInjectable),
selectedUpdateChannel: di.inject(selectedUpdateChannelInjectable), getConfigurationFileModel: di.inject(getConfigurationFileModelInjectable),
emitAppEvent: di.inject(emitAppEventInjectable), logger: di.inject(loggerInjectable),
}); storeMigrationVersion: di.inject(storeMigrationVersionInjectable),
}, migrations: di.inject(storeMigrationsInjectable, userStoreMigrationInjectionToken),
getBasenameOfPath: di.inject(getBasenameOfPathInjectable),
causesSideEffects: true, ipcChannelPrefixes: di.inject(baseStoreIpcChannelPrefixesInjectionToken),
persistStateToConfig: di.inject(persistStateToConfigInjectionToken),
enlistMessageChannelListener: di.inject(enlistMessageChannelListenerInjectionToken),
shouldDisableSyncInListener: di.inject(shouldBaseStoreDisableSyncInIpcListenerInjectionToken),
preferenceDescriptors: di.inject(userStorePreferenceDescriptorsInjectable),
}),
}); });
export default userStoreInjectable; export default userStoreInjectable;

View File

@ -3,44 +3,37 @@
* 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 { app } from "electron"; import { action, observable, makeObservable, isObservableArray, isObservableSet, isObservableMap } from "mobx";
import { action, observable, reaction, makeObservable, isObservableArray, isObservableSet, isObservableMap } from "mobx"; import type { BaseStoreDependencies } from "../base-store/base-store";
import { BaseStore } from "../base-store"; import { BaseStore } from "../base-store/base-store";
import migrations from "../../migrations/user-store";
import { getOrInsertSet, toggle, toJS, object } from "../../renderer/utils"; import { getOrInsertSet, toggle, toJS, object } from "../../renderer/utils";
import { DESCRIPTORS } from "./preferences-helpers";
import type { UserPreferencesModel, StoreType } from "./preferences-helpers"; import type { UserPreferencesModel, StoreType } from "./preferences-helpers";
import logger from "../../main/logger";
import type { EmitAppEvent } from "../app-event-bus/emit-event.injectable"; import type { EmitAppEvent } from "../app-event-bus/emit-event.injectable";
// TODO: Remove coupling with Feature // TODO: Remove coupling with Feature
import type { SelectedUpdateChannel } from "../../features/application-update/common/selected-update-channel/selected-update-channel.injectable"; import type { SelectedUpdateChannel } from "../../features/application-update/common/selected-update-channel/selected-update-channel.injectable";
import type { ReleaseChannel } from "../../features/application-update/common/update-channels"; import type { ReleaseChannel } from "../../features/application-update/common/update-channels";
import type { PreferenceDescriptors } from "./preference-descriptors.injectable";
export interface UserStoreModel { export interface UserStoreModel {
lastSeenAppVersion: string;
preferences: UserPreferencesModel; preferences: UserPreferencesModel;
} }
interface Dependencies { interface Dependencies extends BaseStoreDependencies {
readonly selectedUpdateChannel: SelectedUpdateChannel; readonly selectedUpdateChannel: SelectedUpdateChannel;
readonly preferenceDescriptors: PreferenceDescriptors;
emitAppEvent: EmitAppEvent; emitAppEvent: EmitAppEvent;
} }
export class UserStore extends BaseStore<UserStoreModel> /* implements UserStoreFlatModel (when strict null is enabled) */ { export class UserStore extends BaseStore<UserStoreModel> /* implements UserStoreFlatModel (when strict null is enabled) */ {
readonly displayName = "UserStore"; constructor(protected readonly dependencies: Dependencies) {
super(dependencies, {
constructor(private readonly dependencies: Dependencies) {
super({
configName: "lens-user-store", configName: "lens-user-store",
migrations,
}); });
makeObservable(this); makeObservable(this);
} }
@observable lastSeenAppVersion = "0.0.0";
/** /**
* @deprecated No longer used * @deprecated No longer used
*/ */
@ -51,58 +44,45 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
*/ */
@observable newContexts = observable.set<string>(); @observable newContexts = observable.set<string>();
@observable allowErrorReporting!: StoreType<typeof DESCRIPTORS["allowErrorReporting"]>; @observable allowErrorReporting!: StoreType<PreferenceDescriptors["allowErrorReporting"]>;
@observable allowUntrustedCAs!: StoreType<typeof DESCRIPTORS["allowUntrustedCAs"]>; @observable allowUntrustedCAs!: StoreType<PreferenceDescriptors["allowUntrustedCAs"]>;
@observable colorTheme!: StoreType<typeof DESCRIPTORS["colorTheme"]>; @observable colorTheme!: StoreType<PreferenceDescriptors["colorTheme"]>;
@observable terminalTheme!: StoreType<typeof DESCRIPTORS["terminalTheme"]>; @observable terminalTheme!: StoreType<PreferenceDescriptors["terminalTheme"]>;
@observable localeTimezone!: StoreType<typeof DESCRIPTORS["localeTimezone"]>; @observable localeTimezone!: StoreType<PreferenceDescriptors["localeTimezone"]>;
@observable downloadMirror!: StoreType<typeof DESCRIPTORS["downloadMirror"]>; @observable downloadMirror!: StoreType<PreferenceDescriptors["downloadMirror"]>;
@observable httpsProxy!: StoreType<typeof DESCRIPTORS["httpsProxy"]>; @observable httpsProxy!: StoreType<PreferenceDescriptors["httpsProxy"]>;
@observable shell!: StoreType<typeof DESCRIPTORS["shell"]>; @observable shell!: StoreType<PreferenceDescriptors["shell"]>;
@observable downloadBinariesPath!: StoreType<typeof DESCRIPTORS["downloadBinariesPath"]>; @observable downloadBinariesPath!: StoreType<PreferenceDescriptors["downloadBinariesPath"]>;
@observable kubectlBinariesPath!: StoreType<typeof DESCRIPTORS["kubectlBinariesPath"]>; @observable kubectlBinariesPath!: StoreType<PreferenceDescriptors["kubectlBinariesPath"]>;
@observable terminalCopyOnSelect!: StoreType<typeof DESCRIPTORS["terminalCopyOnSelect"]>; @observable terminalCopyOnSelect!: StoreType<PreferenceDescriptors["terminalCopyOnSelect"]>;
@observable terminalConfig!: StoreType<typeof DESCRIPTORS["terminalConfig"]>; @observable terminalConfig!: StoreType<PreferenceDescriptors["terminalConfig"]>;
@observable extensionRegistryUrl!: StoreType<typeof DESCRIPTORS["extensionRegistryUrl"]>; @observable extensionRegistryUrl!: StoreType<PreferenceDescriptors["extensionRegistryUrl"]>;
/** /**
* Download kubectl binaries matching cluster version * Download kubectl binaries matching cluster version
*/ */
@observable downloadKubectlBinaries!: StoreType<typeof DESCRIPTORS["downloadKubectlBinaries"]>; @observable downloadKubectlBinaries!: StoreType<PreferenceDescriptors["downloadKubectlBinaries"]>;
/** /**
* Whether the application should open itself at login. * Whether the application should open itself at login.
*/ */
@observable openAtLogin!: StoreType<typeof DESCRIPTORS["openAtLogin"]>; @observable openAtLogin!: StoreType<PreferenceDescriptors["openAtLogin"]>;
/** /**
* The column IDs under each configurable table ID that have been configured * The column IDs under each configurable table ID that have been configured
* to not be shown * to not be shown
*/ */
@observable hiddenTableColumns!: StoreType<typeof DESCRIPTORS["hiddenTableColumns"]>; @observable hiddenTableColumns!: StoreType<PreferenceDescriptors["hiddenTableColumns"]>;
/** /**
* Monaco editor configs * Monaco editor configs
*/ */
@observable editorConfiguration!: StoreType<typeof DESCRIPTORS["editorConfiguration"]>; @observable editorConfiguration!: StoreType<PreferenceDescriptors["editorConfiguration"]>;
/** /**
* The set of file/folder paths to be synced * The set of file/folder paths to be synced
*/ */
@observable syncKubeconfigEntries!: StoreType<typeof DESCRIPTORS["syncKubeconfigEntries"]>; @observable syncKubeconfigEntries!: StoreType<PreferenceDescriptors["syncKubeconfigEntries"]>;
startMainReactions() {
// open at system start-up
reaction(() => this.openAtLogin, openAtLogin => {
app.setLoginItemSettings({
openAtLogin,
openAsHidden: true,
args: ["--hidden"],
});
}, {
fireImmediately: true,
});
}
/** /**
* Checks if a column (by ID) for a table (by ID) is configured to be hidden * Checks if a column (by ID) for a table (by ID) is configured to be hidden
@ -133,18 +113,14 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
@action @action
resetTheme() { resetTheme() {
this.colorTheme = DESCRIPTORS.colorTheme.fromStore(undefined); this.colorTheme = this.dependencies.preferenceDescriptors.colorTheme.fromStore(undefined);
} }
@action @action
protected fromStore({ lastSeenAppVersion, preferences }: Partial<UserStoreModel> = {}) { protected fromStore({ preferences }: Partial<UserStoreModel> = {}) {
logger.debug("UserStore.fromStore()", { lastSeenAppVersion, preferences }); this.dependencies.logger.debug("UserStore.fromStore()", { preferences });
if (lastSeenAppVersion) { for (const [key, { fromStore }] of object.entries(this.dependencies.preferenceDescriptors)) {
this.lastSeenAppVersion = lastSeenAppVersion;
}
for (const [key, { fromStore }] of object.entries(DESCRIPTORS)) {
const curVal = this[key]; const curVal = this[key];
const newVal = fromStore((preferences)?.[key] as never) as never; const newVal = fromStore((preferences)?.[key] as never) as never;
@ -165,16 +141,13 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
toJSON(): UserStoreModel { toJSON(): UserStoreModel {
const preferences = object.fromEntries( const preferences = object.fromEntries(
object.entries(DESCRIPTORS) object.entries(this.dependencies.preferenceDescriptors)
.map(([key, { toStore }]) => [key, toStore(this[key] as never)]), .map(([key, { toStore }]) => [key, toStore(this[key] as never)]),
) as UserPreferencesModel; ) as UserPreferencesModel;
return toJS({ return toJS({
lastSeenAppVersion: this.lastSeenAppVersion,
preferences: { preferences: {
...preferences, ...preferences,
updateChannel: this.dependencies.selectedUpdateChannel.value.get().id, updateChannel: this.dependencies.selectedUpdateChannel.value.get().id,
}, },
}); });

View File

@ -48,11 +48,21 @@ export function getOrInsertSet<K, SK>(map: Map<K, Set<SK>>, key: K): Set<SK> {
return getOrInsert(map, key, new Set<SK>()); return getOrInsert(map, key, new Set<SK>());
} }
/**
* A currying version of {@link getOrInsertSet}
*/
export function getOrInsertSetFor<K, SK>(map: Map<K, Set<SK>>): (key: K) => Set<SK> {
return (key) => getOrInsertSet(map, key);
}
/** /**
* Like `getOrInsert` but with delayed creation of the item. Which is useful * Like `getOrInsert` but with delayed creation of the item. Which is useful
* if it is very expensive to create the initial value. * if it is very expensive to create the initial value.
*/ */
export function getOrInsertWith<K, V>(map: Map<K, V>, key: K, builder: () => V): V { export function getOrInsertWith<K, V>(map: Map<K, V>, key: K, builder: () => V): V;
export function getOrInsertWith<K extends object, V>(map: Map<K, V> | WeakMap<K, V>, key: K, builder: () => V): V;
export function getOrInsertWith<K extends object, V>(map: Map<K, V> | WeakMap<K, V>, key: K, builder: () => V): V {
if (!map.has(key)) { if (!map.has(key)) {
map.set(key, builder()); map.set(key, builder());
} }

View File

@ -0,0 +1,17 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "../test-utils/get-global-override";
import randomBytesInjectable from "./random-bytes.injectable";
export default getGlobalOverride(randomBytesInjectable, () => async (size) => {
const res = Buffer.alloc(size);
for (let i = 0; i < size; i += 1) {
res[i] = i;
}
return res;
});

View File

@ -0,0 +1,17 @@
/**
* 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 { randomBytes } from "crypto";
import { promisify } from "util";
export type RandomBytes = (size: number) => Promise<Buffer>;
const randomBytesInjectable = getInjectable({
id: "random-bytes",
instantiate: (): RandomBytes => promisify(randomBytes),
causesSideEffects: true,
});
export default randomBytesInjectable;

View File

@ -3,10 +3,13 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
interface StaticThis<T, R extends any[]> { new(...args: R): T } export interface StaticThis<T, R extends any[]> { new(...args: R): T }
/**
* @deprecated This is a form of global shared state
*/
export class Singleton { export class Singleton {
private static instances = new WeakMap<object, Singleton>(); private static readonly instances = new WeakMap<object, Singleton>();
private static creating = ""; private static creating = "";
constructor() { constructor() {

View File

@ -123,6 +123,10 @@ export function isDefined<T>(val: T | undefined | null): val is T {
return val != null; return val != null;
} }
export function isFunction(val: unknown): val is (...args: unknown[]) => unknown {
return typeof val === "function";
}
/** /**
* Checks if the value in the second position is non-nullable * Checks if the value in the second position is non-nullable
*/ */
@ -146,6 +150,15 @@ export function hasDefiniteField<Field extends keyof T, T>(field: Field): (val:
return (val): val is T & { [f in Field]-?: NonNullable<T[Field]> } => val[field] != null; return (val): val is T & { [f in Field]-?: NonNullable<T[Field]> } => val[field] != null;
} }
export function isPromiseLike(res: unknown): res is (Promise<unknown> | { then: (fn: (val: unknown) => any) => Promise<unknown> }) {
if (res instanceof Promise) {
return true;
}
return isObject(res)
&& hasTypedProperty(res, "then", isFunction);
}
export function isPromiseSettledRejected<T>(result: PromiseSettledResult<T>): result is PromiseRejectedResult { export function isPromiseSettledRejected<T>(result: PromiseSettledResult<T>): result is PromiseRejectedResult {
return result.status === "rejected"; return result.status === "rejected";
} }

View File

@ -4,7 +4,7 @@
*/ */
// App's common configuration for any process (main, renderer, build pipeline, etc.) // App's common configuration for any process (main, renderer, build pipeline, etc.)
import type { ThemeId } from "../renderer/themes/store"; import type { ThemeId } from "../renderer/themes/lens-theme";
/** /**
* @deprecated Switch to using isMacInjectable * @deprecated Switch to using isMacInjectable
@ -55,12 +55,4 @@ export const apiKubePrefix = "/api-kube"; // k8s cluster apis
export const issuesTrackerUrl = "https://github.com/lensapp/lens/issues" as string; export const issuesTrackerUrl = "https://github.com/lensapp/lens/issues" as string;
export const slackUrl = "https://join.slack.com/t/k8slens/shared_invite/zt-wcl8jq3k-68R5Wcmk1o95MLBE5igUDQ" as string; export const slackUrl = "https://join.slack.com/t/k8slens/shared_invite/zt-wcl8jq3k-68R5Wcmk1o95MLBE5igUDQ" as string;
export const supportUrl = "https://docs.k8slens.dev/support/" as string; export const supportUrl = "https://docs.k8slens.dev/support/" as string;
export const lensWebsiteWeblinkId = "lens-website-link";
export const lensDocumentationWeblinkId = "lens-documentation-link";
export const lensSlackWeblinkId = "lens-slack-link";
export const lensTwitterWeblinkId = "lens-twitter-link";
export const lensBlogWeblinkId = "lens-blog-link";
export const kubernetesDocumentationWeblinkId = "kubernetes-documentation-link";
export const docsUrl = "https://docs.k8slens.dev" as string; export const docsUrl = "https://docs.k8slens.dev" as string;

View File

@ -1,18 +0,0 @@
/**
* 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 { WeblinkStore } from "./weblink-store";
const weblinkStoreInjectable = getInjectable({
id: "weblink-store",
instantiate: () => {
WeblinkStore.resetInstance();
return WeblinkStore.createInstance();
},
});
export default weblinkStoreInjectable;

View File

@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type { MigrationDeclaration } from "../base-store/migrations.injectable";
export const weblinkStoreMigrationInjectionToken = getInjectionToken<MigrationDeclaration>({
id: "weblink-store-migration-token",
});

View File

@ -0,0 +1,35 @@
/**
* 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 directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
import { baseStoreIpcChannelPrefixesInjectionToken } from "../base-store/channel-prefix";
import { shouldBaseStoreDisableSyncInIpcListenerInjectionToken } from "../base-store/disable-sync";
import storeMigrationsInjectable from "../base-store/migrations.injectable";
import { persistStateToConfigInjectionToken } from "../base-store/save-to-file";
import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable";
import loggerInjectable from "../logger.injectable";
import getBasenameOfPathInjectable from "../path/get-basename.injectable";
import { enlistMessageChannelListenerInjectionToken } from "../utils/channel/enlist-message-channel-listener-injection-token";
import storeMigrationVersionInjectable from "../vars/store-migration-version.injectable";
import { weblinkStoreMigrationInjectionToken } from "./migration-token";
import { WeblinkStore } from "./weblink-store";
const weblinkStoreInjectable = getInjectable({
id: "weblink-store",
instantiate: (di) => new WeblinkStore({
directoryForUserData: di.inject(directoryForUserDataInjectable),
getConfigurationFileModel: di.inject(getConfigurationFileModelInjectable),
logger: di.inject(loggerInjectable),
storeMigrationVersion: di.inject(storeMigrationVersionInjectable),
migrations: di.inject(storeMigrationsInjectable, weblinkStoreMigrationInjectionToken),
getBasenameOfPath: di.inject(getBasenameOfPathInjectable),
ipcChannelPrefixes: di.inject(baseStoreIpcChannelPrefixesInjectionToken),
persistStateToConfig: di.inject(persistStateToConfigInjectionToken),
enlistMessageChannelListener: di.inject(enlistMessageChannelListenerInjectionToken),
shouldDisableSyncInListener: di.inject(shouldBaseStoreDisableSyncInIpcListenerInjectionToken),
}),
});
export default weblinkStoreInjectable;

View File

@ -4,10 +4,10 @@
*/ */
import { action, comparer, observable, makeObservable } from "mobx"; import { action, comparer, observable, makeObservable } from "mobx";
import { BaseStore } from "./base-store"; import type { BaseStoreDependencies } from "../base-store/base-store";
import migrations from "../migrations/weblinks-store"; import { BaseStore } from "../base-store/base-store";
import * as uuid from "uuid"; import * as uuid from "uuid";
import { toJS } from "./utils"; import { toJS } from "../utils";
export interface WeblinkData { export interface WeblinkData {
id: string; id: string;
@ -27,17 +27,15 @@ export interface WeblinkStoreModel {
} }
export class WeblinkStore extends BaseStore<WeblinkStoreModel> { export class WeblinkStore extends BaseStore<WeblinkStoreModel> {
readonly displayName = "WeblinkStore";
@observable weblinks: WeblinkData[] = []; @observable weblinks: WeblinkData[] = [];
constructor() { constructor(deps: BaseStoreDependencies) {
super({ super(deps, {
configName: "lens-weblink-store", configName: "lens-weblink-store",
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
syncOptions: { syncOptions: {
equals: comparer.structural, equals: comparer.structural,
}, },
migrations,
}); });
makeObservable(this); makeObservable(this);
this.load(); this.load();

View File

@ -14,6 +14,8 @@ import { delay } from "../../renderer/utils";
import { getDiForUnitTesting } from "../../renderer/getDiForUnitTesting"; import { getDiForUnitTesting } from "../../renderer/getDiForUnitTesting";
import ipcRendererInjectable from "../../renderer/utils/channel/ipc-renderer.injectable"; import ipcRendererInjectable from "../../renderer/utils/channel/ipc-renderer.injectable";
import type { IpcRenderer } from "electron"; import type { IpcRenderer } from "electron";
import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import currentlyInClusterFrameInjectable from "../../renderer/routes/currently-in-cluster-frame.injectable";
console = new Console(stdout, stderr); console = new Console(stdout, stderr);
@ -28,6 +30,9 @@ describe("ExtensionLoader", () => {
beforeEach(() => { beforeEach(() => {
const di = getDiForUnitTesting({ doGeneralOverrides: true }); const di = getDiForUnitTesting({ doGeneralOverrides: true });
di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data");
di.override(currentlyInClusterFrameInjectable, () => false);
di.override(ipcRendererInjectable, () => ({ di.override(ipcRendererInjectable, () => ({
invoke: jest.fn(async (channel: string) => { invoke: jest.fn(async (channel: string) => {
if (channel === "extension-loader:main:state") { if (channel === "extension-loader:main:state") {

View File

@ -3,7 +3,8 @@
* 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 { UserStore } from "../../common/user-store"; import userStoreInjectable from "../../common/user-store/user-store.injectable";
import { asLegacyGlobalForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
export interface UserPreferenceExtensionItems { export interface UserPreferenceExtensionItems {
/** /**
* Get the configured kubectl binaries path. * Get the configured kubectl binaries path.
@ -11,6 +12,8 @@ export interface UserPreferenceExtensionItems {
getKubectlPath: () => string | undefined; getKubectlPath: () => string | undefined;
} }
const userStore = asLegacyGlobalForExtensionApi(userStoreInjectable);
export const Preferences: UserPreferenceExtensionItems = { export const Preferences: UserPreferenceExtensionItems = {
getKubectlPath: () => UserStore.getInstance().kubectlBinariesPath, getKubectlPath: () => userStore.kubectlBinariesPath,
}; };

View File

@ -25,7 +25,7 @@ import getBasenameOfPathInjectable from "../../common/path/get-basename.injectab
import getDirnameOfPathInjectable from "../../common/path/get-dirname.injectable"; import getDirnameOfPathInjectable from "../../common/path/get-dirname.injectable";
import getRelativePathInjectable from "../../common/path/get-relative-path.injectable"; import getRelativePathInjectable from "../../common/path/get-relative-path.injectable";
import joinPathsInjectable from "../../common/path/join-paths.injectable"; import joinPathsInjectable from "../../common/path/join-paths.injectable";
import removePathInjectable from "../../common/fs/remove-path.injectable"; import removePathInjectable from "../../common/fs/remove.injectable";
import homeDirectoryPathInjectable from "../../common/os/home-directory-path.injectable"; import homeDirectoryPathInjectable from "../../common/os/home-directory-path.injectable";
import applicationInformationInjectable from "../../common/vars/application-information.injectable"; import applicationInformationInjectable from "../../common/vars/application-information.injectable";
import lensResourcesDirInjectable from "../../common/vars/lens-resources-dir.injectable"; import lensResourcesDirInjectable from "../../common/vars/lens-resources-dir.injectable";

View File

@ -15,10 +15,13 @@ import readJsonFileInjectable from "../../common/fs/read-json-file.injectable";
import pathExistsInjectable from "../../common/fs/path-exists.injectable"; import pathExistsInjectable from "../../common/fs/path-exists.injectable";
import watchInjectable from "../../common/fs/watch/watch.injectable"; import watchInjectable from "../../common/fs/watch/watch.injectable";
import extensionApiVersionInjectable from "../../common/vars/extension-api-version.injectable"; import extensionApiVersionInjectable from "../../common/vars/extension-api-version.injectable";
import removePathInjectable from "../../common/fs/remove-path.injectable"; import removePathInjectable from "../../common/fs/remove.injectable";
import type { JoinPaths } from "../../common/path/join-paths.injectable"; import type { JoinPaths } from "../../common/path/join-paths.injectable";
import joinPathsInjectable from "../../common/path/join-paths.injectable"; import joinPathsInjectable from "../../common/path/join-paths.injectable";
import homeDirectoryPathInjectable from "../../common/os/home-directory-path.injectable"; import homeDirectoryPathInjectable from "../../common/os/home-directory-path.injectable";
import pathExistsSyncInjectable from "../../common/fs/path-exists-sync.injectable";
import readJsonSyncInjectable from "../../common/fs/read-json-sync.injectable";
import writeJsonSyncInjectable from "../../common/fs/write-json-sync.injectable";
describe("ExtensionDiscovery", () => { describe("ExtensionDiscovery", () => {
let extensionDiscovery: ExtensionDiscovery; let extensionDiscovery: ExtensionDiscovery;
@ -34,6 +37,9 @@ describe("ExtensionDiscovery", () => {
di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data"); di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data");
di.override(installExtensionInjectable, () => () => Promise.resolve()); di.override(installExtensionInjectable, () => () => Promise.resolve());
di.override(extensionApiVersionInjectable, () => "5.0.0"); di.override(extensionApiVersionInjectable, () => "5.0.0");
di.override(pathExistsSyncInjectable, () => () => { throw new Error("tried call pathExistsSync without override"); });
di.override(readJsonSyncInjectable, () => () => { throw new Error("tried call readJsonSync without override"); });
di.override(writeJsonSyncInjectable, () => () => { throw new Error("tried call writeJsonSync without override"); });
joinPaths = di.inject(joinPathsInjectable); joinPaths = di.inject(joinPathsInjectable);
homeDirectoryPath = di.inject(homeDirectoryPathInjectable); homeDirectoryPath = di.inject(homeDirectoryPathInjectable);

View File

@ -28,7 +28,7 @@ import type { JoinPaths } from "../../common/path/join-paths.injectable";
import type { GetBasenameOfPath } from "../../common/path/get-basename.injectable"; import type { GetBasenameOfPath } from "../../common/path/get-basename.injectable";
import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable"; import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable";
import type { GetRelativePath } from "../../common/path/get-relative-path.injectable"; import type { GetRelativePath } from "../../common/path/get-relative-path.injectable";
import type { RemovePath } from "../../common/fs/remove-path.injectable"; import type { RemovePath } from "../../common/fs/remove.injectable";
import type TypedEventEmitter from "typed-emitter"; import type TypedEventEmitter from "typed-emitter";
import type { ApplicationInformation } from "../../common/vars/application-information.injectable"; import type { ApplicationInformation } from "../../common/vars/application-information.injectable";

View File

@ -9,12 +9,9 @@ import extensionPackageRootDirectoryInjectable from "./extension-package-root-di
const extensionInstallerInjectable = getInjectable({ const extensionInstallerInjectable = getInjectable({
id: "extension-installer", id: "extension-installer",
instantiate: (di) => instantiate: (di) => new ExtensionInstaller({
new ExtensionInstaller({ extensionPackageRootDirectory: di.inject(extensionPackageRootDirectoryInjectable),
extensionPackageRootDirectory: di.inject( }),
extensionPackageRootDirectoryInjectable,
),
}),
}); });
export default extensionInstallerInjectable; export default extensionInstallerInjectable;

View File

@ -3,8 +3,7 @@
* 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 directoryForUserDataInjectable import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
const extensionPackageRootDirectoryInjectable = getInjectable({ const extensionPackageRootDirectoryInjectable = getInjectable({
id: "extension-package-root-directory", id: "extension-package-root-directory",

View File

@ -5,19 +5,38 @@
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { FileSystemProvisionerStore } from "./file-system-provisioner-store"; import { FileSystemProvisionerStore } from "./file-system-provisioner-store";
import directoryForExtensionDataInjectable from "./directory-for-extension-data.injectable"; import directoryForExtensionDataInjectable from "./directory-for-extension-data.injectable";
import ensureDirectoryInjectable from "../../../common/fs/ensure-dir.injectable";
import joinPathsInjectable from "../../../common/path/join-paths.injectable";
import randomBytesInjectable from "../../../common/utils/random-bytes.injectable";
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import getConfigurationFileModelInjectable from "../../../common/get-configuration-file-model/get-configuration-file-model.injectable";
import loggerInjectable from "../../../common/logger.injectable";
import storeMigrationVersionInjectable from "../../../common/vars/store-migration-version.injectable";
import { baseStoreIpcChannelPrefixesInjectionToken } from "../../../common/base-store/channel-prefix";
import { shouldBaseStoreDisableSyncInIpcListenerInjectionToken } from "../../../common/base-store/disable-sync";
import { persistStateToConfigInjectionToken } from "../../../common/base-store/save-to-file";
import getBasenameOfPathInjectable from "../../../common/path/get-basename.injectable";
import { enlistMessageChannelListenerInjectionToken } from "../../../common/utils/channel/enlist-message-channel-listener-injection-token";
const fileSystemProvisionerStoreInjectable = getInjectable({ const fileSystemProvisionerStoreInjectable = getInjectable({
id: "file-system-provisioner-store", id: "file-system-provisioner-store",
instantiate: (di) => { instantiate: (di) => new FileSystemProvisionerStore({
FileSystemProvisionerStore.resetInstance(); directoryForExtensionData: di.inject(directoryForExtensionDataInjectable),
ensureDirectory: di.inject(ensureDirectoryInjectable),
return FileSystemProvisionerStore.createInstance({ joinPaths: di.inject(joinPathsInjectable),
directoryForExtensionData: di.inject(directoryForExtensionDataInjectable), randomBytes: di.inject(randomBytesInjectable),
}); directoryForUserData: di.inject(directoryForUserDataInjectable),
}, getConfigurationFileModel: di.inject(getConfigurationFileModelInjectable),
logger: di.inject(loggerInjectable),
causesSideEffects: true, storeMigrationVersion: di.inject(storeMigrationVersionInjectable),
migrations: {},
getBasenameOfPath: di.inject(getBasenameOfPathInjectable),
ipcChannelPrefixes: di.inject(baseStoreIpcChannelPrefixesInjectionToken),
persistStateToConfig: di.inject(persistStateToConfigInjectionToken),
enlistMessageChannelListener: di.inject(enlistMessageChannelListenerInjectionToken),
shouldDisableSyncInListener: di.inject(shouldBaseStoreDisableSyncInIpcListenerInjectionToken),
}),
}); });
export default fileSystemProvisionerStoreInjectable; export default fileSystemProvisionerStoreInjectable;

View File

@ -3,35 +3,37 @@
* 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 { randomBytes } from "crypto";
import { SHA256 } from "crypto-js"; import { SHA256 } from "crypto-js";
import fse from "fs-extra";
import { action, makeObservable, observable } from "mobx"; import { action, makeObservable, observable } from "mobx";
import path from "path"; import type { BaseStoreDependencies } from "../../../common/base-store/base-store";
import { BaseStore } from "../../../common/base-store"; import { BaseStore } from "../../../common/base-store/base-store";
import type { LensExtensionId } from "../../lens-extension"; import type { LensExtensionId } from "../../lens-extension";
import { getOrInsertWith, toJS } from "../../../common/utils"; import { getOrInsertWithAsync, toJS } from "../../../common/utils";
import type { EnsureDirectory } from "../../../common/fs/ensure-dir.injectable";
import type { JoinPaths } from "../../../common/path/join-paths.injectable";
import type { RandomBytes } from "../../../common/utils/random-bytes.injectable";
interface FSProvisionModel { interface FSProvisionModel {
extensions: Record<string, string>; // extension names to paths extensions: Record<string, string>; // extension names to paths
} }
interface Dependencies { interface Dependencies extends BaseStoreDependencies {
directoryForExtensionData: string; readonly directoryForExtensionData: string;
ensureDirectory: EnsureDirectory;
joinPaths: JoinPaths;
randomBytes: RandomBytes;
} }
export class FileSystemProvisionerStore extends BaseStore<FSProvisionModel> { export class FileSystemProvisionerStore extends BaseStore<FSProvisionModel> {
readonly displayName = "FilesystemProvisionerStore"; readonly registeredExtensions = observable.map<LensExtensionId, string>();
registeredExtensions = observable.map<LensExtensionId, string>();
constructor(private dependencies: Dependencies) { constructor(protected readonly dependencies: Dependencies) {
super({ super(dependencies, {
configName: "lens-filesystem-provisioner-store", configName: "lens-filesystem-provisioner-store",
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
}); });
makeObservable(this); makeObservable(this);
this.load();
} }
/** /**
@ -41,14 +43,14 @@ export class FileSystemProvisionerStore extends BaseStore<FSProvisionModel> {
* @returns path to the folder that the extension can safely write files to. * @returns path to the folder that the extension can safely write files to.
*/ */
async requestDirectory(extensionName: string): Promise<string> { async requestDirectory(extensionName: string): Promise<string> {
const dirPath = getOrInsertWith(this.registeredExtensions, extensionName, () => { const dirPath = await getOrInsertWithAsync(this.registeredExtensions, extensionName, async () => {
const salt = randomBytes(32).toString("hex"); const salt = (await this.dependencies.randomBytes(32)).toString("hex");
const hashedName = SHA256(`${extensionName}/${salt}`).toString(); const hashedName = SHA256(`${extensionName}/${salt}`).toString();
return path.resolve(this.dependencies.directoryForExtensionData, hashedName); return this.dependencies.joinPaths(this.dependencies.directoryForExtensionData, hashedName);
}); });
await fse.ensureDir(dirPath); await this.dependencies.ensureDirectory(dirPath);
return dirPath; return dirPath;
} }

View File

@ -3,13 +3,76 @@
* 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 { BaseStore } from "../common/base-store"; import type { BaseStoreParams } from "../common/base-store/base-store";
import { BaseStore } from "../common/base-store/base-store";
import * as path from "path"; import * as path from "path";
import type { LensExtension } from "./lens-extension"; import type { LensExtension } from "./lens-extension";
import assert from "assert"; import assert from "assert";
import type { StaticThis } from "../common/utils";
import { getOrInsertWith } from "../common/utils";
import { getLegacyGlobalDiForExtensionApi } from "./as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
import directoryForUserDataInjectable from "../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import getConfigurationFileModelInjectable from "../common/get-configuration-file-model/get-configuration-file-model.injectable";
import loggerInjectable from "../common/logger.injectable";
import storeMigrationVersionInjectable from "../common/vars/store-migration-version.injectable";
import type { Migrations } from "conf/dist/source/types";
import { baseStoreIpcChannelPrefixesInjectionToken } from "../common/base-store/channel-prefix";
import { shouldBaseStoreDisableSyncInIpcListenerInjectionToken } from "../common/base-store/disable-sync";
import { persistStateToConfigInjectionToken } from "../common/base-store/save-to-file";
import getBasenameOfPathInjectable from "../common/path/get-basename.injectable";
import { enlistMessageChannelListenerInjectionToken } from "../common/utils/channel/enlist-message-channel-listener-injection-token";
export interface ExtensionStoreParams<T extends object> extends BaseStoreParams<T> {
migrations?: Migrations<T>;
}
export abstract class ExtensionStore<T extends object> extends BaseStore<T> { export abstract class ExtensionStore<T extends object> extends BaseStore<T> {
readonly displayName = "ExtensionStore<T>"; private static readonly instances = new WeakMap<object, ExtensionStore<object>>();
/**
* @deprecated This is a form of global shared state. Just call `new Store(...)`
*/
static createInstance<T extends ExtensionStore<object>, R extends any[]>(this: StaticThis<T, R>, ...args: R): T {
return getOrInsertWith(ExtensionStore.instances, this, () => new this(...args)) as T;
}
/**
* @deprecated This is a form of global shared state. Just call `new Store(...)`
*/
static getInstance<T, R extends any[]>(this: StaticThis<T, R>, strict?: true): T;
static getInstance<T, R extends any[]>(this: StaticThis<T, R>, strict: false): T | undefined;
static getInstance<T, R extends any[]>(this: StaticThis<T, R>, strict = true): T | undefined {
if (!ExtensionStore.instances.has(this) && strict) {
throw new TypeError(`instance of ${this.name} is not created`);
}
return ExtensionStore.instances.get(this) as (T | undefined);
}
constructor({ migrations, ...params }: ExtensionStoreParams<T>) {
const di = getLegacyGlobalDiForExtensionApi();
super({
directoryForUserData: di.inject(directoryForUserDataInjectable),
getConfigurationFileModel: di.inject(getConfigurationFileModelInjectable),
logger: di.inject(loggerInjectable),
storeMigrationVersion: di.inject(storeMigrationVersionInjectable),
migrations: migrations as Migrations<Record<string, unknown>>,
getBasenameOfPath: di.inject(getBasenameOfPathInjectable),
ipcChannelPrefixes: di.inject(baseStoreIpcChannelPrefixesInjectionToken),
persistStateToConfig: di.inject(persistStateToConfigInjectionToken),
enlistMessageChannelListener: di.inject(enlistMessageChannelListenerInjectionToken),
shouldDisableSyncInListener: di.inject(shouldBaseStoreDisableSyncInIpcListenerInjectionToken),
}, params);
}
/**
* @deprecated This is a form of global shared state. Just call `new Store(...)`
*/
static resetInstance() {
ExtensionStore.instances.delete(this);
}
protected extension?: LensExtension; protected extension?: LensExtension;
loadExtension(extension: LensExtension) { loadExtension(extension: LensExtension) {

View File

@ -3,18 +3,31 @@
* 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 directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import { baseStoreIpcChannelPrefixesInjectionToken } from "../../common/base-store/channel-prefix";
import { shouldBaseStoreDisableSyncInIpcListenerInjectionToken } from "../../common/base-store/disable-sync";
import { persistStateToConfigInjectionToken } from "../../common/base-store/save-to-file";
import getConfigurationFileModelInjectable from "../../common/get-configuration-file-model/get-configuration-file-model.injectable";
import loggerInjectable from "../../common/logger.injectable";
import getBasenameOfPathInjectable from "../../common/path/get-basename.injectable";
import { enlistMessageChannelListenerInjectionToken } from "../../common/utils/channel/enlist-message-channel-listener-injection-token";
import storeMigrationVersionInjectable from "../../common/vars/store-migration-version.injectable";
import { ExtensionsStore } from "./extensions-store"; import { ExtensionsStore } from "./extensions-store";
const extensionsStoreInjectable = getInjectable({ const extensionsStoreInjectable = getInjectable({
id: "extensions-store", id: "extensions-store",
instantiate: (di) => new ExtensionsStore({
instantiate: () => { directoryForUserData: di.inject(directoryForUserDataInjectable),
ExtensionsStore.resetInstance(); getConfigurationFileModel: di.inject(getConfigurationFileModelInjectable),
logger: di.inject(loggerInjectable),
return ExtensionsStore.createInstance(); storeMigrationVersion: di.inject(storeMigrationVersionInjectable),
}, migrations: {},
getBasenameOfPath: di.inject(getBasenameOfPathInjectable),
causesSideEffects: true, ipcChannelPrefixes: di.inject(baseStoreIpcChannelPrefixesInjectionToken),
persistStateToConfig: di.inject(persistStateToConfigInjectionToken),
enlistMessageChannelListener: di.inject(enlistMessageChannelListenerInjectionToken),
shouldDisableSyncInListener: di.inject(shouldBaseStoreDisableSyncInIpcListenerInjectionToken),
}),
}); });
export default extensionsStoreInjectable; export default extensionsStoreInjectable;

View File

@ -6,7 +6,8 @@
import type { LensExtensionId } from "../lens-extension"; import type { LensExtensionId } from "../lens-extension";
import { action, computed, makeObservable, observable } from "mobx"; import { action, computed, makeObservable, observable } from "mobx";
import { toJS } from "../../common/utils"; import { toJS } from "../../common/utils";
import { BaseStore } from "../../common/base-store"; import type { BaseStoreDependencies } from "../../common/base-store/base-store";
import { BaseStore } from "../../common/base-store/base-store";
export interface LensExtensionsStoreModel { export interface LensExtensionsStoreModel {
extensions: Record<LensExtensionId, LensExtensionState>; extensions: Record<LensExtensionId, LensExtensionState>;
@ -23,9 +24,8 @@ export interface IsEnabledExtensionDescriptor {
} }
export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> { export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
readonly displayName = "ExtensionsStore"; constructor(deps: BaseStoreDependencies) {
constructor() { super(deps, {
super({
configName: "lens-extensions", configName: "lens-extensions",
}); });
makeObservable(this); makeObservable(this);

View File

@ -4,7 +4,7 @@
*/ */
import activeThemeInjectable from "../../renderer/themes/active.injectable"; import activeThemeInjectable from "../../renderer/themes/active.injectable";
import type { LensTheme } from "../../renderer/themes/store"; import type { LensTheme } from "../../renderer/themes/lens-theme";
import { asLegacyGlobalForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api"; import { asLegacyGlobalForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
export const activeTheme = asLegacyGlobalForExtensionApi(activeThemeInjectable); export const activeTheme = asLegacyGlobalForExtensionApi(activeThemeInjectable);

View File

@ -164,7 +164,23 @@ exports[`extension special characters in page registrations renders 1`] = `
<div <div
class="HotbarCell isDraggingOwner animateDown" class="HotbarCell isDraggingOwner animateDown"
index="0" index="0"
/> >
<div
style="z-index: 12; position: absolute;"
>
<div
class="HotbarIcon contextMenuAvailable"
>
<div
class="Avatar rounded disabled avatar"
id="hotbarIcon-hotbar-icon-catalog-entity"
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
>
Ca
</div>
</div>
</div>
</div>
<div <div
class="HotbarCell isDraggingOwner animateDown" class="HotbarCell isDraggingOwner animateDown"
index="1" index="1"
@ -231,7 +247,7 @@ exports[`extension special characters in page registrations renders 1`] = `
class="badge Badge small clickable" class="badge Badge small clickable"
id="hotbarIndex" id="hotbarIndex"
> >
0 1
</div> </div>
</div> </div>
<i <i
@ -352,7 +368,23 @@ exports[`extension special characters in page registrations when navigating to r
<div <div
class="HotbarCell isDraggingOwner animateDown" class="HotbarCell isDraggingOwner animateDown"
index="0" index="0"
/> >
<div
style="z-index: 12; position: absolute;"
>
<div
class="HotbarIcon contextMenuAvailable"
>
<div
class="Avatar rounded disabled avatar"
id="hotbarIcon-hotbar-icon-catalog-entity"
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
>
Ca
</div>
</div>
</div>
</div>
<div <div
class="HotbarCell isDraggingOwner animateDown" class="HotbarCell isDraggingOwner animateDown"
index="1" index="1"
@ -419,7 +451,7 @@ exports[`extension special characters in page registrations when navigating to r
class="badge Badge small clickable" class="badge Badge small clickable"
id="hotbarIndex" id="hotbarIndex"
> >
0 1
</div> </div>
</div> </div>
<i <i

View File

@ -164,7 +164,23 @@ exports[`navigate to extension page renders 1`] = `
<div <div
class="HotbarCell isDraggingOwner animateDown" class="HotbarCell isDraggingOwner animateDown"
index="0" index="0"
/> >
<div
style="z-index: 12; position: absolute;"
>
<div
class="HotbarIcon contextMenuAvailable"
>
<div
class="Avatar rounded disabled avatar"
id="hotbarIcon-hotbar-icon-catalog-entity"
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
>
Ca
</div>
</div>
</div>
</div>
<div <div
class="HotbarCell isDraggingOwner animateDown" class="HotbarCell isDraggingOwner animateDown"
index="1" index="1"
@ -231,7 +247,7 @@ exports[`navigate to extension page renders 1`] = `
class="badge Badge small clickable" class="badge Badge small clickable"
id="hotbarIndex" id="hotbarIndex"
> >
0 1
</div> </div>
</div> </div>
<i <i
@ -352,7 +368,23 @@ exports[`navigate to extension page when extension navigates to child route rend
<div <div
class="HotbarCell isDraggingOwner animateDown" class="HotbarCell isDraggingOwner animateDown"
index="0" index="0"
/> >
<div
style="z-index: 12; position: absolute;"
>
<div
class="HotbarIcon contextMenuAvailable"
>
<div
class="Avatar rounded disabled avatar"
id="hotbarIcon-hotbar-icon-catalog-entity"
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
>
Ca
</div>
</div>
</div>
</div>
<div <div
class="HotbarCell isDraggingOwner animateDown" class="HotbarCell isDraggingOwner animateDown"
index="1" index="1"
@ -419,7 +451,7 @@ exports[`navigate to extension page when extension navigates to child route rend
class="badge Badge small clickable" class="badge Badge small clickable"
id="hotbarIndex" id="hotbarIndex"
> >
0 1
</div> </div>
</div> </div>
<i <i
@ -556,7 +588,23 @@ exports[`navigate to extension page when extension navigates to route with param
<div <div
class="HotbarCell isDraggingOwner animateDown" class="HotbarCell isDraggingOwner animateDown"
index="0" index="0"
/> >
<div
style="z-index: 12; position: absolute;"
>
<div
class="HotbarIcon contextMenuAvailable"
>
<div
class="Avatar rounded disabled avatar"
id="hotbarIcon-hotbar-icon-catalog-entity"
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
>
Ca
</div>
</div>
</div>
</div>
<div <div
class="HotbarCell isDraggingOwner animateDown" class="HotbarCell isDraggingOwner animateDown"
index="1" index="1"
@ -623,7 +671,7 @@ exports[`navigate to extension page when extension navigates to route with param
class="badge Badge small clickable" class="badge Badge small clickable"
id="hotbarIndex" id="hotbarIndex"
> >
0 1
</div> </div>
</div> </div>
<i <i
@ -760,7 +808,23 @@ exports[`navigate to extension page when extension navigates to route without pa
<div <div
class="HotbarCell isDraggingOwner animateDown" class="HotbarCell isDraggingOwner animateDown"
index="0" index="0"
/> >
<div
style="z-index: 12; position: absolute;"
>
<div
class="HotbarIcon contextMenuAvailable"
>
<div
class="Avatar rounded disabled avatar"
id="hotbarIcon-hotbar-icon-catalog-entity"
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
>
Ca
</div>
</div>
</div>
</div>
<div <div
class="HotbarCell isDraggingOwner animateDown" class="HotbarCell isDraggingOwner animateDown"
index="1" index="1"
@ -827,7 +891,7 @@ exports[`navigate to extension page when extension navigates to route without pa
class="badge Badge small clickable" class="badge Badge small clickable"
id="hotbarIndex" id="hotbarIndex"
> >
0 1
</div> </div>
</div> </div>
<i <i
@ -964,7 +1028,23 @@ exports[`navigate to extension page when extension navigates to route without pa
<div <div
class="HotbarCell isDraggingOwner animateDown" class="HotbarCell isDraggingOwner animateDown"
index="0" index="0"
/> >
<div
style="z-index: 12; position: absolute;"
>
<div
class="HotbarIcon contextMenuAvailable"
>
<div
class="Avatar rounded disabled avatar"
id="hotbarIcon-hotbar-icon-catalog-entity"
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
>
Ca
</div>
</div>
</div>
</div>
<div <div
class="HotbarCell isDraggingOwner animateDown" class="HotbarCell isDraggingOwner animateDown"
index="1" index="1"
@ -1031,7 +1111,7 @@ exports[`navigate to extension page when extension navigates to route without pa
class="badge Badge small clickable" class="badge Badge small clickable"
id="hotbarIndex" id="hotbarIndex"
> >
0 1
</div> </div>
</div> </div>
<i <i

View File

@ -88,7 +88,23 @@ exports[`navigating between routes given route with optional path parameters whe
<div <div
class="HotbarCell isDraggingOwner animateDown" class="HotbarCell isDraggingOwner animateDown"
index="0" index="0"
/> >
<div
style="z-index: 12; position: absolute;"
>
<div
class="HotbarIcon contextMenuAvailable"
>
<div
class="Avatar rounded disabled avatar"
id="hotbarIcon-hotbar-icon-catalog-entity"
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
>
Ca
</div>
</div>
</div>
</div>
<div <div
class="HotbarCell isDraggingOwner animateDown" class="HotbarCell isDraggingOwner animateDown"
index="1" index="1"
@ -155,7 +171,7 @@ exports[`navigating between routes given route with optional path parameters whe
class="badge Badge small clickable" class="badge Badge small clickable"
id="hotbarIndex" id="hotbarIndex"
> >
0 1
</div> </div>
</div> </div>
<i <i
@ -276,7 +292,23 @@ exports[`navigating between routes given route without path parameters when navi
<div <div
class="HotbarCell isDraggingOwner animateDown" class="HotbarCell isDraggingOwner animateDown"
index="0" index="0"
/> >
<div
style="z-index: 12; position: absolute;"
>
<div
class="HotbarIcon contextMenuAvailable"
>
<div
class="Avatar rounded disabled avatar"
id="hotbarIcon-hotbar-icon-catalog-entity"
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
>
Ca
</div>
</div>
</div>
</div>
<div <div
class="HotbarCell isDraggingOwner animateDown" class="HotbarCell isDraggingOwner animateDown"
index="1" index="1"
@ -343,7 +375,7 @@ exports[`navigating between routes given route without path parameters when navi
class="badge Badge small clickable" class="badge Badge small clickable"
id="hotbarIndex" id="hotbarIndex"
> >
0 1
</div> </div>
</div> </div>
<i <i

View File

@ -164,7 +164,23 @@ exports[`add-cluster - navigation using application menu renders 1`] = `
<div <div
class="HotbarCell isDraggingOwner animateDown" class="HotbarCell isDraggingOwner animateDown"
index="0" index="0"
/> >
<div
style="z-index: 12; position: absolute;"
>
<div
class="HotbarIcon contextMenuAvailable"
>
<div
class="Avatar rounded disabled avatar"
id="hotbarIcon-hotbar-icon-catalog-entity"
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
>
Ca
</div>
</div>
</div>
</div>
<div <div
class="HotbarCell isDraggingOwner animateDown" class="HotbarCell isDraggingOwner animateDown"
index="1" index="1"
@ -231,7 +247,7 @@ exports[`add-cluster - navigation using application menu renders 1`] = `
class="badge Badge small clickable" class="badge Badge small clickable"
id="hotbarIndex" id="hotbarIndex"
> >
0 1
</div> </div>
</div> </div>
<i <i
@ -436,7 +452,23 @@ exports[`add-cluster - navigation using application menu when navigating to add
<div <div
class="HotbarCell isDraggingOwner animateDown" class="HotbarCell isDraggingOwner animateDown"
index="0" index="0"
/> >
<div
style="z-index: 12; position: absolute;"
>
<div
class="HotbarIcon contextMenuAvailable"
>
<div
class="Avatar rounded disabled avatar"
id="hotbarIcon-hotbar-icon-catalog-entity"
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
>
Ca
</div>
</div>
</div>
</div>
<div <div
class="HotbarCell isDraggingOwner animateDown" class="HotbarCell isDraggingOwner animateDown"
index="1" index="1"
@ -503,7 +535,7 @@ exports[`add-cluster - navigation using application menu when navigating to add
class="badge Badge small clickable" class="badge Badge small clickable"
id="hotbarIndex" id="hotbarIndex"
> >
0 1
</div> </div>
</div> </div>
<i <i

Some files were not shown because too many files have changed in this diff Show More