From 51655884c76278b1a2801f9ea72a5b6840e70f7a Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Fri, 25 Jun 2021 09:55:25 -0400 Subject: [PATCH] Fix hotbar migration of workspaces (#3178) --- integration/helpers/utils.ts | 12 ++-- src/common/__tests__/cluster-store.test.ts | 16 ++--- src/common/__tests__/hotbar-store.test.ts | 68 +++++++++---------- src/common/__tests__/user-store.test.ts | 8 +-- src/common/base-store.ts | 67 ++++++------------ src/common/cluster-store.ts | 38 +++++------ src/common/hotbar-store.ts | 48 ++++--------- src/common/user-store.ts | 51 ++++++-------- src/common/weblink-store.ts | 1 + .../__tests__/extension-discovery.test.ts | 6 ++ src/extensions/extension-loader.ts | 4 +- src/extensions/extension-store.ts | 4 +- src/extensions/extensions-store.ts | 1 + src/main/__test__/context-handler.test.ts | 13 ++++ src/main/__test__/kube-auth-proxy.test.ts | 10 +-- src/main/catalog-sources/general.ts | 2 +- src/main/catalog-sources/index.ts | 4 +- src/main/catalog-sources/weblinks.ts | 2 +- src/main/extension-filesystem.ts | 1 + src/main/index.ts | 28 ++++++-- src/main/initializers/general-entities.ts | 22 ------ src/main/initializers/index.ts | 3 - src/main/initializers/stores.ts | 48 ------------- src/main/initializers/weblinks.ts | 22 ------ .../protocol-handler/__test__/router.test.ts | 12 ++++ src/migrations/cluster-store/5.0.0-beta.10.ts | 8 +-- src/migrations/hotbar-store/5.0.0-alpha.0.ts | 30 +++----- src/migrations/hotbar-store/5.0.0-beta.10.ts | 59 +++++++++++++++- src/migrations/hotbar-store/5.0.0-beta.5.ts | 2 +- .../user-store/file-name-migration.ts | 6 +- src/renderer/bootstrap.tsx | 13 +++- .../+catalog/catalog-entity-details.tsx | 6 ++ .../+extensions/__tests__/extensions.test.tsx | 2 +- .../__test__/log-resource-selector.test.tsx | 7 +- .../__tests__/hotbar-remove-command.test.tsx | 15 +++- .../components/hotbar/hotbar-add-command.tsx | 21 ++---- src/renderer/initializers/index.ts | 1 - src/renderer/initializers/stores.ts | 50 -------------- src/renderer/theme.store.ts | 68 +++++-------------- 39 files changed, 333 insertions(+), 446 deletions(-) delete mode 100644 src/main/initializers/general-entities.ts delete mode 100644 src/main/initializers/stores.ts delete mode 100644 src/main/initializers/weblinks.ts delete mode 100644 src/renderer/initializers/stores.ts diff --git a/integration/helpers/utils.ts b/integration/helpers/utils.ts index 3a8181f5c8..d6f633e416 100644 --- a/integration/helpers/utils.ts +++ b/integration/helpers/utils.ts @@ -37,27 +37,27 @@ interface DoneCallback { * This is necessary because Jest doesn't do this correctly. * @param fn The function to call */ -export function wrapJestLifecycle(fn: () => Promise): (done: DoneCallback) => void { +export function wrapJestLifecycle(fn: () => Promise | void): (done: DoneCallback) => void { return function (done: DoneCallback) { - fn() + (async () => fn())() .then(() => done()) .catch(error => done.fail(error)); }; } -export function beforeAllWrapped(fn: () => Promise): void { +export function beforeAllWrapped(fn: () => Promise | void): void { beforeAll(wrapJestLifecycle(fn)); } -export function beforeEachWrapped(fn: () => Promise): void { +export function beforeEachWrapped(fn: () => Promise | void): void { beforeEach(wrapJestLifecycle(fn)); } -export function afterAllWrapped(fn: () => Promise): void { +export function afterAllWrapped(fn: () => Promise | void): void { afterAll(wrapJestLifecycle(fn)); } -export function afterEachWrapped(fn: () => Promise): void { +export function afterEachWrapped(fn: () => Promise | void): void { afterEach(wrapJestLifecycle(fn)); } diff --git a/src/common/__tests__/cluster-store.test.ts b/src/common/__tests__/cluster-store.test.ts index 92839bcb30..6d4198d8d7 100644 --- a/src/common/__tests__/cluster-store.test.ts +++ b/src/common/__tests__/cluster-store.test.ts @@ -95,7 +95,7 @@ describe("empty config", () => { mockFs(mockOpts); - await ClusterStore.createInstance().load(); + ClusterStore.createInstance(); }); afterEach(() => { @@ -203,7 +203,7 @@ describe("config with existing clusters", () => { mockFs(mockOpts); - return ClusterStore.createInstance().load(); + return ClusterStore.createInstance(); }); afterEach(() => { @@ -285,7 +285,7 @@ users: mockFs(mockOpts); - return ClusterStore.createInstance().load(); + return ClusterStore.createInstance(); }); afterEach(() => { @@ -344,7 +344,7 @@ describe("pre 2.0 config with an existing cluster", () => { mockFs(mockOpts); - return ClusterStore.createInstance().load(); + return ClusterStore.createInstance(); }); afterEach(() => { @@ -414,7 +414,7 @@ describe("pre 2.6.0 config with a cluster that has arrays in auth config", () => mockFs(mockOpts); - return ClusterStore.createInstance().load(); + return ClusterStore.createInstance(); }); afterEach(() => { @@ -456,7 +456,7 @@ describe("pre 2.6.0 config with a cluster icon", () => { mockFs(mockOpts); - return ClusterStore.createInstance().load(); + return ClusterStore.createInstance(); }); afterEach(() => { @@ -495,7 +495,7 @@ describe("for a pre 2.7.0-beta.0 config without a workspace", () => { mockFs(mockOpts); - return ClusterStore.createInstance().load(); + return ClusterStore.createInstance(); }); afterEach(() => { @@ -531,7 +531,7 @@ describe("pre 3.6.0-beta.1 config with an existing cluster", () => { mockFs(mockOpts); - return ClusterStore.createInstance().load(); + return ClusterStore.createInstance(); }); afterEach(() => { diff --git a/src/common/__tests__/hotbar-store.test.ts b/src/common/__tests__/hotbar-store.test.ts index 9f54e8731d..267fbe3e8e 100644 --- a/src/common/__tests__/hotbar-store.test.ts +++ b/src/common/__tests__/hotbar-store.test.ts @@ -23,7 +23,7 @@ import mockFs from "mock-fs"; import { ClusterStore } from "../cluster-store"; import { HotbarStore } from "../hotbar-store"; -jest.mock("../../renderer/api/catalog-entity-registry", () => ({ +jest.mock("../../main/catalog/catalog-entity-registry", () => ({ catalogEntityRegistry: { items: [ { @@ -39,7 +39,14 @@ jest.mock("../../renderer/api/catalog-entity-registry", () => ({ name: "my_shiny_cluster", source: "remote" } - } + }, + { + metadata: { + uid: "catalog-entity", + name: "Catalog", + source: "app" + }, + }, ] } })); @@ -120,29 +127,31 @@ jest.mock("electron", () => { describe("HotbarStore", () => { beforeEach(() => { - ClusterStore.resetInstance(); + mockFs({ + "tmp": { + "lens-hotbar-store.json": JSON.stringify({}) + } + }); ClusterStore.createInstance(); - - HotbarStore.resetInstance(); - mockFs({ tmp: { "lens-hotbar-store.json": "{}" } }); + HotbarStore.createInstance(); }); afterEach(() => { + ClusterStore.resetInstance(); + HotbarStore.resetInstance(); mockFs.restore(); }); describe("load", () => { it("loads one hotbar by default", () => { - HotbarStore.createInstance().load(); expect(HotbarStore.getInstance().hotbars.length).toEqual(1); }); }); describe("add", () => { it("adds a hotbar", () => { - const hotbarStore = HotbarStore.createInstance(); + const hotbarStore = HotbarStore.getInstance(); - hotbarStore.load(); hotbarStore.add({ name: "hottest" }); expect(hotbarStore.hotbars.length).toEqual(2); }); @@ -150,23 +159,20 @@ describe("HotbarStore", () => { describe("hotbar items", () => { it("initially creates 12 empty cells", () => { - const hotbarStore = HotbarStore.createInstance(); + const hotbarStore = HotbarStore.getInstance(); - hotbarStore.load(); expect(hotbarStore.getActive().items.length).toEqual(12); }); it("initially adds catalog entity as first item", () => { - const hotbarStore = HotbarStore.createInstance(); + const hotbarStore = HotbarStore.getInstance(); - hotbarStore.load(); expect(hotbarStore.getActive().items[0].entity.name).toEqual("Catalog"); }); it("adds items", () => { - const hotbarStore = HotbarStore.createInstance(); + const hotbarStore = HotbarStore.getInstance(); - hotbarStore.load(); hotbarStore.addToHotbar(testCluster); const items = hotbarStore.getActive().items.filter(Boolean); @@ -174,9 +180,8 @@ describe("HotbarStore", () => { }); it("removes items", () => { - const hotbarStore = HotbarStore.createInstance(); + const hotbarStore = HotbarStore.getInstance(); - hotbarStore.load(); hotbarStore.addToHotbar(testCluster); hotbarStore.removeFromHotbar("test"); hotbarStore.removeFromHotbar("catalog-entity"); @@ -186,9 +191,8 @@ describe("HotbarStore", () => { }); it("does nothing if removing with invalid uid", () => { - const hotbarStore = HotbarStore.createInstance(); + const hotbarStore = HotbarStore.getInstance(); - hotbarStore.load(); hotbarStore.addToHotbar(testCluster); hotbarStore.removeFromHotbar("invalid uid"); const items = hotbarStore.getActive().items.filter(Boolean); @@ -197,9 +201,8 @@ describe("HotbarStore", () => { }); it("moves item to empty cell", () => { - const hotbarStore = HotbarStore.createInstance(); + const hotbarStore = HotbarStore.getInstance(); - hotbarStore.load(); hotbarStore.addToHotbar(testCluster); hotbarStore.addToHotbar(minikubeCluster); hotbarStore.addToHotbar(awsCluster); @@ -213,9 +216,8 @@ describe("HotbarStore", () => { }); it("moves items down", () => { - const hotbarStore = HotbarStore.createInstance(); + const hotbarStore = HotbarStore.getInstance(); - hotbarStore.load(); hotbarStore.addToHotbar(testCluster); hotbarStore.addToHotbar(minikubeCluster); hotbarStore.addToHotbar(awsCluster); @@ -229,9 +231,8 @@ describe("HotbarStore", () => { }); it("moves items up", () => { - const hotbarStore = HotbarStore.createInstance(); + const hotbarStore = HotbarStore.getInstance(); - hotbarStore.load(); hotbarStore.addToHotbar(testCluster); hotbarStore.addToHotbar(minikubeCluster); hotbarStore.addToHotbar(awsCluster); @@ -245,9 +246,8 @@ describe("HotbarStore", () => { }); it("does nothing when item moved to same cell", () => { - const hotbarStore = HotbarStore.createInstance(); + const hotbarStore = HotbarStore.getInstance(); - hotbarStore.load(); hotbarStore.addToHotbar(testCluster); hotbarStore.restackItems(1, 1); @@ -255,9 +255,8 @@ describe("HotbarStore", () => { }); it("new items takes first empty cell", () => { - const hotbarStore = HotbarStore.createInstance(); + const hotbarStore = HotbarStore.getInstance(); - hotbarStore.load(); hotbarStore.addToHotbar(testCluster); hotbarStore.addToHotbar(awsCluster); hotbarStore.restackItems(0, 3); @@ -268,13 +267,13 @@ describe("HotbarStore", () => { it("throws if invalid arguments provided", () => { // Prevent writing to stderr during this render. - const err = console.error; + const { error, warn } = console; console.error = jest.fn(); + console.warn = jest.fn(); - const hotbarStore = HotbarStore.createInstance(); + const hotbarStore = HotbarStore.getInstance(); - hotbarStore.load(); hotbarStore.addToHotbar(testCluster); expect(() => hotbarStore.restackItems(-5, 0)).toThrow(); @@ -283,7 +282,8 @@ describe("HotbarStore", () => { expect(() => hotbarStore.restackItems(11, 112)).toThrow(); // Restore writing to stderr. - console.error = err; + console.error = error; + console.warn = warn; }); }); @@ -354,7 +354,7 @@ describe("HotbarStore", () => { mockFs(mockOpts); - return HotbarStore.createInstance().load(); + HotbarStore.createInstance(); }); afterEach(() => { diff --git a/src/common/__tests__/user-store.test.ts b/src/common/__tests__/user-store.test.ts index 465c598bb0..b1b3f60ce6 100644 --- a/src/common/__tests__/user-store.test.ts +++ b/src/common/__tests__/user-store.test.ts @@ -52,7 +52,7 @@ describe("user store tests", () => { (UserStore.createInstance() as any).refreshNewContexts = jest.fn(() => Promise.resolve()); - return UserStore.getInstance().load(); + UserStore.getInstance(); }); afterEach(() => { @@ -81,10 +81,8 @@ describe("user store tests", () => { it("correctly resets theme to default value", async () => { const us = UserStore.getInstance(); - us.isLoaded = true; - us.colorTheme = "some other theme"; - await us.resetTheme(); + us.resetTheme(); expect(us.colorTheme).toBe(UserStore.defaultTheme); }); @@ -111,7 +109,7 @@ describe("user store tests", () => { } }); - return UserStore.createInstance().load(); + UserStore.createInstance(); }); afterEach(() => { diff --git a/src/common/base-store.ts b/src/common/base-store.ts index 842e0304b4..d30725d6bb 100644 --- a/src/common/base-store.ts +++ b/src/common/base-store.ts @@ -23,41 +23,42 @@ import path from "path"; import Config from "conf"; import type { Options as ConfOptions } from "conf/dist/source/types"; import { app, ipcMain, ipcRenderer, remote } from "electron"; -import { IReactionOptions, makeObservable, observable, reaction, runInAction, when } from "mobx"; +import { IReactionOptions, makeObservable, reaction, runInAction } from "mobx"; import { getAppVersion, Singleton, toJS, Disposer } from "./utils"; import logger from "../main/logger"; import { broadcastMessage, ipcMainOn, ipcRendererOn } from "./ipc"; import isEqual from "lodash/isEqual"; -export interface BaseStoreParams extends ConfOptions { - autoLoad?: boolean; - syncEnabled?: boolean; +export interface BaseStoreParams extends ConfOptions { syncOptions?: IReactionOptions; } /** * Note: T should only contain base JSON serializable types. */ -export abstract class BaseStore extends Singleton { +export abstract class BaseStore extends Singleton { protected storeConfig?: Config; protected syncDisposers: Disposer[] = []; - @observable isLoaded = false; - - get whenLoaded() { - return when(() => this.isLoaded); - } - - protected constructor(protected params: BaseStoreParams) { + protected constructor(protected params: BaseStoreParams) { super(); makeObservable(this); + } - this.params = { - autoLoad: false, - syncEnabled: true, - ...params, - }; - this.init(); + /** + * This must be called after the last child's constructor is finished (or just before it finishes) + */ + load() { + this.storeConfig = new Config({ + ...this.params, + projectName: "lens", + projectVersion: getAppVersion(), + cwd: this.cwd(), + }); + + logger.info(`[STORE]: LOADED from ${this.path}`); + this.fromStore(this.storeConfig.store); + this.enableSync(); } get name() { @@ -76,31 +77,6 @@ export abstract class BaseStore extends Singleton { return this.storeConfig?.path || ""; } - protected async init() { - if (this.params.autoLoad) { - await this.load(); - } - - if (this.params.syncEnabled) { - await this.whenLoaded; - this.enableSync(); - } - } - - async load() { - const { autoLoad, syncEnabled, ...confOptions } = this.params; - - this.storeConfig = new Config({ - ...confOptions, - projectName: "lens", - projectVersion: getAppVersion(), - cwd: this.cwd(), - }); - logger.info(`[STORE]: LOADED from ${this.path}`); - this.fromStore(this.storeConfig.store); - this.isLoaded = true; - } - protected cwd() { return (app || remote.app).getPath("userData"); } @@ -159,10 +135,7 @@ export abstract class BaseStore extends Singleton { protected applyWithoutSync(callback: () => void) { this.disableSync(); runInAction(callback); - - if (this.params.syncEnabled) { - this.enableSync(); - } + this.enableSync(); } protected onSync(model: T) { diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index 26f0e24370..baa7fc1409 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -110,6 +110,8 @@ export interface ClusterPrometheusPreferences { }; } +const initialStates = "cluster:states"; + export class ClusterStore extends BaseStore { private static StateChannel = "cluster:state"; @@ -121,8 +123,8 @@ export class ClusterStore extends BaseStore { return path.resolve(ClusterStore.storedKubeConfigFolder, clusterId); } - @observable clusters = observable.map(); - @observable removedClusters = observable.map(); + clusters = observable.map(); + removedClusters = observable.map(); protected disposer = disposer(); @@ -137,31 +139,27 @@ export class ClusterStore extends BaseStore { }); makeObservable(this); - + this.load(); this.pushStateToViewsAutomatically(); } - async load() { - const initialStates = "cluster:states"; + async loadInitialOnRenderer() { + logger.info("[CLUSTER-STORE] requesting initial state sync"); - await super.load(); - - if (ipcRenderer) { - logger.info("[CLUSTER-STORE] requesting initial state sync"); - - for (const { id, state } of await requestMain(initialStates)) { - this.getById(id)?.setState(state); - } - } else if (ipcMain) { - ipcMainHandle(initialStates, () => { - return this.clustersList.map(cluster => ({ - id: cluster.id, - state: cluster.getState(), - })); - }); + for (const { id, state } of await requestMain(initialStates)) { + this.getById(id)?.setState(state); } } + provideInitialFromMain() { + ipcMainHandle(initialStates, () => { + return this.clustersList.map(cluster => ({ + id: cluster.id, + state: cluster.getState(), + })); + }); + } + protected pushStateToViewsAutomatically() { if (ipcMain) { this.disposer.push( diff --git a/src/common/hotbar-store.ts b/src/common/hotbar-store.ts index ab41583b0a..b90a007b34 100644 --- a/src/common/hotbar-store.ts +++ b/src/common/hotbar-store.ts @@ -26,7 +26,6 @@ import * as uuid from "uuid"; import isNull from "lodash/isNull"; import { toJS } from "./utils"; import { CatalogEntity } from "./catalog"; -import { catalogEntity } from "../main/catalog-sources/general"; export interface HotbarItem { entity: { @@ -39,16 +38,12 @@ export interface HotbarItem { } } -export interface Hotbar { - id: string; - name: string; - items: HotbarItem[]; -} +export type Hotbar = Required; export interface HotbarCreateOptions { id?: string; name: string; - items?: HotbarItem[]; + items?: (HotbarItem | null)[]; } export interface HotbarStoreModel { @@ -72,6 +67,7 @@ export class HotbarStore extends BaseStore { migrations, }); makeObservable(this); + this.load(); } get activeHotbarId() { @@ -92,30 +88,13 @@ export class HotbarStore extends BaseStore { return this.hotbarIndex(this.activeHotbarId); } - get defaultHotbarInitialItems() { - const { metadata: { uid, name, source } } = catalogEntity; - const initialItem = { entity: { uid, name, source }}; - - return [ - initialItem, - ...Array.from(Array(defaultHotbarCells - 1).fill(null)) - ]; - } - - get initialItems() { + static getInitialItems() { return [...Array.from(Array(defaultHotbarCells).fill(null))]; } - @action protected async fromStore(data: Partial = {}) { - if (data.hotbars?.length === 0) { - this.hotbars = [{ - id: uuid.v4(), - name: "Default", - items: this.defaultHotbarInitialItems, - }]; - } else { - this.hotbars = data.hotbars; - } + @action + protected async fromStore(data: Partial = {}) { + this.hotbars = data.hotbars; if (data.activeHotbarId) { if (this.getById(data.activeHotbarId)) { @@ -140,18 +119,19 @@ export class HotbarStore extends BaseStore { return this.hotbars.find((hotbar) => hotbar.id === id); } - add(data: HotbarCreateOptions) { + @action + add(data: HotbarCreateOptions, { setActive = false } = {}) { const { id = uuid.v4(), - items = this.initialItems, + items = HotbarStore.getInitialItems(), name, } = data; - const hotbar = { id, name, items }; + this.hotbars.push({ id, name, items }); - this.hotbars.push(hotbar as Hotbar); - - return hotbar as Hotbar; + if (setActive) { + this._activeHotbarId = id; + } } @action diff --git a/src/common/user-store.ts b/src/common/user-store.ts index f7ab221bb8..c1e956b331 100644 --- a/src/common/user-store.ts +++ b/src/common/user-store.ts @@ -68,7 +68,10 @@ export class UserStore extends BaseStore { configName: "lens-user-store", migrations, }); + makeObservable(this); + fileNameMigration(); + this.load(); } @observable lastSeenAppVersion = "0.0.0"; @@ -101,33 +104,6 @@ export class UserStore extends BaseStore { [path.join(os.homedir(), ".kube"), {}] ]); - async load(): Promise { - /** - * This has to be here before the call to `new Config` in `super.load()` - * as we have to make sure that file is in the expected place for that call - */ - await fileNameMigration(); - await super.load(); - - if (app) { - // track telemetry availability - reaction(() => this.allowTelemetry, allowed => { - appEventBus.emit({ name: "telemetry", action: allowed ? "enabled" : "disabled" }); - }); - - // open at system start-up - reaction(() => this.openAtLogin, openAtLogin => { - app.setLoginItemSettings({ - openAtLogin, - openAsHidden: true, - args: ["--hidden"] - }); - }, { - fireImmediately: true, - }); - } - } - @computed get isNewVersion() { return semver.gt(getAppVersion(), this.lastSeenAppVersion); } @@ -136,6 +112,24 @@ export class UserStore extends BaseStore { return this.shell || process.env.SHELL || process.env.PTYSHELL; } + startMainReactions() { + // track telemetry availability + reaction(() => this.allowTelemetry, allowed => { + appEventBus.emit({ name: "telemetry", action: allowed ? "enabled" : "disabled" }); + }); + + // open at system start-up + reaction(() => this.openAtLogin, openAtLogin => { + app.setLoginItemSettings({ + openAtLogin, + openAsHidden: true, + args: ["--hidden"] + }); + }, { + fireImmediately: true, + }); + } + /** * Checks if a column (by ID) for a table (by ID) is configured to be hidden * @param tableId The ID of the table to be checked against @@ -165,8 +159,7 @@ export class UserStore extends BaseStore { } @action - async resetTheme() { - await this.whenLoaded; + resetTheme() { this.colorTheme = UserStore.defaultTheme; } diff --git a/src/common/weblink-store.ts b/src/common/weblink-store.ts index 0f66725545..7b3a62f946 100644 --- a/src/common/weblink-store.ts +++ b/src/common/weblink-store.ts @@ -55,6 +55,7 @@ export class WeblinkStore extends BaseStore { migrations, }); makeObservable(this); + this.load(); } @action protected async fromStore(data: Partial = {}) { diff --git a/src/extensions/__tests__/extension-discovery.test.ts b/src/extensions/__tests__/extension-discovery.test.ts index 347487e556..6db33acf19 100644 --- a/src/extensions/__tests__/extension-discovery.test.ts +++ b/src/extensions/__tests__/extension-discovery.test.ts @@ -39,6 +39,12 @@ jest.mock("../extension-installer", () => ({ installPackage: jest.fn() } })); +jest.mock("electron", () => ({ + app: { + getPath: () => "tmp", + setLoginItemSettings: jest.fn(), + }, +})); console = new Console(process.stdout, process.stderr); // fix mockFS const mockedWatch = watch as jest.MockedFunction; diff --git a/src/extensions/extension-loader.ts b/src/extensions/extension-loader.ts index 68e54828dc..7c9c4abc5d 100644 --- a/src/extensions/extension-loader.ts +++ b/src/extensions/extension-loader.ts @@ -77,7 +77,7 @@ export class ExtensionLoader extends Singleton { if (this.instancesByName.has(change.newValue.name)) { throw new TypeError("Extension names must be unique"); } - + this.instancesByName.set(change.newValue.name, change.newValue); break; case "delete": @@ -124,7 +124,7 @@ export class ExtensionLoader extends Singleton { await this.initMain(); } - await Promise.all([this.whenLoaded, ExtensionsStore.getInstance().whenLoaded]); + await Promise.all([this.whenLoaded]); // broadcasting extensions between main/renderer processes reaction(() => this.toJSON(), () => this.broadcastExtensions(), { diff --git a/src/extensions/extension-store.ts b/src/extensions/extension-store.ts index 2c5eb44262..6b4e9c5489 100644 --- a/src/extensions/extension-store.ts +++ b/src/extensions/extension-store.ts @@ -26,13 +26,13 @@ import type { LensExtension } from "./lens-extension"; export abstract class ExtensionStore extends BaseStore { protected extension: LensExtension; - async loadExtension(extension: LensExtension) { + loadExtension(extension: LensExtension) { this.extension = extension; return super.load(); } - async load() { + load() { if (!this.extension) { return; } return super.load(); diff --git a/src/extensions/extensions-store.ts b/src/extensions/extensions-store.ts index 86a90e0238..bfc8dc1cd0 100644 --- a/src/extensions/extensions-store.ts +++ b/src/extensions/extensions-store.ts @@ -39,6 +39,7 @@ export class ExtensionsStore extends BaseStore { configName: "lens-extensions", }); makeObservable(this); + this.load(); } @computed diff --git a/src/main/__test__/context-handler.test.ts b/src/main/__test__/context-handler.test.ts index ad3b42f5d2..0ba3a2d028 100644 --- a/src/main/__test__/context-handler.test.ts +++ b/src/main/__test__/context-handler.test.ts @@ -22,6 +22,14 @@ import { UserStore } from "../../common/user-store"; import { ContextHandler } from "../context-handler"; import { PrometheusProvider, PrometheusProviderRegistry, PrometheusService } from "../prometheus"; +import mockFs from "mock-fs"; + +jest.mock("electron", () => ({ + app: { + getPath: () => "tmp", + setLoginItemSettings: jest.fn(), + }, +})); enum ServiceResult { Success, @@ -70,6 +78,10 @@ function getHandler() { describe("ContextHandler", () => { beforeEach(() => { + mockFs({ + "tmp": {} + }); + PrometheusProviderRegistry.createInstance(); UserStore.createInstance(); }); @@ -77,6 +89,7 @@ describe("ContextHandler", () => { afterEach(() => { PrometheusProviderRegistry.resetInstance(); UserStore.resetInstance(); + mockFs.restore(); }); describe("getPrometheusService", () => { diff --git a/src/main/__test__/kube-auth-proxy.test.ts b/src/main/__test__/kube-auth-proxy.test.ts index 5006859637..e0dd7f73c6 100644 --- a/src/main/__test__/kube-auth-proxy.test.ts +++ b/src/main/__test__/kube-auth-proxy.test.ts @@ -46,7 +46,8 @@ jest.mock("winston", () => ({ jest.mock("electron", () => ({ app: { - getPath: () => "/foo", + getPath: () => "tmp", + setLoginItemSettings: jest.fn(), }, })); @@ -77,8 +78,6 @@ const mockWaitUntilUsed = waitUntilUsed as jest.MockedFunction { beforeEach(() => { jest.clearAllMocks(); - UserStore.resetInstance(); - UserStore.createInstance(); const mockMinikubeConfig = { "minikube-config.yml": JSON.stringify({ @@ -102,13 +101,16 @@ describe("kube auth proxy tests", () => { }], kind: "Config", preferences: {}, - }) + }), + "tmp": {}, }; mockFs(mockMinikubeConfig); + UserStore.createInstance(); }); afterEach(() => { + UserStore.resetInstance(); mockFs.restore(); }); diff --git a/src/main/catalog-sources/general.ts b/src/main/catalog-sources/general.ts index 9f64f46b6d..6b16984d1d 100644 --- a/src/main/catalog-sources/general.ts +++ b/src/main/catalog-sources/general.ts @@ -67,6 +67,6 @@ const generalEntities = observable([ preferencesEntity ]); -export function initializeGeneralEntities() { +export function syncGeneralEntities() { catalogEntityRegistry.addObservableSource("lens:general", generalEntities); } diff --git a/src/main/catalog-sources/index.ts b/src/main/catalog-sources/index.ts index 17f383185b..70c2d073fb 100644 --- a/src/main/catalog-sources/index.ts +++ b/src/main/catalog-sources/index.ts @@ -19,6 +19,6 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -export { initializeWeblinks } from "./weblinks"; +export { syncWeblinks } from "./weblinks"; export { KubeconfigSyncManager } from "./kubeconfig-sync"; -export { initializeGeneralEntities } from "./general"; +export { syncGeneralEntities } from "./general"; diff --git a/src/main/catalog-sources/weblinks.ts b/src/main/catalog-sources/weblinks.ts index b6b1ec5a39..2593fedc7c 100644 --- a/src/main/catalog-sources/weblinks.ts +++ b/src/main/catalog-sources/weblinks.ts @@ -69,7 +69,7 @@ async function validateLink(link: WebLink) { } -export function initializeWeblinks() { +export function syncWeblinks() { const weblinkStore = WeblinkStore.getInstance(); const weblinks = observable.array(defaultLinks); diff --git a/src/main/extension-filesystem.ts b/src/main/extension-filesystem.ts index 402619bdc9..46afe45d6e 100644 --- a/src/main/extension-filesystem.ts +++ b/src/main/extension-filesystem.ts @@ -42,6 +42,7 @@ export class FilesystemProvisionerStore extends BaseStore { accessPropertiesByDotNotation: false, // To make dots safe in cluster context names }); makeObservable(this); + this.load(); } /** diff --git a/src/main/index.ts b/src/main/index.ts index f4c4351df9..0599f820cb 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -48,11 +48,17 @@ import { IpcRendererNavigationEvents } from "../renderer/navigation/events"; import { pushCatalogToRenderer } from "./catalog-pusher"; import { catalogEntityRegistry } from "./catalog"; import { HelmRepoManager } from "./helm/helm-repo-manager"; -import { KubeconfigSyncManager } from "./catalog-sources"; +import { syncGeneralEntities, syncWeblinks, KubeconfigSyncManager } from "./catalog-sources"; import { handleWsUpgrade } from "./proxy/ws-upgrade"; import configurePackages from "../common/configure-packages"; import { PrometheusProviderRegistry } from "./prometheus"; import * as initializers from "./initializers"; +import { ClusterStore } from "../common/cluster-store"; +import { HotbarStore } from "../common/hotbar-store"; +import { UserStore } from "../common/user-store"; +import { WeblinkStore } from "../common/weblink-store"; +import { ExtensionsStore } from "../extensions/extensions-store"; +import { FilesystemProvisionerStore } from "./extension-filesystem"; const workingDir = path.join(app.getPath("appData"), appName); const cleanup = disposer(); @@ -123,8 +129,23 @@ app.on("ready", async () => { PrometheusProviderRegistry.createInstance(); initializers.initPrometheusProviderRegistry(); - await initializers.initializeStores(); - initializers.initializeWeblinks(); + /** + * The following sync MUST be done before HotbarStore creation, because that + * store has migrations that will remove items that previous migrations add + * if this is not presant + */ + syncGeneralEntities(); + + logger.info("💾 Loading stores"); + + UserStore.createInstance().startMainReactions(); + ClusterStore.createInstance().provideInitialFromMain(); + HotbarStore.createInstance(); + ExtensionsStore.createInstance(); + FilesystemProvisionerStore.createInstance(); + WeblinkStore.createInstance(); + + syncWeblinks(); HelmRepoManager.createInstance(); // create the instance @@ -182,7 +203,6 @@ app.on("ready", async () => { ipcMainOn(IpcRendererNavigationEvents.LOADED, () => { cleanup.push(pushCatalogToRenderer(catalogEntityRegistry)); KubeconfigSyncManager.getInstance().startSync(); - initializers.initializeGeneralEntities(); startUpdateChecking(); LensProtocolRouterMain.getInstance().rendererLoaded = true; }); diff --git a/src/main/initializers/general-entities.ts b/src/main/initializers/general-entities.ts deleted file mode 100644 index b2e39d8a39..0000000000 --- a/src/main/initializers/general-entities.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) 2021 OpenLens Authors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -export { initializeGeneralEntities } from "../catalog-sources"; diff --git a/src/main/initializers/index.ts b/src/main/initializers/index.ts index 6cd9330bac..526b61d4ed 100644 --- a/src/main/initializers/index.ts +++ b/src/main/initializers/index.ts @@ -22,6 +22,3 @@ export * from "./registries"; export * from "./metrics-providers"; export * from "./ipc"; -export * from "./weblinks"; -export * from "./stores"; -export * from "./general-entities"; diff --git a/src/main/initializers/stores.ts b/src/main/initializers/stores.ts deleted file mode 100644 index 4ea6322457..0000000000 --- a/src/main/initializers/stores.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Copyright (c) 2021 OpenLens Authors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -import { HotbarStore } from "../../common/hotbar-store"; -import { ClusterStore } from "../../common/cluster-store"; -import { UserStore } from "../../common/user-store"; -import { ExtensionsStore } from "../../extensions/extensions-store"; -import { FilesystemProvisionerStore } from "../extension-filesystem"; -import { WeblinkStore } from "../../common/weblink-store"; -import logger from "../logger"; - -export async function initializeStores() { - const userStore = UserStore.createInstance(); - const clusterStore = ClusterStore.createInstance(); - const hotbarStore = HotbarStore.createInstance(); - const extensionsStore = ExtensionsStore.createInstance(); - const filesystemStore = FilesystemProvisionerStore.createInstance(); - const weblinkStore = WeblinkStore.createInstance(); - - logger.info("💾 Loading stores"); - // preload - await Promise.all([ - userStore.load(), - clusterStore.load(), - hotbarStore.load(), - extensionsStore.load(), - filesystemStore.load(), - weblinkStore.load() - ]); -} diff --git a/src/main/initializers/weblinks.ts b/src/main/initializers/weblinks.ts deleted file mode 100644 index 2bd65d0894..0000000000 --- a/src/main/initializers/weblinks.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) 2021 OpenLens Authors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -export { initializeWeblinks } from "../catalog-sources"; diff --git a/src/main/protocol-handler/__test__/router.test.ts b/src/main/protocol-handler/__test__/router.test.ts index 42e9bda3c6..045e05c127 100644 --- a/src/main/protocol-handler/__test__/router.test.ts +++ b/src/main/protocol-handler/__test__/router.test.ts @@ -28,9 +28,17 @@ import { LensExtension } from "../../../extensions/main-api"; import { ExtensionLoader } from "../../../extensions/extension-loader"; import { ExtensionsStore } from "../../../extensions/extensions-store"; import { LensProtocolRouterMain } from "../router"; +import mockFs from "mock-fs"; jest.mock("../../../common/ipc"); +jest.mock("electron", () => ({ + app: { + getPath: () => "tmp", + setLoginItemSettings: jest.fn(), + }, +})); + function throwIfDefined(val: any): void { if (val != null) { throw val; @@ -39,6 +47,9 @@ function throwIfDefined(val: any): void { describe("protocol router tests", () => { beforeEach(() => { + mockFs({ + "tmp": {} + }); ExtensionsStore.createInstance(); ExtensionLoader.createInstance(); @@ -53,6 +64,7 @@ describe("protocol router tests", () => { ExtensionsStore.resetInstance(); ExtensionLoader.resetInstance(); LensProtocolRouterMain.resetInstance(); + mockFs.restore(); }); it("should throw on non-lens URLS", () => { diff --git a/src/migrations/cluster-store/5.0.0-beta.10.ts b/src/migrations/cluster-store/5.0.0-beta.10.ts index daea58279b..3a2b5bdfe3 100644 --- a/src/migrations/cluster-store/5.0.0-beta.10.ts +++ b/src/migrations/cluster-store/5.0.0-beta.10.ts @@ -23,7 +23,7 @@ import path from "path"; import { app } from "electron"; import fse from "fs-extra"; import type { ClusterModel } from "../../common/cluster-store"; -import { MigrationDeclaration, migrationLog } from "../helpers"; +import type { MigrationDeclaration } from "../helpers"; interface Pre500WorkspaceStoreModel { workspaces: { @@ -36,7 +36,7 @@ export default { version: "5.0.0-beta.10", run(store) { const userDataPath = app.getPath("userData"); - + try { const workspaceData: Pre500WorkspaceStoreModel = fse.readJsonSync(path.join(userDataPath, "lens-workspace-store.json")); const workspaces = new Map(); // mapping from WorkspaceId to name @@ -45,8 +45,6 @@ export default { workspaces.set(id, name); } - migrationLog("workspaces", JSON.stringify([...workspaces.entries()])); - const clusters: ClusterModel[] = store.get("clusters"); for (const cluster of clusters) { @@ -58,8 +56,6 @@ export default { store.set("clusters", clusters); } catch (error) { - migrationLog("error", error.path); - if (!(error.code === "ENOENT" && error.path.endsWith("lens-workspace-store.json"))) { // ignore lens-workspace-store.json being missing throw error; diff --git a/src/migrations/hotbar-store/5.0.0-alpha.0.ts b/src/migrations/hotbar-store/5.0.0-alpha.0.ts index 7cd8011414..1a61ce8ef3 100644 --- a/src/migrations/hotbar-store/5.0.0-alpha.0.ts +++ b/src/migrations/hotbar-store/5.0.0-alpha.0.ts @@ -20,34 +20,24 @@ */ // Cleans up a store that had the state related data stored -import type { Hotbar } from "../../common/hotbar-store"; -import { ClusterStore } from "../../common/cluster-store"; +import { Hotbar, HotbarStore } from "../../common/hotbar-store"; import * as uuid from "uuid"; import type { MigrationDeclaration } from "../helpers"; +import { catalogEntity } from "../../main/catalog-sources/general"; export default { version: "5.0.0-alpha.0", run(store) { - const hotbars: Hotbar[] = []; + const hotbar: Hotbar = { + id: uuid.v4(), + name: "default", + items: HotbarStore.getInitialItems(), + }; - ClusterStore.getInstance().clustersList.forEach((cluster: any) => { - const name = cluster.workspace; + const { metadata: { uid, name, source } } = catalogEntity; - if (!name) return; + hotbar.items[0] = { entity: { uid, name, source } }; - let hotbar = hotbars.find((h) => h.name === name); - - if (!hotbar) { - hotbar = { id: uuid.v4(), name, items: [] }; - hotbars.push(hotbar); - } - - hotbar.items.push({ - entity: { uid: cluster.id }, - params: {} - }); - }); - - store.set("hotbars", hotbars); + store.set("hotbars", [hotbar]); } } as MigrationDeclaration; diff --git a/src/migrations/hotbar-store/5.0.0-beta.10.ts b/src/migrations/hotbar-store/5.0.0-beta.10.ts index fe1b06f320..1586f5c700 100644 --- a/src/migrations/hotbar-store/5.0.0-beta.10.ts +++ b/src/migrations/hotbar-store/5.0.0-beta.10.ts @@ -19,12 +19,15 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +import { createHash } from "crypto"; import { app } from "electron"; import fse from "fs-extra"; +import { isNull } from "lodash"; import path from "path"; import * as uuid from "uuid"; import type { ClusterStoreModel } from "../../common/cluster-store"; -import { defaultHotbarCells, Hotbar } from "../../common/hotbar-store"; +import { defaultHotbarCells, Hotbar, HotbarStore } from "../../common/hotbar-store"; +import { catalogEntity } from "../../main/catalog-sources/general"; import type { MigrationDeclaration } from "../helpers"; interface Pre500WorkspaceStoreModel { @@ -49,7 +52,19 @@ export default { workspaceHotbars.set(id, { id: uuid.v4(), // don't use the old IDs as they aren't necessarily UUIDs items: [], + name: `Workspace: ${name}`, + }); + } + + { + // grab the default named hotbar or the first. + const defaultHotbarIndex = Math.max(0, hotbars.findIndex(hotbar => hotbar.name === "default")); + const [{ name, id, items }] = hotbars.splice(defaultHotbarIndex, 1); + + workspaceHotbars.set("default", { name, + id, + items: items.filter(Boolean), }); } @@ -59,7 +74,7 @@ export default { if (workspaceHotbar?.items.length < defaultHotbarCells) { workspaceHotbar.items.push({ entity: { - uid: cluster.id, + uid: createHash("md5").update(`${cluster.kubeConfigPath}:${cluster.contextName}`).digest("hex"), name: cluster.preferences.clusterName || cluster.contextName, } }); @@ -74,6 +89,46 @@ export default { hotbars.push(hotbar); } + /** + * Finally, make sure that the catalog entity hotbar item is in place. + * Just in case something else removed it. + * + * if every hotbar has elements that all not the `catalog-entity` item + */ + if (hotbars.every(hotbar => hotbar.items.every(item => item?.entity?.uid !== "catalog-entity"))) { + // note, we will add a new whole hotbar here called "default" if that was previously removed + const defaultHotbar = hotbars.find(hotbar => hotbar.name === "default"); + const { metadata: { uid, name, source } } = catalogEntity; + + if (defaultHotbar) { + const freeIndex = defaultHotbar.items.findIndex(isNull); + + if (freeIndex === -1) { + // making a new hotbar is less destructive if the first hotbar + // called "default" is full than overriding a hotbar item + const hotbar = { + id: uuid.v4(), + name: "initial", + items: HotbarStore.getInitialItems(), + }; + + hotbar.items[0] = { entity: { uid, name, source } }; + hotbars.unshift(hotbar); + } else { + defaultHotbar.items[freeIndex] = { entity: { uid, name, source } }; + } + } else { + const hotbar = { + id: uuid.v4(), + name: "default", + items: HotbarStore.getInitialItems(), + }; + + hotbar.items[0] = { entity: { uid, name, source } }; + hotbars.unshift(hotbar); + } + } + store.set("hotbars", hotbars); } catch (error) { if (!(error.code === "ENOENT" && error.path.endsWith("lens-workspace-store.json"))) { diff --git a/src/migrations/hotbar-store/5.0.0-beta.5.ts b/src/migrations/hotbar-store/5.0.0-beta.5.ts index 1cc8bc2840..8d589e9543 100644 --- a/src/migrations/hotbar-store/5.0.0-beta.5.ts +++ b/src/migrations/hotbar-store/5.0.0-beta.5.ts @@ -20,7 +20,7 @@ */ import type { Hotbar } from "../../common/hotbar-store"; -import { catalogEntityRegistry } from "../../renderer/api/catalog-entity-registry"; +import { catalogEntityRegistry } from "../../main/catalog"; import type { MigrationDeclaration } from "../helpers"; export default { diff --git a/src/migrations/user-store/file-name-migration.ts b/src/migrations/user-store/file-name-migration.ts index e0c848d3a8..122cd78102 100644 --- a/src/migrations/user-store/file-name-migration.ts +++ b/src/migrations/user-store/file-name-migration.ts @@ -23,18 +23,18 @@ import fse from "fs-extra"; import { app, remote } from "electron"; import path from "path"; -export async function fileNameMigration() { +export function fileNameMigration() { const userDataPath = (app || remote.app).getPath("userData"); const configJsonPath = path.join(userDataPath, "config.json"); const lensUserStoreJsonPath = path.join(userDataPath, "lens-user-store.json"); try { - await fse.move(configJsonPath, lensUserStoreJsonPath); + fse.moveSync(configJsonPath, lensUserStoreJsonPath); } catch (error) { if (error.code === "ENOENT" && error.path === configJsonPath) { // (No such file or directory) return; // file already moved } else if (error.message === "dest already exists.") { - await fse.remove(configJsonPath); + fse.removeSync(configJsonPath); } else { // pass other errors along throw error; diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index df0bd00feb..7768a9e0eb 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -42,6 +42,11 @@ import { ExtensionInstallationStateStore } from "./components/+extensions/extens import { DefaultProps } from "./mui-base-theme"; import configurePackages from "../common/configure-packages"; import * as initializers from "./initializers"; +import { HotbarStore } from "../common/hotbar-store"; +import { WeblinkStore } from "../common/weblink-store"; +import { ExtensionsStore } from "../extensions/extensions-store"; +import { FilesystemProvisionerStore } from "../main/extension-filesystem"; +import { ThemeStore } from "./theme.store"; configurePackages(); @@ -79,7 +84,13 @@ export async function bootstrap(App: AppComponent) { ExtensionLoader.createInstance().init(); ExtensionDiscovery.createInstance().init(); - await initializers.initStores(); + UserStore.createInstance(); + await ClusterStore.createInstance().loadInitialOnRenderer(); + HotbarStore.createInstance(); + ExtensionsStore.createInstance(); + FilesystemProvisionerStore.createInstance(); + ThemeStore.createInstance(); + WeblinkStore.createInstance(); ExtensionInstallationStateStore.bindIpcListeners(); HelmRepoManager.createInstance(); // initialize the manager diff --git a/src/renderer/components/+catalog/catalog-entity-details.tsx b/src/renderer/components/+catalog/catalog-entity-details.tsx index 1a6f0ab03a..fd1b874c82 100644 --- a/src/renderer/components/+catalog/catalog-entity-details.tsx +++ b/src/renderer/components/+catalog/catalog-entity-details.tsx @@ -30,6 +30,7 @@ import { CatalogEntityDrawerMenu } from "./catalog-entity-drawer-menu"; import { CatalogEntityDetailRegistry } from "../../../extensions/registries"; import { HotbarIcon } from "../hotbar/hotbar-icon"; import type { CatalogEntityItem } from "./catalog-entity.store"; +import { isDevelopment } from "../../../common/vars"; interface Props { item: CatalogEntityItem | null | undefined; @@ -91,6 +92,11 @@ export class CatalogEntityDetails extends Component {...item.getLabelBadges(this.props.hideDetails)} + {isDevelopment && ( + + {item.getId()} + + )} )} diff --git a/src/renderer/components/+extensions/__tests__/extensions.test.tsx b/src/renderer/components/+extensions/__tests__/extensions.test.tsx index 3361362b4b..3c2484b92e 100644 --- a/src/renderer/components/+extensions/__tests__/extensions.test.tsx +++ b/src/renderer/components/+extensions/__tests__/extensions.test.tsx @@ -61,7 +61,7 @@ describe("Extensions", () => { ExtensionInstallationStateStore.reset(); UserStore.resetInstance(); - await UserStore.createInstance().load(); + UserStore.createInstance(); ExtensionDiscovery.resetInstance(); ExtensionDiscovery.createInstance().uninstallExtension = jest.fn(() => Promise.resolve()); diff --git a/src/renderer/components/dock/__test__/log-resource-selector.test.tsx b/src/renderer/components/dock/__test__/log-resource-selector.test.tsx index 62566b868a..17ebb92d09 100644 --- a/src/renderer/components/dock/__test__/log-resource-selector.test.tsx +++ b/src/renderer/components/dock/__test__/log-resource-selector.test.tsx @@ -30,10 +30,11 @@ import type { LogTabData } from "../log-tab.store"; import { dockerPod, deploymentPod1 } from "./pod.mock"; import { ThemeStore } from "../../../theme.store"; import { UserStore } from "../../../../common/user-store"; +import mockFs from "mock-fs"; jest.mock("electron", () => ({ app: { - getPath: () => "/foo", + getPath: () => "tmp", }, })); @@ -71,6 +72,9 @@ const getFewPodsTabData = (): LogTabData => { describe("", () => { beforeEach(() => { + mockFs({ + "tmp": {} + }); UserStore.createInstance(); ThemeStore.createInstance(); }); @@ -78,6 +82,7 @@ describe("", () => { afterEach(() => { UserStore.resetInstance(); ThemeStore.resetInstance(); + mockFs.restore(); }); it("renders w/o errors", () => { diff --git a/src/renderer/components/hotbar/__tests__/hotbar-remove-command.test.tsx b/src/renderer/components/hotbar/__tests__/hotbar-remove-command.test.tsx index dfb75ffda3..119212753d 100644 --- a/src/renderer/components/hotbar/__tests__/hotbar-remove-command.test.tsx +++ b/src/renderer/components/hotbar/__tests__/hotbar-remove-command.test.tsx @@ -26,6 +26,14 @@ import React from "react"; import { ThemeStore } from "../../../theme.store"; import { UserStore } from "../../../../common/user-store"; import { Notifications } from "../../notifications"; +import mockFs from "mock-fs"; + +jest.mock("electron", () => ({ + app: { + getPath: () => "tmp", + setLoginItemSettings: jest.fn(), + }, +})); const mockHotbars: {[id: string]: any} = { "1": { @@ -48,6 +56,9 @@ jest.mock("../../../../common/hotbar-store", () => ({ describe("", () => { beforeEach(() => { + mockFs({ + "tmp": {} + }); UserStore.createInstance(); ThemeStore.createInstance(); }); @@ -55,11 +66,12 @@ describe("", () => { afterEach(() => { UserStore.resetInstance(); ThemeStore.resetInstance(); + mockFs.restore(); }); it("renders w/o errors", () => { const { container } = render(); - + expect(container).toBeInstanceOf(HTMLElement); }); @@ -73,4 +85,3 @@ describe("", () => { spy.mockRestore(); }); }); - diff --git a/src/renderer/components/hotbar/hotbar-add-command.tsx b/src/renderer/components/hotbar/hotbar-add-command.tsx index 8141594b7c..fde97ef561 100644 --- a/src/renderer/components/hotbar/hotbar-add-command.tsx +++ b/src/renderer/components/hotbar/hotbar-add-command.tsx @@ -33,22 +33,14 @@ const uniqueHotbarName: InputValidator = { @observer export class HotbarAddCommand extends React.Component { - - onSubmit(name: string) { + onSubmit = (name: string) => { if (!name.trim()) { return; } - const hotbarStore = HotbarStore.getInstance(); - - const hotbar = hotbarStore.add({ - name - }); - - hotbarStore.activeHotbarId = hotbar.id; - + HotbarStore.getInstance().add({ name }, { setActive: true }); CommandOverlay.close(); - } + }; render() { return ( @@ -58,10 +50,11 @@ export class HotbarAddCommand extends React.Component { autoFocus={true} theme="round-black" data-test-id="command-palette-hotbar-add-name" - validators={[uniqueHotbarName]} - onSubmit={(v) => this.onSubmit(v)} + validators={uniqueHotbarName} + onSubmit={this.onSubmit} dirty={true} - showValidationLine={true} /> + showValidationLine={true} + /> Please provide a new hotbar name (Press "Enter" to confirm or "Escape" to cancel) diff --git a/src/renderer/initializers/index.ts b/src/renderer/initializers/index.ts index ed2d8e8575..03f32f1a9d 100644 --- a/src/renderer/initializers/index.ts +++ b/src/renderer/initializers/index.ts @@ -27,5 +27,4 @@ export * from "./registries"; export * from "./welcome-menu-registry"; export * from "./workloads-overview-detail-registry"; export * from "./catalog"; -export * from "./stores"; export * from "./ipc"; diff --git a/src/renderer/initializers/stores.ts b/src/renderer/initializers/stores.ts deleted file mode 100644 index 37bef7530a..0000000000 --- a/src/renderer/initializers/stores.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Copyright (c) 2021 OpenLens Authors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -import { HotbarStore } from "../../common/hotbar-store"; -import { ClusterStore } from "../../common/cluster-store"; -import { UserStore } from "../../common/user-store"; -import { ExtensionsStore } from "../../extensions/extensions-store"; -import { FilesystemProvisionerStore } from "../../main/extension-filesystem"; - -import { ThemeStore } from "../theme.store"; -import { WeblinkStore } from "../../common/weblink-store"; - -export async function initStores() { - const userStore = UserStore.createInstance(); - const clusterStore = ClusterStore.createInstance(); - const extensionsStore = ExtensionsStore.createInstance(); - const filesystemStore = FilesystemProvisionerStore.createInstance(); - const themeStore = ThemeStore.createInstance(); - const hotbarStore = HotbarStore.createInstance(); - const weblinkStore = WeblinkStore.createInstance(); - - // preload common stores - await Promise.all([ - userStore.load(), - hotbarStore.load(), - clusterStore.load(), - extensionsStore.load(), - filesystemStore.load(), - themeStore.init(), - weblinkStore.load() - ]); -} diff --git a/src/renderer/theme.store.ts b/src/renderer/theme.store.ts index e3c1fd71ea..6caf9cf78a 100644 --- a/src/renderer/theme.store.ts +++ b/src/renderer/theme.store.ts @@ -20,9 +20,11 @@ */ import { computed, observable, reaction, makeObservable } from "mobx"; -import { autoBind, boundMethod, Singleton } from "./utils"; +import { autoBind, iter, Singleton } from "./utils"; import { UserStore } from "../common/user-store"; import logger from "../main/logger"; +import darkTheme from "./themes/lens-dark.json"; +import lightTheme from "./themes/lens-light.json"; export type ThemeId = string; @@ -32,12 +34,15 @@ export enum ThemeType { } export interface Theme { - id: ThemeId; // filename without .json-extension type: ThemeType; - name?: string; - colors?: Record; - description?: string; - author?: string; + name: string; + colors: Record; + description: string; + author: string; +} + +export interface ThemeItems extends Theme { + id: string; } export class ThemeStore extends Singleton { @@ -45,16 +50,12 @@ export class ThemeStore extends Singleton { // bundled themes from `themes/${themeId}.json` private allThemes = observable.map([ - ["lens-dark", { id: "lens-dark", type: ThemeType.DARK }], - ["lens-light", { id: "lens-light", type: ThemeType.LIGHT }], + ["lens-dark", { ...darkTheme, type: ThemeType.DARK }], + ["lens-light", { ...lightTheme, type: ThemeType.LIGHT }], ]); - @computed get themeIds(): string[] { - return Array.from(this.allThemes.keys()); - } - - @computed get themes(): Theme[] { - return Array.from(this.allThemes.values()); + @computed get themes(): ThemeItems[] { + return Array.from(iter.map(this.allThemes, ([id, theme]) => ({ id, ...theme }))); } @computed get activeThemeId(): string { @@ -62,12 +63,7 @@ export class ThemeStore extends Singleton { } @computed get activeTheme(): Theme { - const activeTheme = this.allThemes.get(this.activeThemeId) ?? this.allThemes.get("lens-dark"); - - return { - colors: {}, - ...activeTheme, - }; + return this.allThemes.get(this.activeThemeId) ?? this.allThemes.get("lens-dark"); } constructor() { @@ -77,9 +73,9 @@ export class ThemeStore extends Singleton { autoBind(this); // auto-apply active theme - reaction(() => this.activeThemeId, async themeId => { + reaction(() => this.activeThemeId, themeId => { try { - this.applyTheme(await this.loadTheme(themeId)); + this.applyTheme(this.getThemeById(themeId)); } catch (err) { logger.error(err); UserStore.getInstance().resetTheme(); @@ -89,38 +85,10 @@ export class ThemeStore extends Singleton { }); } - async init() { - // preload all themes - await Promise.all(this.themeIds.map(this.loadTheme)); - } - getThemeById(themeId: ThemeId): Theme { return this.allThemes.get(themeId); } - @boundMethod - protected async loadTheme(themeId: ThemeId): Promise { - try { - const existingTheme = this.getThemeById(themeId); - - if (existingTheme) { - const theme = await import( - /* webpackChunkName: "themes/[name]" */ - `./themes/${themeId}.json` - ); - - existingTheme.author = theme.author; - existingTheme.colors = theme.colors; - existingTheme.description = theme.description; - existingTheme.name = theme.name; - } - - return existingTheme; - } catch (err) { - throw new Error(`Can't load theme "${themeId}": ${err}`); - } - } - protected applyTheme(theme: Theme) { if (!this.styles) { this.styles = document.createElement("style");