diff --git a/.eslintrc.js b/.eslintrc.js index 57ee07348f..4854713f0e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -79,6 +79,8 @@ module.exports = { sourceType: "module", }, rules: { + "no-invalid-this": "off", + "@typescript-eslint/no-invalid-this": ["error"], "@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/explicit-module-boundary-types": "off", @@ -137,6 +139,8 @@ module.exports = { jsx: true, }, rules: { + "no-invalid-this": "off", + "@typescript-eslint/no-invalid-this": ["error"], "@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/interface-name-prefix": "off", diff --git a/docs/extensions/guides/README.md b/docs/extensions/guides/README.md index 35a887bea2..06bbbe9e3c 100644 --- a/docs/extensions/guides/README.md +++ b/docs/extensions/guides/README.md @@ -30,7 +30,7 @@ Each guide or code sample includes the following: | Sample | APIs | | ----- | ----- | [hello-world](https://github.com/lensapp/lens-extension-samples/tree/master/helloworld-sample) | LensMainExtension
LensRendererExtension
Component.Icon
Component.IconProps | -[minikube](https://github.com/lensapp/lens-extension-samples/tree/master/minikube-sample) | LensMainExtension
Store.clusterStore
Store.workspaceStore | +[minikube](https://github.com/lensapp/lens-extension-samples/tree/master/minikube-sample) | LensMainExtension
Store.ClusterStore
Store.workspaceStore | [styling-css-modules-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-css-modules-sample) | LensMainExtension
LensRendererExtension
Component.Icon
Component.IconProps | [styling-emotion-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-emotion-sample) | LensMainExtension
LensRendererExtension
Component.Icon
Component.IconProps | [styling-sass-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-sass-sample) | LensMainExtension
LensRendererExtension
Component.Icon
Component.IconProps | diff --git a/docs/extensions/guides/main-extension.md b/docs/extensions/guides/main-extension.md index 30793df9cd..f1212c0d37 100644 --- a/docs/extensions/guides/main-extension.md +++ b/docs/extensions/guides/main-extension.md @@ -45,8 +45,6 @@ It accesses some Lens state data, and it periodically logs the name of the clust ```typescript import { LensMainExtension, Store } from "@k8slens/extensions"; -const clusterStore = Store.clusterStore - export default class ActiveClusterExtensionMain extends LensMainExtension { timer: NodeJS.Timeout @@ -54,11 +52,11 @@ export default class ActiveClusterExtensionMain extends LensMainExtension { onActivate() { console.log("Cluster logger activated"); this.timer = setInterval(() => { - if (!clusterStore.active) { + if (!Store.ClusterStore.getInstance().active) { console.log("No active cluster"); return; } - console.log("active cluster is", clusterStore.active.contextName) + console.log("active cluster is", Store.ClusterStore.getInstance().active.contextName) }, 5000) } diff --git a/docs/extensions/guides/renderer-extension.md b/docs/extensions/guides/renderer-extension.md index 89161ce760..48bcaa5d0c 100644 --- a/docs/extensions/guides/renderer-extension.md +++ b/docs/extensions/guides/renderer-extension.md @@ -57,7 +57,7 @@ The example above logs messages when the extension is enabled and disabled. Cluster pages appear in the cluster dashboard. Use cluster pages to display information about or add functionality to the active cluster. It is also possible to include custom details from other clusters. -Use your extension to access Kubernetes resources in the active cluster with [`clusterStore`](../stores#clusterstore). +Use your extension to access Kubernetes resources in the active cluster with [`ClusterStore.getInstance()`](../stores#Clusterstore). Add a cluster page definition to a `LensRendererExtension` subclass with the following example: diff --git a/docs/extensions/guides/stores.md b/docs/extensions/guides/stores.md index ee24dd0620..c8a5ec270d 100644 --- a/docs/extensions/guides/stores.md +++ b/docs/extensions/guides/stores.md @@ -15,6 +15,12 @@ This guide shows how to create a store for the [`appPreferences`](../renderer-ex The preference is a simple boolean that indicates whether or not something is enabled. However, in the example, the enabled state is not stored anywhere, and it reverts to the default when Lens is restarted. +`Store.ExtensionStore`'s child class will need to be created before being used. +It is recommended to call the inherited static method `getInstanceOrCreate()` only in one place, generally within you extension's `onActivate()` method. +It is also recommenced to delete the instance, using the inherited static method `resetInstance()`, in your extension's `onDeactivate()` method. +Everywhere else in your code you should use the `getInstance()` static method. +This is so that your data is kept up to date and not persisted longer than expected. + The following example code creates a store for the `appPreferences` guide example: ``` typescript @@ -50,8 +56,6 @@ export class ExamplePreferencesStore extends Store.ExtensionStore(); ``` First, our example defines the extension's data model using the simple `ExamplePreferencesModel` type. @@ -71,46 +75,51 @@ It is called when the store is being saved. `toJSON()` must provide a JSON serializable object, facilitating its storage in JSON format. The `toJS()` function from [`mobx`](https://mobx.js.org/README.html) is convenient for this purpose, and is used here. -Finally, `examplePreferencesStore` is created by calling `ExamplePreferencesStore.getInstance()`, and exported for use by other parts of the extension. -Note that `examplePreferencesStore` is a singleton. -Calling this function again will not create a new store. +Finally, `ExamplePreferencesStore` is created by calling `ExamplePreferencesStore.getInstanceOrCreate()`, and exported for use by other parts of the extension. +Note that `ExamplePreferencesStore` is a singleton. +Calling this function will create an instance if one has not been made before. +Through normal use you should call `ExamplePreferencesStore.getInstance()` as that will throw an error if an instance does not exist. +This provides some logical safety in that it limits where a new instance can be created. +Thus it prevents an instance from being created when the constructor args are not present at the call site. + +If you are doing some cleanup it is recommended to call `ExamplePreferencesStore.getInstance(false)` which returns `undefined` instead of throwing when there is no instance. The following example code, modified from the [`appPreferences`](../renderer-extension#apppreferences) guide demonstrates how to use the extension store. -`examplePreferencesStore` must be loaded in the main process, where loaded stores are automatically saved when exiting Lens. +`ExamplePreferencesStore` must be loaded in the main process, where loaded stores are automatically saved when exiting Lens. This can be done in `./main.ts`: ``` typescript import { LensMainExtension } from "@k8slens/extensions"; -import { examplePreferencesStore } from "./src/example-preference-store"; +import { ExamplePreferencesStore } from "./src/example-preference-store"; export default class ExampleMainExtension extends LensMainExtension { async onActivate() { - await examplePreferencesStore.loadExtension(this); + await ExamplePreferencesStore.getInstanceOrCreate().loadExtension(this); } } ``` -Here, `examplePreferencesStore` loads with `examplePreferencesStore.loadExtension(this)`, which is conveniently called from the `onActivate()` method of `ExampleMainExtension`. -Similarly, `examplePreferencesStore` must load in the renderer process where the `appPreferences` are handled. +Here, `ExamplePreferencesStore` loads with `ExamplePreferencesStore.getInstanceOrCreate().loadExtension(this)`, which is conveniently called from the `onActivate()` method of `ExampleMainExtension`. +Similarly, `ExamplePreferencesStore` must load in the renderer process where the `appPreferences` are handled. This can be done in `./renderer.ts`: ``` typescript import { LensRendererExtension } from "@k8slens/extensions"; import { ExamplePreferenceHint, ExamplePreferenceInput } from "./src/example-preference"; -import { examplePreferencesStore } from "./src/example-preference-store"; +import { ExamplePreferencesStore } from "./src/example-preference-store"; import React from "react"; export default class ExampleRendererExtension extends LensRendererExtension { async onActivate() { - await examplePreferencesStore.loadExtension(this); + await ExamplePreferencesStore.getInstanceOrCreate().loadExtension(this); } appPreferences = [ { title: "Example Preferences", components: { - Input: () => , + Input: () => , Hint: () => } } @@ -118,8 +127,8 @@ export default class ExampleRendererExtension extends LensRendererExtension { } ``` -Again, `examplePreferencesStore.loadExtension(this)` is called to load `examplePreferencesStore`, this time from the `onActivate()` method of `ExampleRendererExtension`. -There is no longer the need for the `preference` field in the `ExampleRendererExtension` class because the props for `ExamplePreferenceInput` is now `examplePreferencesStore`. +Again, `ExamplePreferencesStore.getInstanceOrCreate().loadExtension(this)` is called to load `ExamplePreferencesStore`, this time from the `onActivate()` method of `ExampleRendererExtension`. + `ExamplePreferenceInput` is defined in `./src/example-preference.tsx`: ``` typescript @@ -128,21 +137,15 @@ import { observer } from "mobx-react"; import React from "react"; import { ExamplePreferencesStore } from "./example-preference-store"; -export class ExamplePreferenceProps { - preference: ExamplePreferencesStore; -} - @observer -export class ExamplePreferenceInput extends React.Component { +export class ExamplePreferenceInput extends React.Component { render() { - const { preference } = this.props; - return ( { preference.enabled = v; }} + value={ExamplePreferencesStore.getInstace().enabled} + onChange={v => { ExamplePreferencesStore.getInstace().enabled = v; }} /> ); } @@ -159,4 +162,4 @@ export class ExamplePreferenceHint extends React.Component { The only change here is that `ExamplePreferenceProps` defines its `preference` field as an `ExamplePreferencesStore` type. Everything else works as before, except that now the `enabled` state persists across Lens restarts because it is managed by the -`examplePreferencesStore`. +`ExamplePreferencesStore`. diff --git a/integration/__tests__/app.tests.ts b/integration/__tests__/app.tests.ts index 8152c25f35..fa1f4ef286 100644 --- a/integration/__tests__/app.tests.ts +++ b/integration/__tests__/app.tests.ts @@ -60,8 +60,8 @@ describe("Lens integration tests", () => { it("ensures helm repos", async () => { const repos = await listHelmRepositories(); - if (!repos[0]) { - fail("Lens failed to add Bitnami repository"); + if (repos.length === 0) { + fail("Lens failed to add any repositories"); } await app.client.click("[data-testid=kube-tab]"); diff --git a/integration/helpers/utils.ts b/integration/helpers/utils.ts index 00a40052ea..e0862e1d36 100644 --- a/integration/helpers/utils.ts +++ b/integration/helpers/utils.ts @@ -114,16 +114,14 @@ type HelmRepository = { url: string; }; -export async function listHelmRepositories(retries = 0): Promise{ - if (retries < 5) { +export async function listHelmRepositories(): Promise{ + for (let i = 0; i < 10; i += 1) { try { - const { stdout: reposJson } = await promiseExec("helm repo list -o json"); + const { stdout } = await promiseExec("helm repo list -o json"); - return JSON.parse(reposJson); + return JSON.parse(stdout); } catch { await new Promise(r => setTimeout(r, 2000)); // if no repositories, wait for Lens adding bitnami repository - - return await listHelmRepositories((retries + 1)); } } diff --git a/src/common/__tests__/cluster-store.test.ts b/src/common/__tests__/cluster-store.test.ts index b37d4d2b3b..fc1b36c548 100644 --- a/src/common/__tests__/cluster-store.test.ts +++ b/src/common/__tests__/cluster-store.test.ts @@ -4,8 +4,9 @@ import yaml from "js-yaml"; import { Cluster } from "../../main/cluster"; import { ClusterStore, getClusterIdFromHost } from "../cluster-store"; import { Console } from "console"; +import { stdout, stderr } from "process"; -console = new Console(process.stdout, process.stderr); // fix mockFS +console = new Console(stdout, stderr); const testDataIcon = fs.readFileSync("test-data/cluster-store-migration-icon.png"); const kubeconfig = ` @@ -47,10 +48,8 @@ jest.mock("electron", () => { }; }); -let clusterStore: ClusterStore; - describe("empty config", () => { - beforeEach(() => { + beforeEach(async () => { ClusterStore.resetInstance(); const mockOpts = { "tmp": { @@ -59,9 +58,8 @@ describe("empty config", () => { }; mockFs(mockOpts); - clusterStore = ClusterStore.getInstance(); - return clusterStore.load(); + await ClusterStore.getInstanceOrCreate().load(); }); afterEach(() => { @@ -70,7 +68,7 @@ describe("empty config", () => { describe("with foo cluster added", () => { beforeEach(() => { - clusterStore.addCluster( + ClusterStore.getInstance().addCluster( new Cluster({ id: "foo", contextName: "foo", @@ -85,7 +83,7 @@ describe("empty config", () => { }); it("adds new cluster to store", async () => { - const storedCluster = clusterStore.getById("foo"); + const storedCluster = ClusterStore.getInstance().getById("foo"); expect(storedCluster.id).toBe("foo"); expect(storedCluster.preferences.terminalCWD).toBe("/tmp"); @@ -94,19 +92,19 @@ describe("empty config", () => { }); it("removes cluster from store", async () => { - await clusterStore.removeById("foo"); - expect(clusterStore.getById("foo")).toBeNull(); + await ClusterStore.getInstance().removeById("foo"); + expect(ClusterStore.getInstance().getById("foo")).toBeNull(); }); it("sets active cluster", () => { - clusterStore.setActive("foo"); - expect(clusterStore.active.id).toBe("foo"); + ClusterStore.getInstance().setActive("foo"); + expect(ClusterStore.getInstance().active.id).toBe("foo"); }); }); describe("with prod and dev clusters added", () => { beforeEach(() => { - clusterStore.addClusters( + ClusterStore.getInstance().addClusters( new Cluster({ id: "prod", contextName: "foo", @@ -127,8 +125,8 @@ describe("empty config", () => { }); it("check if store can contain multiple clusters", () => { - expect(clusterStore.hasClusters()).toBeTruthy(); - expect(clusterStore.clusters.size).toBe(2); + expect(ClusterStore.getInstance().hasClusters()).toBeTruthy(); + expect(ClusterStore.getInstance().clusters.size).toBe(2); }); it("check if cluster's kubeconfig file saved", () => { @@ -178,9 +176,8 @@ describe("config with existing clusters", () => { }; mockFs(mockOpts); - clusterStore = ClusterStore.getInstance(); - return clusterStore.load(); + return ClusterStore.getInstanceOrCreate().load(); }); afterEach(() => { @@ -188,24 +185,24 @@ describe("config with existing clusters", () => { }); it("allows to retrieve a cluster", () => { - const storedCluster = clusterStore.getById("cluster1"); + const storedCluster = ClusterStore.getInstance().getById("cluster1"); expect(storedCluster.id).toBe("cluster1"); expect(storedCluster.preferences.terminalCWD).toBe("/foo"); }); it("allows to delete a cluster", () => { - clusterStore.removeById("cluster2"); - const storedCluster = clusterStore.getById("cluster1"); + ClusterStore.getInstance().removeById("cluster2"); + const storedCluster = ClusterStore.getInstance().getById("cluster1"); expect(storedCluster).toBeTruthy(); - const storedCluster2 = clusterStore.getById("cluster2"); + const storedCluster2 = ClusterStore.getInstance().getById("cluster2"); expect(storedCluster2).toBeNull(); }); it("allows getting all of the clusters", async () => { - const storedClusters = clusterStore.clustersList; + const storedClusters = ClusterStore.getInstance().clustersList; expect(storedClusters.length).toBe(3); expect(storedClusters[0].id).toBe("cluster1"); @@ -216,7 +213,7 @@ describe("config with existing clusters", () => { }); it("marks owned cluster disabled by default", () => { - const storedClusters = clusterStore.clustersList; + const storedClusters = ClusterStore.getInstance().clustersList; expect(storedClusters[0].enabled).toBe(true); expect(storedClusters[2].enabled).toBe(false); @@ -276,9 +273,8 @@ users: }; mockFs(mockOpts); - clusterStore = ClusterStore.getInstance(); - return clusterStore.load(); + return ClusterStore.getInstanceOrCreate().load(); }); afterEach(() => { @@ -286,7 +282,7 @@ users: }); it("does not enable clusters with invalid kubeconfig", () => { - const storedClusters = clusterStore.clustersList; + const storedClusters = ClusterStore.getInstance().clustersList; expect(storedClusters.length).toBe(2); expect(storedClusters[0].enabled).toBeFalsy; @@ -319,9 +315,8 @@ describe("pre 2.0 config with an existing cluster", () => { }; mockFs(mockOpts); - clusterStore = ClusterStore.getInstance(); - return clusterStore.load(); + return ClusterStore.getInstanceOrCreate().load(); }); afterEach(() => { @@ -329,7 +324,7 @@ describe("pre 2.0 config with an existing cluster", () => { }); it("migrates to modern format with kubeconfig in a file", async () => { - const config = clusterStore.clustersList[0].kubeConfigPath; + const config = ClusterStore.getInstance().clustersList[0].kubeConfigPath; expect(fs.readFileSync(config, "utf8")).toContain(`"contexts":[]`); }); @@ -347,16 +342,51 @@ describe("pre 2.6.0 config with a cluster that has arrays in auth config", () => } }, cluster1: { - kubeConfig: "apiVersion: v1\nclusters:\n- cluster:\n server: https://10.211.55.6:8443\n name: minikube\ncontexts:\n- context:\n cluster: minikube\n user: minikube\n name: minikube\ncurrent-context: minikube\nkind: Config\npreferences: {}\nusers:\n- name: minikube\n user:\n client-certificate: /Users/kimmo/.minikube/client.crt\n client-key: /Users/kimmo/.minikube/client.key\n auth-provider:\n config:\n access-token:\n - should be string\n expiry:\n - should be string\n" + kubeConfig: JSON.stringify({ + apiVersion: "v1", + clusters: [{ + cluster: { + server: "https://10.211.55.6:8443", + }, + name: "minikube", + }], + contexts: [{ + context: { + cluster: "minikube", + user: "minikube", + name: "minikube", + }, + name: "minikube", + }], + "current-context": "minikube", + kind: "Config", + preferences: {}, + users: [{ + name: "minikube", + user: { + "client-certificate": "/Users/foo/.minikube/client.crt", + "client-key": "/Users/foo/.minikube/client.key", + "auth-provider": { + config: { + "access-token": [ + "should be string" + ], + expiry: [ + "should be string" + ], + } + } + }, + }] + }), }, }) } }; mockFs(mockOpts); - clusterStore = ClusterStore.getInstance(); - return clusterStore.load(); + return ClusterStore.getInstanceOrCreate().load(); }); afterEach(() => { @@ -364,10 +394,12 @@ describe("pre 2.6.0 config with a cluster that has arrays in auth config", () => }); it("replaces array format access token and expiry into string", async () => { - const file = clusterStore.clustersList[0].kubeConfigPath; + const file = ClusterStore.getInstance().clustersList[0].kubeConfigPath; const config = fs.readFileSync(file, "utf8"); const kc = yaml.safeLoad(config); + console.log(kc); + expect(kc.users[0].user["auth-provider"].config["access-token"]).toBe("should be string"); expect(kc.users[0].user["auth-provider"].config["expiry"]).toBe("should be string"); }); @@ -397,9 +429,8 @@ describe("pre 2.6.0 config with a cluster icon", () => { }; mockFs(mockOpts); - clusterStore = ClusterStore.getInstance(); - return clusterStore.load(); + return ClusterStore.getInstanceOrCreate().load(); }); afterEach(() => { @@ -407,7 +438,7 @@ describe("pre 2.6.0 config with a cluster icon", () => { }); it("moves the icon into preferences", async () => { - const storedClusterData = clusterStore.clustersList[0]; + const storedClusterData = ClusterStore.getInstance().clustersList[0]; expect(storedClusterData.hasOwnProperty("icon")).toBe(false); expect(storedClusterData.preferences.hasOwnProperty("icon")).toBe(true); @@ -437,9 +468,8 @@ describe("for a pre 2.7.0-beta.0 config without a workspace", () => { }; mockFs(mockOpts); - clusterStore = ClusterStore.getInstance(); - return clusterStore.load(); + return ClusterStore.getInstanceOrCreate().load(); }); afterEach(() => { @@ -474,9 +504,8 @@ describe("pre 3.6.0-beta.1 config with an existing cluster", () => { }; mockFs(mockOpts); - clusterStore = ClusterStore.getInstance(); - return clusterStore.load(); + return ClusterStore.getInstanceOrCreate().load(); }); afterEach(() => { @@ -484,13 +513,13 @@ describe("pre 3.6.0-beta.1 config with an existing cluster", () => { }); it("migrates to modern format with kubeconfig in a file", async () => { - const config = clusterStore.clustersList[0].kubeConfigPath; + const config = ClusterStore.getInstance().clustersList[0].kubeConfigPath; expect(fs.readFileSync(config, "utf8")).toBe(minimalValidKubeConfig); }); it("migrates to modern format with icon not in file", async () => { - const { icon } = clusterStore.clustersList[0].preferences; + const { icon } = ClusterStore.getInstance().clustersList[0].preferences; expect(icon.startsWith("data:;base64,")).toBe(true); }); diff --git a/src/common/__tests__/event-bus.test.ts b/src/common/__tests__/event-bus.test.ts index e8159acaf0..b03cce079a 100644 --- a/src/common/__tests__/event-bus.test.ts +++ b/src/common/__tests__/event-bus.test.ts @@ -1,4 +1,8 @@ import { appEventBus, AppEvent } from "../event-bus"; +import { Console } from "console"; +import { stdout, stderr } from "process"; + +console = new Console(stdout, stderr); describe("event bus tests", () => { describe("emit", () => { diff --git a/src/common/__tests__/hotbar-store.test.ts b/src/common/__tests__/hotbar-store.test.ts index 40f38416c2..8b245b2a0c 100644 --- a/src/common/__tests__/hotbar-store.test.ts +++ b/src/common/__tests__/hotbar-store.test.ts @@ -1,8 +1,12 @@ import mockFs from "mock-fs"; -import { HotbarStore, hotbarStore } from "../hotbar-store"; +import { ClusterStore } from "../cluster-store"; +import { HotbarStore } from "../hotbar-store"; describe("HotbarStore", () => { beforeEach(() => { + ClusterStore.resetInstance(); + ClusterStore.getInstanceOrCreate(); + HotbarStore.resetInstance(); mockFs({ tmp: { "lens-hotbar-store.json": "{}" } }); }); @@ -13,8 +17,8 @@ describe("HotbarStore", () => { describe("load", () => { it("loads one hotbar by default", () => { - hotbarStore.load(); - expect(hotbarStore.hotbars.length).toEqual(1); + HotbarStore.getInstanceOrCreate().load(); + expect(HotbarStore.getInstance().hotbars.length).toEqual(1); }); }); }); diff --git a/src/common/__tests__/search-store.test.ts b/src/common/__tests__/search-store.test.ts index 6193863192..1e0fce3e27 100644 --- a/src/common/__tests__/search-store.test.ts +++ b/src/common/__tests__/search-store.test.ts @@ -1,4 +1,8 @@ import { SearchStore } from "../search-store"; +import { Console } from "console"; +import { stdout, stderr } from "process"; + +console = new Console(stdout, stderr); let searchStore: SearchStore = null; const logs = [ diff --git a/src/common/__tests__/user-store.test.ts b/src/common/__tests__/user-store.test.ts index 1715a1a593..0bb1b90de4 100644 --- a/src/common/__tests__/user-store.test.ts +++ b/src/common/__tests__/user-store.test.ts @@ -10,7 +10,7 @@ jest.mock("electron", () => { getVersion: () => "99.99.99", getPath: () => "tmp", getLocale: () => "en", - setLoginItemSettings: jest.fn(), + setLoginItemSettings: (): void => void 0, } }; }); @@ -18,12 +18,19 @@ jest.mock("electron", () => { import { UserStore } from "../user-store"; import { SemVer } from "semver"; import electron from "electron"; +import { stdout, stderr } from "process"; + +console = new Console(stdout, stderr); describe("user store tests", () => { describe("for an empty config", () => { beforeEach(() => { UserStore.resetInstance(); - mockFs({ tmp: { "config.json": "{}" } }); + mockFs({ tmp: { "config.json": "{}", "kube_config": "{}" } }); + + (UserStore.getInstanceOrCreate() as any).refreshNewContexts = jest.fn(() => Promise.resolve()); + + return UserStore.getInstance().load(); }); afterEach(() => { @@ -31,14 +38,14 @@ describe("user store tests", () => { }); it("allows setting and retrieving lastSeenAppVersion", () => { - const us = UserStore.getInstance(); + const us = UserStore.getInstance(); us.lastSeenAppVersion = "1.2.3"; expect(us.lastSeenAppVersion).toBe("1.2.3"); }); it("allows adding and listing seen contexts", () => { - const us = UserStore.getInstance(); + const us = UserStore.getInstance(); us.seenContexts.add("foo"); expect(us.seenContexts.size).toBe(1); @@ -51,7 +58,7 @@ describe("user store tests", () => { }); it("allows setting and getting preferences", () => { - const us = UserStore.getInstance(); + const us = UserStore.getInstance(); us.preferences.httpsProxy = "abcd://defg"; @@ -63,7 +70,7 @@ describe("user store tests", () => { }); it("correctly resets theme to default value", async () => { - const us = UserStore.getInstance(); + const us = UserStore.getInstance(); us.isLoaded = true; @@ -73,7 +80,7 @@ describe("user store tests", () => { }); it("correctly calculates if the last seen version is an old release", () => { - const us = UserStore.getInstance(); + const us = UserStore.getInstance(); expect(us.isNewVersion).toBe(true); @@ -94,6 +101,8 @@ describe("user store tests", () => { }) } }); + + return UserStore.getInstanceOrCreate().load(); }); afterEach(() => { @@ -101,7 +110,7 @@ describe("user store tests", () => { }); it("sets last seen app version to 0.0.0", () => { - const us = UserStore.getInstance(); + const us = UserStore.getInstance(); expect(us.lastSeenAppVersion).toBe("0.0.0"); }); diff --git a/src/common/catalog-entities/kubernetes-cluster.ts b/src/common/catalog-entities/kubernetes-cluster.ts index 55f9d38feb..1071dc6794 100644 --- a/src/common/catalog-entities/kubernetes-cluster.ts +++ b/src/common/catalog-entities/kubernetes-cluster.ts @@ -3,7 +3,7 @@ import { observable } from "mobx"; import { catalogCategoryRegistry } from "../catalog-category-registry"; import { CatalogCategory, CatalogEntity, CatalogEntityActionContext, CatalogEntityAddMenuContext, CatalogEntityContextMenuContext, CatalogEntityData, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog-entity"; import { clusterDisconnectHandler } from "../cluster-ipc"; -import { clusterStore } from "../cluster-store"; +import { ClusterStore } from "../cluster-store"; import { requestMain } from "../ipc"; export type KubernetesClusterSpec = { @@ -56,7 +56,7 @@ export class KubernetesCluster implements CatalogEntity { icon: "delete", title: "Delete", onlyVisibleForSource: "local", - onClick: async () => clusterStore.removeById(this.metadata.uid), + onClick: async () => ClusterStore.getInstance().removeById(this.metadata.uid), confirm: { message: `Remove Kubernetes Cluster "${this.metadata.name} from Lens?` } @@ -68,7 +68,7 @@ export class KubernetesCluster implements CatalogEntity { icon: "link_off", title: "Disconnect", onClick: async () => { - clusterStore.deactivate(this.metadata.uid); + ClusterStore.getInstance().deactivate(this.metadata.uid); requestMain(clusterDisconnectHandler, this.metadata.uid); } }); diff --git a/src/common/cluster-ipc.ts b/src/common/cluster-ipc.ts index d44634407e..949fee1117 100644 --- a/src/common/cluster-ipc.ts +++ b/src/common/cluster-ipc.ts @@ -1,5 +1,5 @@ import { handleRequest } from "./ipc"; -import { ClusterId, clusterStore } from "./cluster-store"; +import { ClusterId, ClusterStore } from "./cluster-store"; import { appEventBus } from "./event-bus"; import { ResourceApplier } from "../main/resource-applier"; import { ipcMain, IpcMainInvokeEvent } from "electron"; @@ -11,10 +11,9 @@ export const clusterRefreshHandler = "cluster:refresh"; export const clusterDisconnectHandler = "cluster:disconnect"; export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all"; - if (ipcMain) { handleRequest(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => { - const cluster = clusterStore.getById(clusterId); + const cluster = ClusterStore.getInstance().getById(clusterId); if (cluster) { return cluster.activate(force); @@ -22,7 +21,7 @@ if (ipcMain) { }); handleRequest(clusterSetFrameIdHandler, (event: IpcMainInvokeEvent, clusterId: ClusterId) => { - const cluster = clusterStore.getById(clusterId); + const cluster = ClusterStore.getInstance().getById(clusterId); if (cluster) { clusterFrameMap.set(cluster.id, { frameId: event.frameId, processId: event.processId }); @@ -32,14 +31,14 @@ if (ipcMain) { }); handleRequest(clusterRefreshHandler, (event, clusterId: ClusterId) => { - const cluster = clusterStore.getById(clusterId); + const cluster = ClusterStore.getInstance().getById(clusterId); if (cluster) return cluster.refresh({ refreshMetadata: true }); }); handleRequest(clusterDisconnectHandler, (event, clusterId: ClusterId) => { appEventBus.emit({name: "cluster", action: "stop"}); - const cluster = clusterStore.getById(clusterId); + const cluster = ClusterStore.getInstance().getById(clusterId); if (cluster) { cluster.disconnect(); @@ -49,7 +48,7 @@ if (ipcMain) { handleRequest(clusterKubectlApplyAllHandler, (event, clusterId: ClusterId, resources: string[]) => { appEventBus.emit({name: "cluster", action: "kubectl-apply-all"}); - const cluster = clusterStore.getById(clusterId); + const cluster = ClusterStore.getInstance().getById(clusterId); if (cluster) { const applier = new ResourceApplier(cluster); diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index f1f10d29c3..56deb35758 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -112,7 +112,7 @@ export class ClusterStore extends BaseStore { private static stateRequestChannel = "cluster:states"; - private constructor() { + constructor() { super({ configName: "lens-cluster-store", accessPropertiesByDotNotation: false, // To make dots safe in cluster context names @@ -337,8 +337,6 @@ export class ClusterStore extends BaseStore { } } -export const clusterStore = ClusterStore.getInstance(); - export function getClusterIdFromHost(host: string): ClusterId | undefined { // e.g host == "%clusterId.localhost:45345" const subDomains = host.split(":")[0].split("."); @@ -355,5 +353,5 @@ export function getHostedClusterId() { } export function getHostedCluster(): Cluster { - return clusterStore.getById(getHostedClusterId()); + return ClusterStore.getInstance().getById(getHostedClusterId()); } diff --git a/src/common/hotbar-store.ts b/src/common/hotbar-store.ts index aba7c69bc1..88bdff67d9 100644 --- a/src/common/hotbar-store.ts +++ b/src/common/hotbar-store.ts @@ -23,7 +23,7 @@ export interface HotbarStoreModel { export class HotbarStore extends BaseStore { @observable hotbars: Hotbar[] = []; - private constructor() { + constructor() { super({ configName: "lens-hotbar-store", accessPropertiesByDotNotation: false, // To make dots safe in cluster context names @@ -67,5 +67,3 @@ export class HotbarStore extends BaseStore { }); } } - -export const hotbarStore = HotbarStore.getInstance(); diff --git a/src/common/protocol-handler/router.ts b/src/common/protocol-handler/router.ts index 7b7659992f..80c2076cd3 100644 --- a/src/common/protocol-handler/router.ts +++ b/src/common/protocol-handler/router.ts @@ -5,8 +5,8 @@ import { pathToRegexp } from "path-to-regexp"; import logger from "../../main/logger"; import Url from "url-parse"; import { RoutingError, RoutingErrorType } from "./error"; -import { extensionsStore } from "../../extensions/extensions-store"; -import { extensionLoader } from "../../extensions/extension-loader"; +import { ExtensionsStore } from "../../extensions/extensions-store"; +import { ExtensionLoader } from "../../extensions/extension-loader"; import { LensExtension } from "../../extensions/lens-extension"; import { RouteHandler, RouteParams } from "../../extensions/registries/protocol-handler-registry"; @@ -72,7 +72,7 @@ export abstract class LensProtocolRouter extends Singleton { /** * find the most specific matching handler and call it - * @param routes the array of (path schemas, handler) paris to match against + * @param routes the array of (path schemas, handler) pairs to match against * @param url the url (in its current state) */ protected _route(routes: [string, RouteHandler][], url: Url, extensionName?: string): void { @@ -124,7 +124,7 @@ export abstract class LensProtocolRouter extends Singleton { const { [EXTENSION_PUBLISHER_MATCH]: publisher, [EXTENSION_NAME_MATCH]: partialName } = match.params; const name = [publisher, partialName].filter(Boolean).join("/"); - const extension = extensionLoader.userExtensionsByName.get(name); + const extension = ExtensionLoader.getInstance().userExtensionsByName.get(name); if (!extension) { logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not installed`); @@ -132,7 +132,7 @@ export abstract class LensProtocolRouter extends Singleton { return name; } - if (!extensionsStore.isEnabled(extension.id)) { + if (!ExtensionsStore.getInstance().isEnabled(extension.id)) { logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not enabled`); return name; diff --git a/src/common/request.ts b/src/common/request.ts index ca34f4a961..8c78c865fa 100644 --- a/src/common/request.ts +++ b/src/common/request.ts @@ -1,12 +1,12 @@ import request from "request"; import requestPromise from "request-promise-native"; -import { userStore } from "./user-store"; +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.preferences; + const { httpsProxy, allowUntrustedCAs } = UserStore.getInstance().preferences; return { proxy: httpsProxy || undefined, diff --git a/src/common/user-store.ts b/src/common/user-store.ts index 510427a88e..0532cc59bc 100644 --- a/src/common/user-store.ts +++ b/src/common/user-store.ts @@ -38,7 +38,7 @@ export interface UserPreferences { export class UserStore extends BaseStore { static readonly defaultTheme: ThemeId = "lens-dark"; - private constructor() { + constructor() { super({ configName: "lens-user-store", migrations, @@ -163,14 +163,6 @@ export class UserStore extends BaseStore { this.newContexts.clear(); } - /** - * Getting default directory to download kubectl binaries - * @returns string - */ - getDefaultKubectlPath(): string { - return path.join((app || remote.app).getPath("userData"), "binaries"); - } - @action protected async fromStore(data: Partial = {}) { const { lastSeenAppVersion, seenContexts = [], preferences, kubeConfigPath } = data; @@ -200,4 +192,10 @@ export class UserStore extends BaseStore { } } -export const userStore = UserStore.getInstance(); +/** + * Getting default directory to download kubectl binaries + * @returns string + */ +export function getDefaultKubectlPath(): string { + return path.join((app || remote.app).getPath("userData"), "binaries"); +} diff --git a/src/common/utils/debouncePromise.ts b/src/common/utils/debouncePromise.ts index b5ad88d000..01fc419c11 100755 --- a/src/common/utils/debouncePromise.ts +++ b/src/common/utils/debouncePromise.ts @@ -3,8 +3,8 @@ export function debouncePromise(func: (...args: F) => T | Promise, timeout = 0): (...args: F) => Promise { let timer: NodeJS.Timeout; - return (...params: any[]) => new Promise(resolve => { + return (...params: F) => new Promise(resolve => { clearTimeout(timer); - timer = global.setTimeout(() => resolve(func.apply(this, params)), timeout); + timer = global.setTimeout(() => resolve(func(...params)), timeout); }); } diff --git a/src/common/utils/singleton.ts b/src/common/utils/singleton.ts index caa5471072..2f19fd638d 100644 --- a/src/common/utils/singleton.ts +++ b/src/common/utils/singleton.ts @@ -5,25 +5,39 @@ * @example * const usersStore: UsersStore = UsersStore.getInstance(); */ +type StaticThis = { new(...args: R): T }; -type Constructor = new (...args: any[]) => T; - -class Singleton { +export class Singleton { private static instances = new WeakMap(); + private static creating = ""; - // todo: improve types inferring - static getInstance(...args: ConstructorParameters>): T { + constructor() { + if (Singleton.creating.length === 0) { + throw new TypeError("A singleton class must be created by getInstanceOrCreate()"); + } + } + + static getInstanceOrCreate(this: StaticThis, ...args: R): T { if (!Singleton.instances.has(this)) { - Singleton.instances.set(this, Reflect.construct(this, args)); + Singleton.creating = this.name; + Singleton.instances.set(this, new this(...args)); + Singleton.creating = ""; } return Singleton.instances.get(this) as T; } + static getInstance(this: StaticThis, strict = true): T | undefined { + if (!Singleton.instances.has(this) && strict) { + throw new TypeError(`instance of ${this.name} is not created`); + } + + return Singleton.instances.get(this) as (T | undefined); + } + static resetInstance() { Singleton.instances.delete(this); } } -export { Singleton }; export default Singleton; diff --git a/src/extensions/__tests__/extension-discovery.test.ts b/src/extensions/__tests__/extension-discovery.test.ts index d0066c3a7e..d185302b65 100644 --- a/src/extensions/__tests__/extension-discovery.test.ts +++ b/src/extensions/__tests__/extension-discovery.test.ts @@ -1,6 +1,7 @@ import { watch } from "chokidar"; import { join, normalize } from "path"; import { ExtensionDiscovery, InstalledExtension } from "../extension-discovery"; +import { ExtensionsStore } from "../extensions-store"; jest.mock("../../common/ipc"); jest.mock("fs-extra"); @@ -17,6 +18,12 @@ jest.mock("../extension-installer", () => ({ const mockedWatch = watch as jest.MockedFunction; describe("ExtensionDiscovery", () => { + beforeEach(() => { + ExtensionDiscovery.resetInstance(); + ExtensionsStore.resetInstance(); + ExtensionsStore.getInstanceOrCreate(); + }); + it("emits add for added extension", async done => { globalThis.__non_webpack_require__.mockImplementation(() => ({ name: "my-extension" @@ -36,7 +43,7 @@ describe("ExtensionDiscovery", () => { mockedWatch.mockImplementationOnce(() => (mockWatchInstance) as any ); - const extensionDiscovery = new ExtensionDiscovery(); + const extensionDiscovery = ExtensionDiscovery.getInstanceOrCreate(); // Need to force isLoaded to be true so that the file watching is started extensionDiscovery.isLoaded = true; @@ -76,7 +83,7 @@ describe("ExtensionDiscovery", () => { mockedWatch.mockImplementationOnce(() => (mockWatchInstance) as any ); - const extensionDiscovery = new ExtensionDiscovery(); + const extensionDiscovery = ExtensionDiscovery.getInstanceOrCreate(); // Need to force isLoaded to be true so that the file watching is started extensionDiscovery.isLoaded = true; diff --git a/src/extensions/__tests__/extension-loader.test.ts b/src/extensions/__tests__/extension-loader.test.ts index 335eb50912..ed7025f218 100644 --- a/src/extensions/__tests__/extension-loader.test.ts +++ b/src/extensions/__tests__/extension-loader.test.ts @@ -1,15 +1,21 @@ import { ExtensionLoader } from "../extension-loader"; import { ipcRenderer } from "electron"; -import { extensionsStore } from "../extensions-store"; +import { ExtensionsStore } from "../extensions-store"; +import { Console } from "console"; +import { stdout, stderr } from "process"; + +console = new Console(stdout, stderr); const manifestPath = "manifest/path"; const manifestPath2 = "manifest/path2"; const manifestPath3 = "manifest/path3"; jest.mock("../extensions-store", () => ({ - extensionsStore: { - whenLoaded: Promise.resolve(true), - mergeState: jest.fn() + ExtensionsStore: { + getInstance: () => ({ + whenLoaded: Promise.resolve(true), + mergeState: jest.fn() + }) } })); @@ -99,8 +105,12 @@ jest.mock( ); describe("ExtensionLoader", () => { - it("renderer updates extension after ipc broadcast", async (done) => { - const extensionLoader = new ExtensionLoader(); + beforeEach(() => { + ExtensionLoader.resetInstance(); + }); + + it.only("renderer updates extension after ipc broadcast", async (done) => { + const extensionLoader = ExtensionLoader.getInstanceOrCreate(); expect(extensionLoader.userExtensions).toMatchInlineSnapshot(`Map {}`); @@ -140,20 +150,20 @@ describe("ExtensionLoader", () => { }); it("updates ExtensionsStore after isEnabled is changed", async () => { - (extensionsStore.mergeState as any).mockClear(); + (ExtensionsStore.getInstance().mergeState as any).mockClear(); // Disable sending events in this test (ipcRenderer.on as any).mockImplementation(); - const extensionLoader = new ExtensionLoader(); + const extensionLoader = ExtensionLoader.getInstanceOrCreate(); await extensionLoader.init(); - expect(extensionsStore.mergeState).not.toHaveBeenCalled(); + expect(ExtensionsStore.getInstance().mergeState).not.toHaveBeenCalled(); Array.from(extensionLoader.userExtensions.values())[0].isEnabled = false; - expect(extensionsStore.mergeState).toHaveBeenCalledWith({ + expect(ExtensionsStore.getInstance().mergeState).toHaveBeenCalledWith({ "manifest/path": { enabled: false, name: "TestExtension" diff --git a/src/extensions/__tests__/lens-extension.test.ts b/src/extensions/__tests__/lens-extension.test.ts index 897c246674..a7e7239a51 100644 --- a/src/extensions/__tests__/lens-extension.test.ts +++ b/src/extensions/__tests__/lens-extension.test.ts @@ -1,4 +1,8 @@ import { LensExtension } from "../lens-extension"; +import { Console } from "console"; +import { stdout, stderr } from "process"; + +console = new Console(stdout, stderr); let ext: LensExtension = null; diff --git a/src/extensions/cluster-feature.ts b/src/extensions/cluster-feature.ts index 24e3de5df1..fa4fe0e48f 100644 --- a/src/extensions/cluster-feature.ts +++ b/src/extensions/cluster-feature.ts @@ -8,7 +8,7 @@ import logger from "../main/logger"; import { app } from "electron"; import { requestMain } from "../common/ipc"; import { clusterKubectlApplyAllHandler } from "../common/cluster-ipc"; -import { clusterStore } from "../common/cluster-store"; +import { ClusterStore } from "../common/cluster-store"; export interface ClusterFeatureStatus { /** feature's current version, as set by the implementation */ @@ -86,7 +86,7 @@ export abstract class ClusterFeature { protected async applyResources(cluster: KubernetesCluster, resourceSpec: string | string[]) { let resources: string[]; - const clusterModel = clusterStore.getById(cluster.metadata.uid); + const clusterModel = ClusterStore.getInstance().getById(cluster.metadata.uid); if (!clusterModel) { throw new Error(`cluster not found`); diff --git a/src/extensions/core-api/app.ts b/src/extensions/core-api/app.ts index 2c3a7a4f59..a8d3429c1d 100644 --- a/src/extensions/core-api/app.ts +++ b/src/extensions/core-api/app.ts @@ -1,9 +1,9 @@ import { getAppVersion } from "../../common/utils"; -import { extensionsStore } from "../extensions-store"; +import { ExtensionsStore } from "../extensions-store"; export const version = getAppVersion(); export { isSnap, isWindows, isMac, isLinux, appName, slackUrl, issuesTrackerUrl } from "../../common/vars"; export function getEnabledExtensions(): string[] { - return extensionsStore.enabledExtensions; + return ExtensionsStore.getInstance().enabledExtensions; } diff --git a/src/extensions/core-api/catalog.ts b/src/extensions/core-api/catalog.ts index f81846cd4d..a24569c609 100644 --- a/src/extensions/core-api/catalog.ts +++ b/src/extensions/core-api/catalog.ts @@ -1,5 +1,4 @@ -import { computed } from "mobx"; import { CatalogEntity } from "../../common/catalog-entity"; import { catalogEntityRegistry as registry } from "../../common/catalog-entity-registry"; @@ -7,7 +6,7 @@ export { catalogCategoryRegistry as catalogCategories } from "../../common/catal export * from "../../common/catalog-entities"; export class CatalogEntityRegistry { - @computed getItemsForApiKind(apiVersion: string, kind: string): T[] { + getItemsForApiKind(apiVersion: string, kind: string): T[] { return registry.getItemsForApiKind(apiVersion, kind); } } diff --git a/src/extensions/extension-discovery.ts b/src/extensions/extension-discovery.ts index 40d9ba44ac..2ed8f377f3 100644 --- a/src/extensions/extension-discovery.ts +++ b/src/extensions/extension-discovery.ts @@ -6,9 +6,10 @@ import { observable, reaction, toJS, when } from "mobx"; import os from "os"; import path from "path"; import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc"; +import { Singleton } from "../common/utils"; import logger from "../main/logger"; import { extensionInstaller, PackageJson } from "./extension-installer"; -import { extensionsStore } from "./extensions-store"; +import { ExtensionsStore } from "./extensions-store"; import type { LensExtensionId, LensExtensionManifest } from "./lens-extension"; export interface InstalledExtension { @@ -50,7 +51,7 @@ const isDirectoryLike = (lstat: fs.Stats) => lstat.isDirectory() || lstat.isSymb * - "add": When extension is added. The event is of type InstalledExtension * - "remove": When extension is removed. The event is of type LensExtensionId */ -export class ExtensionDiscovery { +export class ExtensionDiscovery extends Singleton { protected bundledFolderPath: string; private loadStarted = false; @@ -66,6 +67,7 @@ export class ExtensionDiscovery { public events: EventEmitter; constructor() { + super(); this.events = new EventEmitter(); } @@ -135,7 +137,7 @@ export class ExtensionDiscovery { depth: 1, ignoreInitial: true, // Try to wait until the file has been completely copied. - // The OS might emit an event for added file even it's not completely written to the filesysten. + // The OS might emit an event for added file even it's not completely written to the file-system. awaitWriteFinish: { // Wait 300ms until the file size doesn't change to consider the file written. // For a small file like package.json this should be plenty of time. @@ -235,7 +237,7 @@ export class ExtensionDiscovery { /** * Uninstalls extension. * The application will detect the folder unlink and remove the extension from the UI automatically. - * @param extension Extension to unistall. + * @param extension Extension to uninstall. */ async uninstallExtension({ absolutePath, manifest }: InstalledExtension) { logger.info(`${logModule} Uninstalling ${manifest.name}`); @@ -325,7 +327,7 @@ export class ExtensionDiscovery { manifestJson = __non_webpack_require__(manifestPath); const installedManifestPath = this.getInstalledManifestPath(manifestJson.name); - const isEnabled = isBundled || extensionsStore.isEnabled(installedManifestPath); + const isEnabled = isBundled || ExtensionsStore.getInstance().isEnabled(installedManifestPath); return { id: installedManifestPath, @@ -455,5 +457,3 @@ export class ExtensionDiscovery { broadcastMessage(ExtensionDiscovery.extensionDiscoveryChannel, this.toJSON()); } } - -export const extensionDiscovery = new ExtensionDiscovery(); diff --git a/src/extensions/extension-loader.ts b/src/extensions/extension-loader.ts index 5ae041067f..78dee4fd8d 100644 --- a/src/extensions/extension-loader.ts +++ b/src/extensions/extension-loader.ts @@ -5,9 +5,10 @@ import { action, computed, observable, reaction, toJS, when } from "mobx"; import path from "path"; import { getHostedCluster } from "../common/cluster-store"; import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc"; +import { Singleton } from "../common/utils"; import logger from "../main/logger"; import type { InstalledExtension } from "./extension-discovery"; -import { extensionsStore } from "./extensions-store"; +import { ExtensionsStore } from "./extensions-store"; import type { LensExtension, LensExtensionConstructor, LensExtensionId } from "./lens-extension"; import type { LensMainExtension } from "./lens-main-extension"; import type { LensRendererExtension } from "./lens-renderer-extension"; @@ -24,7 +25,7 @@ const logModule = "[EXTENSIONS-LOADER]"; /** * Loads installed extensions to the Lens application */ -export class ExtensionLoader { +export class ExtensionLoader extends Singleton { protected extensions = observable.map(); protected instances = observable.map(); @@ -95,11 +96,11 @@ export class ExtensionLoader { await this.initMain(); } - await Promise.all([this.whenLoaded, extensionsStore.whenLoaded]); + await Promise.all([this.whenLoaded, ExtensionsStore.getInstance().whenLoaded]); // save state on change `extension.isEnabled` reaction(() => this.storeState, extensionsState => { - extensionsStore.mergeState(extensionsState); + ExtensionsStore.getInstance().mergeState(extensionsState); }); } @@ -329,5 +330,3 @@ export class ExtensionLoader { broadcastMessage(main ? ExtensionLoader.extensionsMainChannel : ExtensionLoader.extensionsRendererChannel, Array.from(this.toJSON())); } } - -export const extensionLoader = new ExtensionLoader(); diff --git a/src/extensions/extensions-store.ts b/src/extensions/extensions-store.ts index 8e88d22f38..60fcff3c7c 100644 --- a/src/extensions/extensions-store.ts +++ b/src/extensions/extensions-store.ts @@ -53,5 +53,3 @@ export class ExtensionsStore extends BaseStore { }); } } - -export const extensionsStore = new ExtensionsStore(); diff --git a/src/extensions/lens-extension.ts b/src/extensions/lens-extension.ts index a00e289e17..466de0360a 100644 --- a/src/extensions/lens-extension.ts +++ b/src/extensions/lens-extension.ts @@ -1,6 +1,6 @@ import type { InstalledExtension } from "./extension-discovery"; import { action, observable, reaction } from "mobx"; -import { filesystemProvisionerStore } from "../main/extension-filesystem"; +import { FilesystemProvisionerStore } from "../main/extension-filesystem"; import logger from "../main/logger"; import { ProtocolHandlerRegistration } from "./registries/protocol-handler-registry"; @@ -49,7 +49,7 @@ export class LensExtension { * folder name. */ async getExtensionFileFolder(): Promise { - return filesystemProvisionerStore.requestDirectory(this.id); + return FilesystemProvisionerStore.getInstance().requestDirectory(this.id); } get description() { diff --git a/src/extensions/lens-main-extension.ts b/src/extensions/lens-main-extension.ts index 0ebb3d90f4..473f031665 100644 --- a/src/extensions/lens-main-extension.ts +++ b/src/extensions/lens-main-extension.ts @@ -10,7 +10,7 @@ export class LensMainExtension extends LensExtension { appMenus: MenuRegistration[] = []; async navigate

(pageId?: string, params?: P, frameId?: number) { - const windowManager = WindowManager.getInstance(); + const windowManager = WindowManager.getInstance(); const pageUrl = getExtensionPageUrl({ extensionId: this.name, pageId, diff --git a/src/extensions/registries/__tests__/page-registry.test.ts b/src/extensions/registries/__tests__/page-registry.test.ts index 55ba3d6d64..1d8598e451 100644 --- a/src/extensions/registries/__tests__/page-registry.test.ts +++ b/src/extensions/registries/__tests__/page-registry.test.ts @@ -1,6 +1,10 @@ import { getExtensionPageUrl, globalPageRegistry, PageParams } from "../page-registry"; import { LensExtension } from "../../lens-extension"; import React from "react"; +import { Console } from "console"; +import { stdout, stderr } from "process"; + +console = new Console(stdout, stderr); let ext: LensExtension = null; diff --git a/src/extensions/renderer-api/theming.ts b/src/extensions/renderer-api/theming.ts index b3da69bdbc..b2fdbfff56 100644 --- a/src/extensions/renderer-api/theming.ts +++ b/src/extensions/renderer-api/theming.ts @@ -1,5 +1,5 @@ -import { themeStore } from "../../renderer/theme.store"; +import { ThemeStore } from "../../renderer/theme.store"; export function getActiveTheme() { - return themeStore.activeTheme; + return ThemeStore.getInstance().activeTheme; } diff --git a/src/main/__test__/cluster.test.ts b/src/main/__test__/cluster.test.ts index 113bb49a0c..40050e10d1 100644 --- a/src/main/__test__/cluster.test.ts +++ b/src/main/__test__/cluster.test.ts @@ -23,7 +23,6 @@ jest.mock("winston", () => ({ } })); - jest.mock("../../common/ipc"); jest.mock("../context-handler"); jest.mock("request"); diff --git a/src/main/__test__/kube-auth-proxy.test.ts b/src/main/__test__/kube-auth-proxy.test.ts index b161372555..dbed900fd7 100644 --- a/src/main/__test__/kube-auth-proxy.test.ts +++ b/src/main/__test__/kube-auth-proxy.test.ts @@ -36,6 +36,11 @@ import { bundledKubectlPath, Kubectl } from "../kubectl"; import { mock, MockProxy } from "jest-mock-extended"; import { waitUntilUsed } from "tcp-port-used"; import { Readable } from "stream"; +import { UserStore } from "../../common/user-store"; +import { Console } from "console"; +import { stdout, stderr } from "process"; + +console = new Console(stdout, stderr); const mockBroadcastIpc = broadcastMessage as jest.MockedFunction; const mockSpawn = spawn as jest.MockedFunction; @@ -44,6 +49,8 @@ const mockWaitUntilUsed = waitUntilUsed as jest.MockedFunction { beforeEach(() => { jest.clearAllMocks(); + UserStore.resetInstance(); + UserStore.getInstanceOrCreate(); }); it("calling exit multiple times shouldn't throw", async () => { diff --git a/src/main/__test__/kubeconfig-manager.test.ts b/src/main/__test__/kubeconfig-manager.test.ts index 1cc39a4ada..5ae7681d37 100644 --- a/src/main/__test__/kubeconfig-manager.test.ts +++ b/src/main/__test__/kubeconfig-manager.test.ts @@ -36,10 +36,6 @@ import * as path from "path"; console = new Console(process.stdout, process.stderr); // fix mockFS describe("kubeconfig manager tests", () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - beforeEach(() => { const mockOpts = { "minikube-config.yml": JSON.stringify({ @@ -76,7 +72,7 @@ describe("kubeconfig manager tests", () => { const cluster = new Cluster({ id: "foo", contextName: "minikube", - kubeConfigPath: "minikube-config.yml" + kubeConfigPath: "minikube-config.yml", }); const contextHandler = new ContextHandler(cluster); const port = await getFreePort(); @@ -98,7 +94,7 @@ describe("kubeconfig manager tests", () => { const cluster = new Cluster({ id: "foo", contextName: "minikube", - kubeConfigPath: "minikube-config.yml" + kubeConfigPath: "minikube-config.yml", }); const contextHandler = new ContextHandler(cluster); const port = await getFreePort(); diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts index 047fb0ca95..0a413584f8 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster-manager.ts @@ -2,7 +2,7 @@ import "../common/cluster-ipc"; import type http from "http"; import { ipcMain } from "electron"; import { action, autorun, observable, reaction, toJS } from "mobx"; -import { clusterStore, getClusterIdFromHost } from "../common/cluster-store"; +import { ClusterStore, getClusterIdFromHost } from "../common/cluster-store"; import { Cluster } from "./cluster"; import logger from "./logger"; import { apiKubePrefix } from "../common/vars"; @@ -21,7 +21,7 @@ export class ClusterManager extends Singleton { catalogEntityRegistry.addSource("lens:kubernetes-clusters", this.catalogSource); // auto-init clusters - reaction(() => clusterStore.enabledClustersList, (clusters) => { + reaction(() => ClusterStore.getInstance().enabledClustersList, (clusters) => { clusters.forEach((cluster) => { if (!cluster.initialized && !cluster.initializing) { logger.info(`[CLUSTER-MANAGER]: init cluster`, cluster.getMeta()); @@ -31,8 +31,8 @@ export class ClusterManager extends Singleton { }, { fireImmediately: true }); - reaction(() => toJS(clusterStore.enabledClustersList, { recurseEverything: true }), () => { - this.updateCatalogSource(clusterStore.enabledClustersList); + reaction(() => toJS(ClusterStore.getInstance().enabledClustersList, { recurseEverything: true }), () => { + this.updateCatalogSource(ClusterStore.getInstance().enabledClustersList); }, { fireImmediately: true }); reaction(() => catalogEntityRegistry.getItemsForApiKind("entity.k8slens.dev/v1alpha1", "KubernetesCluster"), (entities) => { @@ -42,14 +42,14 @@ export class ClusterManager extends Singleton { // auto-stop removed clusters autorun(() => { - const removedClusters = Array.from(clusterStore.removedClusters.values()); + const removedClusters = Array.from(ClusterStore.getInstance().removedClusters.values()); if (removedClusters.length > 0) { const meta = removedClusters.map(cluster => cluster.getMeta()); logger.info(`[CLUSTER-MANAGER]: removing clusters`, meta); removedClusters.forEach(cluster => cluster.disconnect()); - clusterStore.removedClusters.clear(); + ClusterStore.getInstance().removedClusters.clear(); } }, { delay: 250 @@ -90,10 +90,10 @@ export class ClusterManager extends Singleton { @action syncClustersFromCatalog(entities: KubernetesCluster[]) { entities.filter((entity) => entity.metadata.source !== "local").forEach((entity: KubernetesCluster) => { - const cluster = clusterStore.getById(entity.metadata.uid); + const cluster = ClusterStore.getInstance().getById(entity.metadata.uid); if (!cluster) { - clusterStore.addCluster({ + ClusterStore.getInstance().addCluster({ id: entity.metadata.uid, enabled: true, ownerRef: clusterOwnerRef, @@ -145,7 +145,7 @@ export class ClusterManager extends Singleton { protected onNetworkOffline() { logger.info("[CLUSTER-MANAGER]: network is offline"); - clusterStore.enabledClustersList.forEach((cluster) => { + ClusterStore.getInstance().enabledClustersList.forEach((cluster) => { if (!cluster.disconnected) { cluster.online = false; cluster.accessible = false; @@ -156,7 +156,7 @@ export class ClusterManager extends Singleton { protected onNetworkOnline() { logger.info("[CLUSTER-MANAGER]: network is online"); - clusterStore.enabledClustersList.forEach((cluster) => { + ClusterStore.getInstance().enabledClustersList.forEach((cluster) => { if (!cluster.disconnected) { cluster.refreshConnectionStatus().catch((e) => e); } @@ -164,7 +164,7 @@ export class ClusterManager extends Singleton { } stop() { - clusterStore.clusters.forEach((cluster: Cluster) => { + ClusterStore.getInstance().clusters.forEach((cluster: Cluster) => { cluster.disconnect(); }); } @@ -176,18 +176,18 @@ export class ClusterManager extends Singleton { if (req.headers.host.startsWith("127.0.0.1")) { const clusterId = req.url.split("/")[1]; - cluster = clusterStore.getById(clusterId); + cluster = ClusterStore.getInstance().getById(clusterId); if (cluster) { // we need to swap path prefix so that request is proxied to kube api req.url = req.url.replace(`/${clusterId}`, apiKubePrefix); } } else if (req.headers["x-cluster-id"]) { - cluster = clusterStore.getById(req.headers["x-cluster-id"].toString()); + cluster = ClusterStore.getInstance().getById(req.headers["x-cluster-id"].toString()); } else { const clusterId = getClusterIdFromHost(req.headers.host); - cluster = clusterStore.getById(clusterId); + cluster = ClusterStore.getInstance().getById(clusterId); } return cluster; diff --git a/src/main/developer-tools.ts b/src/main/developer-tools.ts index d0d7e2ae01..866e6fb561 100644 --- a/src/main/developer-tools.ts +++ b/src/main/developer-tools.ts @@ -4,11 +4,12 @@ import logger from "./logger"; * Installs Electron developer tools in the development build. * The dependency is not bundled to the production build. */ -export const installDeveloperTools = async () => { +export const installDeveloperTools = () => { if (process.env.NODE_ENV === "development") { logger.info("🤓 Installing developer tools"); - const { default: devToolsInstaller, REACT_DEVELOPER_TOOLS } = await import("electron-devtools-installer"); - - return devToolsInstaller([REACT_DEVELOPER_TOOLS]); + import("electron-devtools-installer") + .then(({ default: devToolsInstaller, REACT_DEVELOPER_TOOLS }) => devToolsInstaller([REACT_DEVELOPER_TOOLS])) + .then((name) => logger.info(`[DEVTOOLS-INSTALLER]: installed ${name}`)) + .catch(error => logger.error(`[DEVTOOLS-INSTALLER]: failed`, { error })); } }; diff --git a/src/main/exit-app.ts b/src/main/exit-app.ts index eb035eff96..6604562842 100644 --- a/src/main/exit-app.ts +++ b/src/main/exit-app.ts @@ -4,10 +4,14 @@ import { appEventBus } from "../common/event-bus"; import { ClusterManager } from "./cluster-manager"; import logger from "./logger"; - export function exitApp() { - const windowManager = WindowManager.getInstance(); - const clusterManager = ClusterManager.getInstance(); + console.log("before windowManager"); + const windowManager = WindowManager.getInstance(false); + + console.log("before clusterManager"); + const clusterManager = ClusterManager.getInstance(false); + + console.log("after clusterManager"); appEventBus.emit({ name: "service", action: "close" }); windowManager?.hide(); diff --git a/src/main/extension-filesystem.ts b/src/main/extension-filesystem.ts index eddb7b747f..77bb51697a 100644 --- a/src/main/extension-filesystem.ts +++ b/src/main/extension-filesystem.ts @@ -14,7 +14,7 @@ interface FSProvisionModel { export class FilesystemProvisionerStore extends BaseStore { @observable registeredExtensions = observable.map(); - private constructor() { + constructor() { super({ configName: "lens-filesystem-provisioner-store", accessPropertiesByDotNotation: false, // To make dots safe in cluster context names @@ -56,5 +56,3 @@ export class FilesystemProvisionerStore extends BaseStore { }); } } - -export const filesystemProvisionerStore = FilesystemProvisionerStore.getInstance(); diff --git a/src/main/helm/__tests__/helm-service.test.ts b/src/main/helm/__tests__/helm-service.test.ts index 8c1e82ef0a..24b22c43d5 100644 --- a/src/main/helm/__tests__/helm-service.test.ts +++ b/src/main/helm/__tests__/helm-service.test.ts @@ -1,17 +1,24 @@ import { helmService } from "../helm-service"; -import { repoManager } from "../helm-repo-manager"; +import { HelmRepoManager } from "../helm-repo-manager"; -jest.spyOn(repoManager, "init").mockImplementation(); +const mockHelmRepoManager = jest.spyOn(HelmRepoManager, "getInstance").mockImplementation(); jest.mock("../helm-chart-manager"); describe("Helm Service tests", () => { - test("list charts without deprecated ones", async () => { - jest.spyOn(repoManager, "repositories").mockImplementation(async () => { - return [ - { name: "stable", url: "stableurl" }, - { name: "experiment", url: "experimenturl" } - ]; + afterEach(() => { + jest.resetAllMocks(); + }); + + it("list charts without deprecated ones", async () => { + mockHelmRepoManager.mockReturnValue({ + init: jest.fn(), + repositories: jest.fn().mockImplementation(async () => { + return [ + { name: "stable", url: "stableurl" }, + { name: "experiment", url: "experimenturl" }, + ]; + }), }); const charts = await helmService.listCharts(); @@ -55,11 +62,14 @@ describe("Helm Service tests", () => { }); }); - test("list charts sorted by version in descending order", async () => { - jest.spyOn(repoManager, "repositories").mockImplementation(async () => { - return [ - { name: "bitnami", url: "bitnamiurl" } - ]; + it("list charts sorted by version in descending order", async () => { + mockHelmRepoManager.mockReturnValue({ + init: jest.fn(), + repositories: jest.fn().mockImplementation(async () => { + return [ + { name: "bitnami", url: "bitnamiurl" }, + ]; + }), }); const charts = await helmService.listCharts(); diff --git a/src/main/helm/helm-release-manager.ts b/src/main/helm/helm-release-manager.ts index 822b939cf7..dcd66b3875 100644 --- a/src/main/helm/helm-release-manager.ts +++ b/src/main/helm/helm-release-manager.ts @@ -1,17 +1,17 @@ import * as tempy from "tempy"; -import fs from "fs"; +import fse from "fs-extra"; import * as yaml from "js-yaml"; import { promiseExec} from "../promise-exec"; import { helmCli } from "./helm-cli"; import { Cluster } from "../cluster"; import { toCamelCase } from "../../common/utils/camelCase"; -export class HelmReleaseManager { +export async function listReleases(pathToKubeconfig: string, namespace?: string) { + const helm = await helmCli.binaryPath(); + const namespaceFlag = namespace ? `-n ${namespace}` : "--all-namespaces"; - public async listReleases(pathToKubeconfig: string, namespace?: string) { - const helm = await helmCli.binaryPath(); - const namespaceFlag = namespace ? `-n ${namespace}` : "--all-namespaces"; - const { stdout } = await promiseExec(`"${helm}" ls --output json ${namespaceFlag} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);}); + try { + const { stdout } = await promiseExec(`"${helm}" ls --output json ${namespaceFlag} --kubeconfig ${pathToKubeconfig}`); const output = JSON.parse(stdout); if (output.length == 0) { @@ -22,106 +22,132 @@ export class HelmReleaseManager { }); return output; - } - - - public async installChart(chart: string, values: any, name: string, namespace: string, version: string, pathToKubeconfig: string){ - const helm = await helmCli.binaryPath(); - const fileName = tempy.file({name: "values.yaml"}); - - await fs.promises.writeFile(fileName, yaml.safeDump(values)); - - try { - let generateName = ""; - - if (!name) { - generateName = "--generate-name"; - name = ""; - } - const { stdout } = await promiseExec(`"${helm}" install ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} ${generateName}`).catch((error) => { throw(error.stderr);}); - const releaseName = stdout.split("\n")[0].split(" ")[1].trim(); - - return { - log: stdout, - release: { - name: releaseName, - namespace - } - }; - } finally { - await fs.promises.unlink(fileName); - } - } - - public async upgradeRelease(name: string, chart: string, values: any, namespace: string, version: string, cluster: Cluster){ - const helm = await helmCli.binaryPath(); - const fileName = tempy.file({name: "values.yaml"}); - const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); - - await fs.promises.writeFile(fileName, yaml.safeDump(values)); - - try { - const { stdout } = await promiseExec(`"${helm}" upgrade ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${proxyKubeconfig}`).catch((error) => { throw(error.stderr);}); - - return { - log: stdout, - release: this.getRelease(name, namespace, cluster) - }; - } finally { - await fs.promises.unlink(fileName); - } - } - - public async getRelease(name: string, namespace: string, cluster: Cluster) { - const helm = await helmCli.binaryPath(); - const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); - - const { stdout } = await promiseExec(`"${helm}" status ${name} --output json --namespace ${namespace} --kubeconfig ${proxyKubeconfig}`).catch((error) => { throw(error.stderr);}); - const release = JSON.parse(stdout); - - release.resources = await this.getResources(name, namespace, cluster); - - return release; - } - - public async deleteRelease(name: string, namespace: string, pathToKubeconfig: string) { - const helm = await helmCli.binaryPath(); - const { stdout } = await promiseExec(`"${helm}" delete ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);}); - - return stdout; - } - - public async getValues(name: string, namespace: string, all: boolean, pathToKubeconfig: string) { - const helm = await helmCli.binaryPath(); - const { stdout, } = await promiseExec(`"${helm}" get values ${name} ${all? "--all": ""} --output yaml --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);}); - - return stdout; - } - - public async getHistory(name: string, namespace: string, pathToKubeconfig: string) { - const helm = await helmCli.binaryPath(); - const { stdout } = await promiseExec(`"${helm}" history ${name} --output json --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);}); - - return JSON.parse(stdout); - } - - public async rollback(name: string, namespace: string, revision: number, pathToKubeconfig: string) { - const helm = await helmCli.binaryPath(); - const { stdout } = await promiseExec(`"${helm}" rollback ${name} ${revision} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);}); - - return stdout; - } - - protected async getResources(name: string, namespace: string, cluster: Cluster) { - const helm = await helmCli.binaryPath(); - const kubectl = await cluster.kubeCtl.getPath(); - const pathToKubeconfig = await cluster.getProxyKubeconfigPath(); - const { stdout } = await promiseExec(`"${helm}" get manifest ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} | "${kubectl}" get -n ${namespace} --kubeconfig ${pathToKubeconfig} -f - -o=json`).catch(() => { - return { stdout: JSON.stringify({items: []})}; - }); - - return stdout; + } catch ({ stderr }) { + throw stderr; } } -export const releaseManager = new HelmReleaseManager(); + +export async function installChart(chart: string, values: any, name: string | undefined, namespace: string, version: string, pathToKubeconfig: string){ + const helm = await helmCli.binaryPath(); + const fileName = tempy.file({name: "values.yaml"}); + + await fse.writeFile(fileName, yaml.safeDump(values)); + + try { + let generateName = ""; + + if (!name) { + generateName = "--generate-name"; + name = ""; + } + const { stdout } = await promiseExec(`"${helm}" install ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} ${generateName}`); + const releaseName = stdout.split("\n")[0].split(" ")[1].trim(); + + return { + log: stdout, + release: { + name: releaseName, + namespace + } + }; + } catch ({ stderr }) { + throw stderr; + } finally { + await fse.unlink(fileName); + } +} + +export async function upgradeRelease(name: string, chart: string, values: any, namespace: string, version: string, cluster: Cluster) { + const helm = await helmCli.binaryPath(); + const fileName = tempy.file({name: "values.yaml"}); + + await fse.writeFile(fileName, yaml.safeDump(values)); + + try { + const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); + const { stdout } = await promiseExec(`"${helm}" upgrade ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${proxyKubeconfig}`); + + return { + log: stdout, + release: getRelease(name, namespace, cluster) + }; + } catch ({ stderr }) { + throw stderr; + } finally { + await fse.unlink(fileName); + } +} + +export async function getRelease(name: string, namespace: string, cluster: Cluster) { + try { + const helm = await helmCli.binaryPath(); + const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); + + const { stdout } = await promiseExec(`"${helm}" status ${name} --output json --namespace ${namespace} --kubeconfig ${proxyKubeconfig}`); + const release = JSON.parse(stdout); + + release.resources = await getResources(name, namespace, cluster); + + return release; + } catch ({ stderr }) { + throw stderr; + } +} + +export async function deleteRelease(name: string, namespace: string, pathToKubeconfig: string) { + try { + const helm = await helmCli.binaryPath(); + const { stdout } = await promiseExec(`"${helm}" delete ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`); + + return stdout; + } catch ({ stderr }) { + throw stderr; + } +} + +export async function getValues(name: string, namespace: string, all: boolean, pathToKubeconfig: string) { + try { + const helm = await helmCli.binaryPath(); + const { stdout, } = await promiseExec(`"${helm}" get values ${name} ${all ? "--all": ""} --output yaml --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`); + + return stdout; + } catch ({ stderr }) { + throw stderr; + } +} + +export async function getHistory(name: string, namespace: string, pathToKubeconfig: string) { + try { + const helm = await helmCli.binaryPath(); + const { stdout } = await promiseExec(`"${helm}" history ${name} --output json --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`); + + return JSON.parse(stdout); + } catch ({ stderr }) { + throw stderr; + } +} + +export async function rollback(name: string, namespace: string, revision: number, pathToKubeconfig: string) { + try { + const helm = await helmCli.binaryPath(); + const { stdout } = await promiseExec(`"${helm}" rollback ${name} ${revision} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`); + + return stdout; + } catch ({ stderr }) { + throw stderr; + } +} + +async function getResources(name: string, namespace: string, cluster: Cluster) { + try { + const helm = await helmCli.binaryPath(); + const kubectl = await cluster.kubeCtl.getPath(); + const pathToKubeconfig = await cluster.getProxyKubeconfigPath(); + const { stdout } = await promiseExec(`"${helm}" get manifest ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} | "${kubectl}" get -n ${namespace} --kubeconfig ${pathToKubeconfig} -f - -o=json`); + + return stdout; + } catch { + return { stdout: JSON.stringify({ items: [] }) }; + } +} diff --git a/src/main/helm/helm-repo-manager.ts b/src/main/helm/helm-repo-manager.ts index 74afb166b1..5761ac5dec 100644 --- a/src/main/helm/helm-repo-manager.ts +++ b/src/main/helm/helm-repo-manager.ts @@ -77,10 +77,6 @@ export class HelmRepoManager extends Singleton { } public async repositories(): Promise { - if (!this.initialized) { - await this.init(); - } - try { const repoConfigFile = this.helmEnv.HELM_REPOSITORY_CONFIG; const { repositories }: HelmRepoConfig = await readFile(repoConfigFile, "utf8") @@ -160,5 +156,3 @@ export class HelmRepoManager extends Singleton { return stdout; } } - -export const repoManager = HelmRepoManager.getInstance(); diff --git a/src/main/helm/helm-service.ts b/src/main/helm/helm-service.ts index 69e8fa2ccb..9a42616c89 100644 --- a/src/main/helm/helm-service.ts +++ b/src/main/helm/helm-service.ts @@ -1,23 +1,21 @@ import semver from "semver"; import { Cluster } from "../cluster"; import logger from "../logger"; -import { repoManager } from "./helm-repo-manager"; +import { HelmRepoManager } from "./helm-repo-manager"; import { HelmChartManager } from "./helm-chart-manager"; -import { releaseManager } from "./helm-release-manager"; import { HelmChartList, RepoHelmChartList } from "../../renderer/api/endpoints/helm-charts.api"; +import { deleteRelease, getHistory, getRelease, getValues, installChart, listReleases, rollback, upgradeRelease } from "./helm-release-manager"; class HelmService { public async installChart(cluster: Cluster, data: { chart: string; values: {}; name: string; namespace: string; version: string }) { const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); - return await releaseManager.installChart(data.chart, data.values, data.name, data.namespace, data.version, proxyKubeconfig); + return installChart(data.chart, data.values, data.name, data.namespace, data.version, proxyKubeconfig); } public async listCharts() { const charts: HelmChartList = {}; - - await repoManager.init(); - const repositories = await repoManager.repositories(); + const repositories = await HelmRepoManager.getInstance().repositories(); for (const repo of repositories) { charts[repo.name] = {}; @@ -36,7 +34,7 @@ class HelmService { readme: "", versions: {} }; - const repo = await repoManager.repository(repoName); + const repo = await HelmRepoManager.getInstance().repository(repoName); const chartManager = new HelmChartManager(repo); const chart = await chartManager.chart(chartName); @@ -47,23 +45,22 @@ class HelmService { } public async getChartValues(repoName: string, chartName: string, version = "") { - const repo = await repoManager.repository(repoName); + const repo = await HelmRepoManager.getInstance().repository(repoName); const chartManager = new HelmChartManager(repo); return chartManager.getValues(chartName, version); } public async listReleases(cluster: Cluster, namespace: string = null) { - await repoManager.init(); const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); - return await releaseManager.listReleases(proxyKubeconfig, namespace); + return listReleases(proxyKubeconfig, namespace); } public async getRelease(cluster: Cluster, releaseName: string, namespace: string) { logger.debug("Fetch release"); - return await releaseManager.getRelease(releaseName, namespace, cluster); + return getRelease(releaseName, namespace, cluster); } public async getReleaseValues(cluster: Cluster, releaseName: string, namespace: string, all: boolean) { @@ -71,7 +68,7 @@ class HelmService { logger.debug("Fetch release values"); - return await releaseManager.getValues(releaseName, namespace, all, proxyKubeconfig); + return getValues(releaseName, namespace, all, proxyKubeconfig); } public async getReleaseHistory(cluster: Cluster, releaseName: string, namespace: string) { @@ -79,7 +76,7 @@ class HelmService { logger.debug("Fetch release history"); - return await releaseManager.getHistory(releaseName, namespace, proxyKubeconfig); + return getHistory(releaseName, namespace, proxyKubeconfig); } public async deleteRelease(cluster: Cluster, releaseName: string, namespace: string) { @@ -87,20 +84,20 @@ class HelmService { logger.debug("Delete release"); - return await releaseManager.deleteRelease(releaseName, namespace, proxyKubeconfig); + return deleteRelease(releaseName, namespace, proxyKubeconfig); } public async updateRelease(cluster: Cluster, releaseName: string, namespace: string, data: { chart: string; values: {}; version: string }) { logger.debug("Upgrade release"); - return await releaseManager.upgradeRelease(releaseName, data.chart, data.values, namespace, data.version, cluster); + return upgradeRelease(releaseName, data.chart, data.values, namespace, data.version, cluster); } public async rollback(cluster: Cluster, releaseName: string, namespace: string, revision: number) { const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); logger.debug("Rollback release"); - const output = await releaseManager.rollback(releaseName, namespace, revision, proxyKubeconfig); + const output = rollback(releaseName, namespace, revision, proxyKubeconfig); return { message: output }; } diff --git a/src/main/index.ts b/src/main/index.ts index 2ba462ce0c..000678b617 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -15,15 +15,15 @@ import { getFreePort } from "./port"; import { mangleProxyEnv } from "./proxy-env"; import { registerFileProtocol } from "../common/register-protocol"; import logger from "./logger"; -import { clusterStore } from "../common/cluster-store"; -import { userStore } from "../common/user-store"; +import { ClusterStore } from "../common/cluster-store"; +import { UserStore } from "../common/user-store"; import { appEventBus } from "../common/event-bus"; -import { extensionLoader } from "../extensions/extension-loader"; -import { extensionsStore } from "../extensions/extensions-store"; -import { InstalledExtension, extensionDiscovery } from "../extensions/extension-discovery"; +import { ExtensionLoader } from "../extensions/extension-loader"; +import { ExtensionsStore } from "../extensions/extensions-store"; +import { InstalledExtension, ExtensionDiscovery } from "../extensions/extension-discovery"; import type { LensExtensionId } from "../extensions/lens-extension"; +import { FilesystemProvisionerStore } from "./extension-filesystem"; import { installDeveloperTools } from "./developer-tools"; -import { filesystemProvisionerStore } from "./extension-filesystem"; import { LensProtocolRouterMain } from "./protocol-handler"; import { getAppVersion, getAppVersionFromProxyServer } from "../common/utils"; import { bindBroadcastHandlers } from "../common/ipc"; @@ -31,13 +31,9 @@ import { startUpdateChecking } from "./app-updater"; import { IpcRendererNavigationEvents } from "../renderer/navigation/events"; import { CatalogPusher } from "./catalog-pusher"; import { catalogEntityRegistry } from "../common/catalog-entity-registry"; -import { hotbarStore } from "../common/hotbar-store"; +import { HotbarStore } from "../common/hotbar-store"; const workingDir = path.join(app.getPath("appData"), appName); -let proxyPort: number; -let proxyServer: LensProxy; -let clusterManager: ClusterManager; -let windowManager: WindowManager; app.setName(appName); @@ -66,7 +62,7 @@ if (app.commandLine.getSwitchValue("proxy-server") !== "") { if (!app.requestSingleInstanceLock()) { app.exit(); } else { - const lprm = LensProtocolRouterMain.getInstance(); + const lprm = LensProtocolRouterMain.getInstanceOrCreate(); for (const arg of process.argv) { if (arg.toLowerCase().startsWith("lens://")) { @@ -77,7 +73,7 @@ if (!app.requestSingleInstanceLock()) { } app.on("second-instance", (event, argv) => { - const lprm = LensProtocolRouterMain.getInstance(); + const lprm = LensProtocolRouterMain.getInstanceOrCreate(); for (const arg of argv) { if (arg.toLowerCase().startsWith("lens://")) { @@ -86,7 +82,7 @@ app.on("second-instance", (event, argv) => { } } - windowManager?.ensureMainWindow(); + WindowManager.getInstance(false)?.ensureMainWindow(); }); app.on("ready", async () => { @@ -102,7 +98,11 @@ app.on("ready", async () => { registerFileProtocol("static", __static); - await installDeveloperTools(); + const userStore = UserStore.getInstanceOrCreate(); + const clusterStore = ClusterStore.getInstanceOrCreate(); + const hotbarStore = HotbarStore.getInstanceOrCreate(); + const extensionsStore = ExtensionsStore.getInstanceOrCreate(); + const filesystemStore = FilesystemProvisionerStore.getInstanceOrCreate(); logger.info("💾 Loading stores"); // preload @@ -111,10 +111,12 @@ app.on("ready", async () => { clusterStore.load(), hotbarStore.load(), extensionsStore.load(), - filesystemProvisionerStore.load(), + filesystemStore.load(), ]); // find free port + let proxyPort; + try { logger.info("🔑 Getting free port for LensProxy server"); proxyPort = await getFreePort(); @@ -125,13 +127,13 @@ app.on("ready", async () => { } // create cluster manager - clusterManager = ClusterManager.getInstance(proxyPort); + ClusterManager.getInstanceOrCreate(proxyPort); // run proxy try { logger.info("🔌 Starting LensProxy"); // eslint-disable-next-line unused-imports/no-unused-vars-ts - proxyServer = LensProxy.create(proxyPort, clusterManager); + LensProxy.getInstanceOrCreate(proxyPort).listen(); } catch (error) { logger.error(`Could not start proxy (127.0.0:${proxyPort}): ${error?.message}`); dialog.showErrorBox("Lens Error", `Could not start proxy (127.0.0:${proxyPort}): ${error?.message || "unknown error"}`); @@ -151,7 +153,9 @@ app.on("ready", async () => { logger.error("Checking proxy server connection failed", error); } - extensionLoader.init(); + const extensionDiscovery = ExtensionDiscovery.getInstanceOrCreate(); + + ExtensionLoader.getInstanceOrCreate().init(); extensionDiscovery.init(); // Start the app without showing the main window when auto starting on login @@ -159,7 +163,9 @@ app.on("ready", async () => { const startHidden = process.argv.includes("--hidden") || (isMac && app.getLoginItemSettings().wasOpenedAsHidden); logger.info("🖥️ Starting WindowManager"); - windowManager = WindowManager.getInstance(proxyPort); + const windowManager = WindowManager.getInstanceOrCreate(proxyPort); + + installDeveloperTools(); if (!startHidden) { windowManager.initMainWindow(); @@ -169,13 +175,13 @@ app.on("ready", async () => { CatalogPusher.init(catalogEntityRegistry); startUpdateChecking(); LensProtocolRouterMain - .getInstance() + .getInstance() .rendererLoaded = true; }); - extensionLoader.whenLoaded.then(() => { + ExtensionLoader.getInstance().whenLoaded.then(() => { LensProtocolRouterMain - .getInstance() + .getInstance() .extensionsLoaded = true; }); @@ -189,14 +195,15 @@ app.on("ready", async () => { extensionDiscovery.watchExtensions(); // Subscribe to extensions that are copied or deleted to/from the extensions folder - extensionDiscovery.events.on("add", (extension: InstalledExtension) => { - extensionLoader.addExtension(extension); - }); - extensionDiscovery.events.on("remove", (lensExtensionId: LensExtensionId) => { - extensionLoader.removeExtension(lensExtensionId); - }); + extensionDiscovery.events + .on("add", (extension: InstalledExtension) => { + ExtensionLoader.getInstance().addExtension(extension); + }) + .on("remove", (lensExtensionId: LensExtensionId) => { + ExtensionLoader.getInstance().removeExtension(lensExtensionId); + }); - extensionLoader.initExtensions(extensions); + ExtensionLoader.getInstance().initExtensions(extensions); } catch (error) { dialog.showErrorBox("Lens Error", `Could not load extensions${error?.message ? `: ${error.message}` : ""}`); console.error(error); @@ -212,7 +219,7 @@ app.on("activate", (event, hasVisibleWindows) => { logger.info("APP:ACTIVATE", { hasVisibleWindows }); if (!hasVisibleWindows) { - windowManager?.initMainWindow(false); + WindowManager.getInstance(false)?.initMainWindow(false); } }); @@ -227,8 +234,7 @@ app.on("will-quit", (event) => { // Quit app on Cmd+Q (MacOS) logger.info("APP:QUIT"); appEventBus.emit({name: "app", action: "close"}); - - clusterManager?.stop(); // close cluster connections + ClusterManager.getInstance(false)?.stop(); // close cluster connections if (blockQuit) { event.preventDefault(); // prevent app's default shutdown (e.g. required for telemetry, etc.) @@ -242,7 +248,7 @@ app.on("open-url", (event, rawUrl) => { event.preventDefault(); LensProtocolRouterMain - .getInstance() + .getInstance() .route(rawUrl) .catch(error => logger.error(`${LensProtocolRouterMain.LoggingPrefix}: an error occured`, { error, rawUrl })); }); diff --git a/src/main/kubectl.ts b/src/main/kubectl.ts index 7e0d6ed5c7..903f54ff07 100644 --- a/src/main/kubectl.ts +++ b/src/main/kubectl.ts @@ -6,7 +6,7 @@ import logger from "./logger"; import { ensureDir, pathExists } from "fs-extra"; import * as lockFile from "proper-lockfile"; import { helmCli } from "./helm/helm-cli"; -import { userStore } from "../common/user-store"; +import { UserStore } from "../common/user-store"; import { customRequest } from "../common/request"; import { getBundledKubectlVersion } from "../common/utils/app-version"; import { isDevelopment, isWindows, isTestEnv } from "../common/vars"; @@ -113,12 +113,12 @@ export class Kubectl { } public getPathFromPreferences() { - return userStore.preferences?.kubectlBinariesPath || this.getBundledPath(); + return UserStore.getInstance().preferences?.kubectlBinariesPath || this.getBundledPath(); } protected getDownloadDir() { - if (userStore.preferences?.downloadBinariesPath) { - return path.join(userStore.preferences.downloadBinariesPath, "kubectl"); + if (UserStore.getInstance().preferences?.downloadBinariesPath) { + return path.join(UserStore.getInstance().preferences.downloadBinariesPath, "kubectl"); } return Kubectl.kubectlDir; @@ -129,7 +129,7 @@ export class Kubectl { return this.getBundledPath(); } - if (userStore.preferences?.downloadKubectlBinaries === false) { + if (UserStore.getInstance().preferences?.downloadKubectlBinaries === false) { return this.getPathFromPreferences(); } @@ -223,7 +223,7 @@ export class Kubectl { } public async ensureKubectl(): Promise { - if (userStore.preferences?.downloadKubectlBinaries === false) { + if (UserStore.getInstance().preferences?.downloadKubectlBinaries === false) { return true; } @@ -273,7 +273,7 @@ export class Kubectl { logger.info(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const stream = customRequest({ url: this.url, gzip: true, @@ -303,7 +303,7 @@ export class Kubectl { } protected async writeInitScripts() { - const kubectlPath = userStore.preferences?.downloadKubectlBinaries ? this.dirname : path.dirname(this.getPathFromPreferences()); + const kubectlPath = UserStore.getInstance().preferences?.downloadKubectlBinaries ? this.dirname : path.dirname(this.getPathFromPreferences()); const helmPath = helmCli.getBinaryDir(); const fsPromises = fs.promises; const bashScriptPath = path.join(this.dirname, ".bash_set_path"); @@ -361,7 +361,7 @@ export class Kubectl { } protected getDownloadMirror() { - const mirror = packageMirrors.get(userStore.preferences?.downloadMirror); + const mirror = packageMirrors.get(UserStore.getInstance().preferences?.downloadMirror); if (mirror) { return mirror; diff --git a/src/main/lens-proxy.ts b/src/main/lens-proxy.ts index 98bdd97f54..aa92f4b90e 100644 --- a/src/main/lens-proxy.ts +++ b/src/main/lens-proxy.ts @@ -6,23 +6,22 @@ import url from "url"; import * as WebSocket from "ws"; import { apiPrefix, apiKubePrefix } from "../common/vars"; import { Router } from "./router"; -import { ClusterManager } from "./cluster-manager"; import { ContextHandler } from "./context-handler"; import logger from "./logger"; import { NodeShellSession, LocalShellSession } from "./shell-session"; +import { Singleton } from "../common/utils"; +import { ClusterManager } from "./cluster-manager"; -export class LensProxy { +export class LensProxy extends Singleton { protected origin: string; protected proxyServer: http.Server; protected router: Router; protected closed = false; protected retryCounters = new Map(); - static create(port: number, clusterManager: ClusterManager) { - return new LensProxy(port, clusterManager).listen(); - } + constructor(protected port: number) { + super(); - private constructor(protected port: number, protected clusterManager: ClusterManager) { this.origin = `http://localhost:${port}`; this.router = new Router(); } @@ -66,7 +65,7 @@ export class LensProxy { } protected async handleProxyUpgrade(proxy: httpProxy, req: http.IncomingMessage, socket: net.Socket, head: Buffer) { - const cluster = this.clusterManager.getClusterForRequest(req); + const cluster = ClusterManager.getInstance().getClusterForRequest(req); if (cluster) { const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, ""); @@ -171,7 +170,7 @@ export class LensProxy { const ws = new WebSocket.Server({ noServer: true }); return ws.on("connection", ((socket: WebSocket, req: http.IncomingMessage) => { - const cluster = this.clusterManager.getClusterForRequest(req); + const cluster = ClusterManager.getInstance().getClusterForRequest(req); const nodeParam = url.parse(req.url, true).query["node"]?.toString(); const shell = nodeParam ? new NodeShellSession(socket, cluster, nodeParam) @@ -197,7 +196,7 @@ export class LensProxy { } protected async handleRequest(proxy: httpProxy, req: http.IncomingMessage, res: http.ServerResponse) { - const cluster = this.clusterManager.getClusterForRequest(req); + const cluster = ClusterManager.getInstance().getClusterForRequest(req); if (cluster) { const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler); diff --git a/src/main/protocol-handler/__test__/router.test.ts b/src/main/protocol-handler/__test__/router.test.ts index 6b3f668079..88cc4fec44 100644 --- a/src/main/protocol-handler/__test__/router.test.ts +++ b/src/main/protocol-handler/__test__/router.test.ts @@ -1,7 +1,7 @@ import { LensProtocolRouterMain } from "../router"; import { noop } from "../../../common/utils"; -import { extensionsStore } from "../../../extensions/extensions-store"; -import { extensionLoader } from "../../../extensions/extension-loader"; +import { ExtensionsStore } from "../../../extensions/extensions-store"; +import { ExtensionLoader } from "../../../extensions/extension-loader"; import * as uuid from "uuid"; import { LensMainExtension } from "../../../extensions/core-api"; import { broadcastMessage } from "../../../common/ipc"; @@ -16,20 +16,28 @@ function throwIfDefined(val: any): void { } describe("protocol router tests", () => { - let lpr: LensProtocolRouterMain; - beforeEach(() => { - jest.clearAllMocks(); - (extensionsStore as any).state.clear(); - (extensionLoader as any).instances.clear(); - LensProtocolRouterMain.resetInstance(); - lpr = LensProtocolRouterMain.getInstance(); + ExtensionsStore.getInstanceOrCreate(); + ExtensionLoader.getInstanceOrCreate(); + + const lpr = LensProtocolRouterMain.getInstanceOrCreate(); + lpr.extensionsLoaded = true; lpr.rendererLoaded = true; }); + afterEach(() => { + jest.clearAllMocks(); + + ExtensionsStore.resetInstance(); + ExtensionLoader.resetInstance(); + LensProtocolRouterMain.resetInstance(); + }); + it("should throw on non-lens URLS", async () => { try { + const lpr = LensProtocolRouterMain.getInstance(); + expect(await lpr.route("https://google.ca")).toBeUndefined(); } catch (error) { expect(error).toBeInstanceOf(Error); @@ -38,6 +46,8 @@ describe("protocol router tests", () => { it("should throw when host not internal or extension", async () => { try { + const lpr = LensProtocolRouterMain.getInstance(); + expect(await lpr.route("lens://foobar")).toBeUndefined(); } catch (error) { expect(error).toBeInstanceOf(Error); @@ -57,14 +67,15 @@ describe("protocol router tests", () => { isEnabled: true, absolutePath: "/foo/bar", }); + const lpr = LensProtocolRouterMain.getInstance(); ext.protocolHandlers.push({ pathSchema: "/", handler: noop, }); - (extensionLoader as any).instances.set(extId, ext); - (extensionsStore as any).state.set(extId, { enabled: true, name: "@mirantis/minikube" }); + (ExtensionLoader.getInstance() as any).instances.set(extId, ext); + (ExtensionsStore.getInstance() as any).state.set(extId, { enabled: true, name: "@mirantis/minikube" }); lpr.addInternalHandler("/", noop); @@ -86,6 +97,7 @@ describe("protocol router tests", () => { }); it("should call handler if matches", async () => { + const lpr = LensProtocolRouterMain.getInstance(); let called = false; lpr.addInternalHandler("/page", () => { called = true; }); @@ -101,6 +113,7 @@ describe("protocol router tests", () => { }); it("should call most exact handler", async () => { + const lpr = LensProtocolRouterMain.getInstance(); let called: any = 0; lpr.addInternalHandler("/page", () => { called = 1; }); @@ -119,6 +132,7 @@ describe("protocol router tests", () => { it("should call most exact handler for an extension", async () => { let called: any = 0; + const lpr = LensProtocolRouterMain.getInstance(); const extId = uuid.v4(); const ext = new LensMainExtension({ id: extId, @@ -141,8 +155,8 @@ describe("protocol router tests", () => { handler: params => { called = params.pathname.id; }, }); - (extensionLoader as any).instances.set(extId, ext); - (extensionsStore as any).state.set(extId, { enabled: true, name: "@foobar/icecream" }); + (ExtensionLoader.getInstance() as any).instances.set(extId, ext); + (ExtensionsStore.getInstance() as any).state.set(extId, { enabled: true, name: "@foobar/icecream" }); try { expect(await lpr.route("lens://extension/@foobar/icecream/page/foob")).toBeUndefined(); @@ -155,6 +169,7 @@ describe("protocol router tests", () => { }); it("should work with non-org extensions", async () => { + const lpr = LensProtocolRouterMain.getInstance(); let called: any = 0; { @@ -177,8 +192,8 @@ describe("protocol router tests", () => { handler: params => { called = params.pathname.id; }, }); - (extensionLoader as any).instances.set(extId, ext); - (extensionsStore as any).state.set(extId, { enabled: true, name: "@foobar/icecream" }); + (ExtensionLoader.getInstance() as any).instances.set(extId, ext); + (ExtensionsStore.getInstance() as any).state.set(extId, { enabled: true, name: "@foobar/icecream" }); } { @@ -201,12 +216,12 @@ describe("protocol router tests", () => { handler: () => { called = 1; }, }); - (extensionLoader as any).instances.set(extId, ext); - (extensionsStore as any).state.set(extId, { enabled: true, name: "icecream" }); + (ExtensionLoader.getInstance() as any).instances.set(extId, ext); + (ExtensionsStore.getInstance() as any).state.set(extId, { enabled: true, name: "icecream" }); } - (extensionsStore as any).state.set("@foobar/icecream", { enabled: true, name: "@foobar/icecream" }); - (extensionsStore as any).state.set("icecream", { enabled: true, name: "icecream" }); + (ExtensionsStore.getInstance() as any).state.set("@foobar/icecream", { enabled: true, name: "@foobar/icecream" }); + (ExtensionsStore.getInstance() as any).state.set("icecream", { enabled: true, name: "icecream" }); try { expect(await lpr.route("lens://extension/icecream/page")).toBeUndefined(); @@ -219,10 +234,13 @@ describe("protocol router tests", () => { }); it("should throw if urlSchema is invalid", () => { + const lpr = LensProtocolRouterMain.getInstance(); + expect(() => lpr.addInternalHandler("/:@", noop)).toThrowError(); }); it("should call most exact handler with 3 found handlers", async () => { + const lpr = LensProtocolRouterMain.getInstance(); let called: any = 0; lpr.addInternalHandler("/", () => { called = 2; }); @@ -241,6 +259,7 @@ describe("protocol router tests", () => { }); it("should call most exact handler with 2 found handlers", async () => { + const lpr = LensProtocolRouterMain.getInstance(); let called: any = 0; lpr.addInternalHandler("/", () => { called = 2; }); diff --git a/src/main/shell-session/local-shell-session.ts b/src/main/shell-session/local-shell-session.ts index c131ea651c..c500ff169f 100644 --- a/src/main/shell-session/local-shell-session.ts +++ b/src/main/shell-session/local-shell-session.ts @@ -1,6 +1,6 @@ import path from "path"; import { helmCli } from "../helm/helm-cli"; -import { userStore } from "../../common/user-store"; +import { UserStore } from "../../common/user-store"; import { ShellSession } from "./shell-session"; export class LocalShellSession extends ShellSession { @@ -21,8 +21,8 @@ export class LocalShellSession extends ShellSession { protected async getShellArgs(shell: string): Promise { const helmpath = helmCli.getBinaryDir(); - const pathFromPreferences = userStore.preferences.kubectlBinariesPath || this.kubectl.getBundledPath(); - const kubectlPathDir = userStore.preferences.downloadKubectlBinaries ? await this.kubectlBinDirP : path.dirname(pathFromPreferences); + const pathFromPreferences = UserStore.getInstance().preferences.kubectlBinariesPath || this.kubectl.getBundledPath(); + const kubectlPathDir = UserStore.getInstance().preferences.downloadKubectlBinaries ? await this.kubectlBinDirP : path.dirname(pathFromPreferences); switch(path.basename(shell)) { case "powershell.exe": diff --git a/src/main/shell-session/shell-session.ts b/src/main/shell-session/shell-session.ts index eebaed0605..b3710aab70 100644 --- a/src/main/shell-session/shell-session.ts +++ b/src/main/shell-session/shell-session.ts @@ -6,7 +6,7 @@ import { app } from "electron"; import { clearKubeconfigEnvVars } from "../utils/clear-kube-env-vars"; import path from "path"; import { isWindows } from "../../common/vars"; -import { userStore } from "../../common/user-store"; +import { UserStore } from "../../common/user-store"; import * as pty from "node-pty"; import { appEventBus } from "../../common/event-bus"; @@ -119,7 +119,7 @@ export abstract class ShellSession { protected async getShellEnv() { const env = clearKubeconfigEnvVars(JSON.parse(JSON.stringify(await shellEnv()))); const pathStr = [...this.getPathEntries(), await this.kubectlBinDirP, process.env.PATH].join(path.delimiter); - const shell = userStore.preferences.shell || process.env.SHELL || process.env.PTYSHELL; + const shell = UserStore.getInstance().preferences.shell || process.env.SHELL || process.env.PTYSHELL; delete env.DEBUG; // don't pass DEBUG into shells diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts index afd6b03670..ad3cc3a2f2 100644 --- a/src/main/window-manager.ts +++ b/src/main/window-manager.ts @@ -61,35 +61,34 @@ export class WindowManager extends Singleton { this.windowState.manage(this.mainWindow); // open external links in default browser (target=_blank, window.open) - this.mainWindow.webContents.on("new-window", (event, url) => { - event.preventDefault(); - shell.openExternal(url); - }); - this.mainWindow.webContents.on("dom-ready", () => { - appEventBus.emit({ name: "app", action: "dom-ready" }); - }); - this.mainWindow.on("focus", () => { - appEventBus.emit({ name: "app", action: "focus" }); - }); - this.mainWindow.on("blur", () => { - appEventBus.emit({ name: "app", action: "blur" }); - }); - - // clean up - this.mainWindow.on("closed", () => { - this.windowState.unmanage(); - this.mainWindow = null; - this.splashWindow = null; - app.dock?.hide(); // hide icon in dock (mac-os) - }); - - this.mainWindow.webContents.on("did-fail-load", (_event, code, desc) => { - logger.error(`[WINDOW-MANAGER]: Failed to load Main window`, { code, desc }); - }); - - this.mainWindow.webContents.on("did-finish-load", () => { - logger.info("[WINDOW-MANAGER]: Main window loaded"); - }); + this.mainWindow + .on("focus", () => { + appEventBus.emit({ name: "app", action: "focus" }); + }) + .on("blur", () => { + appEventBus.emit({ name: "app", action: "blur" }); + }) + .on("closed", () => { + // clean up + this.windowState.unmanage(); + this.mainWindow = null; + this.splashWindow = null; + app.dock?.hide(); // hide icon in dock (mac-os) + }) + .webContents + .on("new-window", (event, url) => { + event.preventDefault(); + shell.openExternal(url); + }) + .on("dom-ready", () => { + appEventBus.emit({ name: "app", action: "dom-ready" }); + }) + .on("did-fail-load", (_event, code, desc) => { + logger.error(`[WINDOW-MANAGER]: Failed to load Main window`, { code, desc }); + }) + .on("did-finish-load", () => { + logger.info("[WINDOW-MANAGER]: Main window loaded"); + }); } try { @@ -101,8 +100,9 @@ export class WindowManager extends Singleton { setTimeout(() => { appEventBus.emit({ name: "app", action: "start" }); }, 1000); - } catch (err) { - dialog.showErrorBox("ERROR!", err.toString()); + } catch (error) { + logger.error("Showing main window failed", { error }); + dialog.showErrorBox("ERROR!", error.toString()); } } 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 22087ab569..7f2866b193 100644 --- a/src/migrations/hotbar-store/5.0.0-alpha.0.ts +++ b/src/migrations/hotbar-store/5.0.0-alpha.0.ts @@ -1,6 +1,6 @@ // Cleans up a store that had the state related data stored import { Hotbar } from "../../common/hotbar-store"; -import { clusterStore } from "../../common/cluster-store"; +import { ClusterStore } from "../../common/cluster-store"; import { migration } from "../migration-wrapper"; export default migration({ @@ -8,7 +8,7 @@ export default migration({ run(store) { const hotbars: Hotbar[] = []; - clusterStore.enabledClustersList.forEach((cluster: any) => { + ClusterStore.getInstance().enabledClustersList.forEach((cluster: any) => { const name = cluster.workspace || "default"; let hotbar = hotbars.find((h) => h.name === name); diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index c9ef607871..a6fbfdee40 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -6,19 +6,20 @@ import * as MobxReact from "mobx-react"; import * as ReactRouter from "react-router"; import * as ReactRouterDom from "react-router-dom"; import { render, unmountComponentAtNode } from "react-dom"; -import { clusterStore } from "../common/cluster-store"; -import { userStore } from "../common/user-store"; import { delay } from "../common/utils"; import { isMac, isDevelopment } from "../common/vars"; +import { HotbarStore } from "../common/hotbar-store"; +import { ClusterStore } from "../common/cluster-store"; +import { UserStore } from "../common/user-store"; import * as LensExtensions from "../extensions/extension-api"; -import { extensionDiscovery } from "../extensions/extension-discovery"; -import { extensionLoader } from "../extensions/extension-loader"; -import { extensionsStore } from "../extensions/extensions-store"; -import { hotbarStore } from "../common/hotbar-store"; -import { filesystemProvisionerStore } from "../main/extension-filesystem"; +import { ExtensionDiscovery } from "../extensions/extension-discovery"; +import { ExtensionLoader } from "../extensions/extension-loader"; +import { ExtensionsStore } from "../extensions/extensions-store"; +import { FilesystemProvisionerStore } from "../main/extension-filesystem"; import { App } from "./components/app"; import { LensApp } from "./lens-app"; -import { themeStore } from "./theme.store"; +import { ThemeStore } from "./theme.store"; +import { HelmRepoManager } from "../main/helm/helm-repo-manager"; /** * If this is a development buid, wait a second to attach @@ -50,8 +51,16 @@ export async function bootstrap(App: AppComponent) { await attachChromeDebugger(); rootElem.classList.toggle("is-mac", isMac); - extensionLoader.init(); - extensionDiscovery.init(); + ExtensionLoader.getInstanceOrCreate().init(); + ExtensionDiscovery.getInstanceOrCreate().init(); + + const userStore = UserStore.getInstanceOrCreate(); + const clusterStore = ClusterStore.getInstanceOrCreate(); + const extensionsStore = ExtensionsStore.getInstanceOrCreate(); + const filesystemStore = FilesystemProvisionerStore.getInstanceOrCreate(); + const themeStore = ThemeStore.getInstanceOrCreate(); + const hotbarStore = HotbarStore.getInstanceOrCreate(); + const helmRepoManager = HelmRepoManager.getInstanceOrCreate(); // preload common stores await Promise.all([ @@ -59,8 +68,9 @@ export async function bootstrap(App: AppComponent) { hotbarStore.load(), clusterStore.load(), extensionsStore.load(), - filesystemProvisionerStore.load(), + filesystemStore.load(), themeStore.init(), + helmRepoManager.init(), ]); // Register additional store listeners @@ -72,8 +82,8 @@ export async function bootstrap(App: AppComponent) { } window.addEventListener("message", (ev: MessageEvent) => { if (ev.data === "teardown") { - userStore.unregisterIpcListener(); - clusterStore.unregisterIpcListener(); + UserStore.getInstance(false)?.unregisterIpcListener(); + ClusterStore.getInstance(false)?.unregisterIpcListener(); unmountComponentAtNode(rootElem); window.location.href = "about:blank"; } diff --git a/src/renderer/components/+add-cluster/add-cluster.tsx b/src/renderer/components/+add-cluster/add-cluster.tsx index 11681f76fa..9a662636b0 100644 --- a/src/renderer/components/+add-cluster/add-cluster.tsx +++ b/src/renderer/components/+add-cluster/add-cluster.tsx @@ -11,10 +11,10 @@ import { AceEditor } from "../ace-editor"; import { Button } from "../button"; import { Icon } from "../icon"; import { kubeConfigDefaultPath, loadConfig, splitConfig, validateConfig, validateKubeConfig } from "../../../common/kube-helpers"; -import { ClusterModel, ClusterStore, clusterStore } from "../../../common/cluster-store"; +import { ClusterModel, ClusterStore } from "../../../common/cluster-store"; import { v4 as uuid } from "uuid"; import { navigate } from "../../navigation"; -import { userStore } from "../../../common/user-store"; +import { UserStore } from "../../../common/user-store"; import { cssNames } from "../../utils"; import { Notifications } from "../notifications"; import { Tab, Tabs } from "../tabs"; @@ -44,13 +44,13 @@ export class AddCluster extends React.Component { @observable showSettings = false; componentDidMount() { - clusterStore.setActive(null); - this.setKubeConfig(userStore.kubeConfigPath); + ClusterStore.getInstance().setActive(null); + this.setKubeConfig(UserStore.getInstance().kubeConfigPath); appEventBus.emit({ name: "cluster-add", action: "start" }); } componentWillUnmount() { - userStore.markNewContextsAsSeen(); + UserStore.getInstance().markNewContextsAsSeen(); } @action @@ -60,9 +60,9 @@ export class AddCluster extends React.Component { validateConfig(this.kubeConfigLocal); this.refreshContexts(); this.kubeConfigPath = filePath; - userStore.kubeConfigPath = filePath; // save to store + UserStore.getInstance().kubeConfigPath = filePath; // save to store } catch (err) { - if (!userStore.isDefaultKubeConfigPath) { + if (!UserStore.getInstance().isDefaultKubeConfigPath) { Notifications.error(

Can't setup {filePath} as kubeconfig: {String(err)}
); @@ -181,7 +181,7 @@ export class AddCluster extends React.Component { }); runInAction(() => { - clusterStore.addClusters(...newClusters); + ClusterStore.getInstance().addClusters(...newClusters); Notifications.ok( <>Successfully imported {newClusters.length} cluster(s) @@ -308,7 +308,7 @@ export class AddCluster extends React.Component { } onKubeConfigInputBlur = () => { - const isChanged = this.kubeConfigPath !== userStore.kubeConfigPath; + const isChanged = this.kubeConfigPath !== UserStore.getInstance().kubeConfigPath; if (isChanged) { this.kubeConfigPath = this.kubeConfigPath.replace("~", os.homedir()); @@ -316,7 +316,7 @@ export class AddCluster extends React.Component { try { this.setKubeConfig(this.kubeConfigPath, { throwError: true }); } catch (err) { - this.setKubeConfig(userStore.kubeConfigPath); // revert to previous valid path + this.setKubeConfig(UserStore.getInstance().kubeConfigPath); // revert to previous valid path } } }; @@ -328,7 +328,7 @@ export class AddCluster extends React.Component { }; protected formatContextLabel = ({ value: context }: SelectOption) => { - const isNew = userStore.newContexts.has(context); + const isNew = UserStore.getInstance().newContexts.has(context); const isSelected = this.selectedContexts.includes(context); return ( diff --git a/src/renderer/components/+apps-releases/release-details.tsx b/src/renderer/components/+apps-releases/release-details.tsx index f6b8327151..e326df2dc3 100644 --- a/src/renderer/components/+apps-releases/release-details.tsx +++ b/src/renderer/components/+apps-releases/release-details.tsx @@ -19,7 +19,7 @@ import { Button } from "../button"; import { releaseStore } from "./release.store"; import { Notifications } from "../notifications"; import { createUpgradeChartTab } from "../dock/upgrade-chart.store"; -import { themeStore } from "../../theme.store"; +import { ThemeStore } from "../../theme.store"; import { apiManager } from "../../api/api-manager"; import { SubTitle } from "../layout/sub-title"; import { secretsStore } from "../+config-secrets/secrets.store"; @@ -259,7 +259,7 @@ export class ReleaseDetails extends Component { return ( { sortByDefault={{ sortBy: sortBy.object, orderBy: "asc" }} sortSyncWithUrl={false} getTableRow={this.getTableRow} - className={cssNames("box grow", themeStore.activeTheme.type)} + className={cssNames("box grow", ThemeStore.getInstance().activeTheme.type)} > Message diff --git a/src/renderer/components/+cluster/cluster-overview.tsx b/src/renderer/components/+cluster/cluster-overview.tsx index 23d3214340..6e6fd5d58e 100644 --- a/src/renderer/components/+cluster/cluster-overview.tsx +++ b/src/renderer/components/+cluster/cluster-overview.tsx @@ -5,7 +5,7 @@ import { reaction } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; import { nodesStore } from "../+nodes/nodes.store"; import { podsStore } from "../+workloads-pods/pods.store"; -import { clusterStore, getHostedCluster } from "../../../common/cluster-store"; +import { ClusterStore, getHostedCluster } from "../../../common/cluster-store"; import { interval } from "../../utils"; import { TabLayout } from "../layout/tab-layout"; import { Spinner } from "../spinner"; @@ -66,7 +66,7 @@ export class ClusterOverview extends React.Component { render() { const isLoaded = nodesStore.isLoaded && podsStore.isLoaded; - const isMetricsHidden = clusterStore.isMetricHidden(ResourceType.Cluster); + const isMetricsHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.Cluster); return ( diff --git a/src/renderer/components/+cluster/cluster-pie-charts.tsx b/src/renderer/components/+cluster/cluster-pie-charts.tsx index d64efb6b36..e5e3bc6397 100644 --- a/src/renderer/components/+cluster/cluster-pie-charts.tsx +++ b/src/renderer/components/+cluster/cluster-pie-charts.tsx @@ -9,7 +9,7 @@ import { nodesStore } from "../+nodes/nodes.store"; import { ChartData, PieChart } from "../chart"; import { ClusterNoMetrics } from "./cluster-no-metrics"; import { bytesToUnits } from "../../utils"; -import { themeStore } from "../../theme.store"; +import { ThemeStore } from "../../theme.store"; import { getMetricLastPoints } from "../../api/endpoints/metrics.api"; export const ClusterPieCharts = observer(() => { @@ -29,7 +29,7 @@ export const ClusterPieCharts = observer(() => { const { podUsage, podCapacity } = data; const cpuLimitsOverload = cpuLimits > cpuCapacity; const memoryLimitsOverload = memoryLimits > memoryCapacity; - const defaultColor = themeStore.activeTheme.colors.pieChartDefaultColor; + const defaultColor = ThemeStore.getInstance().activeTheme.colors.pieChartDefaultColor; if (!memoryCapacity || !cpuCapacity || !podCapacity) return null; const cpuData: ChartData = { diff --git a/src/renderer/components/+extensions/__tests__/extensions.test.tsx b/src/renderer/components/+extensions/__tests__/extensions.test.tsx index 8899d9d74c..d51b1223f0 100644 --- a/src/renderer/components/+extensions/__tests__/extensions.test.tsx +++ b/src/renderer/components/+extensions/__tests__/extensions.test.tsx @@ -2,10 +2,12 @@ import "@testing-library/jest-dom/extend-expect"; import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import fse from "fs-extra"; import React from "react"; -import { extensionDiscovery } from "../../../../extensions/extension-discovery"; +import { UserStore } from "../../../../common/user-store"; +import { ExtensionDiscovery } from "../../../../extensions/extension-discovery"; +import { ExtensionLoader } from "../../../../extensions/extension-loader"; +import { ThemeStore } from "../../../theme.store"; import { ConfirmDialog } from "../../confirm-dialog"; import { Notifications } from "../../notifications"; -import { ExtensionStateStore } from "../extension-install.store"; import { Extensions } from "../extensions"; jest.mock("fs-extra"); @@ -18,46 +20,52 @@ jest.mock("../../../../common/utils", () => ({ extractTar: jest.fn(() => Promise.resolve()) })); -jest.mock("../../../../extensions/extension-discovery", () => ({ - ...jest.requireActual("../../../../extensions/extension-discovery"), - extensionDiscovery: { - localFolderPath: "/fake/path", - uninstallExtension: jest.fn(() => Promise.resolve()), - isLoaded: true - } -})); - -jest.mock("../../../../extensions/extension-loader", () => ({ - ...jest.requireActual("../../../../extensions/extension-loader"), - extensionLoader: { - userExtensions: new Map([ - ["extensionId", { - id: "extensionId", - manifest: { - name: "test", - version: "1.2.3" - }, - absolutePath: "/absolute/path", - manifestPath: "/symlinked/path/package.json", - isBundled: false, - isEnabled: true - }] - ]) - } -})); - jest.mock("../../notifications", () => ({ ok: jest.fn(), error: jest.fn(), info: jest.fn() })); +jest.mock("electron", () => { + return { + app: { + getVersion: () => "99.99.99", + getPath: () => "tmp", + getLocale: () => "en", + setLoginItemSettings: (): void => void 0, + } + }; +}); + describe("Extensions", () => { - beforeEach(() => { - ExtensionStateStore.resetInstance(); + beforeEach(async () => { + UserStore.resetInstance(); + ThemeStore.resetInstance(); + + await UserStore.getInstanceOrCreate().load(); + await ThemeStore.getInstanceOrCreate().init(); + + ExtensionLoader.resetInstance(); + ExtensionDiscovery.resetInstance(); + Extensions.installStates.clear(); + + ExtensionDiscovery.getInstanceOrCreate().uninstallExtension = jest.fn(() => Promise.resolve()); + + ExtensionLoader.getInstanceOrCreate().addExtension({ + id: "extensionId", + manifest: { + name: "test", + version: "1.2.3" + }, + absolutePath: "/absolute/path", + manifestPath: "/symlinked/path/package.json", + isBundled: false, + isEnabled: true + }); }); it("disables uninstall and disable buttons while uninstalling", async () => { + ExtensionDiscovery.getInstance().isLoaded = true; render(<>); expect(screen.getByText("Disable").closest("button")).not.toBeDisabled(); @@ -68,13 +76,14 @@ describe("Extensions", () => { // Approve confirm dialog fireEvent.click(screen.getByText("Yes")); - expect(extensionDiscovery.uninstallExtension).toHaveBeenCalled(); + expect(ExtensionDiscovery.getInstance().uninstallExtension).toHaveBeenCalled(); expect(screen.getByText("Disable").closest("button")).toBeDisabled(); expect(screen.getByText("Uninstall").closest("button")).toBeDisabled(); }); it("displays error notification on uninstall error", () => { - (extensionDiscovery.uninstallExtension as any).mockImplementationOnce(() => + ExtensionDiscovery.getInstance().isLoaded = true; + (ExtensionDiscovery.getInstance().uninstallExtension as any).mockImplementationOnce(() => Promise.reject() ); render(<>); @@ -115,14 +124,14 @@ describe("Extensions", () => { }); it("displays spinner while extensions are loading", () => { - extensionDiscovery.isLoaded = false; + ExtensionDiscovery.getInstance().isLoaded = false; const { container } = render(); expect(container.querySelector(".Spinner")).toBeInTheDocument(); - extensionDiscovery.isLoaded = true; + ExtensionDiscovery.getInstance().isLoaded = true; - waitFor(() => + waitFor(() => expect(container.querySelector(".Spinner")).not.toBeInTheDocument() ); }); diff --git a/src/renderer/components/+extensions/extension-install.store.ts b/src/renderer/components/+extensions/extension-install.store.ts deleted file mode 100644 index c4a8ed6690..0000000000 --- a/src/renderer/components/+extensions/extension-install.store.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { observable } from "mobx"; -import { autobind, Singleton } from "../../utils"; - -interface ExtensionState { - displayName: string; - // Possible states the extension can be - state: "installing" | "uninstalling"; -} - -@autobind() -export class ExtensionStateStore extends Singleton { - extensionState = observable.map(); -} diff --git a/src/renderer/components/+extensions/extensions.tsx b/src/renderer/components/+extensions/extensions.tsx index 9b69689f76..b2202a6050 100644 --- a/src/renderer/components/+extensions/extensions.tsx +++ b/src/renderer/components/+extensions/extensions.tsx @@ -7,8 +7,8 @@ import path from "path"; import React from "react"; import { downloadFile, extractTar, listTarEntries, readFileFromTar } from "../../../common/utils"; import { docsUrl } from "../../../common/vars"; -import { extensionDiscovery, InstalledExtension, manifestFilename } from "../../../extensions/extension-discovery"; -import { extensionLoader } from "../../../extensions/extension-loader"; +import { ExtensionDiscovery, InstalledExtension, manifestFilename } from "../../../extensions/extension-discovery"; +import { ExtensionLoader } from "../../../extensions/extension-loader"; import { extensionDisplayName, LensExtensionManifest, sanitizeExtensionName } from "../../../extensions/lens-extension"; import logger from "../../../main/logger"; import { prevDefault } from "../../utils"; @@ -21,7 +21,6 @@ import { SubTitle } from "../layout/sub-title"; import { Notifications } from "../notifications"; import { Spinner } from "../spinner/spinner"; import { TooltipPosition } from "../tooltip"; -import { ExtensionStateStore } from "./extension-install.store"; import "./extensions.scss"; interface InstallRequest { @@ -39,6 +38,12 @@ interface InstallRequestValidated extends InstallRequestPreloaded { tempFile: string; // temp system path to packed extension for unpacking } +interface ExtensionState { + displayName: string; + // Possible states the extension can be + state: "installing" | "uninstalling"; +} + @observer export class Extensions extends React.Component { private static supportedFormats = ["tar", "tgz"]; @@ -50,9 +55,7 @@ export class Extensions extends React.Component { } }; - get extensionStateStore() { - return ExtensionStateStore.getInstance(); - } + static installStates = observable.map(); @observable search = ""; @observable installPath = ""; @@ -64,7 +67,7 @@ export class Extensions extends React.Component { * Extensions that were removed from extensions but are still in "uninstalling" state */ @computed get removedUninstalling() { - return Array.from(this.extensionStateStore.extensionState.entries()) + return Array.from(Extensions.installStates.entries()) .filter(([id, extension]) => extension.state === "uninstalling" && !this.extensions.find(extension => extension.id === id) @@ -76,7 +79,7 @@ export class Extensions extends React.Component { * Extensions that were added to extensions but are still in "installing" state */ @computed get addedInstalling() { - return Array.from(this.extensionStateStore.extensionState.entries()) + return Array.from(Extensions.installStates.entries()) .filter(([id, extension]) => extension.state === "installing" && this.extensions.find(extension => extension.id === id) @@ -91,7 +94,7 @@ export class Extensions extends React.Component { Notifications.ok(

Extension {displayName} successfully uninstalled!

); - this.extensionStateStore.extensionState.delete(id); + Extensions.installStates.delete(id); }); this.addedInstalling.forEach(({ id, displayName }) => { @@ -104,7 +107,7 @@ export class Extensions extends React.Component { Notifications.ok(

Extension {displayName} successfully installed!

); - this.extensionStateStore.extensionState.delete(id); + Extensions.installStates.delete(id); this.installPath = ""; // Enable installed extensions by default. @@ -117,7 +120,7 @@ export class Extensions extends React.Component { @computed get extensions() { const searchText = this.search.toLowerCase(); - return Array.from(extensionLoader.userExtensions.values()) + return Array.from(ExtensionLoader.getInstance().userExtensions.values()) .filter(({ manifest: { name, description }}) => ( name.toLowerCase().includes(searchText) || description?.toLowerCase().includes(searchText) @@ -125,7 +128,7 @@ export class Extensions extends React.Component { } get extensionsPath() { - return extensionDiscovery.localFolderPath; + return ExtensionDiscovery.getInstance().localFolderPath; } getExtensionPackageTemp(fileName = "") { @@ -342,11 +345,11 @@ export class Extensions extends React.Component { async unpackExtension({ fileName, tempFile, manifest: { name, version } }: InstallRequestValidated) { const displayName = extensionDisplayName(name, version); - const extensionId = path.join(extensionDiscovery.nodeModulesPath, name, "package.json"); + const extensionId = path.join(ExtensionDiscovery.getInstance().nodeModulesPath, name, "package.json"); logger.info(`Unpacking extension ${displayName}`, { fileName, tempFile }); - this.extensionStateStore.extensionState.set(extensionId, { + Extensions.installStates.set(extensionId, { state: "installing", displayName }); @@ -381,8 +384,8 @@ export class Extensions extends React.Component { ); // Remove install state on install failure - if (this.extensionStateStore.extensionState.get(extensionId)?.state === "installing") { - this.extensionStateStore.extensionState.delete(extensionId); + if (Extensions.installStates.get(extensionId)?.state === "installing") { + Extensions.installStates.delete(extensionId); } } finally { // clean up @@ -406,20 +409,20 @@ export class Extensions extends React.Component { const displayName = extensionDisplayName(extension.manifest.name, extension.manifest.version); try { - this.extensionStateStore.extensionState.set(extension.id, { + Extensions.installStates.set(extension.id, { state: "uninstalling", displayName }); - await extensionDiscovery.uninstallExtension(extension); + await ExtensionDiscovery.getInstance().uninstallExtension(extension); } catch (error) { Notifications.error(

Uninstalling extension {displayName} has failed: {error?.message ?? ""}

); // Remove uninstall state on uninstall failure - if (this.extensionStateStore.extensionState.get(extension.id)?.state === "uninstalling") { - this.extensionStateStore.extensionState.delete(extension.id); + if (Extensions.installStates.get(extension.id)?.state === "uninstalling") { + Extensions.installStates.delete(extension.id); } } } @@ -445,7 +448,7 @@ export class Extensions extends React.Component { return extensions.map(extension => { const { id, isEnabled, manifest } = extension; const { name, description, version } = manifest; - const isUninstalling = this.extensionStateStore.extensionState.get(id)?.state === "uninstalling"; + const isUninstalling = Extensions.installStates.get(id)?.state === "uninstalling"; return (
@@ -478,7 +481,7 @@ export class Extensions extends React.Component { * True if at least one extension is in installing state */ @computed get isInstalling() { - return [...this.extensionStateStore.extensionState.values()].some(extension => extension.state === "installing"); + return [...Extensions.installStates.values()].some(extension => extension.state === "installing"); } render() { @@ -536,7 +539,11 @@ export class Extensions extends React.Component { value={this.search} onChange={(value) => this.search = value} /> - {extensionDiscovery.isLoaded ? this.renderExtensions() :
} + { + ExtensionDiscovery.getInstance().isLoaded + ? this.renderExtensions() + :
+ }
diff --git a/src/renderer/components/+network-ingresses/ingress-details.tsx b/src/renderer/components/+network-ingresses/ingress-details.tsx index 244738a537..6af1fa582f 100644 --- a/src/renderer/components/+network-ingresses/ingress-details.tsx +++ b/src/renderer/components/+network-ingresses/ingress-details.tsx @@ -15,7 +15,7 @@ import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { getBackendServiceNamePort } from "../../api/endpoints/ingress.api"; import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting"; -import { clusterStore } from "../../../common/cluster-store"; +import { ClusterStore } from "../../../common/cluster-store"; interface Props extends KubeObjectDetailsProps { } @@ -101,6 +101,7 @@ export class IngressDetails extends React.Component { if (!ingress) { return null; } + const { spec, status } = ingress; const ingressPoints = status?.loadBalancer?.ingress; const { metrics } = ingressStore; @@ -108,8 +109,7 @@ export class IngressDetails extends React.Component { "Network", "Duration", ]; - const isMetricHidden = clusterStore.isMetricHidden(ResourceType.Ingress); - + const isMetricHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.Ingress); const { serviceName, servicePort } = ingress.getServiceNamePort(); return ( diff --git a/src/renderer/components/+nodes/node-charts.tsx b/src/renderer/components/+nodes/node-charts.tsx index d6e048b910..382917b61b 100644 --- a/src/renderer/components/+nodes/node-charts.tsx +++ b/src/renderer/components/+nodes/node-charts.tsx @@ -6,7 +6,7 @@ import { NoMetrics } from "../resource-metrics/no-metrics"; import { IResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics"; import { observer } from "mobx-react"; import { ChartOptions, ChartPoint } from "chart.js"; -import { themeStore } from "../../theme.store"; +import { ThemeStore } from "../../theme.store"; import { mapValues } from "lodash"; type IContext = IResourceMetricsValue; @@ -14,7 +14,7 @@ type IContext = IResourceMetricsValue; export const NodeCharts = observer(() => { const { params: { metrics }, tabId, object } = useContext(ResourceMetricsContext); const id = object.getId(); - const { chartCapacityColor } = themeStore.activeTheme.colors; + const { chartCapacityColor } = ThemeStore.getInstance().activeTheme.colors; if (!metrics) { return null; diff --git a/src/renderer/components/+nodes/node-details.tsx b/src/renderer/components/+nodes/node-details.tsx index 349bf59051..53bedafde2 100644 --- a/src/renderer/components/+nodes/node-details.tsx +++ b/src/renderer/components/+nodes/node-details.tsx @@ -18,7 +18,7 @@ import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { KubeEventDetails } from "../+events/kube-event-details"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting"; -import { clusterStore } from "../../../common/cluster-store"; +import { ClusterStore } from "../../../common/cluster-store"; interface Props extends KubeObjectDetailsProps { } @@ -54,7 +54,7 @@ export class NodeDetails extends React.Component { "Disk", "Pods", ]; - const isMetricHidden = clusterStore.isMetricHidden(ResourceType.Node); + const isMetricHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.Node); return (
diff --git a/src/renderer/components/+preferences/add-helm-repo-dialog.tsx b/src/renderer/components/+preferences/add-helm-repo-dialog.tsx index a0ede38ece..ae2a8f444c 100644 --- a/src/renderer/components/+preferences/add-helm-repo-dialog.tsx +++ b/src/renderer/components/+preferences/add-helm-repo-dialog.tsx @@ -13,7 +13,7 @@ import { systemName, isUrl, isPath } from "../input/input_validators"; import { SubTitle } from "../layout/sub-title"; import { Icon } from "../icon"; import { Notifications } from "../notifications"; -import { HelmRepo, repoManager } from "../../../main/helm/helm-repo-manager"; +import { HelmRepo, HelmRepoManager } from "../../../main/helm/helm-repo-manager"; interface Props extends Partial { onAddRepo: Function @@ -79,7 +79,7 @@ export class AddHelmRepoDialog extends React.Component { async addCustomRepo() { try { - await repoManager.addСustomRepo(this.helmRepo); + await HelmRepoManager.getInstance().addСustomRepo(this.helmRepo); Notifications.ok(<>Helm repository {this.helmRepo.name} has added); this.props.onAddRepo(); this.close(); diff --git a/src/renderer/components/+preferences/helm-charts.tsx b/src/renderer/components/+preferences/helm-charts.tsx index 1b0d80bf26..caccf271be 100644 --- a/src/renderer/components/+preferences/helm-charts.tsx +++ b/src/renderer/components/+preferences/helm-charts.tsx @@ -3,7 +3,7 @@ import "./helm-charts.scss"; import React from "react"; import { action, computed, observable } from "mobx"; -import { HelmRepo, repoManager } from "../../../main/helm/helm-repo-manager"; +import { HelmRepo, HelmRepoManager } from "../../../main/helm/helm-repo-manager"; import { Button } from "../button"; import { Icon } from "../icon"; import { Notifications } from "../notifications"; @@ -34,9 +34,9 @@ export class HelmCharts extends React.Component { try { if (!this.repos.length) { - this.repos = await repoManager.loadAvailableRepos(); // via https://helm.sh + this.repos = await HelmRepoManager.getInstance().loadAvailableRepos(); // via https://helm.sh } - const repos = await repoManager.repositories(); // via helm-cli + const repos = await HelmRepoManager.getInstance().repositories(); // via helm-cli this.addedRepos.clear(); repos.forEach(repo => this.addedRepos.set(repo.name, repo)); @@ -49,7 +49,7 @@ export class HelmCharts extends React.Component { async addRepo(repo: HelmRepo) { try { - await repoManager.addRepo(repo); + await HelmRepoManager.getInstance().addRepo(repo); this.addedRepos.set(repo.name, repo); } catch (err) { Notifications.error(<>Adding helm branch {repo.name} has failed: {String(err)}); @@ -58,7 +58,7 @@ export class HelmCharts extends React.Component { async removeRepo(repo: HelmRepo) { try { - await repoManager.removeRepo(repo); + await HelmRepoManager.getInstance().removeRepo(repo); this.addedRepos.delete(repo.name); } catch (err) { Notifications.error( diff --git a/src/renderer/components/+preferences/kubectl-binaries.tsx b/src/renderer/components/+preferences/kubectl-binaries.tsx index 0eace69c10..eb1dfca1c5 100644 --- a/src/renderer/components/+preferences/kubectl-binaries.tsx +++ b/src/renderer/components/+preferences/kubectl-binaries.tsx @@ -1,7 +1,7 @@ import React, { useState } from "react"; import { Input, InputValidators } from "../input"; import { SubTitle } from "../layout/sub-title"; -import { UserPreferences, userStore } from "../../../common/user-store"; +import { getDefaultKubectlPath, UserPreferences } from "../../../common/user-store"; import { observer } from "mobx-react"; import { bundledKubectlPath } from "../../../main/kubectl"; import { SelectOption, Select } from "../select"; @@ -59,7 +59,7 @@ export const KubectlBinaries = observer(({ preferences }: { preferences: UserPre [] { - return themeStore.themes.map(theme => ({ + return ThemeStore.getInstance().themes.map(theme => ({ label: theme.name, value: theme.id, })); @@ -98,18 +98,16 @@ export class Preferences extends React.Component { } render() { - const { preferences } = userStore; const extensions = appPreferenceRegistry.getItems(); const telemetryExtensions = extensions.filter(e => e.showInPreferencesTab == Pages.Telemetry); - let defaultShell = process.env.SHELL || process.env.PTYSHELL; - - if (!defaultShell) { - if (isWindows) { - defaultShell = "powershell.exe"; - } else { - defaultShell = "System default shell"; - } - } + const { preferences } = UserStore.getInstance(); + const defaultShell = process.env.SHELL + || process.env.PTYSHELL + || ( + isWindows + ? "powershell.exe" + : "System default shell" + ); return ( userStore.setLocaleTimezone(value)} + onChange={({ value }: SelectOption) => UserStore.getInstance().setLocaleTimezone(value)} themeName="lens" /> diff --git a/src/renderer/components/+storage-volume-claims/volume-claim-details.tsx b/src/renderer/components/+storage-volume-claims/volume-claim-details.tsx index 99642249ab..61380f1e46 100644 --- a/src/renderer/components/+storage-volume-claims/volume-claim-details.tsx +++ b/src/renderer/components/+storage-volume-claims/volume-claim-details.tsx @@ -15,7 +15,7 @@ import { getDetailsUrl, KubeObjectDetailsProps, KubeObjectMeta } from "../kube-o import { PersistentVolumeClaim } from "../../api/endpoints"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting"; -import { clusterStore } from "../../../common/cluster-store"; +import { ClusterStore } from "../../../common/cluster-store"; interface Props extends KubeObjectDetailsProps { } @@ -43,7 +43,7 @@ export class PersistentVolumeClaimDetails extends React.Component { const metricTabs = [ "Disk" ]; - const isMetricHidden = clusterStore.isMetricHidden(ResourceType.VolumeClaim); + const isMetricHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.VolumeClaim); return (
diff --git a/src/renderer/components/+storage-volume-claims/volume-claim-disk-chart.tsx b/src/renderer/components/+storage-volume-claims/volume-claim-disk-chart.tsx index 11f165622c..4bc12a9200 100644 --- a/src/renderer/components/+storage-volume-claims/volume-claim-disk-chart.tsx +++ b/src/renderer/components/+storage-volume-claims/volume-claim-disk-chart.tsx @@ -5,13 +5,13 @@ import { BarChart, ChartDataSets, memoryOptions } from "../chart"; import { isMetricsEmpty, normalizeMetrics } from "../../api/endpoints/metrics.api"; import { NoMetrics } from "../resource-metrics/no-metrics"; import { IResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics"; -import { themeStore } from "../../theme.store"; +import { ThemeStore } from "../../theme.store"; type IContext = IResourceMetricsValue; export const VolumeClaimDiskChart = observer(() => { const { params: { metrics }, object } = useContext(ResourceMetricsContext); - const { chartCapacityColor } = themeStore.activeTheme.colors; + const { chartCapacityColor } = ThemeStore.getInstance().activeTheme.colors; const id = object.getId(); if (!metrics) return null; diff --git a/src/renderer/components/+whats-new/whats-new.tsx b/src/renderer/components/+whats-new/whats-new.tsx index aa132cc178..95542195bb 100644 --- a/src/renderer/components/+whats-new/whats-new.tsx +++ b/src/renderer/components/+whats-new/whats-new.tsx @@ -3,7 +3,7 @@ import fs from "fs"; import path from "path"; import React from "react"; import { observer } from "mobx-react"; -import { userStore } from "../../../common/user-store"; +import { UserStore } from "../../../common/user-store"; import { navigate } from "../../navigation"; import { Button } from "../button"; import marked from "marked"; @@ -14,7 +14,7 @@ export class WhatsNew extends React.Component { ok = () => { navigate("/"); - userStore.saveLastSeenAppVersion(); + UserStore.getInstance().saveLastSeenAppVersion(); }; render() { diff --git a/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx b/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx index be56f5f477..5b9852a992 100644 --- a/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx +++ b/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx @@ -19,7 +19,7 @@ import { PodDetailsList } from "../+workloads-pods/pod-details-list"; import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting"; -import { clusterStore } from "../../../common/cluster-store"; +import { ClusterStore } from "../../../common/cluster-store"; interface Props extends KubeObjectDetailsProps { } @@ -49,7 +49,7 @@ export class DaemonSetDetails extends React.Component { const nodeSelector = daemonSet.getNodeSelectors(); const childPods = daemonSetStore.getChildPods(daemonSet); const metrics = daemonSetStore.metrics; - const isMetricHidden = clusterStore.isMetricHidden(ResourceType.DaemonSet); + const isMetricHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.DaemonSet); return (
diff --git a/src/renderer/components/+workloads-deployments/deployment-details.tsx b/src/renderer/components/+workloads-deployments/deployment-details.tsx index 778fcd9bc2..6a2b9f92a2 100644 --- a/src/renderer/components/+workloads-deployments/deployment-details.tsx +++ b/src/renderer/components/+workloads-deployments/deployment-details.tsx @@ -20,7 +20,7 @@ import { PodDetailsList } from "../+workloads-pods/pod-details-list"; import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting"; -import { clusterStore } from "../../../common/cluster-store"; +import { ClusterStore } from "../../../common/cluster-store"; interface Props extends KubeObjectDetailsProps { } @@ -49,7 +49,7 @@ export class DeploymentDetails extends React.Component { const selectors = deployment.getSelectors(); const childPods = deploymentStore.getChildPods(deployment); const metrics = deploymentStore.metrics; - const isMetricHidden = clusterStore.isMetricHidden(ResourceType.Deployment); + const isMetricHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.Deployment); return (
diff --git a/src/renderer/components/+workloads-overview/overview-workload-status.tsx b/src/renderer/components/+workloads-overview/overview-workload-status.tsx index cb2accb09c..acab487e91 100644 --- a/src/renderer/components/+workloads-overview/overview-workload-status.tsx +++ b/src/renderer/components/+workloads-overview/overview-workload-status.tsx @@ -8,7 +8,7 @@ import { observer } from "mobx-react"; import { PieChart } from "../chart"; import { cssVar } from "../../utils"; import { ChartData, ChartDataSets } from "chart.js"; -import { themeStore } from "../../theme.store"; +import { ThemeStore } from "../../theme.store"; interface SimpleChartDataSets extends ChartDataSets { backgroundColor?: string[]; @@ -41,7 +41,7 @@ export class OverviewWorkloadStatus extends React.Component { labels: [] as string[], datasets: [{ data: [1], - backgroundColor: [themeStore.activeTheme.colors.pieChartDefaultColor], + backgroundColor: [ThemeStore.getInstance().activeTheme.colors.pieChartDefaultColor], label: "Empty" }] }; diff --git a/src/renderer/components/+workloads-pods/container-charts.tsx b/src/renderer/components/+workloads-pods/container-charts.tsx index ccea4ec788..8cd22b0cbb 100644 --- a/src/renderer/components/+workloads-pods/container-charts.tsx +++ b/src/renderer/components/+workloads-pods/container-charts.tsx @@ -5,13 +5,13 @@ import { BarChart, cpuOptions, memoryOptions } from "../chart"; import { isMetricsEmpty, normalizeMetrics } from "../../api/endpoints/metrics.api"; import { NoMetrics } from "../resource-metrics/no-metrics"; import { IResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics"; -import { themeStore } from "../../theme.store"; +import { ThemeStore } from "../../theme.store"; type IContext = IResourceMetricsValue; export const ContainerCharts = observer(() => { const { params: { metrics }, tabId } = useContext(ResourceMetricsContext); - const { chartCapacityColor } = themeStore.activeTheme.colors; + const { chartCapacityColor } = ThemeStore.getInstance().activeTheme.colors; if (!metrics) return null; if (isMetricsEmpty(metrics)) return ; diff --git a/src/renderer/components/+workloads-pods/pod-charts.tsx b/src/renderer/components/+workloads-pods/pod-charts.tsx index c505126acb..62eb8b3a2e 100644 --- a/src/renderer/components/+workloads-pods/pod-charts.tsx +++ b/src/renderer/components/+workloads-pods/pod-charts.tsx @@ -6,7 +6,7 @@ import { isMetricsEmpty, normalizeMetrics } from "../../api/endpoints/metrics.ap import { NoMetrics } from "../resource-metrics/no-metrics"; import { IResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics"; import { WorkloadKubeObject } from "../../api/workload-kube-object"; -import { themeStore } from "../../theme.store"; +import { ThemeStore } from "../../theme.store"; export const podMetricTabs = [ "CPU", @@ -19,7 +19,7 @@ type IContext = IResourceMetricsValue { const { params: { metrics }, tabId, object } = useContext(ResourceMetricsContext); - const { chartCapacityColor } = themeStore.activeTheme.colors; + const { chartCapacityColor } = ThemeStore.getInstance().activeTheme.colors; const id = object.getId(); if (!metrics) return null; diff --git a/src/renderer/components/+workloads-pods/pod-details-container.tsx b/src/renderer/components/+workloads-pods/pod-details-container.tsx index 779a0e0de2..a2874aac60 100644 --- a/src/renderer/components/+workloads-pods/pod-details-container.tsx +++ b/src/renderer/components/+workloads-pods/pod-details-container.tsx @@ -12,8 +12,8 @@ import { ResourceMetrics } from "../resource-metrics"; import { IMetrics } from "../../api/endpoints/metrics.api"; import { ContainerCharts } from "./container-charts"; import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting"; -import { clusterStore } from "../../../common/cluster-store"; import { LocaleDate } from "../locale-date"; +import { ClusterStore } from "../../../common/cluster-store"; interface Props { pod: Pod; @@ -66,7 +66,7 @@ export class PodDetailsContainer extends React.Component { "Memory", "Filesystem", ]; - const isMetricHidden = clusterStore.isMetricHidden(ResourceType.Container); + const isMetricHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.Container); return (
diff --git a/src/renderer/components/+workloads-pods/pod-details.tsx b/src/renderer/components/+workloads-pods/pod-details.tsx index f643d599a5..4150f5807d 100644 --- a/src/renderer/components/+workloads-pods/pod-details.tsx +++ b/src/renderer/components/+workloads-pods/pod-details.tsx @@ -23,7 +23,7 @@ import { PodCharts, podMetricTabs } from "./pod-charts"; import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting"; -import { clusterStore } from "../../../common/cluster-store"; +import { ClusterStore } from "../../../common/cluster-store"; interface Props extends KubeObjectDetailsProps { } @@ -68,7 +68,7 @@ export class PodDetails extends React.Component { const nodeSelector = pod.getNodeSelectors(); const volumes = pod.getVolumes(); const metrics = podsStore.metrics; - const isMetricHidden = clusterStore.isMetricHidden(ResourceType.Pod); + const isMetricHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.Pod); return (
diff --git a/src/renderer/components/+workloads-replicasets/replicaset-details.tsx b/src/renderer/components/+workloads-replicasets/replicaset-details.tsx index b104340612..0f4732fdc2 100644 --- a/src/renderer/components/+workloads-replicasets/replicaset-details.tsx +++ b/src/renderer/components/+workloads-replicasets/replicaset-details.tsx @@ -18,7 +18,7 @@ import { PodDetailsList } from "../+workloads-pods/pod-details-list"; import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting"; -import { clusterStore } from "../../../common/cluster-store"; +import { ClusterStore } from "../../../common/cluster-store"; interface Props extends KubeObjectDetailsProps { } @@ -49,7 +49,7 @@ export class ReplicaSetDetails extends React.Component { const nodeSelector = replicaSet.getNodeSelectors(); const images = replicaSet.getImages(); const childPods = replicaSetStore.getChildPods(replicaSet); - const isMetricHidden = clusterStore.isMetricHidden(ResourceType.ReplicaSet); + const isMetricHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.ReplicaSet); return (
diff --git a/src/renderer/components/+workloads-statefulsets/statefulset-details.tsx b/src/renderer/components/+workloads-statefulsets/statefulset-details.tsx index f21f9efc5d..b6f5812e21 100644 --- a/src/renderer/components/+workloads-statefulsets/statefulset-details.tsx +++ b/src/renderer/components/+workloads-statefulsets/statefulset-details.tsx @@ -19,7 +19,7 @@ import { PodDetailsList } from "../+workloads-pods/pod-details-list"; import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting"; -import { clusterStore } from "../../../common/cluster-store"; +import { ClusterStore } from "../../../common/cluster-store"; interface Props extends KubeObjectDetailsProps { } @@ -48,7 +48,7 @@ export class StatefulSetDetails extends React.Component { const nodeSelector = statefulSet.getNodeSelectors(); const childPods = statefulSetStore.getChildPods(statefulSet); const metrics = statefulSetStore.metrics; - const isMetricHidden = clusterStore.isMetricHidden(ResourceType.StatefulSet); + const isMetricHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.StatefulSet); return (
diff --git a/src/renderer/components/app.tsx b/src/renderer/components/app.tsx index 2110898885..41fc63cf4d 100755 --- a/src/renderer/components/app.tsx +++ b/src/renderer/components/app.tsx @@ -34,7 +34,7 @@ import { getHostedCluster, getHostedClusterId } from "../../common/cluster-store import logger from "../../main/logger"; import { webFrame } from "electron"; import { clusterPageRegistry, getExtensionPageUrl } from "../../extensions/registries/page-registry"; -import { extensionLoader } from "../../extensions/extension-loader"; +import { ExtensionLoader } from "../../extensions/extension-loader"; import { appEventBus } from "../../common/event-bus"; import { requestMain } from "../../common/ipc"; import whatInput from "what-input"; @@ -62,7 +62,7 @@ export class App extends React.Component { await requestMain(clusterSetFrameIdHandler, clusterId); await getHostedCluster().whenReady; // cluster.activate() is done at this point - extensionLoader.loadOnClusterRenderer(); + ExtensionLoader.getInstance().loadOnClusterRenderer(); setTimeout(() => { appEventBus.emit({ name: "cluster", diff --git a/src/renderer/components/chart/bar-chart.tsx b/src/renderer/components/chart/bar-chart.tsx index 74ee203d4c..3ba1807190 100644 --- a/src/renderer/components/chart/bar-chart.tsx +++ b/src/renderer/components/chart/bar-chart.tsx @@ -7,7 +7,7 @@ import { ChartData, ChartOptions, ChartPoint, ChartTooltipItem, Scriptable } fro import { Chart, ChartKind, ChartProps } from "./chart"; import { bytesToUnits, cssNames } from "../../utils"; import { ZebraStripes } from "./zebra-stripes.plugin"; -import { themeStore } from "../../theme.store"; +import { ThemeStore } from "../../theme.store"; import { NoMetrics } from "../resource-metrics/no-metrics"; interface Props extends ChartProps { @@ -26,7 +26,7 @@ export class BarChart extends React.Component { render() { const { name, data, className, timeLabelStep, plugins, options: customOptions, ...settings } = this.props; - const { textColorPrimary, borderFaintColor, chartStripesColor } = themeStore.activeTheme.colors; + const { textColorPrimary, borderFaintColor, chartStripesColor } = ThemeStore.getInstance().activeTheme.colors; const getBarColor: Scriptable = ({ dataset }) => { const color = dataset.borderColor; diff --git a/src/renderer/components/chart/pie-chart.tsx b/src/renderer/components/chart/pie-chart.tsx index 939d6bb612..63bd9a209e 100644 --- a/src/renderer/components/chart/pie-chart.tsx +++ b/src/renderer/components/chart/pie-chart.tsx @@ -4,7 +4,7 @@ import { observer } from "mobx-react"; import ChartJS, { ChartOptions } from "chart.js"; import { Chart, ChartProps } from "./chart"; import { cssNames } from "../../utils"; -import { themeStore } from "../../theme.store"; +import { ThemeStore } from "../../theme.store"; interface Props extends ChartProps { } @@ -13,7 +13,7 @@ interface Props extends ChartProps { export class PieChart extends React.Component { render() { const { data, className, options, ...chartProps } = this.props; - const { contentColor } = themeStore.activeTheme.colors; + const { contentColor } = ThemeStore.getInstance().activeTheme.colors; const cutouts = [88, 76, 63]; const opts: ChartOptions = this.props.showChart === false ? {} : { maintainAspectRatio: false, diff --git a/src/renderer/components/cluster-manager/bottom-bar.test.tsx b/src/renderer/components/cluster-manager/bottom-bar.test.tsx index f13121f3a5..7f8c833c11 100644 --- a/src/renderer/components/cluster-manager/bottom-bar.test.tsx +++ b/src/renderer/components/cluster-manager/bottom-bar.test.tsx @@ -7,7 +7,6 @@ jest.mock("../../../extensions/registries"); import { statusBarRegistry } from "../../../extensions/registries"; describe("", () => { - it("renders w/o errors", () => { const { container } = render(); diff --git a/src/renderer/components/cluster-manager/cluster-manager.tsx b/src/renderer/components/cluster-manager/cluster-manager.tsx index 7b9a7be0ea..489ffae284 100644 --- a/src/renderer/components/cluster-manager/cluster-manager.tsx +++ b/src/renderer/components/cluster-manager/cluster-manager.tsx @@ -10,7 +10,7 @@ import { Preferences, preferencesRoute } from "../+preferences"; import { AddCluster, addClusterRoute } from "../+add-cluster"; import { ClusterView } from "./cluster-view"; import { clusterViewRoute } from "./cluster-view.route"; -import { clusterStore } from "../../../common/cluster-store"; +import { ClusterStore } from "../../../common/cluster-store"; import { hasLoadedView, initView, lensViews, refreshViews } from "./lens-views"; import { globalPageRegistry } from "../../../extensions/registries/page-registry"; import { Extensions, extensionsRoute } from "../+extensions"; @@ -21,7 +21,7 @@ import { EntitySettings, entitySettingsRoute } from "../+entity-settings"; @observer export class ClusterManager extends React.Component { componentDidMount() { - const getMatchedCluster = () => clusterStore.getById(getMatchedClusterId()); + const getMatchedCluster = () => ClusterStore.getInstance().getById(getMatchedClusterId()); disposeOnUnmount(this, [ reaction(getMatchedClusterId, initView, { @@ -59,9 +59,12 @@ export class ClusterManager extends React.Component { - {globalPageRegistry.getItems().map(({ url, components: { Page } }) => { - return ; - })} + { + globalPageRegistry.getItems() + .map(({ url, components: { Page } }) => ( + + )) + } diff --git a/src/renderer/components/cluster-manager/cluster-status.tsx b/src/renderer/components/cluster-manager/cluster-status.tsx index 3e17eb3e4c..7fb68ceb36 100644 --- a/src/renderer/components/cluster-manager/cluster-status.tsx +++ b/src/renderer/components/cluster-manager/cluster-status.tsx @@ -10,7 +10,7 @@ import { Icon } from "../icon"; import { Button } from "../button"; import { cssNames, IClassName } from "../../utils"; import { Cluster } from "../../../main/cluster"; -import { ClusterId, clusterStore } from "../../../common/cluster-store"; +import { ClusterId, ClusterStore } from "../../../common/cluster-store"; import { CubeSpinner } from "../spinner"; import { clusterActivateHandler } from "../../../common/cluster-ipc"; @@ -25,7 +25,7 @@ export class ClusterStatus extends React.Component { @observable isReconnecting = false; get cluster(): Cluster { - return clusterStore.getById(this.props.clusterId); + return ClusterStore.getInstance().getById(this.props.clusterId); } @computed get hasErrors(): boolean { diff --git a/src/renderer/components/cluster-manager/cluster-view.tsx b/src/renderer/components/cluster-manager/cluster-view.tsx index 8d89b7a81d..fbc55d8db3 100644 --- a/src/renderer/components/cluster-manager/cluster-view.tsx +++ b/src/renderer/components/cluster-manager/cluster-view.tsx @@ -7,9 +7,9 @@ import { IClusterViewRouteParams } from "./cluster-view.route"; import { ClusterStatus } from "./cluster-status"; import { hasLoadedView } from "./lens-views"; import { Cluster } from "../../../main/cluster"; -import { clusterStore } from "../../../common/cluster-store"; import { navigate } from "../../navigation"; import { catalogURL } from "../+catalog"; +import { ClusterStore } from "../../../common/cluster-store"; interface Props extends RouteComponentProps { } @@ -21,12 +21,12 @@ export class ClusterView extends React.Component { } get cluster(): Cluster { - return clusterStore.getById(this.clusterId); + return ClusterStore.getInstance().getById(this.clusterId); } async componentDidMount() { disposeOnUnmount(this, [ - reaction(() => this.clusterId, clusterId => clusterStore.setActive(clusterId), { + reaction(() => this.clusterId, clusterId => ClusterStore.getInstance().setActive(clusterId), { fireImmediately: true, }), reaction(() => this.cluster.online, (online) => { diff --git a/src/renderer/components/cluster-manager/lens-views.ts b/src/renderer/components/cluster-manager/lens-views.ts index a3a92e4f64..30fae018cc 100644 --- a/src/renderer/components/cluster-manager/lens-views.ts +++ b/src/renderer/components/cluster-manager/lens-views.ts @@ -1,5 +1,5 @@ import { observable, when } from "mobx"; -import { ClusterId, clusterStore, getClusterFrameUrl } from "../../../common/cluster-store"; +import { ClusterId, ClusterStore, getClusterFrameUrl } from "../../../common/cluster-store"; import { getMatchedClusterId } from "../../navigation"; import logger from "../../../main/logger"; @@ -19,7 +19,7 @@ export async function initView(clusterId: ClusterId) { if (!clusterId || lensViews.has(clusterId)) { return; } - const cluster = clusterStore.getById(clusterId); + const cluster = ClusterStore.getInstance().getById(clusterId); if (!cluster) { return; @@ -44,13 +44,9 @@ export async function initView(clusterId: ClusterId) { export async function autoCleanOnRemove(clusterId: ClusterId, iframe: HTMLIFrameElement) { await when(() => { - const cluster = clusterStore.getById(clusterId); + const cluster = ClusterStore.getInstance().getById(clusterId); - if (!cluster) return true; - - const view = lensViews.get(clusterId); - - return cluster.disconnected && view?.isLoaded; + return !cluster || (cluster.disconnected && lensViews.get(clusterId)?.isLoaded); }); logger.info(`[LENS-VIEW]: remove dashboard, clusterId=${clusterId}`); lensViews.delete(clusterId); @@ -64,7 +60,7 @@ export async function autoCleanOnRemove(clusterId: ClusterId, iframe: HTMLIFrame } export function refreshViews() { - const cluster = clusterStore.getById(getMatchedClusterId()); + const cluster = ClusterStore.getInstance().getById(getMatchedClusterId()); lensViews.forEach(({ clusterId, view, isLoaded }) => { const isCurrent = clusterId === cluster?.id; diff --git a/src/renderer/components/cluster-settings/cluster-settings.command.ts b/src/renderer/components/cluster-settings/cluster-settings.command.ts index 061d135d67..419fcbabe4 100644 --- a/src/renderer/components/cluster-settings/cluster-settings.command.ts +++ b/src/renderer/components/cluster-settings/cluster-settings.command.ts @@ -1,7 +1,7 @@ import { navigate } from "../../navigation"; import { commandRegistry } from "../../../extensions/registries/command-registry"; -import { clusterStore } from "../../../common/cluster-store"; import { entitySettingsURL } from "../+entity-settings"; +import { ClusterStore } from "../../../common/cluster-store"; commandRegistry.add({ id: "cluster.viewCurrentClusterSettings", @@ -9,7 +9,7 @@ commandRegistry.add({ scope: "global", action: () => navigate(entitySettingsURL({ params: { - entityId: clusterStore.active.id + entityId: ClusterStore.getInstance().active.id } })), isActive: (context) => !!context.entity diff --git a/src/renderer/components/cluster-settings/cluster-settings.tsx b/src/renderer/components/cluster-settings/cluster-settings.tsx index 86297e800e..247e098126 100644 --- a/src/renderer/components/cluster-settings/cluster-settings.tsx +++ b/src/renderer/components/cluster-settings/cluster-settings.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { clusterStore } from "../../../common/cluster-store"; +import { ClusterStore } from "../../../common/cluster-store"; import { ClusterProxySetting } from "./components/cluster-proxy-setting"; import { ClusterNameSetting } from "./components/cluster-name-setting"; import { ClusterHomeDirSetting } from "./components/cluster-home-dir-setting"; @@ -13,7 +13,7 @@ import { CatalogEntity } from "../../api/catalog-entity"; function getClusterForEntity(entity: CatalogEntity) { - const cluster = clusterStore.getById(entity.metadata.uid); + const cluster = ClusterStore.getInstance().getById(entity.metadata.uid); if (!cluster?.enabled) { return null; diff --git a/src/renderer/components/cluster-settings/components/remove-cluster-button.tsx b/src/renderer/components/cluster-settings/components/remove-cluster-button.tsx index 2d509c490f..20c1b2b62c 100644 --- a/src/renderer/components/cluster-settings/components/remove-cluster-button.tsx +++ b/src/renderer/components/cluster-settings/components/remove-cluster-button.tsx @@ -1,6 +1,6 @@ import React from "react"; import { observer } from "mobx-react"; -import { clusterStore } from "../../../../common/cluster-store"; +import { ClusterStore } from "../../../../common/cluster-store"; import { Cluster } from "../../../../main/cluster"; import { autobind } from "../../../utils"; import { Button } from "../../button"; @@ -21,7 +21,7 @@ export class RemoveClusterButton extends React.Component { labelOk: "Yes", labelCancel: "No", ok: async () => { - await clusterStore.removeById(cluster.id); + await ClusterStore.getInstance().removeById(cluster.id); } }); } diff --git a/src/renderer/components/command-palette/command-dialog.tsx b/src/renderer/components/command-palette/command-dialog.tsx index c784612345..2ffa56d898 100644 --- a/src/renderer/components/command-palette/command-dialog.tsx +++ b/src/renderer/components/command-palette/command-dialog.tsx @@ -4,7 +4,7 @@ import { computed, observable, toJS } from "mobx"; import { observer } from "mobx-react"; import React from "react"; import { commandRegistry } from "../../../extensions/registries/command-registry"; -import { clusterStore } from "../../../common/cluster-store"; +import { ClusterStore } from "../../../common/cluster-store"; import { CommandOverlay } from "./command-container"; import { broadcastMessage } from "../../../common/ipc"; import { navigate } from "../../navigation"; @@ -20,7 +20,7 @@ export class CommandDialog extends React.Component { }; return commandRegistry.getItems().filter((command) => { - if (command.scope === "entity" && !clusterStore.active) { + if (command.scope === "entity" && !ClusterStore.getInstance().active) { return false; } diff --git a/src/renderer/components/dock/__test__/dock-tabs.test.tsx b/src/renderer/components/dock/__test__/dock-tabs.test.tsx index 1e8a18c6b5..a11c9f34de 100644 --- a/src/renderer/components/dock/__test__/dock-tabs.test.tsx +++ b/src/renderer/components/dock/__test__/dock-tabs.test.tsx @@ -16,6 +16,20 @@ const getComponent = () => ( /> ); +Object.defineProperty(window, "matchMedia", { + writable: true, + value: jest.fn().mockImplementation(query => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), // Deprecated + removeListener: jest.fn(), // Deprecated + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), +}); + const renderTabs = () => render(getComponent()); const getTabKinds = () => dockStore.tabs.map(tab => tab.kind); 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 d373d0fbab..90f4b79972 100644 --- a/src/renderer/components/dock/__test__/log-resource-selector.test.tsx +++ b/src/renderer/components/dock/__test__/log-resource-selector.test.tsx @@ -7,6 +7,8 @@ import { Pod } from "../../../api/endpoints"; import { LogResourceSelector } from "../log-resource-selector"; import { LogTabData } from "../log-tab.store"; import { dockerPod, deploymentPod1 } from "./pod.mock"; +import { ThemeStore } from "../../../theme.store"; +import { UserStore } from "../../../../common/user-store"; const getComponent = (tabData: LogTabData) => { return ( @@ -41,6 +43,16 @@ const getFewPodsTabData = (): LogTabData => { }; describe("", () => { + beforeEach(() => { + UserStore.getInstanceOrCreate(); + ThemeStore.getInstanceOrCreate(); + }); + + afterEach(() => { + UserStore.resetInstance(); + ThemeStore.resetInstance(); + }); + it("renders w/o errors", () => { const tabData = getOnePodTabData(); const { container } = render(getComponent(tabData)); diff --git a/src/renderer/components/dock/log-list.tsx b/src/renderer/components/dock/log-list.tsx index a4167fdcb6..9c5accf1ef 100644 --- a/src/renderer/components/dock/log-list.tsx +++ b/src/renderer/components/dock/log-list.tsx @@ -10,7 +10,7 @@ import moment from "moment-timezone"; import { Align, ListOnScrollProps } from "react-window"; import { SearchStore, searchStore } from "../../../common/search-store"; -import { userStore } from "../../../common/user-store"; +import { UserStore } from "../../../common/user-store"; import { cssNames } from "../../utils"; import { Button } from "../button"; import { Icon } from "../icon"; @@ -81,7 +81,7 @@ export class LogList extends React.Component { @computed get logs() { const showTimestamps = logTabStore.getData(this.props.id).showTimestamps; - const { preferences } = userStore; + const { preferences } = UserStore.getInstance(); if (!showTimestamps) { return logStore.logsWithoutTimestamps; diff --git a/src/renderer/components/dock/terminal-window.tsx b/src/renderer/components/dock/terminal-window.tsx index 094ed50521..7ce69443ed 100644 --- a/src/renderer/components/dock/terminal-window.tsx +++ b/src/renderer/components/dock/terminal-window.tsx @@ -7,7 +7,7 @@ import { cssNames } from "../../utils"; import { IDockTab } from "./dock.store"; import { Terminal } from "./terminal"; import { terminalStore } from "./terminal.store"; -import { themeStore } from "../../theme.store"; +import { ThemeStore } from "../../theme.store"; interface Props { className?: string; @@ -38,7 +38,7 @@ export class TerminalWindow extends React.Component { return (
this.elem = e} /> ); diff --git a/src/renderer/components/dock/terminal.ts b/src/renderer/components/dock/terminal.ts index 7d8c19b8f6..1eb53b6929 100644 --- a/src/renderer/components/dock/terminal.ts +++ b/src/renderer/components/dock/terminal.ts @@ -4,9 +4,10 @@ import { Terminal as XTerm } from "xterm"; import { FitAddon } from "xterm-addon-fit"; import { dockStore, TabId } from "./dock.store"; import { TerminalApi } from "../../api/terminal-api"; -import { themeStore } from "../../theme.store"; +import { ThemeStore } from "../../theme.store"; import { autobind } from "../../utils"; import { isMac } from "../../../common/vars"; +import { camelCase } from "lodash"; export class Terminal { static spawningPool: HTMLElement; @@ -40,16 +41,10 @@ export class Terminal { // Replacing keys stored in styles to format accepted by terminal // E.g. terminalBrightBlack -> brightBlack const colorPrefix = "terminal"; - const terminalColors = Object.entries(colors) + const terminalColorEntries = Object.entries(colors) .filter(([name]) => name.startsWith(colorPrefix)) - .reduce((colors, [name, color]) => { - const colorName = name.split("").slice(colorPrefix.length); - - colorName[0] = colorName[0].toLowerCase(); - colors[colorName.join("")] = color; - - return colors; - }, {}); + .map(([name, color]) => [camelCase(name.slice(colorPrefix.length)), color]); + const terminalColors = Object.fromEntries(terminalColorEntries); this.xterm.setOption("theme", terminalColors); } @@ -109,7 +104,7 @@ export class Terminal { window.addEventListener("resize", this.onResize); this.disposers.push( - reaction(() => toJS(themeStore.activeTheme.colors), this.setTheme, { + reaction(() => toJS(ThemeStore.getInstance().activeTheme.colors), this.setTheme, { fireImmediately: true }), dockStore.onResize(this.onResize), diff --git a/src/renderer/components/hotbar/hotbar-icon.tsx b/src/renderer/components/hotbar/hotbar-icon.tsx index 29902bc6a0..1a7398c55d 100644 --- a/src/renderer/components/hotbar/hotbar-icon.tsx +++ b/src/renderer/components/hotbar/hotbar-icon.tsx @@ -10,7 +10,7 @@ import { Menu, MenuItem } from "../menu"; import { Icon } from "../icon"; import { observable } from "mobx"; import { navigate } from "../../navigation"; -import { hotbarStore } from "../../../common/hotbar-store"; +import { HotbarStore } from "../../../common/hotbar-store"; import { ConfirmDialog } from "../confirm-dialog"; interface Props extends DOMAttributes { @@ -60,7 +60,7 @@ export class HotbarIcon extends React.Component { } removeFromHotbar(item: CatalogEntity) { - const hotbar = hotbarStore.getByName("default"); // FIXME + const hotbar = HotbarStore.getInstance().getByName("default"); // FIXME if (!hotbar) { return; diff --git a/src/renderer/components/hotbar/hotbar-menu.tsx b/src/renderer/components/hotbar/hotbar-menu.tsx index 05a95c606c..056494338d 100644 --- a/src/renderer/components/hotbar/hotbar-menu.tsx +++ b/src/renderer/components/hotbar/hotbar-menu.tsx @@ -5,7 +5,7 @@ import { observer } from "mobx-react"; import { HotbarIcon } from "./hotbar-icon"; import { cssNames, IClassName } from "../../utils"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; -import { hotbarStore } from "../../../common/hotbar-store"; +import { HotbarStore } from "../../../common/hotbar-store"; import { catalogEntityRunContext } from "../../api/catalog-entity"; interface Props { @@ -16,7 +16,7 @@ interface Props { export class HotbarMenu extends React.Component { get hotbarItems() { - const hotbar = hotbarStore.getByName("default"); // FIXME + const hotbar = HotbarStore.getInstance().getByName("default"); // FIXME if (!hotbar) { return []; @@ -47,4 +47,3 @@ export class HotbarMenu extends React.Component { ); } } - diff --git a/src/renderer/components/item-object-list/item-list-layout.tsx b/src/renderer/components/item-object-list/item-list-layout.tsx index b609b046ea..95d9667a56 100644 --- a/src/renderer/components/item-object-list/item-list-layout.tsx +++ b/src/renderer/components/item-object-list/item-list-layout.tsx @@ -16,11 +16,11 @@ import { Filter, FilterType, pageFilters } from "./page-filters.store"; import { PageFiltersList } from "./page-filters-list"; import { PageFiltersSelect } from "./page-filters-select"; import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter"; -import { themeStore } from "../../theme.store"; +import { ThemeStore } from "../../theme.store"; import { MenuActions } from "../menu/menu-actions"; import { MenuItem } from "../menu"; import { Checkbox } from "../checkbox"; -import { userStore } from "../../../common/user-store"; +import { UserStore } from "../../../common/user-store"; import { namespaceStore } from "../+namespaces/namespace.store"; // todo: refactor, split to small re-usable components @@ -440,7 +440,7 @@ export class ItemListLayout extends React.Component { const { removeItemsDialog, items } = this; const { selectedItems } = store; const selectedItemId = detailsItem && detailsItem.getId(); - const classNames = cssNames(className, "box", "grow", themeStore.activeTheme.type); + const classNames = cssNames(className, "box", "grow", ThemeStore.getInstance().activeTheme.type); return (
@@ -469,7 +469,7 @@ export class ItemListLayout extends React.Component { } @computed get hiddenColumns() { - return userStore.getHiddenTableColumns(this.props.tableId); + return UserStore.getInstance().getHiddenTableColumns(this.props.tableId); } isHiddenColumn({ id: columnId, showWithColumn }: TableCellProps): boolean { @@ -491,7 +491,7 @@ export class ItemListLayout extends React.Component { hiddenColumns.delete(columnId); } - userStore.setHiddenTableColumns(this.props.tableId, hiddenColumns); + UserStore.getInstance().setHiddenTableColumns(this.props.tableId, hiddenColumns); } renderColumnVisibilityMenu() { diff --git a/src/renderer/components/layout/__test__/main-layout-header.test.tsx b/src/renderer/components/layout/__test__/main-layout-header.test.tsx index 04e455b33d..d639db92ab 100644 --- a/src/renderer/components/layout/__test__/main-layout-header.test.tsx +++ b/src/renderer/components/layout/__test__/main-layout-header.test.tsx @@ -6,6 +6,7 @@ import "@testing-library/jest-dom/extend-expect"; import { MainLayoutHeader } from "../main-layout-header"; import { Cluster } from "../../../../main/cluster"; +import { ClusterStore } from "../../../../common/cluster-store"; const cluster: Cluster = new Cluster({ id: "foo", @@ -14,6 +15,14 @@ const cluster: Cluster = new Cluster({ }); describe("", () => { + beforeEach(() => { + ClusterStore.getInstanceOrCreate(); + }); + + afterEach(() => { + ClusterStore.resetInstance(); + }); + it("renders w/o errors", () => { const { container } = render(); diff --git a/src/renderer/components/locale-date/locale-date.tsx b/src/renderer/components/locale-date/locale-date.tsx index 0477150c00..42223bbf69 100644 --- a/src/renderer/components/locale-date/locale-date.tsx +++ b/src/renderer/components/locale-date/locale-date.tsx @@ -1,7 +1,7 @@ import React from "react"; import { observer } from "mobx-react"; import moment from "moment-timezone"; -import { userStore } from "../../../common/user-store"; +import { UserStore } from "../../../common/user-store"; interface Props { date: string @@ -10,7 +10,7 @@ interface Props { @observer export class LocaleDate extends React.Component { render() { - const { preferences } = userStore; + const { preferences } = UserStore.getInstance(); const { date } = this.props; return <>{moment.tz(date, preferences.localeTimezone).format()}; diff --git a/src/renderer/components/select/select.tsx b/src/renderer/components/select/select.tsx index e816633804..c0491cf54e 100644 --- a/src/renderer/components/select/select.tsx +++ b/src/renderer/components/select/select.tsx @@ -8,7 +8,7 @@ import { observer } from "mobx-react"; import { autobind, cssNames } from "../../utils"; import ReactSelect, { ActionMeta, components, Props as ReactSelectProps, Styles } from "react-select"; import Creatable, { CreatableProps } from "react-select/creatable"; -import { themeStore } from "../../theme.store"; +import { ThemeStore } from "../../theme.store"; const { Menu } = components; @@ -40,7 +40,7 @@ export class Select extends React.Component { }; @computed get theme() { - return this.props.themeName || themeStore.activeTheme.type; + return this.props.themeName || ThemeStore.getInstance().activeTheme.type; } private styles: Styles = { diff --git a/src/renderer/ipc/index.tsx b/src/renderer/ipc/index.tsx index 119f13a0dc..924e313880 100644 --- a/src/renderer/ipc/index.tsx +++ b/src/renderer/ipc/index.tsx @@ -5,7 +5,7 @@ import { Notifications, notificationsStore } from "../components/notifications"; import { Button } from "../components/button"; import { isMac } from "../../common/vars"; import { invalidKubeconfigHandler } from "./invalid-kubeconfig-handler"; -import { clusterStore } from "../../common/cluster-store"; +import { ClusterStore } from "../../common/cluster-store"; import { navigate } from "../navigation"; import { entitySettingsURL } from "../components/+entity-settings"; @@ -76,7 +76,7 @@ function ListNamespacesForbiddenHandler(event: IpcRendererEvent, ...[clusterId]: (
Add Accessible Namespaces -

Cluster {clusterStore.active.name} does not have permissions to list namespaces. Please add the namespaces you have access to.

+

Cluster {ClusterStore.getInstance().active.name} does not have permissions to list namespaces. Please add the namespaces you have access to.