diff --git a/__mocks__/monaco-editor.ts b/__mocks__/monaco-editor.ts deleted file mode 100644 index 1623a6f00bc2..000000000000 --- a/__mocks__/monaco-editor.ts +++ /dev/null @@ -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: () => ({}), -}; diff --git a/__mocks__/react-beautiful-dnd.tsx b/__mocks__/react-beautiful-dnd.tsx index 79eec81f6ede..e3549c2968d0 100644 --- a/__mocks__/react-beautiful-dnd.tsx +++ b/__mocks__/react-beautiful-dnd.tsx @@ -7,9 +7,49 @@ import React from "react"; import type { DragDropContextProps, DraggableProps, + DraggableProvidedDraggableProps, DroppableProps, + DroppableProvidedProps, } from "react-beautiful-dnd"; export const DragDropContext = ({ children }: DragDropContextProps) => <>{ children }; -export const Draggable = ({ children }: DraggableProps) => <>{ children({} as any, {} as any, {} as any) }; -export const Droppable = ({ children }: DroppableProps) => <>{ children({} as any, {} as any) }; +export const Draggable = ({ children }: DraggableProps) => ( + <> + { + 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, + }, + ) + } + +); diff --git a/build/build_theme_vars.ts b/build/build_theme_vars.ts deleted file mode 100644 index dbade4584725..000000000000 --- a/build/build_theme_vars.ts +++ /dev/null @@ -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); diff --git a/package.json b/package.json index e49acd1a2f3e..14205c6c4e6a 100644 --- a/package.json +++ b/package.json @@ -231,6 +231,7 @@ "grapheme-splitter": "^1.0.4", "handlebars": "^4.7.7", "history": "^4.10.1", + "hpagent": "^1.2.0", "http-proxy": "^1.18.1", "immer": "^9.0.16", "joi": "^17.7.0", diff --git a/src/common/__tests__/base-store.test.ts b/src/common/__tests__/base-store.test.ts deleted file mode 100644 index e6b1c2d1d26c..000000000000 --- a/src/common/__tests__/base-store.test.ts +++ /dev/null @@ -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 { - @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 = {}) { - 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); - }); - }); -}); diff --git a/src/common/__tests__/cluster-store.test.ts b/src/common/__tests__/cluster-store.test.ts index 2253c7662fc3..28357ae5ffb1 100644 --- a/src/common/__tests__/cluster-store.test.ts +++ b/src/common/__tests__/cluster-store.test.ts @@ -93,10 +93,9 @@ describe("cluster-store", () => { mainDi.override(normalizedPlatformInjectable, () => "darwin"); mainDi.permitSideEffects(getConfigurationFileModelInjectable); - mainDi.permitSideEffects(clusterStoreInjectable); - mainDi.permitSideEffects(fsInjectable); + mainDi.unoverride(getConfigurationFileModelInjectable); - mainDi.unoverride(clusterStoreInjectable); + mainDi.permitSideEffects(fsInjectable); }); afterEach(() => { @@ -107,23 +106,19 @@ describe("cluster-store", () => { let getCustomKubeConfigDirectory: (directoryName: string) => string; beforeEach(async () => { - getCustomKubeConfigDirectory = mainDi.inject( - getCustomKubeConfigDirectoryInjectable, - ); + getCustomKubeConfigDirectory = mainDi.inject(getCustomKubeConfigDirectoryInjectable); - const mockOpts = { + mockFs({ "some-directory-for-user-data": { "lens-cluster-store.json": JSON.stringify({}), }, - }; - - mockFs(mockOpts); + }); createCluster = mainDi.inject(createClusterInjectionToken); clusterStore = mainDi.inject(clusterStoreInjectable); - clusterStore.unregisterIpcListener(); + clusterStore.load(); }); afterEach(() => { @@ -207,7 +202,7 @@ describe("cluster-store", () => { describe("config with existing clusters", () => { beforeEach(() => { - const mockOpts = { + mockFs({ "temp-kube-config": kubeconfig, "some-directory-for-user-data": { "lens-cluster-store.json": JSON.stringify({ @@ -241,13 +236,12 @@ describe("cluster-store", () => { ], }), }, - }; - - mockFs(mockOpts); + }); createCluster = mainDi.inject(createClusterInjectionToken); clusterStore = mainDi.inject(clusterStoreInjectable); + clusterStore.load(); }); afterEach(() => { @@ -297,7 +291,7 @@ users: token: kubeconfig-user-q4lm4:xxxyyyy `; - const mockOpts = { + mockFs({ "invalid-kube-config": invalidKubeconfig, "valid-kube-config": kubeconfig, "some-directory-for-user-data": { @@ -325,13 +319,12 @@ users: ], }), }, - }; - - mockFs(mockOpts); + }); createCluster = mainDi.inject(createClusterInjectionToken); clusterStore = mainDi.inject(clusterStoreInjectable); + clusterStore.load(); }); afterEach(() => { @@ -347,7 +340,7 @@ users: describe("pre 3.6.0-beta.1 config with an existing cluster", () => { beforeEach(() => { - const mockOpts = { + mockFs({ "some-directory-for-user-data": { "lens-cluster-store.json": JSON.stringify({ __internal__: { @@ -368,15 +361,14 @@ users: }), icon_path: testDataIcon, }, - }; - - mockFs(mockOpts); + }); mainDi.override(storeMigrationVersionInjectable, () => "3.6.0"); createCluster = mainDi.inject(createClusterInjectionToken); clusterStore = mainDi.inject(clusterStoreInjectable); + clusterStore.load(); }); afterEach(() => { diff --git a/src/common/__tests__/hotbar-store.test.ts b/src/common/__tests__/hotbar-store.test.ts index 0e1b3e27a297..a0a618d4dfe1 100644 --- a/src/common/__tests__/hotbar-store.test.ts +++ b/src/common/__tests__/hotbar-store.test.ts @@ -19,6 +19,7 @@ import loggerInjectable from "../logger.injectable"; import type { Logger } from "../logger"; import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; import storeMigrationVersionInjectable from "../vars/store-migration-version.injectable"; +import fsInjectable from "../fs/fs.injectable"; function getMockCatalogEntity(data: Partial & CatalogEntityKindData): CatalogEntity { return { @@ -46,7 +47,7 @@ describe("HotbarStore", () => { beforeEach(async () => { di = getDiForUnitTesting({ doGeneralOverrides: true }); - (di as any).unoverride(hotbarStoreInjectable); + di.unoverride(hotbarStoreInjectable); testCluster = getMockCatalogEntity({ apiVersion: "v1", @@ -112,8 +113,9 @@ describe("HotbarStore", () => { catalogCatalogEntity, ])); + di.permitSideEffects(fsInjectable); di.permitSideEffects(getConfigurationFileModelInjectable); - di.permitSideEffects(hotbarStoreInjectable); + di.unoverride(getConfigurationFileModelInjectable); }); afterEach(() => { @@ -255,22 +257,12 @@ describe("HotbarStore", () => { }); 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); expect(() => hotbarStore.restackItems(-5, 0)).toThrow(); expect(() => hotbarStore.restackItems(2, -1)).toThrow(); expect(() => hotbarStore.restackItems(14, 1)).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", () => { @@ -284,7 +276,7 @@ describe("HotbarStore", () => { describe("given data from 5.0.0-beta.3 and version being 5.0.0-beta.10", () => { beforeEach(() => { - const configurationToBeMigrated = { + mockFs({ "some-directory-for-user-data": { "lens-hotbar-store.json": JSON.stringify({ __internal__: { @@ -344,9 +336,7 @@ describe("HotbarStore", () => { ], }), }, - }; - - mockFs(configurationToBeMigrated); + }); di.override(storeMigrationVersionInjectable, () => "5.0.0-beta.10"); diff --git a/src/common/__tests__/user-store.test.ts b/src/common/__tests__/user-store.test.ts index eeadbc1e6c8e..8ae213d412e5 100644 --- a/src/common/__tests__/user-store.test.ts +++ b/src/common/__tests__/user-store.test.ts @@ -35,6 +35,7 @@ import getConfigurationFileModelInjectable from "../get-configuration-file-model import storeMigrationVersionInjectable from "../vars/store-migration-version.injectable"; import releaseChannelInjectable from "../vars/release-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); @@ -49,8 +50,10 @@ describe("user store tests", () => { di.override(writeFileInjectable, () => () => Promise.resolve()); di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); + di.permitSideEffects(getConfigurationFileModelInjectable); - di.permitSideEffects(userStoreInjectable); + di.unoverride(getConfigurationFileModelInjectable); + di.permitSideEffects(fsInjectable); di.override(releaseChannelInjectable, () => ({ get: () => "latest" as const, @@ -58,7 +61,7 @@ describe("user store tests", () => { })); await di.inject(defaultUpdateChannelInjectable).init(); - di.unoverride(userStoreInjectable); + userStore = di.inject(userStoreInjectable); }); afterEach(() => { @@ -67,17 +70,11 @@ describe("user store tests", () => { describe("for an empty config", () => { 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(); }); - it("allows setting and retrieving lastSeenAppVersion", () => { - userStore.lastSeenAppVersion = "1.2.3"; - expect(userStore.lastSeenAppVersion).toBe("1.2.3"); - }); - it("allows setting and getting preferences", () => { userStore.httpsProxy = "abcd://defg"; @@ -99,10 +96,8 @@ describe("user store tests", () => { beforeEach(() => { mockFs({ "some-directory-for-user-data": { - "config.json": JSON.stringify({ - user: { username: "foobar" }, + "lens-user-store.json": JSON.stringify({ preferences: { colorTheme: "light" }, - lastSeenAppVersion: "1.2.3", }), "lens-cluster-store.json": JSON.stringify({ clusters: [ @@ -127,17 +122,16 @@ describe("user store tests", () => { di.override(storeMigrationVersionInjectable, () => "10.0.0"); - userStore = di.inject(userStoreInjectable); userStore.load(); }); - it("sets last seen app version to 0.0.0", () => { - expect(userStore.lastSeenAppVersion).toBe("0.0.0"); - }); - - it.only("skips clusters for adding to kube-sync with files under extension_data/", () => { + it("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/other/path")).toBe(true); }); + + it("allows access to the colorTheme preference", () => { + expect(userStore.colorTheme).toBe("light"); + }); }); }); diff --git a/src/common/app-paths/app-path-injection-token.ts b/src/common/app-paths/app-path-injection-token.ts index e29bcdbebf22..91e8a580d836 100644 --- a/src/common/app-paths/app-path-injection-token.ts +++ b/src/common/app-paths/app-path-injection-token.ts @@ -2,11 +2,6 @@ * 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 { PathName } from "./app-path-names"; export type AppPaths = Record; - -export const appPathsInjectionToken = getInjectionToken({ id: "app-paths-token" }); - - diff --git a/src/common/app-paths/app-paths.injectable.ts b/src/common/app-paths/app-paths.injectable.ts index 803f9e13808c..0e836a851453 100644 --- a/src/common/app-paths/app-paths.injectable.ts +++ b/src/common/app-paths/app-paths.injectable.ts @@ -3,13 +3,11 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import { appPathsInjectionToken } from "./app-path-injection-token"; import appPathsStateInjectable from "./app-paths-state.injectable"; const appPathsInjectable = getInjectable({ id: "app-paths", instantiate: (di) => di.inject(appPathsStateInjectable).get(), - injectionToken: appPathsInjectionToken, }); export default appPathsInjectable; diff --git a/src/common/app-paths/app-paths.test.ts b/src/common/app-paths/app-paths.test.ts index 0fa6141f33b1..b26a5aca5d1e 100644 --- a/src/common/app-paths/app-paths.test.ts +++ b/src/common/app-paths/app-paths.test.ts @@ -3,7 +3,6 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ 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 type { PathName } from "./app-path-names"; 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 { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import type { DiContainer } from "@ogre-tools/injectable"; +import appPathsInjectable from "./app-paths.injectable"; describe("app-paths", () => { let builder: ApplicationBuilder; @@ -68,7 +68,7 @@ describe("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({ currentApp: "some-current-app", @@ -92,7 +92,7 @@ describe("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({ 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", () => { - const { appData, userData } = windowDi.inject(appPathsInjectionToken); + const { appData, userData } = windowDi.inject(appPathsInjectable); expect({ appData, userData }).toEqual({ 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", () => { - const { appData, userData } = windowDi.inject(appPathsInjectionToken); + const { appData, userData } = windowDi.inject(appPathsInjectable); expect({ appData, userData }).toEqual({ appData: "some-integration-testing-app-data", diff --git a/src/common/app-paths/directory-for-downloads/directory-for-downloads.injectable.ts b/src/common/app-paths/directory-for-downloads/directory-for-downloads.injectable.ts index 944c8a656a1a..01f97dbaec4f 100644 --- a/src/common/app-paths/directory-for-downloads/directory-for-downloads.injectable.ts +++ b/src/common/app-paths/directory-for-downloads/directory-for-downloads.injectable.ts @@ -3,11 +3,11 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import { appPathsInjectionToken } from "../app-path-injection-token"; +import appPathsInjectable from "../app-paths.injectable"; const directoryForDownloadsInjectable = getInjectable({ id: "directory-for-downloads", - instantiate: (di) => di.inject(appPathsInjectionToken).downloads, + instantiate: (di) => di.inject(appPathsInjectable).downloads, }); export default directoryForDownloadsInjectable; diff --git a/src/common/app-paths/directory-for-exes/directory-for-exes.injectable.ts b/src/common/app-paths/directory-for-exes/directory-for-exes.injectable.ts index 1bd245daa498..690f53d958f5 100644 --- a/src/common/app-paths/directory-for-exes/directory-for-exes.injectable.ts +++ b/src/common/app-paths/directory-for-exes/directory-for-exes.injectable.ts @@ -3,11 +3,11 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import { appPathsInjectionToken } from "../app-path-injection-token"; +import appPathsInjectable from "../app-paths.injectable"; const directoryForExesInjectable = getInjectable({ id: "directory-for-exes", - instantiate: (di) => di.inject(appPathsInjectionToken).exe, + instantiate: (di) => di.inject(appPathsInjectable).exe, }); export default directoryForExesInjectable; diff --git a/src/common/app-paths/directory-for-temp/directory-for-temp.injectable.ts b/src/common/app-paths/directory-for-temp/directory-for-temp.injectable.ts index b51e8e04893a..460efc073df7 100644 --- a/src/common/app-paths/directory-for-temp/directory-for-temp.injectable.ts +++ b/src/common/app-paths/directory-for-temp/directory-for-temp.injectable.ts @@ -3,11 +3,11 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import { appPathsInjectionToken } from "../app-path-injection-token"; +import appPathsInjectable from "../app-paths.injectable"; const directoryForTempInjectable = getInjectable({ id: "directory-for-temp", - instantiate: (di) => di.inject(appPathsInjectionToken).temp, + instantiate: (di) => di.inject(appPathsInjectable).temp, }); export default directoryForTempInjectable; diff --git a/src/common/app-paths/directory-for-user-data/directory-for-user-data.injectable.ts b/src/common/app-paths/directory-for-user-data/directory-for-user-data.injectable.ts index bff067b7e5be..0eb32221c6d2 100644 --- a/src/common/app-paths/directory-for-user-data/directory-for-user-data.injectable.ts +++ b/src/common/app-paths/directory-for-user-data/directory-for-user-data.injectable.ts @@ -3,11 +3,11 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import { appPathsInjectionToken } from "../app-path-injection-token"; +import appPathsInjectable from "../app-paths.injectable"; const directoryForUserDataInjectable = getInjectable({ id: "directory-for-user-data", - instantiate: (di) => di.inject(appPathsInjectionToken).userData, + instantiate: (di) => di.inject(appPathsInjectable).userData, }); export default directoryForUserDataInjectable; diff --git a/src/common/base-store.ts b/src/common/base-store.ts deleted file mode 100644 index 92383b328d7d..000000000000 --- a/src/common/base-store.ts +++ /dev/null @@ -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 extends ConfOptions { - syncOptions?: { - fireImmediately?: boolean; - equals?: IEqualsComparer; - }; -} - -/** - * Note: T should only contain base JSON serializable types. - */ -export abstract class BaseStore extends Singleton { - protected storeConfig?: Config; - protected syncDisposers: Disposer[] = []; - - readonly displayName: string = this.constructor.name; - - protected constructor(protected params: BaseStoreParams) { - 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'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; -} diff --git a/src/common/base-store/base-store.ts b/src/common/base-store/base-store.ts new file mode 100644 index 000000000000..9a786329e416 --- /dev/null +++ b/src/common/base-store/base-store.ts @@ -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 extends Omit, "migrations"> { + syncOptions?: { + fireImmediately?: boolean; + equals?: IEqualsComparer; + }; + configName: string; +} + +export interface IpcChannelPrefixes { + local: string; + remote: string; +} + +export interface BaseStoreDependencies { + readonly logger: Logger; + readonly storeMigrationVersion: string; + readonly directoryForUserData: string; + readonly migrations: Migrations>; + 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 { + private readonly syncDisposers = disposer(); + + readonly displayName = kebabCase(this.params.configName).toUpperCase(); + + protected constructor( + protected readonly dependencies: BaseStoreDependencies, + protected readonly params: BaseStoreParams, + ) { + 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, + }); + + const res = this.fromStore(config.store); + + if (isPromiseLike(res)) { + this.dependencies.logger.error(`${this.displayName} extends BaseStore'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) { + 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; +} diff --git a/src/common/base-store/channel-prefix.ts b/src/common/base-store/channel-prefix.ts new file mode 100644 index 000000000000..f2662c65e0f6 --- /dev/null +++ b/src/common/base-store/channel-prefix.ts @@ -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({ + id: "base-store-ipc-channel-prefix-token", +}); diff --git a/src/renderer/before-frame-starts/before-frame-starts-injection-token.ts b/src/common/base-store/disable-sync.ts similarity index 54% rename from src/renderer/before-frame-starts/before-frame-starts-injection-token.ts rename to src/common/base-store/disable-sync.ts index e494508329f2..ce7abd16a1ae 100644 --- a/src/renderer/before-frame-starts/before-frame-starts-injection-token.ts +++ b/src/common/base-store/disable-sync.ts @@ -2,9 +2,9 @@ * 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 { Runnable } from "../../common/runnable/run-many-for"; -export const beforeFrameStartsInjectionToken = getInjectionToken({ - id: "before-frame-starts", +export const shouldBaseStoreDisableSyncInIpcListenerInjectionToken = getInjectionToken({ + id: "should-base-store-disable-sync-in-ipc-listener-token", }); diff --git a/src/common/base-store/migrations.injectable.ts b/src/common/base-store/migrations.injectable.ts new file mode 100644 index 000000000000..27f7489dfa1b --- /dev/null +++ b/src/common/base-store/migrations.injectable.ts @@ -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>>): void; +} + +const storeMigrationsInjectable = getInjectable({ + id: "store-migrations", + instantiate: (di, token): Migrations> => { + const logger = di.inject(loggerInjectable); + const declarations = di.injectMany(token); + const migrations = new Map(); + + 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) => token.id, + }), +}); + +export default storeMigrationsInjectable; diff --git a/src/common/base-store/save-to-file.ts b/src/common/base-store/save-to-file.ts new file mode 100644 index 000000000000..b4d21ea9504c --- /dev/null +++ b/src/common/base-store/save-to-file.ts @@ -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 = (config: Config, state: T) => void; + +export const persistStateToConfigInjectionToken = getInjectionToken({ + id: "persist-state-to-config-token", +}); diff --git a/src/common/catalog-entities/kubernetes-cluster.ts b/src/common/catalog-entities/kubernetes-cluster.ts index 53e7a52e4bd6..1af4302bb130 100644 --- a/src/common/catalog-entities/kubernetes-cluster.ts +++ b/src/common/catalog-entities/kubernetes-cluster.ts @@ -5,13 +5,14 @@ import type { CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus, CatalogCategorySpec } from "../catalog"; import { CatalogEntity, CatalogCategory, categoryVersion } from "../catalog/catalog-entity"; -import { ClusterStore } from "../cluster-store/cluster-store"; import { broadcastMessage } from "../ipc"; import { app } from "electron"; import type { CatalogEntityConstructor, CatalogEntitySpec } from "../catalog/catalog-entity"; import { IpcRendererNavigationEvents } from "../../renderer/navigation/events"; import { requestClusterActivation, requestClusterDisconnection } from "../../renderer/ipc"; 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 { address?: { @@ -63,6 +64,8 @@ export function isKubernetesCluster(item: unknown): item is KubernetesCluster { return item instanceof KubernetesCluster; } +const getClusterById = asLegacyGlobalFunctionForExtensionApi(getClusterByIdInjectable); + export class KubernetesCluster< Metadata extends KubernetesClusterMetadata = KubernetesClusterMetadata, Status extends KubernetesClusterStatus = KubernetesClusterStatus, @@ -76,7 +79,7 @@ export class KubernetesCluster< async connect(): Promise { if (app) { - await ClusterStore.getInstance().getById(this.getId())?.activate(); + await getClusterById(this.getId())?.activate(); } else { await requestClusterActivation(this.getId(), false); } @@ -84,7 +87,7 @@ export class KubernetesCluster< async disconnect(): Promise { if (app) { - ClusterStore.getInstance().getById(this.getId())?.disconnect(); + getClusterById(this.getId())?.disconnect(); } else { await requestClusterDisconnection(this.getId(), false); } diff --git a/src/common/catalog-entities/web-link.ts b/src/common/catalog-entities/web-link.ts index b79f798566d2..7c83051c8bda 100644 --- a/src/common/catalog-entities/web-link.ts +++ b/src/common/catalog-entities/web-link.ts @@ -7,7 +7,7 @@ import { Environments, getEnvironmentSpecificLegacyGlobalDiForExtensionApi } fro import type { CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog"; import { CatalogCategory, CatalogEntity, categoryVersion } from "../catalog/catalog-entity"; 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"; @@ -34,12 +34,13 @@ export class WebLink extends CatalogEntity di.inject(weblinkStoreInjectable).removeById(this.getId()), + onClick: async () => weblinkStore.removeById(this.getId()), confirm: { message: `Remove Web Link "${this.getName()}" from ${productName}?`, }, diff --git a/src/common/catalog/catalog-category-registry.ts b/src/common/catalog/catalog-category-registry.ts deleted file mode 100644 index d456f38f061b..000000000000 --- a/src/common/catalog/catalog-category-registry.ts +++ /dev/null @@ -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); diff --git a/src/common/catalog/index.ts b/src/common/catalog/index.ts index a5c5ec427653..4964dada6a72 100644 --- a/src/common/catalog/index.ts +++ b/src/common/catalog/index.ts @@ -3,6 +3,5 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -export * from "./catalog-category-registry"; export * from "./category-registry"; export * from "./catalog-entity"; diff --git a/src/common/cluster-store/cluster-store.global-override-for-injectable.ts b/src/common/cluster-store/cluster-store.global-override-for-injectable.ts deleted file mode 100644 index 32a1ee62a1b3..000000000000 --- a/src/common/cluster-store/cluster-store.global-override-for-injectable.ts +++ /dev/null @@ -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), -); diff --git a/src/common/cluster-store/cluster-store.injectable.ts b/src/common/cluster-store/cluster-store.injectable.ts index 3e7cf86c539b..9712e3fdb0a5 100644 --- a/src/common/cluster-store/cluster-store.injectable.ts +++ b/src/common/cluster-store/cluster-store.injectable.ts @@ -7,21 +7,36 @@ import { ClusterStore } from "./cluster-store"; import { createClusterInjectionToken } from "../cluster/create-cluster-injection-token"; import readClusterConfigSyncInjectable from "./read-cluster-config.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({ id: "cluster-store", - instantiate: (di) => { - ClusterStore.resetInstance(); - - return ClusterStore.createInstance({ - createCluster: di.inject(createClusterInjectionToken), - readClusterConfigSync: di.inject(readClusterConfigSyncInjectable), - emitAppEvent: di.inject(emitAppEventInjectable), - }); - }, - - causesSideEffects: true, + instantiate: (di) => new ClusterStore({ + createCluster: di.inject(createClusterInjectionToken), + readClusterConfigSync: di.inject(readClusterConfigSyncInjectable), + emitAppEvent: di.inject(emitAppEventInjectable), + directoryForUserData: di.inject(directoryForUserDataInjectable), + getConfigurationFileModel: di.inject(getConfigurationFileModelInjectable), + logger: di.inject(loggerInjectable), + storeMigrationVersion: di.inject(storeMigrationVersionInjectable), + migrations: di.inject(storeMigrationsInjectable, clusterStoreMigrationInjectionToken), + getBasenameOfPath: di.inject(getBasenameOfPathInjectable), + ipcChannelPrefixes: di.inject(baseStoreIpcChannelPrefixesInjectionToken), + persistStateToConfig: di.inject(persistStateToConfigInjectionToken), + enlistMessageChannelListener: di.inject(enlistMessageChannelListenerInjectionToken), + shouldDisableSyncInListener: di.inject(shouldBaseStoreDisableSyncInIpcListenerInjectionToken), + }), }); export default clusterStoreInjectable; diff --git a/src/common/cluster-store/cluster-store.ts b/src/common/cluster-store/cluster-store.ts index 7b464600133d..20929cf77ec7 100644 --- a/src/common/cluster-store/cluster-store.ts +++ b/src/common/cluster-store/cluster-store.ts @@ -4,17 +4,12 @@ */ -import { ipcMain, ipcRenderer, webFrame } from "electron"; -import { action, comparer, computed, makeObservable, observable, reaction } from "mobx"; -import { BaseStore } from "../base-store"; +import { action, comparer, computed, makeObservable, observable } from "mobx"; +import type { BaseStoreDependencies } from "../base-store/base-store"; +import { BaseStore } from "../base-store/base-store"; import { Cluster } from "../cluster/cluster"; -import migrations from "../../migrations/cluster-store"; -import logger from "../../main/logger"; -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 { toJS } from "../utils"; +import type { ClusterModel, ClusterId } from "../cluster-types"; import type { CreateCluster } from "../cluster/create-cluster-injection-token"; import type { ReadClusterConfigSync } from "./read-cluster-config.injectable"; import type { EmitAppEvent } from "../app-event-bus/emit-event.injectable"; @@ -23,76 +18,25 @@ export interface ClusterStoreModel { clusters?: ClusterModel[]; } -interface Dependencies { +interface Dependencies extends BaseStoreDependencies { createCluster: CreateCluster; readClusterConfigSync: ReadClusterConfigSync; emitAppEvent: EmitAppEvent; } export class ClusterStore extends BaseStore { - readonly displayName = "ClusterStore"; - clusters = observable.map(); + readonly clusters = observable.map(); - protected disposer = disposer(); - - constructor(private readonly dependencies: Dependencies) { - super({ + constructor(protected readonly dependencies: Dependencies) { + super(dependencies, { configName: "lens-cluster-store", accessPropertiesByDotNotation: false, // To make dots safe in cluster context names syncOptions: { equals: comparer.structural, }, - migrations, }); 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[] { @@ -150,7 +94,7 @@ export class ClusterStore extends BaseStore { } newClusters.set(clusterModel.id, cluster); } 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}`); } } diff --git a/src/common/cluster-store/migration-token.ts b/src/common/cluster-store/migration-token.ts new file mode 100644 index 000000000000..86489509a2b3 --- /dev/null +++ b/src/common/cluster-store/migration-token.ts @@ -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({ + id: "cluster-store-migration", +}); diff --git a/src/common/cluster/cluster.ts b/src/common/cluster/cluster.ts index 7f2702519026..fe66c9fe1ba7 100644 --- a/src/common/cluster/cluster.ts +++ b/src/common/cluster/cluster.ts @@ -316,7 +316,6 @@ export class Cluster implements ClusterModel, ClusterState { const refreshMetadataTimer = setInterval(() => this.available && this.refreshAccessibilityAndMetadata(), 900000); // every 15 minutes this.eventsDisposer.push( - reaction(() => this.getState(), state => this.pushState(state)), reaction( () => this.prometheusPreferences, prefs => this.contextHandler.setupPrometheus(prefs), @@ -349,7 +348,7 @@ export class Cluster implements ClusterModel, ClusterState { @action async activate(force = false) { if (this.activated && !force) { - return this.pushState(); + return; } this.dependencies.logger.info(`[CLUSTER]: activate`, this.getMeta()); @@ -395,7 +394,6 @@ export class Cluster implements ClusterModel, ClusterState { } this.activated = true; - this.pushState(); } /** @@ -437,7 +435,6 @@ export class Cluster implements ClusterModel, ClusterState { this.activated = false; this.allowedNamespaces = []; this.resourceAccessStatuses.clear(); - this.pushState(); this.dependencies.logger.info(`[CLUSTER]: disconnected`, { id: this.id }); } @@ -448,7 +445,6 @@ export class Cluster implements ClusterModel, ClusterState { async refresh() { this.dependencies.logger.info(`[CLUSTER]: refresh`, this.getMeta()); await this.refreshConnectionStatus(); - this.pushState(); } /** @@ -614,16 +610,15 @@ export class Cluster implements ClusterModel, ClusterState { * @param state cluster state */ @action setState(state: ClusterState) { - Object.assign(this, state); - } - - /** - * @internal - * @param state cluster state - */ - pushState(state = this.getState()) { - this.dependencies.logger.silly(`[CLUSTER]: push-state`, state); - this.dependencies.broadcastMessage("cluster:state", this.id, state); + this.accessible = state.accessible; + this.allowedNamespaces = state.allowedNamespaces; + this.allowedResources = state.allowedResources; + this.apiUrl = state.apiUrl; + this.disconnected = state.disconnected; + this.isAdmin = state.isAdmin; + this.isGlobalWatchEnabled = state.isGlobalWatchEnabled; + this.online = state.online; + this.ready = state.ready; } // get cluster system meta, e.g. use in "logger" diff --git a/src/common/cluster/current-cluster-channel.ts b/src/common/cluster/current-cluster-channel.ts new file mode 100644 index 000000000000..957baa6f9c3e --- /dev/null +++ b/src/common/cluster/current-cluster-channel.ts @@ -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 = { + id: "current-visible-cluster", +}; diff --git a/src/common/fetch/fetch.injectable.ts b/src/common/fetch/fetch.injectable.ts index e320c0128a0e..bd1ba89db7e9 100644 --- a/src/common/fetch/fetch.injectable.ts +++ b/src/common/fetch/fetch.injectable.ts @@ -3,7 +3,9 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; +import { HttpsProxyAgent } from "hpagent"; 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 }; @@ -14,7 +16,20 @@ export type Fetch = (url: string, init?: RequestInit) => Promise; const fetchInjectable = getInjectable({ 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, }); diff --git a/src/common/fs/delete-file.global-override-for-injectable.ts b/src/common/fs/delete-file.global-override-for-injectable.ts deleted file mode 100644 index c03dca88dc16..000000000000 --- a/src/common/fs/delete-file.global-override-for-injectable.ts +++ /dev/null @@ -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"); -}); diff --git a/src/common/fs/exec-file.global-override-for-injectable.ts b/src/common/fs/exec-file.global-override-for-injectable.ts new file mode 100644 index 000000000000..162666a13055 --- /dev/null +++ b/src/common/fs/exec-file.global-override-for-injectable.ts @@ -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); diff --git a/src/common/fs/fs.injectable.ts b/src/common/fs/fs.injectable.ts index ab385e5871d3..f80375095c65 100644 --- a/src/common/fs/fs.injectable.ts +++ b/src/common/fs/fs.injectable.ts @@ -3,11 +3,61 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; +import type { ReadOptions } 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({ 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, + writeFile, + writeJson, + pathExists, + readdir, + readFileSync, + readJsonSync, + writeFileSync, + writeJsonSync, + pathExistsSync, + lstat, + rm, + access, + copy: copy as (src: string, dest: string, options?: fse.CopyOptions) => Promise, + ensureDir: ensureDir as (path: string, options?: number | fse.EnsureOptions ) => Promise, + ensureDirSync, + createReadStream, + stat, + }; + }, causesSideEffects: true, }); diff --git a/src/common/fs/move.injectable.ts b/src/common/fs/move.injectable.ts deleted file mode 100644 index ff11120d8005..000000000000 --- a/src/common/fs/move.injectable.ts +++ /dev/null @@ -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; - -const moveInjectable = getInjectable({ - id: "move", - instantiate: (di): Move => di.inject(fsInjectable).move, -}); - -export default moveInjectable; diff --git a/src/common/fs/delete-file.injectable.ts b/src/common/fs/path-exists-sync.injectable.ts similarity index 52% rename from src/common/fs/delete-file.injectable.ts rename to src/common/fs/path-exists-sync.injectable.ts index 57aba5b379c8..21bcb6d7d19b 100644 --- a/src/common/fs/delete-file.injectable.ts +++ b/src/common/fs/path-exists-sync.injectable.ts @@ -5,11 +5,9 @@ import { getInjectable } from "@ogre-tools/injectable"; import fsInjectable from "./fs.injectable"; -export type DeleteFile = (filePath: string) => Promise; - -const deleteFileInjectable = getInjectable({ - id: "delete-file", - instantiate: (di): DeleteFile => di.inject(fsInjectable).unlink, +const pathExistsSyncInjectable = getInjectable({ + id: "path-exists-sync", + instantiate: (di) => di.inject(fsInjectable).pathExistsSync, }); -export default deleteFileInjectable; +export default pathExistsSyncInjectable; diff --git a/src/common/fs/path-exists.global-override-for-injectable.ts b/src/common/fs/path-exists.global-override-for-injectable.ts deleted file mode 100644 index 1b9b96c8dd25..000000000000 --- a/src/common/fs/path-exists.global-override-for-injectable.ts +++ /dev/null @@ -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"); -}); diff --git a/src/common/fs/read-directory.injectable.ts b/src/common/fs/read-directory.injectable.ts index 57632bd4d7cc..8ebeebe75a08 100644 --- a/src/common/fs/read-directory.injectable.ts +++ b/src/common/fs/read-directory.injectable.ts @@ -14,18 +14,16 @@ export interface ReadDirectory { ( path: string, options?: - | { encoding: BufferEncoding | string | null; withFileTypes?: false | undefined } + | { encoding: BufferEncoding; withFileTypes?: false | undefined } | BufferEncoding - | string - | null, ): Promise; ( path: string, - options?: { encoding?: BufferEncoding | string | null | undefined; withFileTypes?: false | undefined }, + options?: { encoding?: BufferEncoding; withFileTypes?: false | undefined }, ): Promise; ( path: string, - options: { encoding?: BufferEncoding | string | null | undefined; withFileTypes: true }, + options: { encoding?: BufferEncoding; withFileTypes: true }, ): Promise; } diff --git a/src/common/fs/read-file-buffer-sync.injectable.ts b/src/common/fs/read-file-buffer-sync.injectable.ts new file mode 100644 index 000000000000..98ba8e6d4bf3 --- /dev/null +++ b/src/common/fs/read-file-buffer-sync.injectable.ts @@ -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; diff --git a/src/common/fs/read-json-sync.injectable.ts b/src/common/fs/read-json-sync.injectable.ts new file mode 100644 index 000000000000..81a9ef478ff0 --- /dev/null +++ b/src/common/fs/read-json-sync.injectable.ts @@ -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; diff --git a/src/common/fs/remove-path.global-override-for-injectable.ts b/src/common/fs/remove.global-override-for-injectable.ts similarity index 70% rename from src/common/fs/remove-path.global-override-for-injectable.ts rename to src/common/fs/remove.global-override-for-injectable.ts index 5b9720a837fe..4b92353344ac 100644 --- a/src/common/fs/remove-path.global-override-for-injectable.ts +++ b/src/common/fs/remove.global-override-for-injectable.ts @@ -4,8 +4,8 @@ */ import { getGlobalOverride } from "../test-utils/get-global-override"; -import removePathInjectable from "./remove-path.injectable"; +import removePathInjectable from "./remove.injectable"; export default getGlobalOverride(removePathInjectable, () => async () => { - throw new Error("tried to remove a path without override"); + throw new Error("tried to remove path without override"); }); diff --git a/src/common/fs/remove-path.injectable.ts b/src/common/fs/remove.injectable.ts similarity index 63% rename from src/common/fs/remove-path.injectable.ts rename to src/common/fs/remove.injectable.ts index 02c8da0e1e07..0c6a629754df 100644 --- a/src/common/fs/remove-path.injectable.ts +++ b/src/common/fs/remove.injectable.ts @@ -5,11 +5,15 @@ import { getInjectable } from "@ogre-tools/injectable"; import fsInjectable from "./fs.injectable"; -export type RemovePath = (path: string) => Promise; +export type RemovePath = (filePath: string) => Promise; const removePathInjectable = getInjectable({ 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; diff --git a/src/common/fs/stat/stat.injectable.ts b/src/common/fs/stat.injectable.ts similarity index 90% rename from src/common/fs/stat/stat.injectable.ts rename to src/common/fs/stat.injectable.ts index e9924fc088d9..07f2b298b1d7 100644 --- a/src/common/fs/stat/stat.injectable.ts +++ b/src/common/fs/stat.injectable.ts @@ -4,7 +4,7 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import type { Stats } from "fs"; -import fsInjectable from "../fs.injectable"; +import fsInjectable from "./fs.injectable"; export type Stat = (path: string) => Promise; diff --git a/src/common/fs/stat/stat.global-override-for-injectable.ts b/src/common/fs/stat/stat.global-override-for-injectable.ts deleted file mode 100644 index 2afeda7b7705..000000000000 --- a/src/common/fs/stat/stat.global-override-for-injectable.ts +++ /dev/null @@ -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"); -}); diff --git a/src/common/fs/validate-directory.injectable.ts b/src/common/fs/validate-directory.injectable.ts index 9d68ede8d5f3..efce915238e8 100644 --- a/src/common/fs/validate-directory.injectable.ts +++ b/src/common/fs/validate-directory.injectable.ts @@ -7,7 +7,7 @@ import type { AsyncResult } from "../utils/async-result"; import { isErrnoException } from "../utils"; import type { Stats } from "fs-extra"; import { lowerFirst } from "lodash/fp"; -import statInjectable from "./stat/stat.injectable"; +import statInjectable from "./stat.injectable"; export type ValidateDirectory = (path: string) => Promise>; diff --git a/src/common/fs/write-file-sync.injectable.ts b/src/common/fs/write-file-sync.injectable.ts new file mode 100644 index 000000000000..3daccaf6102b --- /dev/null +++ b/src/common/fs/write-file-sync.injectable.ts @@ -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; diff --git a/src/common/fs/write-file.injectable.ts b/src/common/fs/write-file.injectable.ts index faa5285ca102..75e07775e32e 100644 --- a/src/common/fs/write-file.injectable.ts +++ b/src/common/fs/write-file.injectable.ts @@ -16,15 +16,17 @@ const writeFileInjectable = getInjectable({ const { writeFile, ensureDir } = di.inject(fsInjectable); const getDirnameOfPath = di.inject(getDirnameOfPathInjectable); - return async (filePath, content, opts) => { + return async (filePath, content, opts = {}) => { await ensureDir(getDirnameOfPath(filePath), { mode: 0o755, - ...(opts ?? {}), + ...opts, }); + const { encoding = "utf-8", ...options } = opts; + await writeFile(filePath, content, { - encoding: "utf-8", - ...(opts ?? {}), + encoding: encoding as BufferEncoding, + ...options, }); }; }, diff --git a/src/common/fs/write-json-file.injectable.ts b/src/common/fs/write-json-file.injectable.ts index a7079d7f8497..5491487849fa 100644 --- a/src/common/fs/write-json-file.injectable.ts +++ b/src/common/fs/write-json-file.injectable.ts @@ -3,11 +3,10 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import type { JsonValue } from "type-fest"; import getDirnameOfPathInjectable from "../path/get-dirname.injectable"; import fsInjectable from "./fs.injectable"; -export type WriteJson = (filePath: string, contents: JsonValue) => Promise; +export type WriteJson = (filePath: string, contents: unknown) => Promise; const writeJsonFileInjectable = getInjectable({ id: "write-json-file", diff --git a/src/common/fs/write-json-sync.injectable.ts b/src/common/fs/write-json-sync.injectable.ts new file mode 100644 index 000000000000..eb4abc393699 --- /dev/null +++ b/src/common/fs/write-json-sync.injectable.ts @@ -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; diff --git a/src/common/get-configuration-file-model/get-configuration-file-model.global-override-for-injectable.ts b/src/common/get-configuration-file-model/get-configuration-file-model.global-override-for-injectable.ts new file mode 100644 index 000000000000..fba8939880c4 --- /dev/null +++ b/src/common/get-configuration-file-model/get-configuration-file-model.global-override-for-injectable.ts @@ -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 as Config; + }; +}); diff --git a/src/common/get-configuration-file-model/get-configuration-file-model.injectable.ts b/src/common/get-configuration-file-model/get-configuration-file-model.injectable.ts index dc54e96de137..e167e464effa 100644 --- a/src/common/get-configuration-file-model/get-configuration-file-model.injectable.ts +++ b/src/common/get-configuration-file-model/get-configuration-file-model.injectable.ts @@ -4,11 +4,13 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import Config from "conf"; -import type { BaseStoreParams } from "../base-store"; +import type { Options as ConfOptions } from "conf/dist/source/types"; + +export type GetConfigurationFileModel = (content: ConfOptions) => Config; const getConfigurationFileModelInjectable = getInjectable({ id: "get-configuration-file-model", - instantiate: () => (content: BaseStoreParams) => new Config(content), + instantiate: (): GetConfigurationFileModel => (content) => new Config(content), causesSideEffects: true, }); diff --git a/src/common/helm/helm-repo.ts b/src/common/helm/helm-repo.ts index 9405d59bed30..cb4af3449b5f 100644 --- a/src/common/helm/helm-repo.ts +++ b/src/common/helm/helm-repo.ts @@ -2,15 +2,15 @@ * Copyright (c) OpenLens Authors. All rights reserved. * 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; url: string; - cacheFilePath?: string; + cacheFilePath: string; caFile?: string; certFile?: string; insecureSkipTlsVerify?: boolean; keyFile?: string; username?: string; password?: string; -}; +} diff --git a/src/common/hotbars/migrations-token.ts b/src/common/hotbars/migrations-token.ts new file mode 100644 index 000000000000..5441844933db --- /dev/null +++ b/src/common/hotbars/migrations-token.ts @@ -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({ + id: "hotbar-store-migration-token", +}); diff --git a/src/common/hotbars/store.injectable.ts b/src/common/hotbars/store.injectable.ts index ace13b8be42c..cc15f93bf857 100644 --- a/src/common/hotbars/store.injectable.ts +++ b/src/common/hotbars/store.injectable.ts @@ -6,20 +6,33 @@ import { getInjectable } from "@ogre-tools/injectable"; import catalogCatalogEntityInjectable from "../catalog-entities/general-catalog-entities/implementations/catalog-catalog-entity.injectable"; import { HotbarStore } from "./store"; 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({ id: "hotbar-store", - instantiate: (di) => { - HotbarStore.resetInstance(); - - return HotbarStore.createInstance({ - catalogCatalogEntity: di.inject(catalogCatalogEntityInjectable), - logger: di.inject(loggerInjectable), - }); - }, - - causesSideEffects: true, + instantiate: (di) => new HotbarStore({ + catalogCatalogEntity: di.inject(catalogCatalogEntityInjectable), + logger: di.inject(loggerInjectable), + directoryForUserData: di.inject(directoryForUserDataInjectable), + getConfigurationFileModel: di.inject(getConfigurationFileModelInjectable), + storeMigrationVersion: di.inject(storeMigrationVersionInjectable), + migrations: di.inject(storeMigrationsInjectable, hotbarStoreMigrationInjectionToken), + getBasenameOfPath: di.inject(getBasenameOfPathInjectable), + ipcChannelPrefixes: di.inject(baseStoreIpcChannelPrefixesInjectionToken), + persistStateToConfig: di.inject(persistStateToConfigInjectionToken), + enlistMessageChannelListener: di.inject(enlistMessageChannelListenerInjectionToken), + shouldDisableSyncInListener: di.inject(shouldBaseStoreDisableSyncInIpcListenerInjectionToken), + }), }); export default hotbarStoreInjectable; diff --git a/src/common/hotbars/store.ts b/src/common/hotbars/store.ts index a75182b23b37..709242f20e61 100644 --- a/src/common/hotbars/store.ts +++ b/src/common/hotbars/store.ts @@ -4,8 +4,8 @@ */ import { action, comparer, observable, makeObservable, computed } from "mobx"; -import { BaseStore } from "../base-store"; -import migrations from "../../migrations/hotbar-store"; +import type { BaseStoreDependencies } from "../base-store/base-store"; +import { BaseStore } from "../base-store/base-store"; import { toJS } from "../utils"; import type { CatalogEntity } from "../catalog"; import { broadcastMessage } from "../ipc"; @@ -21,26 +21,23 @@ export interface HotbarStoreModel { activeHotbarId: string; } -interface Dependencies { +interface Dependencies extends BaseStoreDependencies { readonly catalogCatalogEntity: GeneralEntity; readonly logger: Logger; } export class HotbarStore extends BaseStore { - readonly displayName = "HotbarStore"; @observable hotbars: Hotbar[] = []; @observable private _activeHotbarId!: string; - constructor(private readonly dependencies: Dependencies) { - super({ + constructor(protected readonly dependencies: Dependencies) { + super(dependencies, { configName: "lens-hotbar-store", accessPropertiesByDotNotation: false, // To make dots safe in cluster context names syncOptions: { equals: comparer.structural, }, - migrations, }); - makeObservable(this); } @@ -99,21 +96,19 @@ export class HotbarStore extends BaseStore { this.hotbars.forEach(ensureExactHotbarItemLength); if (data.activeHotbarId) { - this.setActiveHotbar(data.activeHotbarId); + this._activeHotbarId = data.activeHotbarId; } - if (!this.activeHotbarId) { - this.setActiveHotbar(0); + if (!this._activeHotbarId) { + this._activeHotbarId = this.hotbars[0].id; } } toJSON(): HotbarStoreModel { - const model: HotbarStoreModel = { + return toJS({ hotbars: this.hotbars, activeHotbarId: this.activeHotbarId, - }; - - return toJS(model); + }); } getActive(): Hotbar { @@ -148,7 +143,7 @@ export class HotbarStore extends BaseStore { const index = this.hotbars.findIndex((hotbar) => hotbar.id === id); if (index < 0) { - return void console.warn( + return this.dependencies.logger.warn( `[HOTBAR-STORE]: cannot setHotbarName: unknown id`, { id }, ); diff --git a/src/common/ipc/dialog.ts b/src/common/ipc/dialog.ts deleted file mode 100644 index eab621a28038..000000000000 --- a/src/common/ipc/dialog.ts +++ /dev/null @@ -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"; diff --git a/src/common/ipc/native-theme.ts b/src/common/ipc/native-theme.ts deleted file mode 100644 index 4708a3c9b33b..000000000000 --- a/src/common/ipc/native-theme.ts +++ /dev/null @@ -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"; diff --git a/src/common/request.ts b/src/common/request.ts deleted file mode 100644 index 331b257f4de4..000000000000 --- a/src/common/request.ts +++ /dev/null @@ -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 { - 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); -} diff --git a/src/common/runnable/run-many-for.test.ts b/src/common/runnable/run-many-for.test.ts index c2cc152681d2..6002a149db8a 100644 --- a/src/common/runnable/run-many-for.test.ts +++ b/src/common/runnable/run-many-for.test.ts @@ -8,6 +8,8 @@ import { createContainer, getInjectable, getInjectionToken } from "@ogre-tools/i import type { Runnable } from "./run-many-for"; import { runManyFor } from "./run-many-for"; import { getPromiseStatus } from "../test-utils/get-promise-status"; +import { runInAction } from "mobx"; +import { flushPromises } from "../test-utils/flush-promises"; describe("runManyFor", () => { describe("given no hierarchy, when running many", () => { @@ -223,7 +225,68 @@ describe("runManyFor", () => { ); 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({ + id: "some-injection-token", + }); + + const someOtherInjectionToken = getInjectionToken({ + 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; + + beforeEach(async () => { + const rootDi = createContainer("irrelevant"); + + runMock = asyncFn<(...args: unknown[]) => void>(); + + const someInjectionToken = getInjectionToken({ + 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); + }); + }); + }); }); diff --git a/src/common/runnable/run-many-for.ts b/src/common/runnable/run-many-for.ts index ce3123c0b016..106cc74da1db 100644 --- a/src/common/runnable/run-many-for.ts +++ b/src/common/runnable/run-many-for.ts @@ -3,46 +3,138 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import type { DiContainerForInjection, InjectionToken } from "@ogre-tools/injectable"; -import type { Composite } from "../utils/composite/get-composite/get-composite"; -import { getCompositeFor } from "../utils/composite/get-composite/get-composite"; +import type { SingleOrMany } from "../utils"; +import { getOrInsert, getOrInsertSetFor, isDefined } from "../utils"; 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 { id: string; run: Run; - runAfter?: Runnable; + runAfter?: SingleOrMany>; } type Run = (parameter: Param) => Promise | void; -export type RunMany = (injectionToken: InjectionToken, void>) => Run; +export type RunMany = (injectionToken: InjectionToken, void>) => Asyncify>; -async function runCompositeRunnables(param: Param, composite: Composite>) { - await composite.value.run(param); - await Promise.all(composite.children.map(composite => runCompositeRunnables(param, composite))); +const computedNextEdge = (traversed: string[], graph: Map>, currentId: string, seenIds: Set) => { + seenIds.add(currentId); + 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 = (injectionToken: InjectionToken, void>, runnables: Runnable[]) => { + const rootId = uuid.v4(); + const runnableGraph = new Map>(); + const seenIds = new Set(); + 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>(); + private readonly events: TypedEventEmitter = new EventEmitter(); + + private initFinishingPromise(id: string): Promise { + 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 { + await this.initFinishingPromise(id); + } +} + +const executeRunnableWith = (param: Param) => { + const barrier = new DynamicBarrier(); + + return async (runnable: Runnable): Promise => { + 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 { return (injectionToken: InjectionToken, void>) => async (param: Param) => { + const executeRunnable = executeRunnableWith(param); const allRunnables = di.injectMany(injectionToken); - const rootId = uuid.v4(); - const getCompositeRunnables = getCompositeFor>({ - 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)); }; } diff --git a/src/common/fs/move.global-override-for-injectable.ts b/src/common/user-store/current-timezone.global-override-for-injectable.ts similarity index 55% rename from src/common/fs/move.global-override-for-injectable.ts rename to src/common/user-store/current-timezone.global-override-for-injectable.ts index c39907ee6ee7..6056074d3c1d 100644 --- a/src/common/fs/move.global-override-for-injectable.ts +++ b/src/common/user-store/current-timezone.global-override-for-injectable.ts @@ -4,8 +4,6 @@ */ import { getGlobalOverride } from "../test-utils/get-global-override"; -import moveInjectable from "./move.injectable"; +import currentTimezoneInjectable from "./current-timezone.injectable"; -export default getGlobalOverride(moveInjectable, () => async () => { - throw new Error("tried to move without override"); -}); +export default getGlobalOverride(currentTimezoneInjectable, () => "Etc/GMT"); diff --git a/src/common/user-store/current-timezone.injectable.ts b/src/common/user-store/current-timezone.injectable.ts new file mode 100644 index 000000000000..6a6043eaf922 --- /dev/null +++ b/src/common/user-store/current-timezone.injectable.ts @@ -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; diff --git a/src/common/user-store/https-proxy.injectable.ts b/src/common/user-store/https-proxy.injectable.ts new file mode 100644 index 000000000000..30569d4e77d8 --- /dev/null +++ b/src/common/user-store/https-proxy.injectable.ts @@ -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; diff --git a/src/common/user-store/kubeconfig-syncs.injectable.ts b/src/common/user-store/kubeconfig-syncs.injectable.ts index bbe02fffad13..7327b9d8e4e9 100644 --- a/src/common/user-store/kubeconfig-syncs.injectable.ts +++ b/src/common/user-store/kubeconfig-syncs.injectable.ts @@ -7,11 +7,7 @@ import userStoreInjectable from "./user-store.injectable"; const kubeconfigSyncsInjectable = getInjectable({ id: "kubeconfig-syncs", - instantiate: (di) => { - const store = di.inject(userStoreInjectable); - - return store.syncKubeconfigEntries; - }, + instantiate: (di) => di.inject(userStoreInjectable).syncKubeconfigEntries, }); export default kubeconfigSyncsInjectable; diff --git a/src/common/user-store/lens-color-theme.injectable.ts b/src/common/user-store/lens-color-theme.injectable.ts new file mode 100644 index 000000000000..5b48de1a37f0 --- /dev/null +++ b/src/common/user-store/lens-color-theme.injectable.ts @@ -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; diff --git a/src/common/user-store/migrations-token.ts b/src/common/user-store/migrations-token.ts new file mode 100644 index 000000000000..f3959beb3a22 --- /dev/null +++ b/src/common/user-store/migrations-token.ts @@ -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({ + id: "user-store-migration-token", +}); diff --git a/src/common/user-store/preference-descriptors.injectable.ts b/src/common/user-store/preference-descriptors.injectable.ts new file mode 100644 index 000000000000..35815dbbea8c --- /dev/null +++ b/src/common/user-store/preference-descriptors.injectable.ts @@ -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; + +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({ + fromStore: val => val, + toStore: val => val || undefined, + }), + shell: getPreferenceDescriptor({ + fromStore: val => val, + toStore: val => val || undefined, + }), + colorTheme: getPreferenceDescriptor({ + fromStore: val => val || defaultThemeId, + toStore: val => !val || val === defaultThemeId + ? undefined + : val, + }), + terminalTheme: getPreferenceDescriptor({ + fromStore: val => val || "", + toStore: val => val || undefined, + }), + localeTimezone: getPreferenceDescriptor({ + fromStore: val => val || currentTimezone, + toStore: val => !val || val === currentTimezone + ? undefined + : val, + }), + allowUntrustedCAs: getPreferenceDescriptor({ + fromStore: val => val ?? false, + toStore: val => !val + ? undefined + : val, + }), + allowErrorReporting: getPreferenceDescriptor({ + fromStore: val => val ?? true, + toStore: val => val + ? undefined + : val, + }), + downloadMirror: getPreferenceDescriptor({ + fromStore: val => !val || !packageMirrors.has(val) + ? defaultPackageMirror + : val, + toStore: val => val === defaultPackageMirror + ? undefined + : val, + }), + downloadKubectlBinaries: getPreferenceDescriptor({ + fromStore: val => val ?? true, + toStore: val => val + ? undefined + : val, + }), + downloadBinariesPath: getPreferenceDescriptor({ + fromStore: val => val, + toStore: val => val || undefined, + }), + kubectlBinariesPath: getPreferenceDescriptor({ + fromStore: val => val, + toStore: val => val || undefined, + }), + openAtLogin: getPreferenceDescriptor({ + fromStore: val => val ?? false, + toStore: val => !val + ? undefined + : val, + }), + terminalCopyOnSelect: getPreferenceDescriptor({ + fromStore: val => val ?? false, + toStore: val => !val + ? undefined + : val, + }), + hiddenTableColumns: getPreferenceDescriptor<[string, string[]][], Map>>({ + 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>({ + 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, EditorConfiguration>({ + fromStore: val => merge(defaultEditorConfig, val), + toStore: val => val, + }), + terminalConfig: getPreferenceDescriptor, TerminalConfig>({ + fromStore: val => merge(defaultTerminalConfig, val), + toStore: val => val, + }), + extensionRegistryUrl: getPreferenceDescriptor({ + fromStore: val => val ?? { + location: defaultExtensionRegistryUrlLocation, + }, + toStore: val => val.location === defaultExtensionRegistryUrlLocation + ? undefined + : val, + }), + }) as const; + }, +}); + +export default userStorePreferenceDescriptorsInjectable; diff --git a/src/common/user-store/preferences-helpers.ts b/src/common/user-store/preferences-helpers.ts index ed3fb7c24945..5bdc2a3852df 100644 --- a/src/common/user-store/preferences-helpers.ts +++ b/src/common/user-store/preferences-helpers.ts @@ -3,14 +3,9 @@ * 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 merge from "lodash/merge"; -import { defaultThemeId, defaultEditorFontFamily, defaultFontSize, defaultTerminalFontFamily } from "../vars"; -import type { ObservableMap } from "mobx"; -import { observable } from "mobx"; +import { defaultEditorFontFamily, defaultFontSize, defaultTerminalFontFamily } from "../vars"; +import type { PreferenceDescriptors } from "./preference-descriptors.injectable"; export interface KubeconfigSyncEntry extends KubeconfigSyncValue { filePath: string; @@ -54,86 +49,8 @@ export interface PreferenceDescription { toStore(val: R): T | undefined; } -const httpsProxy: PreferenceDescription = { - fromStore(val) { - return val; - }, - toStore(val) { - return val || undefined; - }, -}; - -const shell: PreferenceDescription = { - fromStore(val) { - return val; - }, - toStore(val) { - return val || undefined; - }, -}; - -const colorTheme: PreferenceDescription = { - fromStore(val) { - return val || defaultThemeId; - }, - toStore(val) { - if (!val || val === defaultThemeId) { - return undefined; - } - - return val; - }, -}; - -const terminalTheme: PreferenceDescription = { - fromStore(val) { - return val || ""; - }, - toStore(val) { - return val || undefined; - }, -}; - -export const defaultLocaleTimezone = "UTC"; - -const localeTimezone: PreferenceDescription = { - fromStore(val) { - return val || moment.tz.guess(true) || defaultLocaleTimezone; - }, - toStore(val) { - if (!val || val === moment.tz.guess(true) || val === defaultLocaleTimezone) { - return undefined; - } - - return val; - }, -}; +export const getPreferenceDescriptor = (desc: PreferenceDescription) => desc; -const allowUntrustedCAs: PreferenceDescription = { - fromStore(val) { - return val ?? false; - }, - toStore(val) { - if (!val) { - return undefined; - } - - return val; - }, -}; - -const allowErrorReporting: PreferenceDescription = { - fromStore(val) { - return val ?? true; - }, - toStore(val) { - if (val === true) { - return undefined; - } - - return val; - }, -}; export interface DownloadMirror { url: string; @@ -157,142 +74,6 @@ export const packageMirrors = new Map([ }], ]); -const downloadMirror: PreferenceDescription = { - fromStore(val) { - return !val || !packageMirrors.has(val) - ? defaultPackageMirror - : val; - }, - toStore(val) { - if (!val || val === defaultPackageMirror) { - return undefined; - } - - return val; - }, -}; - -const downloadKubectlBinaries: PreferenceDescription = { - fromStore(val) { - return val ?? true; - }, - toStore(val) { - if (val === true) { - return undefined; - } - - return val; - }, -}; - -const downloadBinariesPath: PreferenceDescription = { - fromStore(val) { - return val; - }, - toStore(val) { - if (!val) { - return undefined; - } - - return val; - }, -}; - -const kubectlBinariesPath: PreferenceDescription = { - fromStore(val) { - return val; - }, - toStore(val) { - if (!val) { - return undefined; - } - - return val; - }, -}; - -const openAtLogin: PreferenceDescription = { - fromStore(val) { - return val ?? false; - }, - toStore(val) { - if (!val) { - return undefined; - } - - return val; - }, -}; - -const terminalCopyOnSelect: PreferenceDescription = { - fromStore(val) { - return val ?? false; - }, - toStore(val) { - if (!val) { - return undefined; - } - - return val; - }, -}; - -const hiddenTableColumns: PreferenceDescription<[string, string[]][], Map>> = { - 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> = { - 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 | undefined, EditorConfiguration> = { - fromStore(val) { - return merge(defaultEditorConfig, val); - }, - toStore(val) { - return val; - }, -}; - -const terminalConfig: PreferenceDescription = { - fromStore(val) { - return merge(defaultTerminalConfig, val); - }, - toStore(val) { - return val; - }, -}; - export type ExtensionRegistryLocation = "default" | "npmrc" | "custom"; export type ExtensionRegistry = { @@ -306,49 +87,13 @@ export type ExtensionRegistry = { export const defaultExtensionRegistryUrlLocation = "default"; export const defaultExtensionRegistryUrl = "https://registry.npmjs.org"; -const extensionRegistryUrl: PreferenceDescription = { - fromStore(val) { - return val ?? { - location: defaultExtensionRegistryUrlLocation, - }; - }, - toStore(val) { - if (val.location === defaultExtensionRegistryUrlLocation) { - return undefined; - } - - return val; - }, -}; - -type PreferencesModelType = typeof DESCRIPTORS[field] extends PreferenceDescription ? T : never; -type UserStoreModelType = typeof DESCRIPTORS[field] extends PreferenceDescription ? T : never; +type PreferencesModelType = PreferenceDescriptors[field] extends PreferenceDescription ? T : never; +type UserStoreModelType = PreferenceDescriptors[field] extends PreferenceDescription ? T : never; export type UserStoreFlatModel = { - [field in keyof typeof DESCRIPTORS]: UserStoreModelType; + [field in keyof PreferenceDescriptors]: UserStoreModelType; }; export type UserPreferencesModel = { - [field in keyof typeof DESCRIPTORS]: PreferencesModelType; + [field in keyof PreferenceDescriptors]: PreferencesModelType; } & { updateChannel: string }; - -export const DESCRIPTORS = { - httpsProxy, - shell, - colorTheme, - terminalTheme, - localeTimezone, - allowUntrustedCAs, - allowErrorReporting, - downloadMirror, - downloadKubectlBinaries, - downloadBinariesPath, - kubectlBinariesPath, - openAtLogin, - hiddenTableColumns, - syncKubeconfigEntries, - editorConfiguration, - terminalCopyOnSelect, - terminalConfig, - extensionRegistryUrl, -}; diff --git a/src/common/user-store/terminal-theme.injectable.ts b/src/common/user-store/terminal-theme.injectable.ts new file mode 100644 index 000000000000..a0a00c325332 --- /dev/null +++ b/src/common/user-store/terminal-theme.injectable.ts @@ -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; diff --git a/src/common/user-store/user-store.injectable.ts b/src/common/user-store/user-store.injectable.ts index 4e01cc50eba7..3b45b03b1d7c 100644 --- a/src/common/user-store/user-store.injectable.ts +++ b/src/common/user-store/user-store.injectable.ts @@ -6,20 +6,37 @@ import { getInjectable } from "@ogre-tools/injectable"; import { UserStore } from "./user-store"; import selectedUpdateChannelInjectable from "../../features/application-update/common/selected-update-channel/selected-update-channel.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({ id: "user-store", - instantiate: (di) => { - UserStore.resetInstance(); - - return UserStore.createInstance({ - selectedUpdateChannel: di.inject(selectedUpdateChannelInjectable), - emitAppEvent: di.inject(emitAppEventInjectable), - }); - }, - - causesSideEffects: true, + instantiate: (di) => new UserStore({ + selectedUpdateChannel: di.inject(selectedUpdateChannelInjectable), + emitAppEvent: di.inject(emitAppEventInjectable), + directoryForUserData: di.inject(directoryForUserDataInjectable), + getConfigurationFileModel: di.inject(getConfigurationFileModelInjectable), + logger: di.inject(loggerInjectable), + storeMigrationVersion: di.inject(storeMigrationVersionInjectable), + migrations: di.inject(storeMigrationsInjectable, userStoreMigrationInjectionToken), + getBasenameOfPath: di.inject(getBasenameOfPathInjectable), + ipcChannelPrefixes: di.inject(baseStoreIpcChannelPrefixesInjectionToken), + persistStateToConfig: di.inject(persistStateToConfigInjectionToken), + enlistMessageChannelListener: di.inject(enlistMessageChannelListenerInjectionToken), + shouldDisableSyncInListener: di.inject(shouldBaseStoreDisableSyncInIpcListenerInjectionToken), + preferenceDescriptors: di.inject(userStorePreferenceDescriptorsInjectable), + }), }); export default userStoreInjectable; diff --git a/src/common/user-store/user-store.ts b/src/common/user-store/user-store.ts index 7db6127ed853..8979ba33513a 100644 --- a/src/common/user-store/user-store.ts +++ b/src/common/user-store/user-store.ts @@ -3,44 +3,37 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { app } from "electron"; -import { action, observable, reaction, makeObservable, isObservableArray, isObservableSet, isObservableMap } from "mobx"; -import { BaseStore } from "../base-store"; -import migrations from "../../migrations/user-store"; +import { action, observable, makeObservable, isObservableArray, isObservableSet, isObservableMap } from "mobx"; +import type { BaseStoreDependencies } from "../base-store/base-store"; +import { BaseStore } from "../base-store/base-store"; import { getOrInsertSet, toggle, toJS, object } from "../../renderer/utils"; -import { DESCRIPTORS } 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"; // TODO: Remove coupling with Feature 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 { PreferenceDescriptors } from "./preference-descriptors.injectable"; export interface UserStoreModel { - lastSeenAppVersion: string; preferences: UserPreferencesModel; } -interface Dependencies { +interface Dependencies extends BaseStoreDependencies { readonly selectedUpdateChannel: SelectedUpdateChannel; + readonly preferenceDescriptors: PreferenceDescriptors; emitAppEvent: EmitAppEvent; } export class UserStore extends BaseStore /* implements UserStoreFlatModel (when strict null is enabled) */ { - readonly displayName = "UserStore"; - - constructor(private readonly dependencies: Dependencies) { - super({ + constructor(protected readonly dependencies: Dependencies) { + super(dependencies, { configName: "lens-user-store", - migrations, }); makeObservable(this); } - @observable lastSeenAppVersion = "0.0.0"; - /** * @deprecated No longer used */ @@ -51,58 +44,45 @@ export class UserStore extends BaseStore /* implements UserStore */ @observable newContexts = observable.set(); - @observable allowErrorReporting!: StoreType; - @observable allowUntrustedCAs!: StoreType; - @observable colorTheme!: StoreType; - @observable terminalTheme!: StoreType; - @observable localeTimezone!: StoreType; - @observable downloadMirror!: StoreType; - @observable httpsProxy!: StoreType; - @observable shell!: StoreType; - @observable downloadBinariesPath!: StoreType; - @observable kubectlBinariesPath!: StoreType; - @observable terminalCopyOnSelect!: StoreType; - @observable terminalConfig!: StoreType; - @observable extensionRegistryUrl!: StoreType; + @observable allowErrorReporting!: StoreType; + @observable allowUntrustedCAs!: StoreType; + @observable colorTheme!: StoreType; + @observable terminalTheme!: StoreType; + @observable localeTimezone!: StoreType; + @observable downloadMirror!: StoreType; + @observable httpsProxy!: StoreType; + @observable shell!: StoreType; + @observable downloadBinariesPath!: StoreType; + @observable kubectlBinariesPath!: StoreType; + @observable terminalCopyOnSelect!: StoreType; + @observable terminalConfig!: StoreType; + @observable extensionRegistryUrl!: StoreType; /** * Download kubectl binaries matching cluster version */ - @observable downloadKubectlBinaries!: StoreType; + @observable downloadKubectlBinaries!: StoreType; /** * Whether the application should open itself at login. */ - @observable openAtLogin!: StoreType; + @observable openAtLogin!: StoreType; /** * The column IDs under each configurable table ID that have been configured * to not be shown */ - @observable hiddenTableColumns!: StoreType; + @observable hiddenTableColumns!: StoreType; /** * Monaco editor configs */ - @observable editorConfiguration!: StoreType; + @observable editorConfiguration!: StoreType; /** * The set of file/folder paths to be synced */ - @observable syncKubeconfigEntries!: StoreType; - - startMainReactions() { - // open at system start-up - reaction(() => this.openAtLogin, openAtLogin => { - app.setLoginItemSettings({ - openAtLogin, - openAsHidden: true, - args: ["--hidden"], - }); - }, { - fireImmediately: true, - }); - } + @observable syncKubeconfigEntries!: StoreType; /** * 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 /* implements UserStore @action resetTheme() { - this.colorTheme = DESCRIPTORS.colorTheme.fromStore(undefined); + this.colorTheme = this.dependencies.preferenceDescriptors.colorTheme.fromStore(undefined); } @action - protected fromStore({ lastSeenAppVersion, preferences }: Partial = {}) { - logger.debug("UserStore.fromStore()", { lastSeenAppVersion, preferences }); + protected fromStore({ preferences }: Partial = {}) { + this.dependencies.logger.debug("UserStore.fromStore()", { preferences }); - if (lastSeenAppVersion) { - this.lastSeenAppVersion = lastSeenAppVersion; - } - - for (const [key, { fromStore }] of object.entries(DESCRIPTORS)) { + for (const [key, { fromStore }] of object.entries(this.dependencies.preferenceDescriptors)) { const curVal = this[key]; const newVal = fromStore((preferences)?.[key] as never) as never; @@ -165,16 +141,13 @@ export class UserStore extends BaseStore /* implements UserStore toJSON(): UserStoreModel { const preferences = object.fromEntries( - object.entries(DESCRIPTORS) + object.entries(this.dependencies.preferenceDescriptors) .map(([key, { toStore }]) => [key, toStore(this[key] as never)]), ) as UserPreferencesModel; return toJS({ - lastSeenAppVersion: this.lastSeenAppVersion, - preferences: { ...preferences, - updateChannel: this.dependencies.selectedUpdateChannel.value.get().id, }, }); diff --git a/src/common/utils/collection-functions.ts b/src/common/utils/collection-functions.ts index 070223046495..5d6dc8054818 100644 --- a/src/common/utils/collection-functions.ts +++ b/src/common/utils/collection-functions.ts @@ -48,11 +48,21 @@ export function getOrInsertSet(map: Map>, key: K): Set { return getOrInsert(map, key, new Set()); } +/** + * A currying version of {@link getOrInsertSet} + */ +export function getOrInsertSetFor(map: Map>): (key: K) => Set { + return (key) => getOrInsertSet(map, key); +} + /** * Like `getOrInsert` but with delayed creation of the item. Which is useful * if it is very expensive to create the initial value. */ -export function getOrInsertWith(map: Map, key: K, builder: () => V): V { +export function getOrInsertWith(map: Map, key: K, builder: () => V): V; +export function getOrInsertWith(map: Map | WeakMap, key: K, builder: () => V): V; + +export function getOrInsertWith(map: Map | WeakMap, key: K, builder: () => V): V { if (!map.has(key)) { map.set(key, builder()); } diff --git a/src/migrations/utils.ts b/src/common/utils/generate-new-id-for.ts similarity index 100% rename from src/migrations/utils.ts rename to src/common/utils/generate-new-id-for.ts diff --git a/src/common/utils/random-bytes.global-override-for-injectable.ts b/src/common/utils/random-bytes.global-override-for-injectable.ts new file mode 100644 index 000000000000..6f83a264e48c --- /dev/null +++ b/src/common/utils/random-bytes.global-override-for-injectable.ts @@ -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; +}); diff --git a/src/common/utils/random-bytes.injectable.ts b/src/common/utils/random-bytes.injectable.ts new file mode 100644 index 000000000000..9f00961824d5 --- /dev/null +++ b/src/common/utils/random-bytes.injectable.ts @@ -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; + +const randomBytesInjectable = getInjectable({ + id: "random-bytes", + instantiate: (): RandomBytes => promisify(randomBytes), + causesSideEffects: true, +}); + +export default randomBytesInjectable; diff --git a/src/common/utils/singleton.ts b/src/common/utils/singleton.ts index d60fdb0490b8..0dea1f7526d0 100644 --- a/src/common/utils/singleton.ts +++ b/src/common/utils/singleton.ts @@ -3,10 +3,13 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -interface StaticThis { new(...args: R): T } +export interface StaticThis { new(...args: R): T } +/** + * @deprecated This is a form of global shared state + */ export class Singleton { - private static instances = new WeakMap(); + private static readonly instances = new WeakMap(); private static creating = ""; constructor() { diff --git a/src/common/utils/type-narrowing.ts b/src/common/utils/type-narrowing.ts index 936fb488165a..41552c313601 100644 --- a/src/common/utils/type-narrowing.ts +++ b/src/common/utils/type-narrowing.ts @@ -123,6 +123,10 @@ export function isDefined(val: T | undefined | null): val is T { 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 */ @@ -146,6 +150,15 @@ export function hasDefiniteField(field: Field): (val: return (val): val is T & { [f in Field]-?: NonNullable } => val[field] != null; } +export function isPromiseLike(res: unknown): res is (Promise | { then: (fn: (val: unknown) => any) => Promise }) { + if (res instanceof Promise) { + return true; + } + + return isObject(res) + && hasTypedProperty(res, "then", isFunction); +} + export function isPromiseSettledRejected(result: PromiseSettledResult): result is PromiseRejectedResult { return result.status === "rejected"; } diff --git a/src/common/vars.ts b/src/common/vars.ts index fe568b5b8654..cbde12ff5d59 100644 --- a/src/common/vars.ts +++ b/src/common/vars.ts @@ -4,7 +4,7 @@ */ // 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 @@ -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 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 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; diff --git a/src/common/weblink-store.injectable.ts b/src/common/weblink-store.injectable.ts deleted file mode 100644 index 4aca7dce2a66..000000000000 --- a/src/common/weblink-store.injectable.ts +++ /dev/null @@ -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; diff --git a/src/common/weblinks-store/migration-token.ts b/src/common/weblinks-store/migration-token.ts new file mode 100644 index 000000000000..d1cea9334bb6 --- /dev/null +++ b/src/common/weblinks-store/migration-token.ts @@ -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({ + id: "weblink-store-migration-token", +}); diff --git a/src/common/weblinks-store/weblink-store.injectable.ts b/src/common/weblinks-store/weblink-store.injectable.ts new file mode 100644 index 000000000000..cf793a2e582e --- /dev/null +++ b/src/common/weblinks-store/weblink-store.injectable.ts @@ -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; diff --git a/src/common/weblink-store.ts b/src/common/weblinks-store/weblink-store.ts similarity index 86% rename from src/common/weblink-store.ts rename to src/common/weblinks-store/weblink-store.ts index a8430666e5c9..2044ca08c11d 100644 --- a/src/common/weblink-store.ts +++ b/src/common/weblinks-store/weblink-store.ts @@ -4,10 +4,10 @@ */ import { action, comparer, observable, makeObservable } from "mobx"; -import { BaseStore } from "./base-store"; -import migrations from "../migrations/weblinks-store"; +import type { BaseStoreDependencies } from "../base-store/base-store"; +import { BaseStore } from "../base-store/base-store"; import * as uuid from "uuid"; -import { toJS } from "./utils"; +import { toJS } from "../utils"; export interface WeblinkData { id: string; @@ -27,17 +27,15 @@ export interface WeblinkStoreModel { } export class WeblinkStore extends BaseStore { - readonly displayName = "WeblinkStore"; @observable weblinks: WeblinkData[] = []; - constructor() { - super({ + constructor(deps: BaseStoreDependencies) { + super(deps, { configName: "lens-weblink-store", accessPropertiesByDotNotation: false, // To make dots safe in cluster context names syncOptions: { equals: comparer.structural, }, - migrations, }); makeObservable(this); this.load(); diff --git a/src/extensions/__tests__/extension-loader.test.ts b/src/extensions/__tests__/extension-loader.test.ts index fa14f5856f5b..6e9c7cd0f95b 100644 --- a/src/extensions/__tests__/extension-loader.test.ts +++ b/src/extensions/__tests__/extension-loader.test.ts @@ -14,6 +14,8 @@ import { delay } from "../../renderer/utils"; import { getDiForUnitTesting } from "../../renderer/getDiForUnitTesting"; import ipcRendererInjectable from "../../renderer/utils/channel/ipc-renderer.injectable"; 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); @@ -28,6 +30,9 @@ describe("ExtensionLoader", () => { beforeEach(() => { const di = getDiForUnitTesting({ doGeneralOverrides: true }); + di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data"); + di.override(currentlyInClusterFrameInjectable, () => false); + di.override(ipcRendererInjectable, () => ({ invoke: jest.fn(async (channel: string) => { if (channel === "extension-loader:main:state") { diff --git a/src/extensions/common-api/user-preferences.ts b/src/extensions/common-api/user-preferences.ts index 3a0a93793b5b..ed925bd05d1a 100644 --- a/src/extensions/common-api/user-preferences.ts +++ b/src/extensions/common-api/user-preferences.ts @@ -3,7 +3,8 @@ * 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 { /** * Get the configured kubectl binaries path. @@ -11,6 +12,8 @@ export interface UserPreferenceExtensionItems { getKubectlPath: () => string | undefined; } +const userStore = asLegacyGlobalForExtensionApi(userStoreInjectable); + export const Preferences: UserPreferenceExtensionItems = { - getKubectlPath: () => UserStore.getInstance().kubectlBinariesPath, + getKubectlPath: () => userStore.kubectlBinariesPath, }; diff --git a/src/extensions/extension-discovery/extension-discovery.injectable.ts b/src/extensions/extension-discovery/extension-discovery.injectable.ts index 549f9c9de7c5..d088942552a8 100644 --- a/src/extensions/extension-discovery/extension-discovery.injectable.ts +++ b/src/extensions/extension-discovery/extension-discovery.injectable.ts @@ -26,7 +26,7 @@ import getBasenameOfPathInjectable from "../../common/path/get-basename.injectab import getDirnameOfPathInjectable from "../../common/path/get-dirname.injectable"; import getRelativePathInjectable from "../../common/path/get-relative-path.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 lensResourcesDirInjectable from "../../common/vars/lens-resources-dir.injectable"; diff --git a/src/extensions/extension-discovery/extension-discovery.test.ts b/src/extensions/extension-discovery/extension-discovery.test.ts index f3630addafdc..d71f8c529221 100644 --- a/src/extensions/extension-discovery/extension-discovery.test.ts +++ b/src/extensions/extension-discovery/extension-discovery.test.ts @@ -15,10 +15,13 @@ import readJsonFileInjectable from "../../common/fs/read-json-file.injectable"; import pathExistsInjectable from "../../common/fs/path-exists.injectable"; import watchInjectable from "../../common/fs/watch/watch.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 joinPathsInjectable from "../../common/path/join-paths.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", () => { let extensionDiscovery: ExtensionDiscovery; @@ -34,6 +37,9 @@ describe("ExtensionDiscovery", () => { di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data"); di.override(installExtensionInjectable, () => () => Promise.resolve()); 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); homeDirectoryPath = di.inject(homeDirectoryPathInjectable); diff --git a/src/extensions/extension-discovery/extension-discovery.ts b/src/extensions/extension-discovery/extension-discovery.ts index a23cebd9c2fd..cad4ebdc65bf 100644 --- a/src/extensions/extension-discovery/extension-discovery.ts +++ b/src/extensions/extension-discovery/extension-discovery.ts @@ -30,7 +30,7 @@ import type { JoinPaths } from "../../common/path/join-paths.injectable"; import type { GetBasenameOfPath } from "../../common/path/get-basename.injectable"; import type { GetDirnameOfPath } from "../../common/path/get-dirname.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"; interface Dependencies { diff --git a/src/extensions/extension-installer/extension-installer.injectable.ts b/src/extensions/extension-installer/extension-installer.injectable.ts index 169b758049fa..528d50495865 100644 --- a/src/extensions/extension-installer/extension-installer.injectable.ts +++ b/src/extensions/extension-installer/extension-installer.injectable.ts @@ -9,12 +9,9 @@ import extensionPackageRootDirectoryInjectable from "./extension-package-root-di const extensionInstallerInjectable = getInjectable({ id: "extension-installer", - instantiate: (di) => - new ExtensionInstaller({ - extensionPackageRootDirectory: di.inject( - extensionPackageRootDirectoryInjectable, - ), - }), + instantiate: (di) => new ExtensionInstaller({ + extensionPackageRootDirectory: di.inject(extensionPackageRootDirectoryInjectable), + }), }); export default extensionInstallerInjectable; diff --git a/src/extensions/extension-installer/extension-package-root-directory/extension-package-root-directory.injectable.ts b/src/extensions/extension-installer/extension-package-root-directory/extension-package-root-directory.injectable.ts index 6ab004ef6d0d..72bd0ad8c22c 100644 --- a/src/extensions/extension-installer/extension-package-root-directory/extension-package-root-directory.injectable.ts +++ b/src/extensions/extension-installer/extension-package-root-directory/extension-package-root-directory.injectable.ts @@ -3,8 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import directoryForUserDataInjectable - from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; +import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; const extensionPackageRootDirectoryInjectable = getInjectable({ id: "extension-package-root-directory", diff --git a/src/extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.injectable.ts b/src/extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.injectable.ts index a0951baa8ff1..b511437da97f 100644 --- a/src/extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.injectable.ts +++ b/src/extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.injectable.ts @@ -5,19 +5,38 @@ import { getInjectable } from "@ogre-tools/injectable"; import { FileSystemProvisionerStore } from "./file-system-provisioner-store"; 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({ id: "file-system-provisioner-store", - instantiate: (di) => { - FileSystemProvisionerStore.resetInstance(); - - return FileSystemProvisionerStore.createInstance({ - directoryForExtensionData: di.inject(directoryForExtensionDataInjectable), - }); - }, - - causesSideEffects: true, + instantiate: (di) => new FileSystemProvisionerStore({ + directoryForExtensionData: di.inject(directoryForExtensionDataInjectable), + ensureDirectory: di.inject(ensureDirectoryInjectable), + joinPaths: di.inject(joinPathsInjectable), + randomBytes: di.inject(randomBytesInjectable), + directoryForUserData: di.inject(directoryForUserDataInjectable), + getConfigurationFileModel: di.inject(getConfigurationFileModelInjectable), + logger: di.inject(loggerInjectable), + 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; diff --git a/src/extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.ts b/src/extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.ts index fab21df3cc78..5655a0cf0620 100644 --- a/src/extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.ts +++ b/src/extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.ts @@ -3,35 +3,37 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { randomBytes } from "crypto"; import { SHA256 } from "crypto-js"; -import fse from "fs-extra"; import { action, makeObservable, observable } from "mobx"; -import path from "path"; -import { BaseStore } from "../../../common/base-store"; +import type { BaseStoreDependencies } from "../../../common/base-store/base-store"; +import { BaseStore } from "../../../common/base-store/base-store"; 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 { extensions: Record; // extension names to paths } -interface Dependencies { - directoryForExtensionData: string; +interface Dependencies extends BaseStoreDependencies { + readonly directoryForExtensionData: string; + ensureDirectory: EnsureDirectory; + joinPaths: JoinPaths; + randomBytes: RandomBytes; } export class FileSystemProvisionerStore extends BaseStore { - readonly displayName = "FilesystemProvisionerStore"; - registeredExtensions = observable.map(); + readonly registeredExtensions = observable.map(); - constructor(private dependencies: Dependencies) { - super({ + constructor(protected readonly dependencies: Dependencies) { + super(dependencies, { configName: "lens-filesystem-provisioner-store", accessPropertiesByDotNotation: false, // To make dots safe in cluster context names }); makeObservable(this); - this.load(); } /** @@ -41,14 +43,14 @@ export class FileSystemProvisionerStore extends BaseStore { * @returns path to the folder that the extension can safely write files to. */ async requestDirectory(extensionName: string): Promise { - const dirPath = getOrInsertWith(this.registeredExtensions, extensionName, () => { - const salt = randomBytes(32).toString("hex"); + const dirPath = await getOrInsertWithAsync(this.registeredExtensions, extensionName, async () => { + const salt = (await this.dependencies.randomBytes(32)).toString("hex"); 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; } diff --git a/src/extensions/extension-store.ts b/src/extensions/extension-store.ts index 4217576d2ff9..bda3c09c10f6 100644 --- a/src/extensions/extension-store.ts +++ b/src/extensions/extension-store.ts @@ -3,13 +3,76 @@ * 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 type { LensExtension } from "./lens-extension"; 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 extends BaseStoreParams { + migrations?: Migrations; +} export abstract class ExtensionStore extends BaseStore { - readonly displayName = "ExtensionStore"; + private static readonly instances = new WeakMap>(); + + /** + * @deprecated This is a form of global shared state. Just call `new Store(...)` + */ + static createInstance, R extends any[]>(this: StaticThis, ...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(this: StaticThis, strict?: true): T; + static getInstance(this: StaticThis, strict: false): T | undefined; + static getInstance(this: StaticThis, 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) { + const di = getLegacyGlobalDiForExtensionApi(); + + super({ + directoryForUserData: di.inject(directoryForUserDataInjectable), + getConfigurationFileModel: di.inject(getConfigurationFileModelInjectable), + logger: di.inject(loggerInjectable), + storeMigrationVersion: di.inject(storeMigrationVersionInjectable), + migrations: migrations as Migrations>, + 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; loadExtension(extension: LensExtension) { diff --git a/src/extensions/extensions-store/extensions-store.injectable.ts b/src/extensions/extensions-store/extensions-store.injectable.ts index edf84e4b6641..9f5ff8327000 100644 --- a/src/extensions/extensions-store/extensions-store.injectable.ts +++ b/src/extensions/extensions-store/extensions-store.injectable.ts @@ -3,18 +3,31 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ 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"; const extensionsStoreInjectable = getInjectable({ id: "extensions-store", - - instantiate: () => { - ExtensionsStore.resetInstance(); - - return ExtensionsStore.createInstance(); - }, - - causesSideEffects: true, + instantiate: (di) => new ExtensionsStore({ + directoryForUserData: di.inject(directoryForUserDataInjectable), + getConfigurationFileModel: di.inject(getConfigurationFileModelInjectable), + logger: di.inject(loggerInjectable), + 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 extensionsStoreInjectable; diff --git a/src/extensions/extensions-store/extensions-store.ts b/src/extensions/extensions-store/extensions-store.ts index 2ec1c8fd55ff..3b2dc80eb11e 100644 --- a/src/extensions/extensions-store/extensions-store.ts +++ b/src/extensions/extensions-store/extensions-store.ts @@ -6,7 +6,8 @@ import type { LensExtensionId } from "../lens-extension"; import { action, computed, makeObservable, observable } from "mobx"; 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 { extensions: Record; @@ -23,9 +24,8 @@ export interface IsEnabledExtensionDescriptor { } export class ExtensionsStore extends BaseStore { - readonly displayName = "ExtensionsStore"; - constructor() { - super({ + constructor(deps: BaseStoreDependencies) { + super(deps, { configName: "lens-extensions", }); makeObservable(this); diff --git a/src/extensions/renderer-api/theming.ts b/src/extensions/renderer-api/theming.ts index 435cf23504ff..3e4c9fe97bb8 100644 --- a/src/extensions/renderer-api/theming.ts +++ b/src/extensions/renderer-api/theming.ts @@ -4,7 +4,7 @@ */ 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"; export const activeTheme = asLegacyGlobalForExtensionApi(activeThemeInjectable); diff --git a/src/features/__snapshots__/extension-special-characters-in-page-registrations.test.tsx.snap b/src/features/__snapshots__/extension-special-characters-in-page-registrations.test.tsx.snap index 9046b189a9aa..c520be7f588f 100644 --- a/src/features/__snapshots__/extension-special-characters-in-page-registrations.test.tsx.snap +++ b/src/features/__snapshots__/extension-special-characters-in-page-registrations.test.tsx.snap @@ -164,7 +164,23 @@ exports[`extension special characters in page registrations renders 1`] = `
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
{ - await applicationMenu.start(); + run: () => { + applicationMenu.start(); }, }; }, diff --git a/src/features/application-update/__snapshots__/installing-update.test.ts.snap b/src/features/application-update/__snapshots__/installing-update.test.ts.snap index deae3b999d0a..20df788ab7af 100644 --- a/src/features/application-update/__snapshots__/installing-update.test.ts.snap +++ b/src/features/application-update/__snapshots__/installing-update.test.ts.snap @@ -165,7 +165,23 @@ exports[`installing update when started renders 1`] = `
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
({ - start: async () => {}, - stop: async () => {}, + start: () => {}, + stop: () => {}, started: false, })); diff --git a/src/features/application-update/child-features/periodical-checking-of-updates/main/start-checking-for-updates.injectable.ts b/src/features/application-update/child-features/periodical-checking-of-updates/main/start-checking-for-updates.injectable.ts index 0292d148df43..20b60af2fac9 100644 --- a/src/features/application-update/child-features/periodical-checking-of-updates/main/start-checking-for-updates.injectable.ts +++ b/src/features/application-update/child-features/periodical-checking-of-updates/main/start-checking-for-updates.injectable.ts @@ -16,9 +16,9 @@ const startCheckingForUpdatesInjectable = getInjectable({ return { id: "start-checking-for-updates", - run: async () => { + run: () => { if (updatingIsEnabled && !periodicalCheckForUpdates.started) { - await periodicalCheckForUpdates.start(); + periodicalCheckForUpdates.start(); } }, }; diff --git a/src/features/application-update/child-features/selection-of-update-stability/__snapshots__/selection-of-update-stability.test.ts.snap b/src/features/application-update/child-features/selection-of-update-stability/__snapshots__/selection-of-update-stability.test.ts.snap index bf6bf4674e4c..de277ddc97dd 100644 --- a/src/features/application-update/child-features/selection-of-update-stability/__snapshots__/selection-of-update-stability.test.ts.snap +++ b/src/features/application-update/child-features/selection-of-update-stability/__snapshots__/selection-of-update-stability.test.ts.snap @@ -165,7 +165,23 @@ exports[`selection of update stability when started renders 1`] = `
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
{ let checkForPlatformUpdates: CheckForPlatformUpdates; @@ -37,7 +37,13 @@ describe("check-for-platform-updates", () => { logErrorMock = jest.fn(); - di.override(loggerInjectable, () => ({ error: logErrorMock }) as unknown as Logger); + di.override(loggerInjectable, () => ({ + error: logErrorMock, + debug: noop, + info: noop, + silly: noop, + warn: noop, + })); checkForPlatformUpdates = di.inject(checkForPlatformUpdatesInjectable); }); diff --git a/src/features/application-update/main/download-update/download-platform-update/download-platform-update.test.ts b/src/features/application-update/main/download-update/download-platform-update/download-platform-update.test.ts index d60b4eb897c9..2cedacb68b41 100644 --- a/src/features/application-update/main/download-update/download-platform-update/download-platform-update.test.ts +++ b/src/features/application-update/main/download-update/download-platform-update/download-platform-update.test.ts @@ -12,7 +12,7 @@ import asyncFn from "@async-fn/jest"; import { getPromiseStatus } from "../../../../../common/test-utils/get-promise-status"; import type { DiContainer } from "@ogre-tools/injectable"; import loggerInjectable from "../../../../../common/logger.injectable"; -import type { Logger } from "../../../../../common/logger"; +import { noop } from "../../../../../common/utils"; describe("download-platform-update", () => { let downloadPlatformUpdate: DownloadPlatformUpdate; @@ -43,7 +43,13 @@ describe("download-platform-update", () => { di.override(electronUpdaterInjectable, () => electronUpdaterFake); logErrorMock = jest.fn(); - di.override(loggerInjectable, () => ({ error: logErrorMock }) as unknown as Logger); + di.override(loggerInjectable, () => ({ + error: logErrorMock, + debug: noop, + info: noop, + silly: noop, + warn: noop, + })); downloadPlatformUpdate = di.inject(downloadPlatformUpdateInjectable); }); diff --git a/src/features/catalog/__snapshots__/opening-entity-details.test.tsx.snap b/src/features/catalog/__snapshots__/opening-entity-details.test.tsx.snap index 56c4a912e4a9..f459bdca0a83 100644 --- a/src/features/catalog/__snapshots__/opening-entity-details.test.tsx.snap +++ b/src/features/catalog/__snapshots__/opening-entity-details.test.tsx.snap @@ -165,7 +165,23 @@ exports[`opening catalog entity details panel renders 1`] = `
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
{ emitAppEvent({ name: "cluster", action: "remove" }); diff --git a/src/features/cluster/sidebar-and-tab-navigation-for-core.test.tsx b/src/features/cluster/sidebar-and-tab-navigation-for-core.test.tsx index 519e6bb3c839..fd4d95d76015 100644 --- a/src/features/cluster/sidebar-and-tab-navigation-for-core.test.tsx +++ b/src/features/cluster/sidebar-and-tab-navigation-for-core.test.tsx @@ -24,6 +24,7 @@ import { navigateToRouteInjectionToken } from "../../common/front-end-routing/na import sidebarStorageInjectable from "../../renderer/components/layout/sidebar-storage/sidebar-storage.injectable"; import { advanceFakeTime, useFakeTime } from "../../common/test-utils/use-fake-time"; import storageSaveDelayInjectable from "../../renderer/utils/create-storage/storage-save-delay.injectable"; +import { flushPromises } from "../../common/test-utils/flush-promises"; describe("cluster - sidebar and tab navigation for core", () => { let builder: ApplicationBuilder; @@ -283,6 +284,8 @@ describe("cluster - sidebar and tab navigation for core", () => { const readJsonFileFake = windowDi.inject(readJsonFileInjectable); + await flushPromises(); + const actual = await readJsonFileFake( "/some-directory-for-lens-local-storage/some-cluster-id.json", ); diff --git a/src/features/cluster/sidebar-and-tab-navigation-for-extensions.test.tsx b/src/features/cluster/sidebar-and-tab-navigation-for-extensions.test.tsx index a228cc880e52..2441afa000e9 100644 --- a/src/features/cluster/sidebar-and-tab-navigation-for-extensions.test.tsx +++ b/src/features/cluster/sidebar-and-tab-navigation-for-extensions.test.tsx @@ -20,6 +20,7 @@ import type { IObservableValue } from "mobx"; import { runInAction, computed, observable } from "mobx"; import storageSaveDelayInjectable from "../../renderer/utils/create-storage/storage-save-delay.injectable"; import type { DiContainer } from "@ogre-tools/injectable"; +import { flushPromises } from "../../common/test-utils/flush-promises"; describe("cluster - sidebar and tab navigation for extensions", () => { let applicationBuilder: ApplicationBuilder; @@ -399,6 +400,8 @@ describe("cluster - sidebar and tab navigation for extensions", () => { const readJsonFileFake = windowDi.inject(readJsonFileInjectable); + await flushPromises(); // Needed because of several async calls + const actual = await readJsonFileFake( "/some-directory-for-lens-local-storage/some-cluster-id.json", ); diff --git a/src/features/cluster/state-sync/common/channels.ts b/src/features/cluster/state-sync/common/channels.ts new file mode 100644 index 000000000000..7ceeb82f84a7 --- /dev/null +++ b/src/features/cluster/state-sync/common/channels.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { ClusterId, ClusterState } from "../../../../common/cluster-types"; +import type { MessageChannel } from "../../../../common/utils/channel/message-channel-listener-injection-token"; +import type { RequestChannel } from "../../../../common/utils/channel/request-channel-listener-injection-token"; + +export interface ClusterStateSync { + clusterId: ClusterId; + state: ClusterState; +} + +export const clusterStateSyncChannel: MessageChannel = { + id: "cluster-state-sync", +}; + +export const initialClusterStatesChannel: RequestChannel = { + id: "initial-cluster-state-sync", +}; diff --git a/src/features/cluster/state-sync/main/emit-update.injectable.ts b/src/features/cluster/state-sync/main/emit-update.injectable.ts new file mode 100644 index 000000000000..8cadd32864f6 --- /dev/null +++ b/src/features/cluster/state-sync/main/emit-update.injectable.ts @@ -0,0 +1,21 @@ +/** + * 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 { MessageChannelHandler } from "../../../../common/utils/channel/message-channel-listener-injection-token"; +import { sendMessageToChannelInjectionToken } from "../../../../common/utils/channel/message-to-channel-injection-token"; +import { clusterStateSyncChannel } from "../common/channels"; + +export type EmitClusterStateUpdate = MessageChannelHandler; + +const emitClusterStateUpdateInjectable = getInjectable({ + id: "emit-cluster-state-update", + instantiate: (di): EmitClusterStateUpdate => { + const sendMessageToChannel = di.inject(sendMessageToChannelInjectionToken); + + return (message) => sendMessageToChannel(clusterStateSyncChannel, message); + }, +}); + +export default emitClusterStateUpdateInjectable; diff --git a/src/features/cluster/state-sync/main/handle-initial.injectable.ts b/src/features/cluster/state-sync/main/handle-initial.injectable.ts new file mode 100644 index 000000000000..708f032d4856 --- /dev/null +++ b/src/features/cluster/state-sync/main/handle-initial.injectable.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import clusterStoreInjectable from "../../../../common/cluster-store/cluster-store.injectable"; +import { getRequestChannelListenerInjectable } from "../../../../main/utils/channel/channel-listeners/listener-tokens"; +import { initialClusterStatesChannel } from "../common/channels"; + +const handleInitialClusterStateSyncInjectable = getRequestChannelListenerInjectable({ + channel: initialClusterStatesChannel, + handler: (di) => { + const clusterStore = di.inject(clusterStoreInjectable); + + return () => clusterStore.clustersList.map(cluster => ({ + clusterId: cluster.id, + state: cluster.getState(), + })); + }, +}); + +export default handleInitialClusterStateSyncInjectable; diff --git a/src/features/cluster/state-sync/main/setup-sync.injectable.ts b/src/features/cluster/state-sync/main/setup-sync.injectable.ts new file mode 100644 index 000000000000..9b0e13249d21 --- /dev/null +++ b/src/features/cluster/state-sync/main/setup-sync.injectable.ts @@ -0,0 +1,44 @@ +/** + * 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 { isEqual } from "lodash"; +import { autorun } from "mobx"; +import clusterStoreInjectable from "../../../../common/cluster-store/cluster-store.injectable"; +import type { ClusterId, ClusterState } from "../../../../common/cluster-types"; +import { beforeApplicationIsLoadingInjectionToken } from "../../../../main/start-main-application/runnable-tokens/before-application-is-loading-injection-token"; +import initClusterStoreInjectable from "../../store/main/init.injectable"; +import emitClusterStateUpdateInjectable from "./emit-update.injectable"; + +const setupClusterStateBroadcastingInjectable = getInjectable({ + id: "setup-cluster-state-broadcasting", + instantiate: (di) => ({ + id: "setup-cluster-state-broadcasting", + run: () => { + const emitClusterStateUpdate = di.inject(emitClusterStateUpdateInjectable); + const clusterStore = di.inject(clusterStoreInjectable); + const prevStates = new Map(); + + autorun(() => { + for (const cluster of clusterStore.clusters.values()) { + const prevState = prevStates.get(cluster.id); + const curState = cluster.getState(); + + if (!prevState || !isEqual(prevState, curState)) { + prevStates.set(cluster.id, curState); + + emitClusterStateUpdate({ + clusterId: cluster.id, + state: cluster.getState(), + }); + } + } + }); + }, + runAfter: di.inject(initClusterStoreInjectable), + }), + injectionToken: beforeApplicationIsLoadingInjectionToken, +}); + +export default setupClusterStateBroadcastingInjectable; diff --git a/src/features/cluster/state-sync/renderer/listener.injectable.ts b/src/features/cluster/state-sync/renderer/listener.injectable.ts new file mode 100644 index 000000000000..9863a391e8e1 --- /dev/null +++ b/src/features/cluster/state-sync/renderer/listener.injectable.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import getClusterByIdInjectable from "../../../../common/cluster-store/get-by-id.injectable"; +import { getMessageChannelListenerInjectable } from "../../../../common/utils/channel/message-channel-listener-injection-token"; +import { clusterStateSyncChannel } from "../common/channels"; + +const clusterStateListenerInjectable = getMessageChannelListenerInjectable({ + channel: clusterStateSyncChannel, + id: "main", + handler: (di) => { + const getClusterById = di.inject(getClusterByIdInjectable); + + return ({ clusterId, state }) => getClusterById(clusterId)?.setState(state); + }, +}); + +export default clusterStateListenerInjectable; diff --git a/src/features/cluster/state-sync/renderer/request-initial.injectable.ts b/src/features/cluster/state-sync/renderer/request-initial.injectable.ts new file mode 100644 index 000000000000..89f72fbcf5d0 --- /dev/null +++ b/src/features/cluster/state-sync/renderer/request-initial.injectable.ts @@ -0,0 +1,21 @@ +/** + * 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 { RequestChannelHandler } from "../../../../main/utils/channel/channel-listeners/listener-tokens"; +import requestFromChannelInjectable from "../../../../renderer/utils/channel/request-from-channel.injectable"; +import { initialClusterStatesChannel } from "../common/channels"; + +export type RequestInitialClusterStates = RequestChannelHandler; + +const requestInitialClusterStatesInjectable = getInjectable({ + id: "request-initial-cluster-states", + instantiate: (di): RequestInitialClusterStates => { + const requestFromChannel = di.inject(requestFromChannelInjectable); + + return () => requestFromChannel(initialClusterStatesChannel); + }, +}); + +export default requestInitialClusterStatesInjectable; diff --git a/src/features/cluster/state-sync/renderer/setup-sync.injectable.ts b/src/features/cluster/state-sync/renderer/setup-sync.injectable.ts new file mode 100644 index 000000000000..93005543db0f --- /dev/null +++ b/src/features/cluster/state-sync/renderer/setup-sync.injectable.ts @@ -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 getClusterByIdInjectable from "../../../../common/cluster-store/get-by-id.injectable"; +import { beforeFrameStartsInjectionToken } from "../../../../renderer/before-frame-starts/tokens"; +import initClusterStoreInjectable from "../../store/renderer/init.injectable"; +import requestInitialClusterStatesInjectable from "./request-initial.injectable"; + +const setupClusterStateSyncInjectable = getInjectable({ + id: "setup-cluster-state-sync", + instantiate: (di) => ({ + id: "setup-cluster-state-sync", + run: async () => { + const requestInitialClusterStates = di.inject(requestInitialClusterStatesInjectable); + const getClusterById = di.inject(getClusterByIdInjectable); + const initalStates = await requestInitialClusterStates(); + + for (const { clusterId, state } of initalStates) { + getClusterById(clusterId)?.setState(state); + } + }, + runAfter: di.inject(initClusterStoreInjectable), + }), + injectionToken: beforeFrameStartsInjectionToken, +}); + +export default setupClusterStateSyncInjectable; diff --git a/src/features/cluster/store/main/init.injectable.ts b/src/features/cluster/store/main/init.injectable.ts new file mode 100644 index 000000000000..7849ab6acdbc --- /dev/null +++ b/src/features/cluster/store/main/init.injectable.ts @@ -0,0 +1,26 @@ +/** + * 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 clusterStoreInjectable from "../../../../common/cluster-store/cluster-store.injectable"; +import { beforeApplicationIsLoadingInjectionToken } from "../../../../main/start-main-application/runnable-tokens/before-application-is-loading-injection-token"; +import initUserStoreInjectable from "../../../../main/stores/init-user-store.injectable"; + +const initClusterStoreInjectable = getInjectable({ + id: "init-cluster-store", + instantiate: (di) => { + const clusterStore = di.inject(clusterStoreInjectable); + + return { + id: "init-cluster-store", + run: () => { + clusterStore.load(); + }, + runAfter: di.inject(initUserStoreInjectable), + }; + }, + injectionToken: beforeApplicationIsLoadingInjectionToken, +}); + +export default initClusterStoreInjectable; diff --git a/src/features/cluster/store/renderer/init.injectable.ts b/src/features/cluster/store/renderer/init.injectable.ts new file mode 100644 index 000000000000..2c2795de5ca6 --- /dev/null +++ b/src/features/cluster/store/renderer/init.injectable.ts @@ -0,0 +1,24 @@ +/** + * 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 clusterStoreInjectable from "../../../../common/cluster-store/cluster-store.injectable"; +import { beforeFrameStartsInjectionToken } from "../../../../renderer/before-frame-starts/tokens"; +import initUserStoreInjectable from "../../../../renderer/stores/init-user-store.injectable"; + +const initClusterStoreInjectable = getInjectable({ + id: "init-cluster-store", + instantiate: (di) => ({ + id: "init-cluster-store", + run: () => { + const clusterStore = di.inject(clusterStoreInjectable); + + clusterStore.load(); + }, + runAfter: di.inject(initUserStoreInjectable), + }), + injectionToken: beforeFrameStartsInjectionToken, +}); + +export default initClusterStoreInjectable; diff --git a/src/features/command-pallet/__snapshots__/keyboard-shortcuts.test.tsx.snap b/src/features/command-pallet/__snapshots__/keyboard-shortcuts.test.tsx.snap index 31cc3f76ecff..7bc003b7844a 100644 --- a/src/features/command-pallet/__snapshots__/keyboard-shortcuts.test.tsx.snap +++ b/src/features/command-pallet/__snapshots__/keyboard-shortcuts.test.tsx.snap @@ -256,7 +256,23 @@ exports[`Command Pallet: keyboard shortcut tests when on linux renders 1`] = `
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
Proxy -
-
-
- HTTP Proxy - -
-
- -
-
- - HTTP Proxy server. Used for communicating with Kubernetes API. - -
-
+
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
+ > +
+
+
+ Ca +
+
+
+
- 0 + 1
| undefined; +} + +export const navigateForExtensionChannel: MessageChannel = { + id: "navigate-for-extension", +}; diff --git a/src/features/extensions/navigate/renderer/listener.injectable.ts b/src/features/extensions/navigate/renderer/listener.injectable.ts new file mode 100644 index 000000000000..fccf0efca63a --- /dev/null +++ b/src/features/extensions/navigate/renderer/listener.injectable.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getMessageChannelListenerInjectable } from "../../../../common/utils/channel/message-channel-listener-injection-token"; +import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable"; +import type { LensRendererExtension } from "../../../../extensions/lens-renderer-extension"; +import { navigateForExtensionChannel } from "../common/channel"; + +const navigateForExtensionListenerInjectable = getMessageChannelListenerInjectable({ + channel: navigateForExtensionChannel, + id: "main", + handler: (di) => { + const extensionLoader = di.inject(extensionLoaderInjectable); + + return ({ extId, pageId, params }) => { + const extension = extensionLoader.getInstanceById(extId) as LensRendererExtension | undefined; + + if (extension) { + extension.navigate(pageId, params); + } + }; + }, +}); + +export default navigateForExtensionListenerInjectable; diff --git a/src/features/file-system-provisioner/main/init-store.injectable.ts b/src/features/file-system-provisioner/main/init-store.injectable.ts new file mode 100644 index 000000000000..0fe3d4f77be6 --- /dev/null +++ b/src/features/file-system-provisioner/main/init-store.injectable.ts @@ -0,0 +1,22 @@ +/** + * 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 fileSystemProvisionerStoreInjectable from "../../../extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.injectable"; +import { onLoadOfApplicationInjectionToken } from "../../../main/start-main-application/runnable-tokens/on-load-of-application-injection-token"; + +const initFileSystemProvisionerStoreInjectable = getInjectable({ + id: "init-file-system-provisioner-store", + instantiate: (di) => ({ + id: "init-file-system-provisioner-store", + run: () => { + const store = di.inject(fileSystemProvisionerStoreInjectable); + + store.load(); + }, + }), + injectionToken: onLoadOfApplicationInjectionToken, +}); + +export default initFileSystemProvisionerStoreInjectable; diff --git a/src/features/file-system-provisioner/renderer/init-store.injectable.ts b/src/features/file-system-provisioner/renderer/init-store.injectable.ts new file mode 100644 index 000000000000..d241dcad389e --- /dev/null +++ b/src/features/file-system-provisioner/renderer/init-store.injectable.ts @@ -0,0 +1,22 @@ +/** + * 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 fileSystemProvisionerStoreInjectable from "../../../extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store.injectable"; +import { beforeFrameStartsInjectionToken } from "../../../renderer/before-frame-starts/tokens"; + +const initFileSystemProvisionerStoreInjectable = getInjectable({ + id: "init-file-system-provisioner-store", + instantiate: (di) => ({ + id: "init-file-system-provisioner-store", + run: () => { + const store = di.inject(fileSystemProvisionerStoreInjectable); + + store.load(); + }, + }), + injectionToken: beforeFrameStartsInjectionToken, +}); + +export default initFileSystemProvisionerStoreInjectable; diff --git a/src/features/helm-charts/__snapshots__/add-custom-helm-repository-in-preferences.test.ts.snap b/src/features/helm-charts/__snapshots__/add-custom-helm-repository-in-preferences.test.ts.snap index 095977e3740c..3c7fbe7b4ac6 100644 --- a/src/features/helm-charts/__snapshots__/add-custom-helm-repository-in-preferences.test.ts.snap +++ b/src/features/helm-charts/__snapshots__/add-custom-helm-repository-in-preferences.test.ts.snap @@ -195,6 +195,7 @@ exports[`add custom helm repository in preferences when navigating to preference > Download kubectl binaries matching the Kubernetes cluster version @@ -215,7 +216,7 @@ exports[`add custom helm repository in preferences when navigating to preference
- Download mirror for kubectl + Default (Google)
- Download mirror for kubectl + Default (Google)