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

Fixing Singleton typing to correctly return child class (#1914)

- Add distinction between `getInstance` and `getInstanceOrCreate` since
  it is not always possible to create an instance (since you might not
  know the correct arguments)

- Remove all the `export const *Store = *Store.getInstance<*Store>();`
  calls as it defeats the purpose of `Singleton`. Plus with the typing
  changes the appropriate `*Store.getInstance()` is "short enough".

- Special case the two extension export facades to not need to use
  `getInstanceOrCreate`. Plus since they are just facades it is always
  possible to create them.

- Move some other types to be also `Singleton`'s: ExtensionLoader,
  ExtensionDiscovery, ThemeStore, LocalizationStore, ...

- Fixed dev-run always using the same port with electron inspect

- Update Store documentation with new recommendations about creating
  instances of singletons

- Fix all unit tests to create their dependent singletons

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2021-04-21 09:59:59 -04:00 committed by GitHub
parent b2a570ce28
commit 9563ead2e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
114 changed files with 924 additions and 736 deletions

View File

@ -79,6 +79,8 @@ module.exports = {
sourceType: "module", sourceType: "module",
}, },
rules: { rules: {
"no-invalid-this": "off",
"@typescript-eslint/no-invalid-this": ["error"],
"@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/explicit-module-boundary-types": "off",
@ -137,6 +139,8 @@ module.exports = {
jsx: true, jsx: true,
}, },
rules: { rules: {
"no-invalid-this": "off",
"@typescript-eslint/no-invalid-this": ["error"],
"@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/interface-name-prefix": "off", "@typescript-eslint/interface-name-prefix": "off",

View File

@ -30,7 +30,7 @@ Each guide or code sample includes the following:
| Sample | APIs | | Sample | APIs |
| ----- | ----- | | ----- | ----- |
[hello-world](https://github.com/lensapp/lens-extension-samples/tree/master/helloworld-sample) | LensMainExtension <br> LensRendererExtension <br> Component.Icon <br> Component.IconProps | [hello-world](https://github.com/lensapp/lens-extension-samples/tree/master/helloworld-sample) | LensMainExtension <br> LensRendererExtension <br> Component.Icon <br> Component.IconProps |
[minikube](https://github.com/lensapp/lens-extension-samples/tree/master/minikube-sample) | LensMainExtension <br> Store.clusterStore <br> Store.workspaceStore | [minikube](https://github.com/lensapp/lens-extension-samples/tree/master/minikube-sample) | LensMainExtension <br> Store.ClusterStore <br> Store.workspaceStore |
[styling-css-modules-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-css-modules-sample) | LensMainExtension <br> LensRendererExtension <br> Component.Icon <br> Component.IconProps | [styling-css-modules-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-css-modules-sample) | LensMainExtension <br> LensRendererExtension <br> Component.Icon <br> Component.IconProps |
[styling-emotion-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-emotion-sample) | LensMainExtension <br> LensRendererExtension <br> Component.Icon <br> Component.IconProps | [styling-emotion-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-emotion-sample) | LensMainExtension <br> LensRendererExtension <br> Component.Icon <br> Component.IconProps |
[styling-sass-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-sass-sample) | LensMainExtension <br> LensRendererExtension <br> Component.Icon <br> Component.IconProps | [styling-sass-sample](https://github.com/lensapp/lens-extension-samples/tree/master/styling-sass-sample) | LensMainExtension <br> LensRendererExtension <br> Component.Icon <br> Component.IconProps |

View File

@ -45,8 +45,6 @@ It accesses some Lens state data, and it periodically logs the name of the clust
```typescript ```typescript
import { LensMainExtension, Store } from "@k8slens/extensions"; import { LensMainExtension, Store } from "@k8slens/extensions";
const clusterStore = Store.clusterStore
export default class ActiveClusterExtensionMain extends LensMainExtension { export default class ActiveClusterExtensionMain extends LensMainExtension {
timer: NodeJS.Timeout timer: NodeJS.Timeout
@ -54,11 +52,11 @@ export default class ActiveClusterExtensionMain extends LensMainExtension {
onActivate() { onActivate() {
console.log("Cluster logger activated"); console.log("Cluster logger activated");
this.timer = setInterval(() => { this.timer = setInterval(() => {
if (!clusterStore.active) { if (!Store.ClusterStore.getInstance().active) {
console.log("No active cluster"); console.log("No active cluster");
return; return;
} }
console.log("active cluster is", clusterStore.active.contextName) console.log("active cluster is", Store.ClusterStore.getInstance().active.contextName)
}, 5000) }, 5000)
} }

View File

@ -57,7 +57,7 @@ The example above logs messages when the extension is enabled and disabled.
Cluster pages appear in the cluster dashboard. Cluster pages appear in the cluster dashboard.
Use cluster pages to display information about or add functionality to the active cluster. 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. 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: Add a cluster page definition to a `LensRendererExtension` subclass with the following example:

View File

@ -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. 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. 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: The following example code creates a store for the `appPreferences` guide example:
``` typescript ``` typescript
@ -50,8 +56,6 @@ export class ExamplePreferencesStore extends Store.ExtensionStore<ExamplePrefere
}); });
} }
} }
export const examplePreferencesStore = ExamplePreferencesStore.getInstance<ExamplePreferencesStore>();
``` ```
First, our example defines the extension's data model using the simple `ExamplePreferencesModel` type. 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. `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. 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<ExamplePreferencesStore>()`, and exported for use by other parts of the extension. Finally, `ExamplePreferencesStore` is created by calling `ExamplePreferencesStore.getInstanceOrCreate()`, and exported for use by other parts of the extension.
Note that `examplePreferencesStore` is a singleton. Note that `ExamplePreferencesStore` is a singleton.
Calling this function again will not create a new store. 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. 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`: This can be done in `./main.ts`:
``` typescript ``` typescript
import { LensMainExtension } from "@k8slens/extensions"; 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 { export default class ExampleMainExtension extends LensMainExtension {
async onActivate() { 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`. 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. Similarly, `ExamplePreferencesStore` must load in the renderer process where the `appPreferences` are handled.
This can be done in `./renderer.ts`: This can be done in `./renderer.ts`:
``` typescript ``` typescript
import { LensRendererExtension } from "@k8slens/extensions"; import { LensRendererExtension } from "@k8slens/extensions";
import { ExamplePreferenceHint, ExamplePreferenceInput } from "./src/example-preference"; 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"; import React from "react";
export default class ExampleRendererExtension extends LensRendererExtension { export default class ExampleRendererExtension extends LensRendererExtension {
async onActivate() { async onActivate() {
await examplePreferencesStore.loadExtension(this); await ExamplePreferencesStore.getInstanceOrCreate().loadExtension(this);
} }
appPreferences = [ appPreferences = [
{ {
title: "Example Preferences", title: "Example Preferences",
components: { components: {
Input: () => <ExamplePreferenceInput preference={examplePreferencesStore}/>, Input: () => <ExamplePreferenceInput />,
Hint: () => <ExamplePreferenceHint/> Hint: () => <ExamplePreferenceHint/>
} }
} }
@ -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`. Again, `ExamplePreferencesStore.getInstanceOrCreate().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`.
`ExamplePreferenceInput` is defined in `./src/example-preference.tsx`: `ExamplePreferenceInput` is defined in `./src/example-preference.tsx`:
``` typescript ``` typescript
@ -128,21 +137,15 @@ import { observer } from "mobx-react";
import React from "react"; import React from "react";
import { ExamplePreferencesStore } from "./example-preference-store"; import { ExamplePreferencesStore } from "./example-preference-store";
export class ExamplePreferenceProps {
preference: ExamplePreferencesStore;
}
@observer @observer
export class ExamplePreferenceInput extends React.Component<ExamplePreferenceProps> { export class ExamplePreferenceInput extends React.Component {
render() { render() {
const { preference } = this.props;
return ( return (
<Component.Checkbox <Component.Checkbox
label="I understand appPreferences" label="I understand appPreferences"
value={preference.enabled} value={ExamplePreferencesStore.getInstace().enabled}
onChange={v => { preference.enabled = v; }} 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. 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 Everything else works as before, except that now the `enabled` state persists across Lens restarts because it is managed by the
`examplePreferencesStore`. `ExamplePreferencesStore`.

View File

@ -60,8 +60,8 @@ describe("Lens integration tests", () => {
it("ensures helm repos", async () => { it("ensures helm repos", async () => {
const repos = await listHelmRepositories(); const repos = await listHelmRepositories();
if (!repos[0]) { if (repos.length === 0) {
fail("Lens failed to add Bitnami repository"); fail("Lens failed to add any repositories");
} }
await app.client.click("[data-testid=kube-tab]"); await app.client.click("[data-testid=kube-tab]");

View File

@ -114,16 +114,14 @@ type HelmRepository = {
url: string; url: string;
}; };
export async function listHelmRepositories(retries = 0): Promise<HelmRepository[]>{ export async function listHelmRepositories(): Promise<HelmRepository[]>{
if (retries < 5) { for (let i = 0; i < 10; i += 1) {
try { 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 { } catch {
await new Promise(r => setTimeout(r, 2000)); // if no repositories, wait for Lens adding bitnami repository await new Promise(r => setTimeout(r, 2000)); // if no repositories, wait for Lens adding bitnami repository
return await listHelmRepositories((retries + 1));
} }
} }

View File

@ -4,8 +4,9 @@ import yaml from "js-yaml";
import { Cluster } from "../../main/cluster"; import { Cluster } from "../../main/cluster";
import { ClusterStore, getClusterIdFromHost } from "../cluster-store"; import { ClusterStore, getClusterIdFromHost } from "../cluster-store";
import { Console } from "console"; 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 testDataIcon = fs.readFileSync("test-data/cluster-store-migration-icon.png");
const kubeconfig = ` const kubeconfig = `
@ -47,10 +48,8 @@ jest.mock("electron", () => {
}; };
}); });
let clusterStore: ClusterStore;
describe("empty config", () => { describe("empty config", () => {
beforeEach(() => { beforeEach(async () => {
ClusterStore.resetInstance(); ClusterStore.resetInstance();
const mockOpts = { const mockOpts = {
"tmp": { "tmp": {
@ -59,9 +58,8 @@ describe("empty config", () => {
}; };
mockFs(mockOpts); mockFs(mockOpts);
clusterStore = ClusterStore.getInstance<ClusterStore>();
return clusterStore.load(); await ClusterStore.getInstanceOrCreate().load();
}); });
afterEach(() => { afterEach(() => {
@ -70,7 +68,7 @@ describe("empty config", () => {
describe("with foo cluster added", () => { describe("with foo cluster added", () => {
beforeEach(() => { beforeEach(() => {
clusterStore.addCluster( ClusterStore.getInstance().addCluster(
new Cluster({ new Cluster({
id: "foo", id: "foo",
contextName: "foo", contextName: "foo",
@ -85,7 +83,7 @@ describe("empty config", () => {
}); });
it("adds new cluster to store", async () => { 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.id).toBe("foo");
expect(storedCluster.preferences.terminalCWD).toBe("/tmp"); expect(storedCluster.preferences.terminalCWD).toBe("/tmp");
@ -94,19 +92,19 @@ describe("empty config", () => {
}); });
it("removes cluster from store", async () => { it("removes cluster from store", async () => {
await clusterStore.removeById("foo"); await ClusterStore.getInstance().removeById("foo");
expect(clusterStore.getById("foo")).toBeNull(); expect(ClusterStore.getInstance().getById("foo")).toBeNull();
}); });
it("sets active cluster", () => { it("sets active cluster", () => {
clusterStore.setActive("foo"); ClusterStore.getInstance().setActive("foo");
expect(clusterStore.active.id).toBe("foo"); expect(ClusterStore.getInstance().active.id).toBe("foo");
}); });
}); });
describe("with prod and dev clusters added", () => { describe("with prod and dev clusters added", () => {
beforeEach(() => { beforeEach(() => {
clusterStore.addClusters( ClusterStore.getInstance().addClusters(
new Cluster({ new Cluster({
id: "prod", id: "prod",
contextName: "foo", contextName: "foo",
@ -127,8 +125,8 @@ describe("empty config", () => {
}); });
it("check if store can contain multiple clusters", () => { it("check if store can contain multiple clusters", () => {
expect(clusterStore.hasClusters()).toBeTruthy(); expect(ClusterStore.getInstance().hasClusters()).toBeTruthy();
expect(clusterStore.clusters.size).toBe(2); expect(ClusterStore.getInstance().clusters.size).toBe(2);
}); });
it("check if cluster's kubeconfig file saved", () => { it("check if cluster's kubeconfig file saved", () => {
@ -178,9 +176,8 @@ describe("config with existing clusters", () => {
}; };
mockFs(mockOpts); mockFs(mockOpts);
clusterStore = ClusterStore.getInstance<ClusterStore>();
return clusterStore.load(); return ClusterStore.getInstanceOrCreate().load();
}); });
afterEach(() => { afterEach(() => {
@ -188,24 +185,24 @@ describe("config with existing clusters", () => {
}); });
it("allows to retrieve a cluster", () => { it("allows to retrieve a cluster", () => {
const storedCluster = clusterStore.getById("cluster1"); const storedCluster = ClusterStore.getInstance().getById("cluster1");
expect(storedCluster.id).toBe("cluster1"); expect(storedCluster.id).toBe("cluster1");
expect(storedCluster.preferences.terminalCWD).toBe("/foo"); expect(storedCluster.preferences.terminalCWD).toBe("/foo");
}); });
it("allows to delete a cluster", () => { it("allows to delete a cluster", () => {
clusterStore.removeById("cluster2"); ClusterStore.getInstance().removeById("cluster2");
const storedCluster = clusterStore.getById("cluster1"); const storedCluster = ClusterStore.getInstance().getById("cluster1");
expect(storedCluster).toBeTruthy(); expect(storedCluster).toBeTruthy();
const storedCluster2 = clusterStore.getById("cluster2"); const storedCluster2 = ClusterStore.getInstance().getById("cluster2");
expect(storedCluster2).toBeNull(); expect(storedCluster2).toBeNull();
}); });
it("allows getting all of the clusters", async () => { it("allows getting all of the clusters", async () => {
const storedClusters = clusterStore.clustersList; const storedClusters = ClusterStore.getInstance().clustersList;
expect(storedClusters.length).toBe(3); expect(storedClusters.length).toBe(3);
expect(storedClusters[0].id).toBe("cluster1"); expect(storedClusters[0].id).toBe("cluster1");
@ -216,7 +213,7 @@ describe("config with existing clusters", () => {
}); });
it("marks owned cluster disabled by default", () => { it("marks owned cluster disabled by default", () => {
const storedClusters = clusterStore.clustersList; const storedClusters = ClusterStore.getInstance().clustersList;
expect(storedClusters[0].enabled).toBe(true); expect(storedClusters[0].enabled).toBe(true);
expect(storedClusters[2].enabled).toBe(false); expect(storedClusters[2].enabled).toBe(false);
@ -276,9 +273,8 @@ users:
}; };
mockFs(mockOpts); mockFs(mockOpts);
clusterStore = ClusterStore.getInstance<ClusterStore>();
return clusterStore.load(); return ClusterStore.getInstanceOrCreate().load();
}); });
afterEach(() => { afterEach(() => {
@ -286,7 +282,7 @@ users:
}); });
it("does not enable clusters with invalid kubeconfig", () => { it("does not enable clusters with invalid kubeconfig", () => {
const storedClusters = clusterStore.clustersList; const storedClusters = ClusterStore.getInstance().clustersList;
expect(storedClusters.length).toBe(2); expect(storedClusters.length).toBe(2);
expect(storedClusters[0].enabled).toBeFalsy; expect(storedClusters[0].enabled).toBeFalsy;
@ -319,9 +315,8 @@ describe("pre 2.0 config with an existing cluster", () => {
}; };
mockFs(mockOpts); mockFs(mockOpts);
clusterStore = ClusterStore.getInstance<ClusterStore>();
return clusterStore.load(); return ClusterStore.getInstanceOrCreate().load();
}); });
afterEach(() => { 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 () => { 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":[]`); 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: { 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); mockFs(mockOpts);
clusterStore = ClusterStore.getInstance<ClusterStore>();
return clusterStore.load(); return ClusterStore.getInstanceOrCreate().load();
}); });
afterEach(() => { 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 () => { 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 config = fs.readFileSync(file, "utf8");
const kc = yaml.safeLoad(config); 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["access-token"]).toBe("should be string");
expect(kc.users[0].user["auth-provider"].config["expiry"]).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); mockFs(mockOpts);
clusterStore = ClusterStore.getInstance<ClusterStore>();
return clusterStore.load(); return ClusterStore.getInstanceOrCreate().load();
}); });
afterEach(() => { afterEach(() => {
@ -407,7 +438,7 @@ describe("pre 2.6.0 config with a cluster icon", () => {
}); });
it("moves the icon into preferences", async () => { 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.hasOwnProperty("icon")).toBe(false);
expect(storedClusterData.preferences.hasOwnProperty("icon")).toBe(true); 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); mockFs(mockOpts);
clusterStore = ClusterStore.getInstance<ClusterStore>();
return clusterStore.load(); return ClusterStore.getInstanceOrCreate().load();
}); });
afterEach(() => { afterEach(() => {
@ -474,9 +504,8 @@ describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
}; };
mockFs(mockOpts); mockFs(mockOpts);
clusterStore = ClusterStore.getInstance<ClusterStore>();
return clusterStore.load(); return ClusterStore.getInstanceOrCreate().load();
}); });
afterEach(() => { 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 () => { 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); expect(fs.readFileSync(config, "utf8")).toBe(minimalValidKubeConfig);
}); });
it("migrates to modern format with icon not in file", async () => { 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); expect(icon.startsWith("data:;base64,")).toBe(true);
}); });

View File

@ -1,4 +1,8 @@
import { appEventBus, AppEvent } from "../event-bus"; 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("event bus tests", () => {
describe("emit", () => { describe("emit", () => {

View File

@ -1,8 +1,12 @@
import mockFs from "mock-fs"; import mockFs from "mock-fs";
import { HotbarStore, hotbarStore } from "../hotbar-store"; import { ClusterStore } from "../cluster-store";
import { HotbarStore } from "../hotbar-store";
describe("HotbarStore", () => { describe("HotbarStore", () => {
beforeEach(() => { beforeEach(() => {
ClusterStore.resetInstance();
ClusterStore.getInstanceOrCreate();
HotbarStore.resetInstance(); HotbarStore.resetInstance();
mockFs({ tmp: { "lens-hotbar-store.json": "{}" } }); mockFs({ tmp: { "lens-hotbar-store.json": "{}" } });
}); });
@ -13,8 +17,8 @@ describe("HotbarStore", () => {
describe("load", () => { describe("load", () => {
it("loads one hotbar by default", () => { it("loads one hotbar by default", () => {
hotbarStore.load(); HotbarStore.getInstanceOrCreate().load();
expect(hotbarStore.hotbars.length).toEqual(1); expect(HotbarStore.getInstance().hotbars.length).toEqual(1);
}); });
}); });
}); });

View File

@ -1,4 +1,8 @@
import { SearchStore } from "../search-store"; import { SearchStore } from "../search-store";
import { Console } from "console";
import { stdout, stderr } from "process";
console = new Console(stdout, stderr);
let searchStore: SearchStore = null; let searchStore: SearchStore = null;
const logs = [ const logs = [

View File

@ -10,7 +10,7 @@ jest.mock("electron", () => {
getVersion: () => "99.99.99", getVersion: () => "99.99.99",
getPath: () => "tmp", getPath: () => "tmp",
getLocale: () => "en", getLocale: () => "en",
setLoginItemSettings: jest.fn(), setLoginItemSettings: (): void => void 0,
} }
}; };
}); });
@ -18,12 +18,19 @@ jest.mock("electron", () => {
import { UserStore } from "../user-store"; import { UserStore } from "../user-store";
import { SemVer } from "semver"; import { SemVer } from "semver";
import electron from "electron"; import electron from "electron";
import { stdout, stderr } from "process";
console = new Console(stdout, stderr);
describe("user store tests", () => { describe("user store tests", () => {
describe("for an empty config", () => { describe("for an empty config", () => {
beforeEach(() => { beforeEach(() => {
UserStore.resetInstance(); 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(() => { afterEach(() => {
@ -31,14 +38,14 @@ describe("user store tests", () => {
}); });
it("allows setting and retrieving lastSeenAppVersion", () => { it("allows setting and retrieving lastSeenAppVersion", () => {
const us = UserStore.getInstance<UserStore>(); const us = UserStore.getInstance();
us.lastSeenAppVersion = "1.2.3"; us.lastSeenAppVersion = "1.2.3";
expect(us.lastSeenAppVersion).toBe("1.2.3"); expect(us.lastSeenAppVersion).toBe("1.2.3");
}); });
it("allows adding and listing seen contexts", () => { it("allows adding and listing seen contexts", () => {
const us = UserStore.getInstance<UserStore>(); const us = UserStore.getInstance();
us.seenContexts.add("foo"); us.seenContexts.add("foo");
expect(us.seenContexts.size).toBe(1); expect(us.seenContexts.size).toBe(1);
@ -51,7 +58,7 @@ describe("user store tests", () => {
}); });
it("allows setting and getting preferences", () => { it("allows setting and getting preferences", () => {
const us = UserStore.getInstance<UserStore>(); const us = UserStore.getInstance();
us.preferences.httpsProxy = "abcd://defg"; us.preferences.httpsProxy = "abcd://defg";
@ -63,7 +70,7 @@ describe("user store tests", () => {
}); });
it("correctly resets theme to default value", async () => { it("correctly resets theme to default value", async () => {
const us = UserStore.getInstance<UserStore>(); const us = UserStore.getInstance();
us.isLoaded = true; us.isLoaded = true;
@ -73,7 +80,7 @@ describe("user store tests", () => {
}); });
it("correctly calculates if the last seen version is an old release", () => { it("correctly calculates if the last seen version is an old release", () => {
const us = UserStore.getInstance<UserStore>(); const us = UserStore.getInstance();
expect(us.isNewVersion).toBe(true); expect(us.isNewVersion).toBe(true);
@ -94,6 +101,8 @@ describe("user store tests", () => {
}) })
} }
}); });
return UserStore.getInstanceOrCreate().load();
}); });
afterEach(() => { afterEach(() => {
@ -101,7 +110,7 @@ describe("user store tests", () => {
}); });
it("sets last seen app version to 0.0.0", () => { it("sets last seen app version to 0.0.0", () => {
const us = UserStore.getInstance<UserStore>(); const us = UserStore.getInstance();
expect(us.lastSeenAppVersion).toBe("0.0.0"); expect(us.lastSeenAppVersion).toBe("0.0.0");
}); });

View File

@ -3,7 +3,7 @@ import { observable } from "mobx";
import { catalogCategoryRegistry } from "../catalog-category-registry"; import { catalogCategoryRegistry } from "../catalog-category-registry";
import { CatalogCategory, CatalogEntity, CatalogEntityActionContext, CatalogEntityAddMenuContext, CatalogEntityContextMenuContext, CatalogEntityData, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog-entity"; import { CatalogCategory, CatalogEntity, CatalogEntityActionContext, CatalogEntityAddMenuContext, CatalogEntityContextMenuContext, CatalogEntityData, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog-entity";
import { clusterDisconnectHandler } from "../cluster-ipc"; import { clusterDisconnectHandler } from "../cluster-ipc";
import { clusterStore } from "../cluster-store"; import { ClusterStore } from "../cluster-store";
import { requestMain } from "../ipc"; import { requestMain } from "../ipc";
export type KubernetesClusterSpec = { export type KubernetesClusterSpec = {
@ -56,7 +56,7 @@ export class KubernetesCluster implements CatalogEntity {
icon: "delete", icon: "delete",
title: "Delete", title: "Delete",
onlyVisibleForSource: "local", onlyVisibleForSource: "local",
onClick: async () => clusterStore.removeById(this.metadata.uid), onClick: async () => ClusterStore.getInstance().removeById(this.metadata.uid),
confirm: { confirm: {
message: `Remove Kubernetes Cluster "${this.metadata.name} from Lens?` message: `Remove Kubernetes Cluster "${this.metadata.name} from Lens?`
} }
@ -68,7 +68,7 @@ export class KubernetesCluster implements CatalogEntity {
icon: "link_off", icon: "link_off",
title: "Disconnect", title: "Disconnect",
onClick: async () => { onClick: async () => {
clusterStore.deactivate(this.metadata.uid); ClusterStore.getInstance().deactivate(this.metadata.uid);
requestMain(clusterDisconnectHandler, this.metadata.uid); requestMain(clusterDisconnectHandler, this.metadata.uid);
} }
}); });

View File

@ -1,5 +1,5 @@
import { handleRequest } from "./ipc"; import { handleRequest } from "./ipc";
import { ClusterId, clusterStore } from "./cluster-store"; import { ClusterId, ClusterStore } from "./cluster-store";
import { appEventBus } from "./event-bus"; import { appEventBus } from "./event-bus";
import { ResourceApplier } from "../main/resource-applier"; import { ResourceApplier } from "../main/resource-applier";
import { ipcMain, IpcMainInvokeEvent } from "electron"; import { ipcMain, IpcMainInvokeEvent } from "electron";
@ -11,10 +11,9 @@ export const clusterRefreshHandler = "cluster:refresh";
export const clusterDisconnectHandler = "cluster:disconnect"; export const clusterDisconnectHandler = "cluster:disconnect";
export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all"; export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all";
if (ipcMain) { if (ipcMain) {
handleRequest(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => { handleRequest(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => {
const cluster = clusterStore.getById(clusterId); const cluster = ClusterStore.getInstance().getById(clusterId);
if (cluster) { if (cluster) {
return cluster.activate(force); return cluster.activate(force);
@ -22,7 +21,7 @@ if (ipcMain) {
}); });
handleRequest(clusterSetFrameIdHandler, (event: IpcMainInvokeEvent, clusterId: ClusterId) => { handleRequest(clusterSetFrameIdHandler, (event: IpcMainInvokeEvent, clusterId: ClusterId) => {
const cluster = clusterStore.getById(clusterId); const cluster = ClusterStore.getInstance().getById(clusterId);
if (cluster) { if (cluster) {
clusterFrameMap.set(cluster.id, { frameId: event.frameId, processId: event.processId }); clusterFrameMap.set(cluster.id, { frameId: event.frameId, processId: event.processId });
@ -32,14 +31,14 @@ if (ipcMain) {
}); });
handleRequest(clusterRefreshHandler, (event, clusterId: ClusterId) => { handleRequest(clusterRefreshHandler, (event, clusterId: ClusterId) => {
const cluster = clusterStore.getById(clusterId); const cluster = ClusterStore.getInstance().getById(clusterId);
if (cluster) return cluster.refresh({ refreshMetadata: true }); if (cluster) return cluster.refresh({ refreshMetadata: true });
}); });
handleRequest(clusterDisconnectHandler, (event, clusterId: ClusterId) => { handleRequest(clusterDisconnectHandler, (event, clusterId: ClusterId) => {
appEventBus.emit({name: "cluster", action: "stop"}); appEventBus.emit({name: "cluster", action: "stop"});
const cluster = clusterStore.getById(clusterId); const cluster = ClusterStore.getInstance().getById(clusterId);
if (cluster) { if (cluster) {
cluster.disconnect(); cluster.disconnect();
@ -49,7 +48,7 @@ if (ipcMain) {
handleRequest(clusterKubectlApplyAllHandler, (event, clusterId: ClusterId, resources: string[]) => { handleRequest(clusterKubectlApplyAllHandler, (event, clusterId: ClusterId, resources: string[]) => {
appEventBus.emit({name: "cluster", action: "kubectl-apply-all"}); appEventBus.emit({name: "cluster", action: "kubectl-apply-all"});
const cluster = clusterStore.getById(clusterId); const cluster = ClusterStore.getInstance().getById(clusterId);
if (cluster) { if (cluster) {
const applier = new ResourceApplier(cluster); const applier = new ResourceApplier(cluster);

View File

@ -112,7 +112,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
private static stateRequestChannel = "cluster:states"; private static stateRequestChannel = "cluster:states";
private constructor() { constructor() {
super({ super({
configName: "lens-cluster-store", configName: "lens-cluster-store",
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
@ -337,8 +337,6 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
} }
} }
export const clusterStore = ClusterStore.getInstance<ClusterStore>();
export function getClusterIdFromHost(host: string): ClusterId | undefined { export function getClusterIdFromHost(host: string): ClusterId | undefined {
// e.g host == "%clusterId.localhost:45345" // e.g host == "%clusterId.localhost:45345"
const subDomains = host.split(":")[0].split("."); const subDomains = host.split(":")[0].split(".");
@ -355,5 +353,5 @@ export function getHostedClusterId() {
} }
export function getHostedCluster(): Cluster { export function getHostedCluster(): Cluster {
return clusterStore.getById(getHostedClusterId()); return ClusterStore.getInstance().getById(getHostedClusterId());
} }

View File

@ -23,7 +23,7 @@ export interface HotbarStoreModel {
export class HotbarStore extends BaseStore<HotbarStoreModel> { export class HotbarStore extends BaseStore<HotbarStoreModel> {
@observable hotbars: Hotbar[] = []; @observable hotbars: Hotbar[] = [];
private constructor() { constructor() {
super({ super({
configName: "lens-hotbar-store", configName: "lens-hotbar-store",
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
@ -67,5 +67,3 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
}); });
} }
} }
export const hotbarStore = HotbarStore.getInstance<HotbarStore>();

View File

@ -5,8 +5,8 @@ import { pathToRegexp } from "path-to-regexp";
import logger from "../../main/logger"; import logger from "../../main/logger";
import Url from "url-parse"; import Url from "url-parse";
import { RoutingError, RoutingErrorType } from "./error"; import { RoutingError, RoutingErrorType } from "./error";
import { extensionsStore } from "../../extensions/extensions-store"; import { ExtensionsStore } from "../../extensions/extensions-store";
import { extensionLoader } from "../../extensions/extension-loader"; import { ExtensionLoader } from "../../extensions/extension-loader";
import { LensExtension } from "../../extensions/lens-extension"; import { LensExtension } from "../../extensions/lens-extension";
import { RouteHandler, RouteParams } from "../../extensions/registries/protocol-handler-registry"; 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 * 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) * @param url the url (in its current state)
*/ */
protected _route(routes: [string, RouteHandler][], url: Url, extensionName?: string): void { 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 { [EXTENSION_PUBLISHER_MATCH]: publisher, [EXTENSION_NAME_MATCH]: partialName } = match.params;
const name = [publisher, partialName].filter(Boolean).join("/"); const name = [publisher, partialName].filter(Boolean).join("/");
const extension = extensionLoader.userExtensionsByName.get(name); const extension = ExtensionLoader.getInstance().userExtensionsByName.get(name);
if (!extension) { if (!extension) {
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not installed`); logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not installed`);
@ -132,7 +132,7 @@ export abstract class LensProtocolRouter extends Singleton {
return name; return name;
} }
if (!extensionsStore.isEnabled(extension.id)) { if (!ExtensionsStore.getInstance().isEnabled(extension.id)) {
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not enabled`); logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not enabled`);
return name; return name;

View File

@ -1,12 +1,12 @@
import request from "request"; import request from "request";
import requestPromise from "request-promise-native"; import requestPromise from "request-promise-native";
import { userStore } from "./user-store"; import { UserStore } from "./user-store";
// todo: get rid of "request" (deprecated) // todo: get rid of "request" (deprecated)
// https://github.com/lensapp/lens/issues/459 // https://github.com/lensapp/lens/issues/459
function getDefaultRequestOpts(): Partial<request.Options> { function getDefaultRequestOpts(): Partial<request.Options> {
const { httpsProxy, allowUntrustedCAs } = userStore.preferences; const { httpsProxy, allowUntrustedCAs } = UserStore.getInstance().preferences;
return { return {
proxy: httpsProxy || undefined, proxy: httpsProxy || undefined,

View File

@ -38,7 +38,7 @@ export interface UserPreferences {
export class UserStore extends BaseStore<UserStoreModel> { export class UserStore extends BaseStore<UserStoreModel> {
static readonly defaultTheme: ThemeId = "lens-dark"; static readonly defaultTheme: ThemeId = "lens-dark";
private constructor() { constructor() {
super({ super({
configName: "lens-user-store", configName: "lens-user-store",
migrations, migrations,
@ -163,14 +163,6 @@ export class UserStore extends BaseStore<UserStoreModel> {
this.newContexts.clear(); this.newContexts.clear();
} }
/**
* Getting default directory to download kubectl binaries
* @returns string
*/
getDefaultKubectlPath(): string {
return path.join((app || remote.app).getPath("userData"), "binaries");
}
@action @action
protected async fromStore(data: Partial<UserStoreModel> = {}) { protected async fromStore(data: Partial<UserStoreModel> = {}) {
const { lastSeenAppVersion, seenContexts = [], preferences, kubeConfigPath } = data; const { lastSeenAppVersion, seenContexts = [], preferences, kubeConfigPath } = data;
@ -200,4 +192,10 @@ export class UserStore extends BaseStore<UserStoreModel> {
} }
} }
export const userStore = UserStore.getInstance<UserStore>(); /**
* Getting default directory to download kubectl binaries
* @returns string
*/
export function getDefaultKubectlPath(): string {
return path.join((app || remote.app).getPath("userData"), "binaries");
}

View File

@ -3,8 +3,8 @@
export function debouncePromise<T, F extends any[]>(func: (...args: F) => T | Promise<T>, timeout = 0): (...args: F) => Promise<T> { export function debouncePromise<T, F extends any[]>(func: (...args: F) => T | Promise<T>, timeout = 0): (...args: F) => Promise<T> {
let timer: NodeJS.Timeout; let timer: NodeJS.Timeout;
return (...params: any[]) => new Promise(resolve => { return (...params: F) => new Promise(resolve => {
clearTimeout(timer); clearTimeout(timer);
timer = global.setTimeout(() => resolve(func.apply(this, params)), timeout); timer = global.setTimeout(() => resolve(func(...params)), timeout);
}); });
} }

View File

@ -5,25 +5,39 @@
* @example * @example
* const usersStore: UsersStore = UsersStore.getInstance(); * const usersStore: UsersStore = UsersStore.getInstance();
*/ */
type StaticThis<T, R extends any[]> = { new(...args: R): T };
type Constructor<T = {}> = new (...args: any[]) => T; export class Singleton {
class Singleton {
private static instances = new WeakMap<object, Singleton>(); private static instances = new WeakMap<object, Singleton>();
private static creating = "";
// todo: improve types inferring constructor() {
static getInstance<T>(...args: ConstructorParameters<Constructor<T>>): T { if (Singleton.creating.length === 0) {
throw new TypeError("A singleton class must be created by getInstanceOrCreate()");
}
}
static getInstanceOrCreate<T, R extends any[]>(this: StaticThis<T, R>, ...args: R): T {
if (!Singleton.instances.has(this)) { 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; return Singleton.instances.get(this) as T;
} }
static getInstance<T, R extends any[]>(this: StaticThis<T, R>, 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() { static resetInstance() {
Singleton.instances.delete(this); Singleton.instances.delete(this);
} }
} }
export { Singleton };
export default Singleton; export default Singleton;

View File

@ -1,6 +1,7 @@
import { watch } from "chokidar"; import { watch } from "chokidar";
import { join, normalize } from "path"; import { join, normalize } from "path";
import { ExtensionDiscovery, InstalledExtension } from "../extension-discovery"; import { ExtensionDiscovery, InstalledExtension } from "../extension-discovery";
import { ExtensionsStore } from "../extensions-store";
jest.mock("../../common/ipc"); jest.mock("../../common/ipc");
jest.mock("fs-extra"); jest.mock("fs-extra");
@ -17,6 +18,12 @@ jest.mock("../extension-installer", () => ({
const mockedWatch = watch as jest.MockedFunction<typeof watch>; const mockedWatch = watch as jest.MockedFunction<typeof watch>;
describe("ExtensionDiscovery", () => { describe("ExtensionDiscovery", () => {
beforeEach(() => {
ExtensionDiscovery.resetInstance();
ExtensionsStore.resetInstance();
ExtensionsStore.getInstanceOrCreate();
});
it("emits add for added extension", async done => { it("emits add for added extension", async done => {
globalThis.__non_webpack_require__.mockImplementation(() => ({ globalThis.__non_webpack_require__.mockImplementation(() => ({
name: "my-extension" name: "my-extension"
@ -36,7 +43,7 @@ describe("ExtensionDiscovery", () => {
mockedWatch.mockImplementationOnce(() => mockedWatch.mockImplementationOnce(() =>
(mockWatchInstance) as any (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 // Need to force isLoaded to be true so that the file watching is started
extensionDiscovery.isLoaded = true; extensionDiscovery.isLoaded = true;
@ -76,7 +83,7 @@ describe("ExtensionDiscovery", () => {
mockedWatch.mockImplementationOnce(() => mockedWatch.mockImplementationOnce(() =>
(mockWatchInstance) as any (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 // Need to force isLoaded to be true so that the file watching is started
extensionDiscovery.isLoaded = true; extensionDiscovery.isLoaded = true;

View File

@ -1,15 +1,21 @@
import { ExtensionLoader } from "../extension-loader"; import { ExtensionLoader } from "../extension-loader";
import { ipcRenderer } from "electron"; 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 manifestPath = "manifest/path";
const manifestPath2 = "manifest/path2"; const manifestPath2 = "manifest/path2";
const manifestPath3 = "manifest/path3"; const manifestPath3 = "manifest/path3";
jest.mock("../extensions-store", () => ({ jest.mock("../extensions-store", () => ({
extensionsStore: { ExtensionsStore: {
getInstance: () => ({
whenLoaded: Promise.resolve(true), whenLoaded: Promise.resolve(true),
mergeState: jest.fn() mergeState: jest.fn()
})
} }
})); }));
@ -99,8 +105,12 @@ jest.mock(
); );
describe("ExtensionLoader", () => { describe("ExtensionLoader", () => {
it("renderer updates extension after ipc broadcast", async (done) => { beforeEach(() => {
const extensionLoader = new ExtensionLoader(); ExtensionLoader.resetInstance();
});
it.only("renderer updates extension after ipc broadcast", async (done) => {
const extensionLoader = ExtensionLoader.getInstanceOrCreate();
expect(extensionLoader.userExtensions).toMatchInlineSnapshot(`Map {}`); expect(extensionLoader.userExtensions).toMatchInlineSnapshot(`Map {}`);
@ -140,20 +150,20 @@ describe("ExtensionLoader", () => {
}); });
it("updates ExtensionsStore after isEnabled is changed", async () => { 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 // Disable sending events in this test
(ipcRenderer.on as any).mockImplementation(); (ipcRenderer.on as any).mockImplementation();
const extensionLoader = new ExtensionLoader(); const extensionLoader = ExtensionLoader.getInstanceOrCreate();
await extensionLoader.init(); await extensionLoader.init();
expect(extensionsStore.mergeState).not.toHaveBeenCalled(); expect(ExtensionsStore.getInstance().mergeState).not.toHaveBeenCalled();
Array.from(extensionLoader.userExtensions.values())[0].isEnabled = false; Array.from(extensionLoader.userExtensions.values())[0].isEnabled = false;
expect(extensionsStore.mergeState).toHaveBeenCalledWith({ expect(ExtensionsStore.getInstance().mergeState).toHaveBeenCalledWith({
"manifest/path": { "manifest/path": {
enabled: false, enabled: false,
name: "TestExtension" name: "TestExtension"

View File

@ -1,4 +1,8 @@
import { LensExtension } from "../lens-extension"; import { LensExtension } from "../lens-extension";
import { Console } from "console";
import { stdout, stderr } from "process";
console = new Console(stdout, stderr);
let ext: LensExtension = null; let ext: LensExtension = null;

View File

@ -8,7 +8,7 @@ import logger from "../main/logger";
import { app } from "electron"; import { app } from "electron";
import { requestMain } from "../common/ipc"; import { requestMain } from "../common/ipc";
import { clusterKubectlApplyAllHandler } from "../common/cluster-ipc"; import { clusterKubectlApplyAllHandler } from "../common/cluster-ipc";
import { clusterStore } from "../common/cluster-store"; import { ClusterStore } from "../common/cluster-store";
export interface ClusterFeatureStatus { export interface ClusterFeatureStatus {
/** feature's current version, as set by the implementation */ /** 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[]) { protected async applyResources(cluster: KubernetesCluster, resourceSpec: string | string[]) {
let resources: string[]; let resources: string[];
const clusterModel = clusterStore.getById(cluster.metadata.uid); const clusterModel = ClusterStore.getInstance().getById(cluster.metadata.uid);
if (!clusterModel) { if (!clusterModel) {
throw new Error(`cluster not found`); throw new Error(`cluster not found`);

View File

@ -1,9 +1,9 @@
import { getAppVersion } from "../../common/utils"; import { getAppVersion } from "../../common/utils";
import { extensionsStore } from "../extensions-store"; import { ExtensionsStore } from "../extensions-store";
export const version = getAppVersion(); export const version = getAppVersion();
export { isSnap, isWindows, isMac, isLinux, appName, slackUrl, issuesTrackerUrl } from "../../common/vars"; export { isSnap, isWindows, isMac, isLinux, appName, slackUrl, issuesTrackerUrl } from "../../common/vars";
export function getEnabledExtensions(): string[] { export function getEnabledExtensions(): string[] {
return extensionsStore.enabledExtensions; return ExtensionsStore.getInstance().enabledExtensions;
} }

View File

@ -1,5 +1,4 @@
import { computed } from "mobx";
import { CatalogEntity } from "../../common/catalog-entity"; import { CatalogEntity } from "../../common/catalog-entity";
import { catalogEntityRegistry as registry } from "../../common/catalog-entity-registry"; 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 * from "../../common/catalog-entities";
export class CatalogEntityRegistry { export class CatalogEntityRegistry {
@computed getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] { getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
return registry.getItemsForApiKind<T>(apiVersion, kind); return registry.getItemsForApiKind<T>(apiVersion, kind);
} }
} }

View File

@ -6,9 +6,10 @@ import { observable, reaction, toJS, when } from "mobx";
import os from "os"; import os from "os";
import path from "path"; import path from "path";
import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc"; import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc";
import { Singleton } from "../common/utils";
import logger from "../main/logger"; import logger from "../main/logger";
import { extensionInstaller, PackageJson } from "./extension-installer"; import { extensionInstaller, PackageJson } from "./extension-installer";
import { extensionsStore } from "./extensions-store"; import { ExtensionsStore } from "./extensions-store";
import type { LensExtensionId, LensExtensionManifest } from "./lens-extension"; import type { LensExtensionId, LensExtensionManifest } from "./lens-extension";
export interface InstalledExtension { 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 * - "add": When extension is added. The event is of type InstalledExtension
* - "remove": When extension is removed. The event is of type LensExtensionId * - "remove": When extension is removed. The event is of type LensExtensionId
*/ */
export class ExtensionDiscovery { export class ExtensionDiscovery extends Singleton {
protected bundledFolderPath: string; protected bundledFolderPath: string;
private loadStarted = false; private loadStarted = false;
@ -66,6 +67,7 @@ export class ExtensionDiscovery {
public events: EventEmitter; public events: EventEmitter;
constructor() { constructor() {
super();
this.events = new EventEmitter(); this.events = new EventEmitter();
} }
@ -135,7 +137,7 @@ export class ExtensionDiscovery {
depth: 1, depth: 1,
ignoreInitial: true, ignoreInitial: true,
// Try to wait until the file has been completely copied. // 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: { awaitWriteFinish: {
// Wait 300ms until the file size doesn't change to consider the file written. // 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. // For a small file like package.json this should be plenty of time.
@ -235,7 +237,7 @@ export class ExtensionDiscovery {
/** /**
* Uninstalls extension. * Uninstalls extension.
* The application will detect the folder unlink and remove the extension from the UI automatically. * 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) { async uninstallExtension({ absolutePath, manifest }: InstalledExtension) {
logger.info(`${logModule} Uninstalling ${manifest.name}`); logger.info(`${logModule} Uninstalling ${manifest.name}`);
@ -325,7 +327,7 @@ export class ExtensionDiscovery {
manifestJson = __non_webpack_require__(manifestPath); manifestJson = __non_webpack_require__(manifestPath);
const installedManifestPath = this.getInstalledManifestPath(manifestJson.name); const installedManifestPath = this.getInstalledManifestPath(manifestJson.name);
const isEnabled = isBundled || extensionsStore.isEnabled(installedManifestPath); const isEnabled = isBundled || ExtensionsStore.getInstance().isEnabled(installedManifestPath);
return { return {
id: installedManifestPath, id: installedManifestPath,
@ -455,5 +457,3 @@ export class ExtensionDiscovery {
broadcastMessage(ExtensionDiscovery.extensionDiscoveryChannel, this.toJSON()); broadcastMessage(ExtensionDiscovery.extensionDiscoveryChannel, this.toJSON());
} }
} }
export const extensionDiscovery = new ExtensionDiscovery();

View File

@ -5,9 +5,10 @@ import { action, computed, observable, reaction, toJS, when } from "mobx";
import path from "path"; import path from "path";
import { getHostedCluster } from "../common/cluster-store"; import { getHostedCluster } from "../common/cluster-store";
import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc"; import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc";
import { Singleton } from "../common/utils";
import logger from "../main/logger"; import logger from "../main/logger";
import type { InstalledExtension } from "./extension-discovery"; 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 { LensExtension, LensExtensionConstructor, LensExtensionId } from "./lens-extension";
import type { LensMainExtension } from "./lens-main-extension"; import type { LensMainExtension } from "./lens-main-extension";
import type { LensRendererExtension } from "./lens-renderer-extension"; import type { LensRendererExtension } from "./lens-renderer-extension";
@ -24,7 +25,7 @@ const logModule = "[EXTENSIONS-LOADER]";
/** /**
* Loads installed extensions to the Lens application * Loads installed extensions to the Lens application
*/ */
export class ExtensionLoader { export class ExtensionLoader extends Singleton {
protected extensions = observable.map<LensExtensionId, InstalledExtension>(); protected extensions = observable.map<LensExtensionId, InstalledExtension>();
protected instances = observable.map<LensExtensionId, LensExtension>(); protected instances = observable.map<LensExtensionId, LensExtension>();
@ -95,11 +96,11 @@ export class ExtensionLoader {
await this.initMain(); await this.initMain();
} }
await Promise.all([this.whenLoaded, extensionsStore.whenLoaded]); await Promise.all([this.whenLoaded, ExtensionsStore.getInstance().whenLoaded]);
// save state on change `extension.isEnabled` // save state on change `extension.isEnabled`
reaction(() => this.storeState, extensionsState => { 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())); broadcastMessage(main ? ExtensionLoader.extensionsMainChannel : ExtensionLoader.extensionsRendererChannel, Array.from(this.toJSON()));
} }
} }
export const extensionLoader = new ExtensionLoader();

View File

@ -53,5 +53,3 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
}); });
} }
} }
export const extensionsStore = new ExtensionsStore();

View File

@ -1,6 +1,6 @@
import type { InstalledExtension } from "./extension-discovery"; import type { InstalledExtension } from "./extension-discovery";
import { action, observable, reaction } from "mobx"; import { action, observable, reaction } from "mobx";
import { filesystemProvisionerStore } from "../main/extension-filesystem"; import { FilesystemProvisionerStore } from "../main/extension-filesystem";
import logger from "../main/logger"; import logger from "../main/logger";
import { ProtocolHandlerRegistration } from "./registries/protocol-handler-registry"; import { ProtocolHandlerRegistration } from "./registries/protocol-handler-registry";
@ -49,7 +49,7 @@ export class LensExtension {
* folder name. * folder name.
*/ */
async getExtensionFileFolder(): Promise<string> { async getExtensionFileFolder(): Promise<string> {
return filesystemProvisionerStore.requestDirectory(this.id); return FilesystemProvisionerStore.getInstance().requestDirectory(this.id);
} }
get description() { get description() {

View File

@ -10,7 +10,7 @@ export class LensMainExtension extends LensExtension {
appMenus: MenuRegistration[] = []; appMenus: MenuRegistration[] = [];
async navigate<P extends object>(pageId?: string, params?: P, frameId?: number) { async navigate<P extends object>(pageId?: string, params?: P, frameId?: number) {
const windowManager = WindowManager.getInstance<WindowManager>(); const windowManager = WindowManager.getInstance();
const pageUrl = getExtensionPageUrl({ const pageUrl = getExtensionPageUrl({
extensionId: this.name, extensionId: this.name,
pageId, pageId,

View File

@ -1,6 +1,10 @@
import { getExtensionPageUrl, globalPageRegistry, PageParams } from "../page-registry"; import { getExtensionPageUrl, globalPageRegistry, PageParams } from "../page-registry";
import { LensExtension } from "../../lens-extension"; import { LensExtension } from "../../lens-extension";
import React from "react"; import React from "react";
import { Console } from "console";
import { stdout, stderr } from "process";
console = new Console(stdout, stderr);
let ext: LensExtension = null; let ext: LensExtension = null;

View File

@ -1,5 +1,5 @@
import { themeStore } from "../../renderer/theme.store"; import { ThemeStore } from "../../renderer/theme.store";
export function getActiveTheme() { export function getActiveTheme() {
return themeStore.activeTheme; return ThemeStore.getInstance().activeTheme;
} }

View File

@ -23,7 +23,6 @@ jest.mock("winston", () => ({
} }
})); }));
jest.mock("../../common/ipc"); jest.mock("../../common/ipc");
jest.mock("../context-handler"); jest.mock("../context-handler");
jest.mock("request"); jest.mock("request");

View File

@ -36,6 +36,11 @@ import { bundledKubectlPath, Kubectl } from "../kubectl";
import { mock, MockProxy } from "jest-mock-extended"; import { mock, MockProxy } from "jest-mock-extended";
import { waitUntilUsed } from "tcp-port-used"; import { waitUntilUsed } from "tcp-port-used";
import { Readable } from "stream"; 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<typeof broadcastMessage>; const mockBroadcastIpc = broadcastMessage as jest.MockedFunction<typeof broadcastMessage>;
const mockSpawn = spawn as jest.MockedFunction<typeof spawn>; const mockSpawn = spawn as jest.MockedFunction<typeof spawn>;
@ -44,6 +49,8 @@ const mockWaitUntilUsed = waitUntilUsed as jest.MockedFunction<typeof waitUntilU
describe("kube auth proxy tests", () => { describe("kube auth proxy tests", () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
UserStore.resetInstance();
UserStore.getInstanceOrCreate();
}); });
it("calling exit multiple times shouldn't throw", async () => { it("calling exit multiple times shouldn't throw", async () => {

View File

@ -36,10 +36,6 @@ import * as path from "path";
console = new Console(process.stdout, process.stderr); // fix mockFS console = new Console(process.stdout, process.stderr); // fix mockFS
describe("kubeconfig manager tests", () => { describe("kubeconfig manager tests", () => {
beforeEach(() => {
jest.clearAllMocks();
});
beforeEach(() => { beforeEach(() => {
const mockOpts = { const mockOpts = {
"minikube-config.yml": JSON.stringify({ "minikube-config.yml": JSON.stringify({
@ -76,7 +72,7 @@ describe("kubeconfig manager tests", () => {
const cluster = new Cluster({ const cluster = new Cluster({
id: "foo", id: "foo",
contextName: "minikube", contextName: "minikube",
kubeConfigPath: "minikube-config.yml" kubeConfigPath: "minikube-config.yml",
}); });
const contextHandler = new ContextHandler(cluster); const contextHandler = new ContextHandler(cluster);
const port = await getFreePort(); const port = await getFreePort();
@ -98,7 +94,7 @@ describe("kubeconfig manager tests", () => {
const cluster = new Cluster({ const cluster = new Cluster({
id: "foo", id: "foo",
contextName: "minikube", contextName: "minikube",
kubeConfigPath: "minikube-config.yml" kubeConfigPath: "minikube-config.yml",
}); });
const contextHandler = new ContextHandler(cluster); const contextHandler = new ContextHandler(cluster);
const port = await getFreePort(); const port = await getFreePort();

View File

@ -2,7 +2,7 @@ import "../common/cluster-ipc";
import type http from "http"; import type http from "http";
import { ipcMain } from "electron"; import { ipcMain } from "electron";
import { action, autorun, observable, reaction, toJS } from "mobx"; 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 { Cluster } from "./cluster";
import logger from "./logger"; import logger from "./logger";
import { apiKubePrefix } from "../common/vars"; import { apiKubePrefix } from "../common/vars";
@ -21,7 +21,7 @@ export class ClusterManager extends Singleton {
catalogEntityRegistry.addSource("lens:kubernetes-clusters", this.catalogSource); catalogEntityRegistry.addSource("lens:kubernetes-clusters", this.catalogSource);
// auto-init clusters // auto-init clusters
reaction(() => clusterStore.enabledClustersList, (clusters) => { reaction(() => ClusterStore.getInstance().enabledClustersList, (clusters) => {
clusters.forEach((cluster) => { clusters.forEach((cluster) => {
if (!cluster.initialized && !cluster.initializing) { if (!cluster.initialized && !cluster.initializing) {
logger.info(`[CLUSTER-MANAGER]: init cluster`, cluster.getMeta()); logger.info(`[CLUSTER-MANAGER]: init cluster`, cluster.getMeta());
@ -31,8 +31,8 @@ export class ClusterManager extends Singleton {
}, { fireImmediately: true }); }, { fireImmediately: true });
reaction(() => toJS(clusterStore.enabledClustersList, { recurseEverything: true }), () => { reaction(() => toJS(ClusterStore.getInstance().enabledClustersList, { recurseEverything: true }), () => {
this.updateCatalogSource(clusterStore.enabledClustersList); this.updateCatalogSource(ClusterStore.getInstance().enabledClustersList);
}, { fireImmediately: true }); }, { fireImmediately: true });
reaction(() => catalogEntityRegistry.getItemsForApiKind<KubernetesCluster>("entity.k8slens.dev/v1alpha1", "KubernetesCluster"), (entities) => { reaction(() => catalogEntityRegistry.getItemsForApiKind<KubernetesCluster>("entity.k8slens.dev/v1alpha1", "KubernetesCluster"), (entities) => {
@ -42,14 +42,14 @@ export class ClusterManager extends Singleton {
// auto-stop removed clusters // auto-stop removed clusters
autorun(() => { autorun(() => {
const removedClusters = Array.from(clusterStore.removedClusters.values()); const removedClusters = Array.from(ClusterStore.getInstance().removedClusters.values());
if (removedClusters.length > 0) { if (removedClusters.length > 0) {
const meta = removedClusters.map(cluster => cluster.getMeta()); const meta = removedClusters.map(cluster => cluster.getMeta());
logger.info(`[CLUSTER-MANAGER]: removing clusters`, meta); logger.info(`[CLUSTER-MANAGER]: removing clusters`, meta);
removedClusters.forEach(cluster => cluster.disconnect()); removedClusters.forEach(cluster => cluster.disconnect());
clusterStore.removedClusters.clear(); ClusterStore.getInstance().removedClusters.clear();
} }
}, { }, {
delay: 250 delay: 250
@ -90,10 +90,10 @@ export class ClusterManager extends Singleton {
@action syncClustersFromCatalog(entities: KubernetesCluster[]) { @action syncClustersFromCatalog(entities: KubernetesCluster[]) {
entities.filter((entity) => entity.metadata.source !== "local").forEach((entity: 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) { if (!cluster) {
clusterStore.addCluster({ ClusterStore.getInstance().addCluster({
id: entity.metadata.uid, id: entity.metadata.uid,
enabled: true, enabled: true,
ownerRef: clusterOwnerRef, ownerRef: clusterOwnerRef,
@ -145,7 +145,7 @@ export class ClusterManager extends Singleton {
protected onNetworkOffline() { protected onNetworkOffline() {
logger.info("[CLUSTER-MANAGER]: network is offline"); logger.info("[CLUSTER-MANAGER]: network is offline");
clusterStore.enabledClustersList.forEach((cluster) => { ClusterStore.getInstance().enabledClustersList.forEach((cluster) => {
if (!cluster.disconnected) { if (!cluster.disconnected) {
cluster.online = false; cluster.online = false;
cluster.accessible = false; cluster.accessible = false;
@ -156,7 +156,7 @@ export class ClusterManager extends Singleton {
protected onNetworkOnline() { protected onNetworkOnline() {
logger.info("[CLUSTER-MANAGER]: network is online"); logger.info("[CLUSTER-MANAGER]: network is online");
clusterStore.enabledClustersList.forEach((cluster) => { ClusterStore.getInstance().enabledClustersList.forEach((cluster) => {
if (!cluster.disconnected) { if (!cluster.disconnected) {
cluster.refreshConnectionStatus().catch((e) => e); cluster.refreshConnectionStatus().catch((e) => e);
} }
@ -164,7 +164,7 @@ export class ClusterManager extends Singleton {
} }
stop() { stop() {
clusterStore.clusters.forEach((cluster: Cluster) => { ClusterStore.getInstance().clusters.forEach((cluster: Cluster) => {
cluster.disconnect(); cluster.disconnect();
}); });
} }
@ -176,18 +176,18 @@ export class ClusterManager extends Singleton {
if (req.headers.host.startsWith("127.0.0.1")) { if (req.headers.host.startsWith("127.0.0.1")) {
const clusterId = req.url.split("/")[1]; const clusterId = req.url.split("/")[1];
cluster = clusterStore.getById(clusterId); cluster = ClusterStore.getInstance().getById(clusterId);
if (cluster) { if (cluster) {
// we need to swap path prefix so that request is proxied to kube api // we need to swap path prefix so that request is proxied to kube api
req.url = req.url.replace(`/${clusterId}`, apiKubePrefix); req.url = req.url.replace(`/${clusterId}`, apiKubePrefix);
} }
} else if (req.headers["x-cluster-id"]) { } 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 { } else {
const clusterId = getClusterIdFromHost(req.headers.host); const clusterId = getClusterIdFromHost(req.headers.host);
cluster = clusterStore.getById(clusterId); cluster = ClusterStore.getInstance().getById(clusterId);
} }
return cluster; return cluster;

View File

@ -4,11 +4,12 @@ import logger from "./logger";
* Installs Electron developer tools in the development build. * Installs Electron developer tools in the development build.
* The dependency is not bundled to the production build. * The dependency is not bundled to the production build.
*/ */
export const installDeveloperTools = async () => { export const installDeveloperTools = () => {
if (process.env.NODE_ENV === "development") { if (process.env.NODE_ENV === "development") {
logger.info("🤓 Installing developer tools"); logger.info("🤓 Installing developer tools");
const { default: devToolsInstaller, REACT_DEVELOPER_TOOLS } = await import("electron-devtools-installer"); import("electron-devtools-installer")
.then(({ default: devToolsInstaller, REACT_DEVELOPER_TOOLS }) => devToolsInstaller([REACT_DEVELOPER_TOOLS]))
return devToolsInstaller([REACT_DEVELOPER_TOOLS]); .then((name) => logger.info(`[DEVTOOLS-INSTALLER]: installed ${name}`))
.catch(error => logger.error(`[DEVTOOLS-INSTALLER]: failed`, { error }));
} }
}; };

View File

@ -4,10 +4,14 @@ import { appEventBus } from "../common/event-bus";
import { ClusterManager } from "./cluster-manager"; import { ClusterManager } from "./cluster-manager";
import logger from "./logger"; import logger from "./logger";
export function exitApp() { export function exitApp() {
const windowManager = WindowManager.getInstance<WindowManager>(); console.log("before windowManager");
const clusterManager = ClusterManager.getInstance<ClusterManager>(); const windowManager = WindowManager.getInstance(false);
console.log("before clusterManager");
const clusterManager = ClusterManager.getInstance(false);
console.log("after clusterManager");
appEventBus.emit({ name: "service", action: "close" }); appEventBus.emit({ name: "service", action: "close" });
windowManager?.hide(); windowManager?.hide();

View File

@ -14,7 +14,7 @@ interface FSProvisionModel {
export class FilesystemProvisionerStore extends BaseStore<FSProvisionModel> { export class FilesystemProvisionerStore extends BaseStore<FSProvisionModel> {
@observable registeredExtensions = observable.map<LensExtensionId, string>(); @observable registeredExtensions = observable.map<LensExtensionId, string>();
private constructor() { constructor() {
super({ super({
configName: "lens-filesystem-provisioner-store", configName: "lens-filesystem-provisioner-store",
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
@ -56,5 +56,3 @@ export class FilesystemProvisionerStore extends BaseStore<FSProvisionModel> {
}); });
} }
} }
export const filesystemProvisionerStore = FilesystemProvisionerStore.getInstance<FilesystemProvisionerStore>();

View File

@ -1,17 +1,24 @@
import { helmService } from "../helm-service"; 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"); jest.mock("../helm-chart-manager");
describe("Helm Service tests", () => { describe("Helm Service tests", () => {
test("list charts without deprecated ones", async () => { afterEach(() => {
jest.spyOn(repoManager, "repositories").mockImplementation(async () => { jest.resetAllMocks();
});
it("list charts without deprecated ones", async () => {
mockHelmRepoManager.mockReturnValue({
init: jest.fn(),
repositories: jest.fn().mockImplementation(async () => {
return [ return [
{ name: "stable", url: "stableurl" }, { name: "stable", url: "stableurl" },
{ name: "experiment", url: "experimenturl" } { name: "experiment", url: "experimenturl" },
]; ];
}),
}); });
const charts = await helmService.listCharts(); const charts = await helmService.listCharts();
@ -55,11 +62,14 @@ describe("Helm Service tests", () => {
}); });
}); });
test("list charts sorted by version in descending order", async () => { it("list charts sorted by version in descending order", async () => {
jest.spyOn(repoManager, "repositories").mockImplementation(async () => { mockHelmRepoManager.mockReturnValue({
init: jest.fn(),
repositories: jest.fn().mockImplementation(async () => {
return [ return [
{ name: "bitnami", url: "bitnamiurl" } { name: "bitnami", url: "bitnamiurl" },
]; ];
}),
}); });
const charts = await helmService.listCharts(); const charts = await helmService.listCharts();

View File

@ -1,17 +1,17 @@
import * as tempy from "tempy"; import * as tempy from "tempy";
import fs from "fs"; import fse from "fs-extra";
import * as yaml from "js-yaml"; import * as yaml from "js-yaml";
import { promiseExec} from "../promise-exec"; import { promiseExec} from "../promise-exec";
import { helmCli } from "./helm-cli"; import { helmCli } from "./helm-cli";
import { Cluster } from "../cluster"; import { Cluster } from "../cluster";
import { toCamelCase } from "../../common/utils/camelCase"; import { toCamelCase } from "../../common/utils/camelCase";
export class HelmReleaseManager { export async function listReleases(pathToKubeconfig: string, namespace?: string) {
public async listReleases(pathToKubeconfig: string, namespace?: string) {
const helm = await helmCli.binaryPath(); const helm = await helmCli.binaryPath();
const namespaceFlag = namespace ? `-n ${namespace}` : "--all-namespaces"; 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); const output = JSON.parse(stdout);
if (output.length == 0) { if (output.length == 0) {
@ -22,14 +22,17 @@ export class HelmReleaseManager {
}); });
return output; return output;
} catch ({ stderr }) {
throw stderr;
}
} }
public async installChart(chart: string, values: any, name: string, namespace: string, version: string, pathToKubeconfig: string){ export async function installChart(chart: string, values: any, name: string | undefined, namespace: string, version: string, pathToKubeconfig: string){
const helm = await helmCli.binaryPath(); const helm = await helmCli.binaryPath();
const fileName = tempy.file({name: "values.yaml"}); const fileName = tempy.file({name: "values.yaml"});
await fs.promises.writeFile(fileName, yaml.safeDump(values)); await fse.writeFile(fileName, yaml.safeDump(values));
try { try {
let generateName = ""; let generateName = "";
@ -38,7 +41,7 @@ export class HelmReleaseManager {
generateName = "--generate-name"; generateName = "--generate-name";
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 { 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(); const releaseName = stdout.split("\n")[0].split(" ")[1].trim();
return { return {
@ -48,80 +51,103 @@ export class HelmReleaseManager {
namespace namespace
} }
}; };
} catch ({ stderr }) {
throw stderr;
} finally { } finally {
await fs.promises.unlink(fileName); await fse.unlink(fileName);
} }
} }
public async upgradeRelease(name: string, chart: string, values: any, namespace: string, version: string, cluster: Cluster){ export async function upgradeRelease(name: string, chart: string, values: any, namespace: string, version: string, cluster: Cluster) {
const helm = await helmCli.binaryPath(); const helm = await helmCli.binaryPath();
const fileName = tempy.file({name: "values.yaml"}); const fileName = tempy.file({name: "values.yaml"});
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
await fs.promises.writeFile(fileName, yaml.safeDump(values)); await fse.writeFile(fileName, yaml.safeDump(values));
try { try {
const { stdout } = await promiseExec(`"${helm}" upgrade ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${proxyKubeconfig}`).catch((error) => { throw(error.stderr);}); const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
const { stdout } = await promiseExec(`"${helm}" upgrade ${name} ${chart} --version ${version} -f ${fileName} --namespace ${namespace} --kubeconfig ${proxyKubeconfig}`);
return { return {
log: stdout, log: stdout,
release: this.getRelease(name, namespace, cluster) release: getRelease(name, namespace, cluster)
}; };
} catch ({ stderr }) {
throw stderr;
} finally { } finally {
await fs.promises.unlink(fileName); await fse.unlink(fileName);
} }
} }
public async getRelease(name: string, namespace: string, cluster: Cluster) { export async function getRelease(name: string, namespace: string, cluster: Cluster) {
try {
const helm = await helmCli.binaryPath(); const helm = await helmCli.binaryPath();
const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
const { stdout } = await promiseExec(`"${helm}" status ${name} --output json --namespace ${namespace} --kubeconfig ${proxyKubeconfig}`).catch((error) => { throw(error.stderr);}); const { stdout } = await promiseExec(`"${helm}" status ${name} --output json --namespace ${namespace} --kubeconfig ${proxyKubeconfig}`);
const release = JSON.parse(stdout); const release = JSON.parse(stdout);
release.resources = await this.getResources(name, namespace, cluster); release.resources = await getResources(name, namespace, cluster);
return release; return release;
} catch ({ stderr }) {
throw stderr;
}
} }
public async deleteRelease(name: string, namespace: string, pathToKubeconfig: string) { export async function deleteRelease(name: string, namespace: string, pathToKubeconfig: string) {
try {
const helm = await helmCli.binaryPath(); const helm = await helmCli.binaryPath();
const { stdout } = await promiseExec(`"${helm}" delete ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);}); const { stdout } = await promiseExec(`"${helm}" delete ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
return stdout; return stdout;
} catch ({ stderr }) {
throw stderr;
}
} }
public async getValues(name: string, namespace: string, all: boolean, pathToKubeconfig: string) { export async function getValues(name: string, namespace: string, all: boolean, pathToKubeconfig: string) {
try {
const helm = await helmCli.binaryPath(); 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);}); const { stdout, } = await promiseExec(`"${helm}" get values ${name} ${all ? "--all": ""} --output yaml --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
return stdout; return stdout;
} catch ({ stderr }) {
throw stderr;
}
} }
public async getHistory(name: string, namespace: string, pathToKubeconfig: string) { export async function getHistory(name: string, namespace: string, pathToKubeconfig: string) {
try {
const helm = await helmCli.binaryPath(); const helm = await helmCli.binaryPath();
const { stdout } = await promiseExec(`"${helm}" history ${name} --output json --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);}); const { stdout } = await promiseExec(`"${helm}" history ${name} --output json --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
return JSON.parse(stdout); return JSON.parse(stdout);
} catch ({ stderr }) {
throw stderr;
}
} }
public async rollback(name: string, namespace: string, revision: number, pathToKubeconfig: string) { export async function rollback(name: string, namespace: string, revision: number, pathToKubeconfig: string) {
try {
const helm = await helmCli.binaryPath(); const helm = await helmCli.binaryPath();
const { stdout } = await promiseExec(`"${helm}" rollback ${name} ${revision} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`).catch((error) => { throw(error.stderr);}); const { stdout } = await promiseExec(`"${helm}" rollback ${name} ${revision} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
return stdout; return stdout;
} catch ({ stderr }) {
throw stderr;
}
} }
protected async getResources(name: string, namespace: string, cluster: Cluster) { async function getResources(name: string, namespace: string, cluster: Cluster) {
try {
const helm = await helmCli.binaryPath(); const helm = await helmCli.binaryPath();
const kubectl = await cluster.kubeCtl.getPath(); const kubectl = await cluster.kubeCtl.getPath();
const pathToKubeconfig = await cluster.getProxyKubeconfigPath(); 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(() => { const { stdout } = await promiseExec(`"${helm}" get manifest ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} | "${kubectl}" get -n ${namespace} --kubeconfig ${pathToKubeconfig} -f - -o=json`);
return { stdout: JSON.stringify({items: []})};
});
return stdout; return stdout;
} catch {
return { stdout: JSON.stringify({ items: [] }) };
} }
} }
export const releaseManager = new HelmReleaseManager();

View File

@ -77,10 +77,6 @@ export class HelmRepoManager extends Singleton {
} }
public async repositories(): Promise<HelmRepo[]> { public async repositories(): Promise<HelmRepo[]> {
if (!this.initialized) {
await this.init();
}
try { try {
const repoConfigFile = this.helmEnv.HELM_REPOSITORY_CONFIG; const repoConfigFile = this.helmEnv.HELM_REPOSITORY_CONFIG;
const { repositories }: HelmRepoConfig = await readFile(repoConfigFile, "utf8") const { repositories }: HelmRepoConfig = await readFile(repoConfigFile, "utf8")
@ -160,5 +156,3 @@ export class HelmRepoManager extends Singleton {
return stdout; return stdout;
} }
} }
export const repoManager = HelmRepoManager.getInstance<HelmRepoManager>();

View File

@ -1,23 +1,21 @@
import semver from "semver"; import semver from "semver";
import { Cluster } from "../cluster"; import { Cluster } from "../cluster";
import logger from "../logger"; import logger from "../logger";
import { repoManager } from "./helm-repo-manager"; import { HelmRepoManager } from "./helm-repo-manager";
import { HelmChartManager } from "./helm-chart-manager"; import { HelmChartManager } from "./helm-chart-manager";
import { releaseManager } from "./helm-release-manager";
import { HelmChartList, RepoHelmChartList } from "../../renderer/api/endpoints/helm-charts.api"; 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 { class HelmService {
public async installChart(cluster: Cluster, data: { chart: string; values: {}; name: string; namespace: string; version: string }) { public async installChart(cluster: Cluster, data: { chart: string; values: {}; name: string; namespace: string; version: string }) {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); 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() { public async listCharts() {
const charts: HelmChartList = {}; const charts: HelmChartList = {};
const repositories = await HelmRepoManager.getInstance().repositories();
await repoManager.init();
const repositories = await repoManager.repositories();
for (const repo of repositories) { for (const repo of repositories) {
charts[repo.name] = {}; charts[repo.name] = {};
@ -36,7 +34,7 @@ class HelmService {
readme: "", readme: "",
versions: {} versions: {}
}; };
const repo = await repoManager.repository(repoName); const repo = await HelmRepoManager.getInstance().repository(repoName);
const chartManager = new HelmChartManager(repo); const chartManager = new HelmChartManager(repo);
const chart = await chartManager.chart(chartName); const chart = await chartManager.chart(chartName);
@ -47,23 +45,22 @@ class HelmService {
} }
public async getChartValues(repoName: string, chartName: string, version = "") { 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); const chartManager = new HelmChartManager(repo);
return chartManager.getValues(chartName, version); return chartManager.getValues(chartName, version);
} }
public async listReleases(cluster: Cluster, namespace: string = null) { public async listReleases(cluster: Cluster, namespace: string = null) {
await repoManager.init();
const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
return await releaseManager.listReleases(proxyKubeconfig, namespace); return listReleases(proxyKubeconfig, namespace);
} }
public async getRelease(cluster: Cluster, releaseName: string, namespace: string) { public async getRelease(cluster: Cluster, releaseName: string, namespace: string) {
logger.debug("Fetch release"); 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) { public async getReleaseValues(cluster: Cluster, releaseName: string, namespace: string, all: boolean) {
@ -71,7 +68,7 @@ class HelmService {
logger.debug("Fetch release values"); 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) { public async getReleaseHistory(cluster: Cluster, releaseName: string, namespace: string) {
@ -79,7 +76,7 @@ class HelmService {
logger.debug("Fetch release history"); 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) { public async deleteRelease(cluster: Cluster, releaseName: string, namespace: string) {
@ -87,20 +84,20 @@ class HelmService {
logger.debug("Delete release"); 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 }) { public async updateRelease(cluster: Cluster, releaseName: string, namespace: string, data: { chart: string; values: {}; version: string }) {
logger.debug("Upgrade release"); 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) { public async rollback(cluster: Cluster, releaseName: string, namespace: string, revision: number) {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
logger.debug("Rollback release"); logger.debug("Rollback release");
const output = await releaseManager.rollback(releaseName, namespace, revision, proxyKubeconfig); const output = rollback(releaseName, namespace, revision, proxyKubeconfig);
return { message: output }; return { message: output };
} }

View File

@ -15,15 +15,15 @@ import { getFreePort } from "./port";
import { mangleProxyEnv } from "./proxy-env"; import { mangleProxyEnv } from "./proxy-env";
import { registerFileProtocol } from "../common/register-protocol"; import { registerFileProtocol } from "../common/register-protocol";
import logger from "./logger"; import logger from "./logger";
import { clusterStore } from "../common/cluster-store"; import { ClusterStore } from "../common/cluster-store";
import { userStore } from "../common/user-store"; import { UserStore } from "../common/user-store";
import { appEventBus } from "../common/event-bus"; import { appEventBus } from "../common/event-bus";
import { extensionLoader } from "../extensions/extension-loader"; import { ExtensionLoader } from "../extensions/extension-loader";
import { extensionsStore } from "../extensions/extensions-store"; import { ExtensionsStore } from "../extensions/extensions-store";
import { InstalledExtension, extensionDiscovery } from "../extensions/extension-discovery"; import { InstalledExtension, ExtensionDiscovery } from "../extensions/extension-discovery";
import type { LensExtensionId } from "../extensions/lens-extension"; import type { LensExtensionId } from "../extensions/lens-extension";
import { FilesystemProvisionerStore } from "./extension-filesystem";
import { installDeveloperTools } from "./developer-tools"; import { installDeveloperTools } from "./developer-tools";
import { filesystemProvisionerStore } from "./extension-filesystem";
import { LensProtocolRouterMain } from "./protocol-handler"; import { LensProtocolRouterMain } from "./protocol-handler";
import { getAppVersion, getAppVersionFromProxyServer } from "../common/utils"; import { getAppVersion, getAppVersionFromProxyServer } from "../common/utils";
import { bindBroadcastHandlers } from "../common/ipc"; import { bindBroadcastHandlers } from "../common/ipc";
@ -31,13 +31,9 @@ import { startUpdateChecking } from "./app-updater";
import { IpcRendererNavigationEvents } from "../renderer/navigation/events"; import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
import { CatalogPusher } from "./catalog-pusher"; import { CatalogPusher } from "./catalog-pusher";
import { catalogEntityRegistry } from "../common/catalog-entity-registry"; 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); const workingDir = path.join(app.getPath("appData"), appName);
let proxyPort: number;
let proxyServer: LensProxy;
let clusterManager: ClusterManager;
let windowManager: WindowManager;
app.setName(appName); app.setName(appName);
@ -66,7 +62,7 @@ if (app.commandLine.getSwitchValue("proxy-server") !== "") {
if (!app.requestSingleInstanceLock()) { if (!app.requestSingleInstanceLock()) {
app.exit(); app.exit();
} else { } else {
const lprm = LensProtocolRouterMain.getInstance<LensProtocolRouterMain>(); const lprm = LensProtocolRouterMain.getInstanceOrCreate();
for (const arg of process.argv) { for (const arg of process.argv) {
if (arg.toLowerCase().startsWith("lens://")) { if (arg.toLowerCase().startsWith("lens://")) {
@ -77,7 +73,7 @@ if (!app.requestSingleInstanceLock()) {
} }
app.on("second-instance", (event, argv) => { app.on("second-instance", (event, argv) => {
const lprm = LensProtocolRouterMain.getInstance<LensProtocolRouterMain>(); const lprm = LensProtocolRouterMain.getInstanceOrCreate();
for (const arg of argv) { for (const arg of argv) {
if (arg.toLowerCase().startsWith("lens://")) { 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 () => { app.on("ready", async () => {
@ -102,7 +98,11 @@ app.on("ready", async () => {
registerFileProtocol("static", __static); 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"); logger.info("💾 Loading stores");
// preload // preload
@ -111,10 +111,12 @@ app.on("ready", async () => {
clusterStore.load(), clusterStore.load(),
hotbarStore.load(), hotbarStore.load(),
extensionsStore.load(), extensionsStore.load(),
filesystemProvisionerStore.load(), filesystemStore.load(),
]); ]);
// find free port // find free port
let proxyPort;
try { try {
logger.info("🔑 Getting free port for LensProxy server"); logger.info("🔑 Getting free port for LensProxy server");
proxyPort = await getFreePort(); proxyPort = await getFreePort();
@ -125,13 +127,13 @@ app.on("ready", async () => {
} }
// create cluster manager // create cluster manager
clusterManager = ClusterManager.getInstance<ClusterManager>(proxyPort); ClusterManager.getInstanceOrCreate(proxyPort);
// run proxy // run proxy
try { try {
logger.info("🔌 Starting LensProxy"); logger.info("🔌 Starting LensProxy");
// eslint-disable-next-line unused-imports/no-unused-vars-ts // eslint-disable-next-line unused-imports/no-unused-vars-ts
proxyServer = LensProxy.create(proxyPort, clusterManager); LensProxy.getInstanceOrCreate(proxyPort).listen();
} catch (error) { } catch (error) {
logger.error(`Could not start proxy (127.0.0:${proxyPort}): ${error?.message}`); 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"}`); 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); logger.error("Checking proxy server connection failed", error);
} }
extensionLoader.init(); const extensionDiscovery = ExtensionDiscovery.getInstanceOrCreate();
ExtensionLoader.getInstanceOrCreate().init();
extensionDiscovery.init(); extensionDiscovery.init();
// Start the app without showing the main window when auto starting on login // 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); const startHidden = process.argv.includes("--hidden") || (isMac && app.getLoginItemSettings().wasOpenedAsHidden);
logger.info("🖥️ Starting WindowManager"); logger.info("🖥️ Starting WindowManager");
windowManager = WindowManager.getInstance<WindowManager>(proxyPort); const windowManager = WindowManager.getInstanceOrCreate(proxyPort);
installDeveloperTools();
if (!startHidden) { if (!startHidden) {
windowManager.initMainWindow(); windowManager.initMainWindow();
@ -169,13 +175,13 @@ app.on("ready", async () => {
CatalogPusher.init(catalogEntityRegistry); CatalogPusher.init(catalogEntityRegistry);
startUpdateChecking(); startUpdateChecking();
LensProtocolRouterMain LensProtocolRouterMain
.getInstance<LensProtocolRouterMain>() .getInstance()
.rendererLoaded = true; .rendererLoaded = true;
}); });
extensionLoader.whenLoaded.then(() => { ExtensionLoader.getInstance().whenLoaded.then(() => {
LensProtocolRouterMain LensProtocolRouterMain
.getInstance<LensProtocolRouterMain>() .getInstance()
.extensionsLoaded = true; .extensionsLoaded = true;
}); });
@ -189,14 +195,15 @@ app.on("ready", async () => {
extensionDiscovery.watchExtensions(); extensionDiscovery.watchExtensions();
// Subscribe to extensions that are copied or deleted to/from the extensions folder // Subscribe to extensions that are copied or deleted to/from the extensions folder
extensionDiscovery.events.on("add", (extension: InstalledExtension) => { extensionDiscovery.events
extensionLoader.addExtension(extension); .on("add", (extension: InstalledExtension) => {
}); ExtensionLoader.getInstance().addExtension(extension);
extensionDiscovery.events.on("remove", (lensExtensionId: LensExtensionId) => { })
extensionLoader.removeExtension(lensExtensionId); .on("remove", (lensExtensionId: LensExtensionId) => {
ExtensionLoader.getInstance().removeExtension(lensExtensionId);
}); });
extensionLoader.initExtensions(extensions); ExtensionLoader.getInstance().initExtensions(extensions);
} catch (error) { } catch (error) {
dialog.showErrorBox("Lens Error", `Could not load extensions${error?.message ? `: ${error.message}` : ""}`); dialog.showErrorBox("Lens Error", `Could not load extensions${error?.message ? `: ${error.message}` : ""}`);
console.error(error); console.error(error);
@ -212,7 +219,7 @@ app.on("activate", (event, hasVisibleWindows) => {
logger.info("APP:ACTIVATE", { hasVisibleWindows }); logger.info("APP:ACTIVATE", { hasVisibleWindows });
if (!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) // Quit app on Cmd+Q (MacOS)
logger.info("APP:QUIT"); logger.info("APP:QUIT");
appEventBus.emit({name: "app", action: "close"}); appEventBus.emit({name: "app", action: "close"});
ClusterManager.getInstance(false)?.stop(); // close cluster connections
clusterManager?.stop(); // close cluster connections
if (blockQuit) { if (blockQuit) {
event.preventDefault(); // prevent app's default shutdown (e.g. required for telemetry, etc.) 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(); event.preventDefault();
LensProtocolRouterMain LensProtocolRouterMain
.getInstance<LensProtocolRouterMain>() .getInstance()
.route(rawUrl) .route(rawUrl)
.catch(error => logger.error(`${LensProtocolRouterMain.LoggingPrefix}: an error occured`, { error, rawUrl })); .catch(error => logger.error(`${LensProtocolRouterMain.LoggingPrefix}: an error occured`, { error, rawUrl }));
}); });

View File

@ -6,7 +6,7 @@ import logger from "./logger";
import { ensureDir, pathExists } from "fs-extra"; import { ensureDir, pathExists } from "fs-extra";
import * as lockFile from "proper-lockfile"; import * as lockFile from "proper-lockfile";
import { helmCli } from "./helm/helm-cli"; import { helmCli } from "./helm/helm-cli";
import { userStore } from "../common/user-store"; import { UserStore } from "../common/user-store";
import { customRequest } from "../common/request"; import { customRequest } from "../common/request";
import { getBundledKubectlVersion } from "../common/utils/app-version"; import { getBundledKubectlVersion } from "../common/utils/app-version";
import { isDevelopment, isWindows, isTestEnv } from "../common/vars"; import { isDevelopment, isWindows, isTestEnv } from "../common/vars";
@ -113,12 +113,12 @@ export class Kubectl {
} }
public getPathFromPreferences() { public getPathFromPreferences() {
return userStore.preferences?.kubectlBinariesPath || this.getBundledPath(); return UserStore.getInstance().preferences?.kubectlBinariesPath || this.getBundledPath();
} }
protected getDownloadDir() { protected getDownloadDir() {
if (userStore.preferences?.downloadBinariesPath) { if (UserStore.getInstance().preferences?.downloadBinariesPath) {
return path.join(userStore.preferences.downloadBinariesPath, "kubectl"); return path.join(UserStore.getInstance().preferences.downloadBinariesPath, "kubectl");
} }
return Kubectl.kubectlDir; return Kubectl.kubectlDir;
@ -129,7 +129,7 @@ export class Kubectl {
return this.getBundledPath(); return this.getBundledPath();
} }
if (userStore.preferences?.downloadKubectlBinaries === false) { if (UserStore.getInstance().preferences?.downloadKubectlBinaries === false) {
return this.getPathFromPreferences(); return this.getPathFromPreferences();
} }
@ -223,7 +223,7 @@ export class Kubectl {
} }
public async ensureKubectl(): Promise<boolean> { public async ensureKubectl(): Promise<boolean> {
if (userStore.preferences?.downloadKubectlBinaries === false) { if (UserStore.getInstance().preferences?.downloadKubectlBinaries === false) {
return true; return true;
} }
@ -273,7 +273,7 @@ export class Kubectl {
logger.info(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`); logger.info(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`);
return new Promise((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const stream = customRequest({ const stream = customRequest({
url: this.url, url: this.url,
gzip: true, gzip: true,
@ -303,7 +303,7 @@ export class Kubectl {
} }
protected async writeInitScripts() { 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 helmPath = helmCli.getBinaryDir();
const fsPromises = fs.promises; const fsPromises = fs.promises;
const bashScriptPath = path.join(this.dirname, ".bash_set_path"); const bashScriptPath = path.join(this.dirname, ".bash_set_path");
@ -361,7 +361,7 @@ export class Kubectl {
} }
protected getDownloadMirror() { protected getDownloadMirror() {
const mirror = packageMirrors.get(userStore.preferences?.downloadMirror); const mirror = packageMirrors.get(UserStore.getInstance().preferences?.downloadMirror);
if (mirror) { if (mirror) {
return mirror; return mirror;

View File

@ -6,23 +6,22 @@ import url from "url";
import * as WebSocket from "ws"; import * as WebSocket from "ws";
import { apiPrefix, apiKubePrefix } from "../common/vars"; import { apiPrefix, apiKubePrefix } from "../common/vars";
import { Router } from "./router"; import { Router } from "./router";
import { ClusterManager } from "./cluster-manager";
import { ContextHandler } from "./context-handler"; import { ContextHandler } from "./context-handler";
import logger from "./logger"; import logger from "./logger";
import { NodeShellSession, LocalShellSession } from "./shell-session"; 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 origin: string;
protected proxyServer: http.Server; protected proxyServer: http.Server;
protected router: Router; protected router: Router;
protected closed = false; protected closed = false;
protected retryCounters = new Map<string, number>(); protected retryCounters = new Map<string, number>();
static create(port: number, clusterManager: ClusterManager) { constructor(protected port: number) {
return new LensProxy(port, clusterManager).listen(); super();
}
private constructor(protected port: number, protected clusterManager: ClusterManager) {
this.origin = `http://localhost:${port}`; this.origin = `http://localhost:${port}`;
this.router = new Router(); this.router = new Router();
} }
@ -66,7 +65,7 @@ export class LensProxy {
} }
protected async handleProxyUpgrade(proxy: httpProxy, req: http.IncomingMessage, socket: net.Socket, head: Buffer) { 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) { if (cluster) {
const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, ""); const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, "");
@ -171,7 +170,7 @@ export class LensProxy {
const ws = new WebSocket.Server({ noServer: true }); const ws = new WebSocket.Server({ noServer: true });
return ws.on("connection", ((socket: WebSocket, req: http.IncomingMessage) => { 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 nodeParam = url.parse(req.url, true).query["node"]?.toString();
const shell = nodeParam const shell = nodeParam
? new NodeShellSession(socket, cluster, nodeParam) ? new NodeShellSession(socket, cluster, nodeParam)
@ -197,7 +196,7 @@ export class LensProxy {
} }
protected async handleRequest(proxy: httpProxy, req: http.IncomingMessage, res: http.ServerResponse) { 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) { if (cluster) {
const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler); const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler);

View File

@ -1,7 +1,7 @@
import { LensProtocolRouterMain } from "../router"; import { LensProtocolRouterMain } from "../router";
import { noop } from "../../../common/utils"; import { noop } from "../../../common/utils";
import { extensionsStore } from "../../../extensions/extensions-store"; import { ExtensionsStore } from "../../../extensions/extensions-store";
import { extensionLoader } from "../../../extensions/extension-loader"; import { ExtensionLoader } from "../../../extensions/extension-loader";
import * as uuid from "uuid"; import * as uuid from "uuid";
import { LensMainExtension } from "../../../extensions/core-api"; import { LensMainExtension } from "../../../extensions/core-api";
import { broadcastMessage } from "../../../common/ipc"; import { broadcastMessage } from "../../../common/ipc";
@ -16,20 +16,28 @@ function throwIfDefined(val: any): void {
} }
describe("protocol router tests", () => { describe("protocol router tests", () => {
let lpr: LensProtocolRouterMain;
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks(); ExtensionsStore.getInstanceOrCreate();
(extensionsStore as any).state.clear(); ExtensionLoader.getInstanceOrCreate();
(extensionLoader as any).instances.clear();
LensProtocolRouterMain.resetInstance(); const lpr = LensProtocolRouterMain.getInstanceOrCreate();
lpr = LensProtocolRouterMain.getInstance<LensProtocolRouterMain>();
lpr.extensionsLoaded = true; lpr.extensionsLoaded = true;
lpr.rendererLoaded = true; lpr.rendererLoaded = true;
}); });
afterEach(() => {
jest.clearAllMocks();
ExtensionsStore.resetInstance();
ExtensionLoader.resetInstance();
LensProtocolRouterMain.resetInstance();
});
it("should throw on non-lens URLS", async () => { it("should throw on non-lens URLS", async () => {
try { try {
const lpr = LensProtocolRouterMain.getInstance();
expect(await lpr.route("https://google.ca")).toBeUndefined(); expect(await lpr.route("https://google.ca")).toBeUndefined();
} catch (error) { } catch (error) {
expect(error).toBeInstanceOf(Error); expect(error).toBeInstanceOf(Error);
@ -38,6 +46,8 @@ describe("protocol router tests", () => {
it("should throw when host not internal or extension", async () => { it("should throw when host not internal or extension", async () => {
try { try {
const lpr = LensProtocolRouterMain.getInstance();
expect(await lpr.route("lens://foobar")).toBeUndefined(); expect(await lpr.route("lens://foobar")).toBeUndefined();
} catch (error) { } catch (error) {
expect(error).toBeInstanceOf(Error); expect(error).toBeInstanceOf(Error);
@ -57,14 +67,15 @@ describe("protocol router tests", () => {
isEnabled: true, isEnabled: true,
absolutePath: "/foo/bar", absolutePath: "/foo/bar",
}); });
const lpr = LensProtocolRouterMain.getInstance();
ext.protocolHandlers.push({ ext.protocolHandlers.push({
pathSchema: "/", pathSchema: "/",
handler: noop, handler: noop,
}); });
(extensionLoader as any).instances.set(extId, ext); (ExtensionLoader.getInstance() as any).instances.set(extId, ext);
(extensionsStore as any).state.set(extId, { enabled: true, name: "@mirantis/minikube" }); (ExtensionsStore.getInstance() as any).state.set(extId, { enabled: true, name: "@mirantis/minikube" });
lpr.addInternalHandler("/", noop); lpr.addInternalHandler("/", noop);
@ -86,6 +97,7 @@ describe("protocol router tests", () => {
}); });
it("should call handler if matches", async () => { it("should call handler if matches", async () => {
const lpr = LensProtocolRouterMain.getInstance();
let called = false; let called = false;
lpr.addInternalHandler("/page", () => { called = true; }); lpr.addInternalHandler("/page", () => { called = true; });
@ -101,6 +113,7 @@ describe("protocol router tests", () => {
}); });
it("should call most exact handler", async () => { it("should call most exact handler", async () => {
const lpr = LensProtocolRouterMain.getInstance();
let called: any = 0; let called: any = 0;
lpr.addInternalHandler("/page", () => { called = 1; }); lpr.addInternalHandler("/page", () => { called = 1; });
@ -119,6 +132,7 @@ describe("protocol router tests", () => {
it("should call most exact handler for an extension", async () => { it("should call most exact handler for an extension", async () => {
let called: any = 0; let called: any = 0;
const lpr = LensProtocolRouterMain.getInstance();
const extId = uuid.v4(); const extId = uuid.v4();
const ext = new LensMainExtension({ const ext = new LensMainExtension({
id: extId, id: extId,
@ -141,8 +155,8 @@ describe("protocol router tests", () => {
handler: params => { called = params.pathname.id; }, handler: params => { called = params.pathname.id; },
}); });
(extensionLoader as any).instances.set(extId, ext); (ExtensionLoader.getInstance() as any).instances.set(extId, ext);
(extensionsStore as any).state.set(extId, { enabled: true, name: "@foobar/icecream" }); (ExtensionsStore.getInstance() as any).state.set(extId, { enabled: true, name: "@foobar/icecream" });
try { try {
expect(await lpr.route("lens://extension/@foobar/icecream/page/foob")).toBeUndefined(); 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 () => { it("should work with non-org extensions", async () => {
const lpr = LensProtocolRouterMain.getInstance();
let called: any = 0; let called: any = 0;
{ {
@ -177,8 +192,8 @@ describe("protocol router tests", () => {
handler: params => { called = params.pathname.id; }, handler: params => { called = params.pathname.id; },
}); });
(extensionLoader as any).instances.set(extId, ext); (ExtensionLoader.getInstance() as any).instances.set(extId, ext);
(extensionsStore as any).state.set(extId, { enabled: true, name: "@foobar/icecream" }); (ExtensionsStore.getInstance() as any).state.set(extId, { enabled: true, name: "@foobar/icecream" });
} }
{ {
@ -201,12 +216,12 @@ describe("protocol router tests", () => {
handler: () => { called = 1; }, handler: () => { called = 1; },
}); });
(extensionLoader as any).instances.set(extId, ext); (ExtensionLoader.getInstance() as any).instances.set(extId, ext);
(extensionsStore as any).state.set(extId, { enabled: true, name: "icecream" }); (ExtensionsStore.getInstance() as any).state.set(extId, { enabled: true, name: "icecream" });
} }
(extensionsStore as any).state.set("@foobar/icecream", { enabled: true, name: "@foobar/icecream" }); (ExtensionsStore.getInstance() 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("icecream", { enabled: true, name: "icecream" });
try { try {
expect(await lpr.route("lens://extension/icecream/page")).toBeUndefined(); expect(await lpr.route("lens://extension/icecream/page")).toBeUndefined();
@ -219,10 +234,13 @@ describe("protocol router tests", () => {
}); });
it("should throw if urlSchema is invalid", () => { it("should throw if urlSchema is invalid", () => {
const lpr = LensProtocolRouterMain.getInstance();
expect(() => lpr.addInternalHandler("/:@", noop)).toThrowError(); expect(() => lpr.addInternalHandler("/:@", noop)).toThrowError();
}); });
it("should call most exact handler with 3 found handlers", async () => { it("should call most exact handler with 3 found handlers", async () => {
const lpr = LensProtocolRouterMain.getInstance();
let called: any = 0; let called: any = 0;
lpr.addInternalHandler("/", () => { called = 2; }); lpr.addInternalHandler("/", () => { called = 2; });
@ -241,6 +259,7 @@ describe("protocol router tests", () => {
}); });
it("should call most exact handler with 2 found handlers", async () => { it("should call most exact handler with 2 found handlers", async () => {
const lpr = LensProtocolRouterMain.getInstance();
let called: any = 0; let called: any = 0;
lpr.addInternalHandler("/", () => { called = 2; }); lpr.addInternalHandler("/", () => { called = 2; });

View File

@ -1,6 +1,6 @@
import path from "path"; import path from "path";
import { helmCli } from "../helm/helm-cli"; import { helmCli } from "../helm/helm-cli";
import { userStore } from "../../common/user-store"; import { UserStore } from "../../common/user-store";
import { ShellSession } from "./shell-session"; import { ShellSession } from "./shell-session";
export class LocalShellSession extends ShellSession { export class LocalShellSession extends ShellSession {
@ -21,8 +21,8 @@ export class LocalShellSession extends ShellSession {
protected async getShellArgs(shell: string): Promise<string[]> { protected async getShellArgs(shell: string): Promise<string[]> {
const helmpath = helmCli.getBinaryDir(); const helmpath = helmCli.getBinaryDir();
const pathFromPreferences = userStore.preferences.kubectlBinariesPath || this.kubectl.getBundledPath(); const pathFromPreferences = UserStore.getInstance().preferences.kubectlBinariesPath || this.kubectl.getBundledPath();
const kubectlPathDir = userStore.preferences.downloadKubectlBinaries ? await this.kubectlBinDirP : path.dirname(pathFromPreferences); const kubectlPathDir = UserStore.getInstance().preferences.downloadKubectlBinaries ? await this.kubectlBinDirP : path.dirname(pathFromPreferences);
switch(path.basename(shell)) { switch(path.basename(shell)) {
case "powershell.exe": case "powershell.exe":

View File

@ -6,7 +6,7 @@ import { app } from "electron";
import { clearKubeconfigEnvVars } from "../utils/clear-kube-env-vars"; import { clearKubeconfigEnvVars } from "../utils/clear-kube-env-vars";
import path from "path"; import path from "path";
import { isWindows } from "../../common/vars"; import { isWindows } from "../../common/vars";
import { userStore } from "../../common/user-store"; import { UserStore } from "../../common/user-store";
import * as pty from "node-pty"; import * as pty from "node-pty";
import { appEventBus } from "../../common/event-bus"; import { appEventBus } from "../../common/event-bus";
@ -119,7 +119,7 @@ export abstract class ShellSession {
protected async getShellEnv() { protected async getShellEnv() {
const env = clearKubeconfigEnvVars(JSON.parse(JSON.stringify(await shellEnv()))); const env = clearKubeconfigEnvVars(JSON.parse(JSON.stringify(await shellEnv())));
const pathStr = [...this.getPathEntries(), await this.kubectlBinDirP, process.env.PATH].join(path.delimiter); 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 delete env.DEBUG; // don't pass DEBUG into shells

View File

@ -61,33 +61,32 @@ export class WindowManager extends Singleton {
this.windowState.manage(this.mainWindow); this.windowState.manage(this.mainWindow);
// open external links in default browser (target=_blank, window.open) // open external links in default browser (target=_blank, window.open)
this.mainWindow.webContents.on("new-window", (event, url) => { this.mainWindow
event.preventDefault(); .on("focus", () => {
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" }); appEventBus.emit({ name: "app", action: "focus" });
}); })
this.mainWindow.on("blur", () => { .on("blur", () => {
appEventBus.emit({ name: "app", action: "blur" }); appEventBus.emit({ name: "app", action: "blur" });
}); })
.on("closed", () => {
// clean up // clean up
this.mainWindow.on("closed", () => {
this.windowState.unmanage(); this.windowState.unmanage();
this.mainWindow = null; this.mainWindow = null;
this.splashWindow = null; this.splashWindow = null;
app.dock?.hide(); // hide icon in dock (mac-os) app.dock?.hide(); // hide icon in dock (mac-os)
}); })
.webContents
this.mainWindow.webContents.on("did-fail-load", (_event, code, desc) => { .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 }); logger.error(`[WINDOW-MANAGER]: Failed to load Main window`, { code, desc });
}); })
.on("did-finish-load", () => {
this.mainWindow.webContents.on("did-finish-load", () => {
logger.info("[WINDOW-MANAGER]: Main window loaded"); logger.info("[WINDOW-MANAGER]: Main window loaded");
}); });
} }
@ -101,8 +100,9 @@ export class WindowManager extends Singleton {
setTimeout(() => { setTimeout(() => {
appEventBus.emit({ name: "app", action: "start" }); appEventBus.emit({ name: "app", action: "start" });
}, 1000); }, 1000);
} catch (err) { } catch (error) {
dialog.showErrorBox("ERROR!", err.toString()); logger.error("Showing main window failed", { error });
dialog.showErrorBox("ERROR!", error.toString());
} }
} }

View File

@ -1,6 +1,6 @@
// Cleans up a store that had the state related data stored // Cleans up a store that had the state related data stored
import { Hotbar } from "../../common/hotbar-store"; import { Hotbar } from "../../common/hotbar-store";
import { clusterStore } from "../../common/cluster-store"; import { ClusterStore } from "../../common/cluster-store";
import { migration } from "../migration-wrapper"; import { migration } from "../migration-wrapper";
export default migration({ export default migration({
@ -8,7 +8,7 @@ export default migration({
run(store) { run(store) {
const hotbars: Hotbar[] = []; const hotbars: Hotbar[] = [];
clusterStore.enabledClustersList.forEach((cluster: any) => { ClusterStore.getInstance().enabledClustersList.forEach((cluster: any) => {
const name = cluster.workspace || "default"; const name = cluster.workspace || "default";
let hotbar = hotbars.find((h) => h.name === name); let hotbar = hotbars.find((h) => h.name === name);

View File

@ -6,19 +6,20 @@ import * as MobxReact from "mobx-react";
import * as ReactRouter from "react-router"; import * as ReactRouter from "react-router";
import * as ReactRouterDom from "react-router-dom"; import * as ReactRouterDom from "react-router-dom";
import { render, unmountComponentAtNode } from "react-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 { delay } from "../common/utils";
import { isMac, isDevelopment } from "../common/vars"; 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 * as LensExtensions from "../extensions/extension-api";
import { extensionDiscovery } from "../extensions/extension-discovery"; import { ExtensionDiscovery } from "../extensions/extension-discovery";
import { extensionLoader } from "../extensions/extension-loader"; import { ExtensionLoader } from "../extensions/extension-loader";
import { extensionsStore } from "../extensions/extensions-store"; import { ExtensionsStore } from "../extensions/extensions-store";
import { hotbarStore } from "../common/hotbar-store"; import { FilesystemProvisionerStore } from "../main/extension-filesystem";
import { filesystemProvisionerStore } from "../main/extension-filesystem";
import { App } from "./components/app"; import { App } from "./components/app";
import { LensApp } from "./lens-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 * If this is a development buid, wait a second to attach
@ -50,8 +51,16 @@ export async function bootstrap(App: AppComponent) {
await attachChromeDebugger(); await attachChromeDebugger();
rootElem.classList.toggle("is-mac", isMac); rootElem.classList.toggle("is-mac", isMac);
extensionLoader.init(); ExtensionLoader.getInstanceOrCreate().init();
extensionDiscovery.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 // preload common stores
await Promise.all([ await Promise.all([
@ -59,8 +68,9 @@ export async function bootstrap(App: AppComponent) {
hotbarStore.load(), hotbarStore.load(),
clusterStore.load(), clusterStore.load(),
extensionsStore.load(), extensionsStore.load(),
filesystemProvisionerStore.load(), filesystemStore.load(),
themeStore.init(), themeStore.init(),
helmRepoManager.init(),
]); ]);
// Register additional store listeners // Register additional store listeners
@ -72,8 +82,8 @@ export async function bootstrap(App: AppComponent) {
} }
window.addEventListener("message", (ev: MessageEvent) => { window.addEventListener("message", (ev: MessageEvent) => {
if (ev.data === "teardown") { if (ev.data === "teardown") {
userStore.unregisterIpcListener(); UserStore.getInstance(false)?.unregisterIpcListener();
clusterStore.unregisterIpcListener(); ClusterStore.getInstance(false)?.unregisterIpcListener();
unmountComponentAtNode(rootElem); unmountComponentAtNode(rootElem);
window.location.href = "about:blank"; window.location.href = "about:blank";
} }

View File

@ -11,10 +11,10 @@ import { AceEditor } from "../ace-editor";
import { Button } from "../button"; import { Button } from "../button";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { kubeConfigDefaultPath, loadConfig, splitConfig, validateConfig, validateKubeConfig } from "../../../common/kube-helpers"; 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 { v4 as uuid } from "uuid";
import { navigate } from "../../navigation"; import { navigate } from "../../navigation";
import { userStore } from "../../../common/user-store"; import { UserStore } from "../../../common/user-store";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
import { Notifications } from "../notifications"; import { Notifications } from "../notifications";
import { Tab, Tabs } from "../tabs"; import { Tab, Tabs } from "../tabs";
@ -44,13 +44,13 @@ export class AddCluster extends React.Component {
@observable showSettings = false; @observable showSettings = false;
componentDidMount() { componentDidMount() {
clusterStore.setActive(null); ClusterStore.getInstance().setActive(null);
this.setKubeConfig(userStore.kubeConfigPath); this.setKubeConfig(UserStore.getInstance().kubeConfigPath);
appEventBus.emit({ name: "cluster-add", action: "start" }); appEventBus.emit({ name: "cluster-add", action: "start" });
} }
componentWillUnmount() { componentWillUnmount() {
userStore.markNewContextsAsSeen(); UserStore.getInstance().markNewContextsAsSeen();
} }
@action @action
@ -60,9 +60,9 @@ export class AddCluster extends React.Component {
validateConfig(this.kubeConfigLocal); validateConfig(this.kubeConfigLocal);
this.refreshContexts(); this.refreshContexts();
this.kubeConfigPath = filePath; this.kubeConfigPath = filePath;
userStore.kubeConfigPath = filePath; // save to store UserStore.getInstance().kubeConfigPath = filePath; // save to store
} catch (err) { } catch (err) {
if (!userStore.isDefaultKubeConfigPath) { if (!UserStore.getInstance().isDefaultKubeConfigPath) {
Notifications.error( Notifications.error(
<div>Can&apos;t setup <code>{filePath}</code> as kubeconfig: {String(err)}</div> <div>Can&apos;t setup <code>{filePath}</code> as kubeconfig: {String(err)}</div>
); );
@ -181,7 +181,7 @@ export class AddCluster extends React.Component {
}); });
runInAction(() => { runInAction(() => {
clusterStore.addClusters(...newClusters); ClusterStore.getInstance().addClusters(...newClusters);
Notifications.ok( Notifications.ok(
<>Successfully imported <b>{newClusters.length}</b> cluster(s)</> <>Successfully imported <b>{newClusters.length}</b> cluster(s)</>
@ -308,7 +308,7 @@ export class AddCluster extends React.Component {
} }
onKubeConfigInputBlur = () => { onKubeConfigInputBlur = () => {
const isChanged = this.kubeConfigPath !== userStore.kubeConfigPath; const isChanged = this.kubeConfigPath !== UserStore.getInstance().kubeConfigPath;
if (isChanged) { if (isChanged) {
this.kubeConfigPath = this.kubeConfigPath.replace("~", os.homedir()); this.kubeConfigPath = this.kubeConfigPath.replace("~", os.homedir());
@ -316,7 +316,7 @@ export class AddCluster extends React.Component {
try { try {
this.setKubeConfig(this.kubeConfigPath, { throwError: true }); this.setKubeConfig(this.kubeConfigPath, { throwError: true });
} catch (err) { } 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<string>) => { protected formatContextLabel = ({ value: context }: SelectOption<string>) => {
const isNew = userStore.newContexts.has(context); const isNew = UserStore.getInstance().newContexts.has(context);
const isSelected = this.selectedContexts.includes(context); const isSelected = this.selectedContexts.includes(context);
return ( return (

View File

@ -19,7 +19,7 @@ import { Button } from "../button";
import { releaseStore } from "./release.store"; import { releaseStore } from "./release.store";
import { Notifications } from "../notifications"; import { Notifications } from "../notifications";
import { createUpgradeChartTab } from "../dock/upgrade-chart.store"; import { createUpgradeChartTab } from "../dock/upgrade-chart.store";
import { themeStore } from "../../theme.store"; import { ThemeStore } from "../../theme.store";
import { apiManager } from "../../api/api-manager"; import { apiManager } from "../../api/api-manager";
import { SubTitle } from "../layout/sub-title"; import { SubTitle } from "../layout/sub-title";
import { secretsStore } from "../+config-secrets/secrets.store"; import { secretsStore } from "../+config-secrets/secrets.store";
@ -259,7 +259,7 @@ export class ReleaseDetails extends Component<Props> {
return ( return (
<Drawer <Drawer
className={cssNames("ReleaseDetails", themeStore.activeTheme.type)} className={cssNames("ReleaseDetails", ThemeStore.getInstance().activeTheme.type)}
usePortal={true} usePortal={true}
open={!!release} open={!!release}
title={title} title={title}

View File

@ -11,7 +11,7 @@ import { MenuItem, MenuActions } from "../menu";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { CatalogEntityContextMenu, CatalogEntityContextMenuContext, catalogEntityRunContext } from "../../api/catalog-entity"; import { CatalogEntityContextMenu, CatalogEntityContextMenuContext, catalogEntityRunContext } from "../../api/catalog-entity";
import { Badge } from "../badge"; import { Badge } from "../badge";
import { hotbarStore } from "../../../common/hotbar-store"; import { HotbarStore } from "../../../common/hotbar-store";
import { autobind } from "../../utils"; import { autobind } from "../../utils";
import { Notifications } from "../notifications"; import { Notifications } from "../notifications";
import { ConfirmDialog } from "../confirm-dialog"; import { ConfirmDialog } from "../confirm-dialog";
@ -57,7 +57,7 @@ export class Catalog extends React.Component {
} }
addToHotbar(item: CatalogEntityItem) { addToHotbar(item: CatalogEntityItem) {
const hotbar = hotbarStore.getByName("default"); // FIXME const hotbar = HotbarStore.getInstance().getByName("default"); // FIXME
if (!hotbar) { if (!hotbar) {
return; return;
@ -67,7 +67,7 @@ export class Catalog extends React.Component {
} }
removeFromHotbar(item: CatalogEntityItem) { removeFromHotbar(item: CatalogEntityItem) {
const hotbar = hotbarStore.getByName("default"); // FIXME const hotbar = HotbarStore.getInstance().getByName("default"); // FIXME
if (!hotbar) { if (!hotbar) {
return; return;

View File

@ -11,7 +11,7 @@ import { eventStore } from "../+events/event.store";
import { autobind, cssNames, prevDefault } from "../../utils"; import { autobind, cssNames, prevDefault } from "../../utils";
import { ItemObject } from "../../item.store"; import { ItemObject } from "../../item.store";
import { Spinner } from "../spinner"; import { Spinner } from "../spinner";
import { themeStore } from "../../theme.store"; import { ThemeStore } from "../../theme.store";
import { lookupApiLink } from "../../api/kube-api"; import { lookupApiLink } from "../../api/kube-api";
import { kubeSelectedUrlParam, showDetails } from "../kube-object"; import { kubeSelectedUrlParam, showDetails } from "../kube-object";
@ -145,7 +145,7 @@ export class ClusterIssues extends React.Component<Props> {
sortByDefault={{ sortBy: sortBy.object, orderBy: "asc" }} sortByDefault={{ sortBy: sortBy.object, orderBy: "asc" }}
sortSyncWithUrl={false} sortSyncWithUrl={false}
getTableRow={this.getTableRow} getTableRow={this.getTableRow}
className={cssNames("box grow", themeStore.activeTheme.type)} className={cssNames("box grow", ThemeStore.getInstance().activeTheme.type)}
> >
<TableHead nowrap> <TableHead nowrap>
<TableCell className="message">Message</TableCell> <TableCell className="message">Message</TableCell>

View File

@ -5,7 +5,7 @@ import { reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react"; import { disposeOnUnmount, observer } from "mobx-react";
import { nodesStore } from "../+nodes/nodes.store"; import { nodesStore } from "../+nodes/nodes.store";
import { podsStore } from "../+workloads-pods/pods.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 { interval } from "../../utils";
import { TabLayout } from "../layout/tab-layout"; import { TabLayout } from "../layout/tab-layout";
import { Spinner } from "../spinner"; import { Spinner } from "../spinner";
@ -66,7 +66,7 @@ export class ClusterOverview extends React.Component {
render() { render() {
const isLoaded = nodesStore.isLoaded && podsStore.isLoaded; const isLoaded = nodesStore.isLoaded && podsStore.isLoaded;
const isMetricsHidden = clusterStore.isMetricHidden(ResourceType.Cluster); const isMetricsHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.Cluster);
return ( return (
<TabLayout> <TabLayout>

View File

@ -9,7 +9,7 @@ import { nodesStore } from "../+nodes/nodes.store";
import { ChartData, PieChart } from "../chart"; import { ChartData, PieChart } from "../chart";
import { ClusterNoMetrics } from "./cluster-no-metrics"; import { ClusterNoMetrics } from "./cluster-no-metrics";
import { bytesToUnits } from "../../utils"; import { bytesToUnits } from "../../utils";
import { themeStore } from "../../theme.store"; import { ThemeStore } from "../../theme.store";
import { getMetricLastPoints } from "../../api/endpoints/metrics.api"; import { getMetricLastPoints } from "../../api/endpoints/metrics.api";
export const ClusterPieCharts = observer(() => { export const ClusterPieCharts = observer(() => {
@ -29,7 +29,7 @@ export const ClusterPieCharts = observer(() => {
const { podUsage, podCapacity } = data; const { podUsage, podCapacity } = data;
const cpuLimitsOverload = cpuLimits > cpuCapacity; const cpuLimitsOverload = cpuLimits > cpuCapacity;
const memoryLimitsOverload = memoryLimits > memoryCapacity; const memoryLimitsOverload = memoryLimits > memoryCapacity;
const defaultColor = themeStore.activeTheme.colors.pieChartDefaultColor; const defaultColor = ThemeStore.getInstance().activeTheme.colors.pieChartDefaultColor;
if (!memoryCapacity || !cpuCapacity || !podCapacity) return null; if (!memoryCapacity || !cpuCapacity || !podCapacity) return null;
const cpuData: ChartData = { const cpuData: ChartData = {

View File

@ -2,10 +2,12 @@ import "@testing-library/jest-dom/extend-expect";
import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import fse from "fs-extra"; import fse from "fs-extra";
import React from "react"; 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 { ConfirmDialog } from "../../confirm-dialog";
import { Notifications } from "../../notifications"; import { Notifications } from "../../notifications";
import { ExtensionStateStore } from "../extension-install.store";
import { Extensions } from "../extensions"; import { Extensions } from "../extensions";
jest.mock("fs-extra"); jest.mock("fs-extra");
@ -18,20 +20,38 @@ jest.mock("../../../../common/utils", () => ({
extractTar: jest.fn(() => Promise.resolve()) extractTar: jest.fn(() => Promise.resolve())
})); }));
jest.mock("../../../../extensions/extension-discovery", () => ({ jest.mock("../../notifications", () => ({
...jest.requireActual("../../../../extensions/extension-discovery"), ok: jest.fn(),
extensionDiscovery: { error: jest.fn(),
localFolderPath: "/fake/path", info: jest.fn()
uninstallExtension: jest.fn(() => Promise.resolve()),
isLoaded: true
}
})); }));
jest.mock("../../../../extensions/extension-loader", () => ({ jest.mock("electron", () => {
...jest.requireActual("../../../../extensions/extension-loader"), return {
extensionLoader: { app: {
userExtensions: new Map([ getVersion: () => "99.99.99",
["extensionId", { getPath: () => "tmp",
getLocale: () => "en",
setLoginItemSettings: (): void => void 0,
}
};
});
describe("Extensions", () => {
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", id: "extensionId",
manifest: { manifest: {
name: "test", name: "test",
@ -41,23 +61,11 @@ jest.mock("../../../../extensions/extension-loader", () => ({
manifestPath: "/symlinked/path/package.json", manifestPath: "/symlinked/path/package.json",
isBundled: false, isBundled: false,
isEnabled: true isEnabled: true
}] });
])
}
}));
jest.mock("../../notifications", () => ({
ok: jest.fn(),
error: jest.fn(),
info: jest.fn()
}));
describe("Extensions", () => {
beforeEach(() => {
ExtensionStateStore.resetInstance();
}); });
it("disables uninstall and disable buttons while uninstalling", async () => { it("disables uninstall and disable buttons while uninstalling", async () => {
ExtensionDiscovery.getInstance().isLoaded = true;
render(<><Extensions /><ConfirmDialog/></>); render(<><Extensions /><ConfirmDialog/></>);
expect(screen.getByText("Disable").closest("button")).not.toBeDisabled(); expect(screen.getByText("Disable").closest("button")).not.toBeDisabled();
@ -68,13 +76,14 @@ describe("Extensions", () => {
// Approve confirm dialog // Approve confirm dialog
fireEvent.click(screen.getByText("Yes")); fireEvent.click(screen.getByText("Yes"));
expect(extensionDiscovery.uninstallExtension).toHaveBeenCalled(); expect(ExtensionDiscovery.getInstance().uninstallExtension).toHaveBeenCalled();
expect(screen.getByText("Disable").closest("button")).toBeDisabled(); expect(screen.getByText("Disable").closest("button")).toBeDisabled();
expect(screen.getByText("Uninstall").closest("button")).toBeDisabled(); expect(screen.getByText("Uninstall").closest("button")).toBeDisabled();
}); });
it("displays error notification on uninstall error", () => { it("displays error notification on uninstall error", () => {
(extensionDiscovery.uninstallExtension as any).mockImplementationOnce(() => ExtensionDiscovery.getInstance().isLoaded = true;
(ExtensionDiscovery.getInstance().uninstallExtension as any).mockImplementationOnce(() =>
Promise.reject() Promise.reject()
); );
render(<><Extensions /><ConfirmDialog/></>); render(<><Extensions /><ConfirmDialog/></>);
@ -115,12 +124,12 @@ describe("Extensions", () => {
}); });
it("displays spinner while extensions are loading", () => { it("displays spinner while extensions are loading", () => {
extensionDiscovery.isLoaded = false; ExtensionDiscovery.getInstance().isLoaded = false;
const { container } = render(<Extensions />); const { container } = render(<Extensions />);
expect(container.querySelector(".Spinner")).toBeInTheDocument(); expect(container.querySelector(".Spinner")).toBeInTheDocument();
extensionDiscovery.isLoaded = true; ExtensionDiscovery.getInstance().isLoaded = true;
waitFor(() => waitFor(() =>
expect(container.querySelector(".Spinner")).not.toBeInTheDocument() expect(container.querySelector(".Spinner")).not.toBeInTheDocument()

View File

@ -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<string, ExtensionState>();
}

View File

@ -7,8 +7,8 @@ import path from "path";
import React from "react"; import React from "react";
import { downloadFile, extractTar, listTarEntries, readFileFromTar } from "../../../common/utils"; import { downloadFile, extractTar, listTarEntries, readFileFromTar } from "../../../common/utils";
import { docsUrl } from "../../../common/vars"; import { docsUrl } from "../../../common/vars";
import { extensionDiscovery, InstalledExtension, manifestFilename } from "../../../extensions/extension-discovery"; import { ExtensionDiscovery, InstalledExtension, manifestFilename } from "../../../extensions/extension-discovery";
import { extensionLoader } from "../../../extensions/extension-loader"; import { ExtensionLoader } from "../../../extensions/extension-loader";
import { extensionDisplayName, LensExtensionManifest, sanitizeExtensionName } from "../../../extensions/lens-extension"; import { extensionDisplayName, LensExtensionManifest, sanitizeExtensionName } from "../../../extensions/lens-extension";
import logger from "../../../main/logger"; import logger from "../../../main/logger";
import { prevDefault } from "../../utils"; import { prevDefault } from "../../utils";
@ -21,7 +21,6 @@ import { SubTitle } from "../layout/sub-title";
import { Notifications } from "../notifications"; import { Notifications } from "../notifications";
import { Spinner } from "../spinner/spinner"; import { Spinner } from "../spinner/spinner";
import { TooltipPosition } from "../tooltip"; import { TooltipPosition } from "../tooltip";
import { ExtensionStateStore } from "./extension-install.store";
import "./extensions.scss"; import "./extensions.scss";
interface InstallRequest { interface InstallRequest {
@ -39,6 +38,12 @@ interface InstallRequestValidated extends InstallRequestPreloaded {
tempFile: string; // temp system path to packed extension for unpacking tempFile: string; // temp system path to packed extension for unpacking
} }
interface ExtensionState {
displayName: string;
// Possible states the extension can be
state: "installing" | "uninstalling";
}
@observer @observer
export class Extensions extends React.Component { export class Extensions extends React.Component {
private static supportedFormats = ["tar", "tgz"]; private static supportedFormats = ["tar", "tgz"];
@ -50,9 +55,7 @@ export class Extensions extends React.Component {
} }
}; };
get extensionStateStore() { static installStates = observable.map<string, ExtensionState>();
return ExtensionStateStore.getInstance<ExtensionStateStore>();
}
@observable search = ""; @observable search = "";
@observable installPath = ""; @observable installPath = "";
@ -64,7 +67,7 @@ export class Extensions extends React.Component {
* Extensions that were removed from extensions but are still in "uninstalling" state * Extensions that were removed from extensions but are still in "uninstalling" state
*/ */
@computed get removedUninstalling() { @computed get removedUninstalling() {
return Array.from(this.extensionStateStore.extensionState.entries()) return Array.from(Extensions.installStates.entries())
.filter(([id, extension]) => .filter(([id, extension]) =>
extension.state === "uninstalling" extension.state === "uninstalling"
&& !this.extensions.find(extension => extension.id === id) && !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 * Extensions that were added to extensions but are still in "installing" state
*/ */
@computed get addedInstalling() { @computed get addedInstalling() {
return Array.from(this.extensionStateStore.extensionState.entries()) return Array.from(Extensions.installStates.entries())
.filter(([id, extension]) => .filter(([id, extension]) =>
extension.state === "installing" extension.state === "installing"
&& this.extensions.find(extension => extension.id === id) && this.extensions.find(extension => extension.id === id)
@ -91,7 +94,7 @@ export class Extensions extends React.Component {
Notifications.ok( Notifications.ok(
<p>Extension <b>{displayName}</b> successfully uninstalled!</p> <p>Extension <b>{displayName}</b> successfully uninstalled!</p>
); );
this.extensionStateStore.extensionState.delete(id); Extensions.installStates.delete(id);
}); });
this.addedInstalling.forEach(({ id, displayName }) => { this.addedInstalling.forEach(({ id, displayName }) => {
@ -104,7 +107,7 @@ export class Extensions extends React.Component {
Notifications.ok( Notifications.ok(
<p>Extension <b>{displayName}</b> successfully installed!</p> <p>Extension <b>{displayName}</b> successfully installed!</p>
); );
this.extensionStateStore.extensionState.delete(id); Extensions.installStates.delete(id);
this.installPath = ""; this.installPath = "";
// Enable installed extensions by default. // Enable installed extensions by default.
@ -117,7 +120,7 @@ export class Extensions extends React.Component {
@computed get extensions() { @computed get extensions() {
const searchText = this.search.toLowerCase(); const searchText = this.search.toLowerCase();
return Array.from(extensionLoader.userExtensions.values()) return Array.from(ExtensionLoader.getInstance().userExtensions.values())
.filter(({ manifest: { name, description }}) => ( .filter(({ manifest: { name, description }}) => (
name.toLowerCase().includes(searchText) name.toLowerCase().includes(searchText)
|| description?.toLowerCase().includes(searchText) || description?.toLowerCase().includes(searchText)
@ -125,7 +128,7 @@ export class Extensions extends React.Component {
} }
get extensionsPath() { get extensionsPath() {
return extensionDiscovery.localFolderPath; return ExtensionDiscovery.getInstance().localFolderPath;
} }
getExtensionPackageTemp(fileName = "") { getExtensionPackageTemp(fileName = "") {
@ -342,11 +345,11 @@ export class Extensions extends React.Component {
async unpackExtension({ fileName, tempFile, manifest: { name, version } }: InstallRequestValidated) { async unpackExtension({ fileName, tempFile, manifest: { name, version } }: InstallRequestValidated) {
const displayName = extensionDisplayName(name, version); 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 }); logger.info(`Unpacking extension ${displayName}`, { fileName, tempFile });
this.extensionStateStore.extensionState.set(extensionId, { Extensions.installStates.set(extensionId, {
state: "installing", state: "installing",
displayName displayName
}); });
@ -381,8 +384,8 @@ export class Extensions extends React.Component {
); );
// Remove install state on install failure // Remove install state on install failure
if (this.extensionStateStore.extensionState.get(extensionId)?.state === "installing") { if (Extensions.installStates.get(extensionId)?.state === "installing") {
this.extensionStateStore.extensionState.delete(extensionId); Extensions.installStates.delete(extensionId);
} }
} finally { } finally {
// clean up // clean up
@ -406,20 +409,20 @@ export class Extensions extends React.Component {
const displayName = extensionDisplayName(extension.manifest.name, extension.manifest.version); const displayName = extensionDisplayName(extension.manifest.name, extension.manifest.version);
try { try {
this.extensionStateStore.extensionState.set(extension.id, { Extensions.installStates.set(extension.id, {
state: "uninstalling", state: "uninstalling",
displayName displayName
}); });
await extensionDiscovery.uninstallExtension(extension); await ExtensionDiscovery.getInstance().uninstallExtension(extension);
} catch (error) { } catch (error) {
Notifications.error( Notifications.error(
<p>Uninstalling extension <b>{displayName}</b> has failed: <em>{error?.message ?? ""}</em></p> <p>Uninstalling extension <b>{displayName}</b> has failed: <em>{error?.message ?? ""}</em></p>
); );
// Remove uninstall state on uninstall failure // Remove uninstall state on uninstall failure
if (this.extensionStateStore.extensionState.get(extension.id)?.state === "uninstalling") { if (Extensions.installStates.get(extension.id)?.state === "uninstalling") {
this.extensionStateStore.extensionState.delete(extension.id); Extensions.installStates.delete(extension.id);
} }
} }
} }
@ -445,7 +448,7 @@ export class Extensions extends React.Component {
return extensions.map(extension => { return extensions.map(extension => {
const { id, isEnabled, manifest } = extension; const { id, isEnabled, manifest } = extension;
const { name, description, version } = manifest; const { name, description, version } = manifest;
const isUninstalling = this.extensionStateStore.extensionState.get(id)?.state === "uninstalling"; const isUninstalling = Extensions.installStates.get(id)?.state === "uninstalling";
return ( return (
<div key={id} className="extension flex gaps align-center"> <div key={id} className="extension flex gaps align-center">
@ -478,7 +481,7 @@ export class Extensions extends React.Component {
* True if at least one extension is in installing state * True if at least one extension is in installing state
*/ */
@computed get isInstalling() { @computed get isInstalling() {
return [...this.extensionStateStore.extensionState.values()].some(extension => extension.state === "installing"); return [...Extensions.installStates.values()].some(extension => extension.state === "installing");
} }
render() { render() {
@ -536,7 +539,11 @@ export class Extensions extends React.Component {
value={this.search} value={this.search}
onChange={(value) => this.search = value} onChange={(value) => this.search = value}
/> />
{extensionDiscovery.isLoaded ? this.renderExtensions() : <div className="spinner-wrapper"><Spinner/></div>} {
ExtensionDiscovery.getInstance().isLoaded
? this.renderExtensions()
: <div className="spinner-wrapper"><Spinner/></div>
}
</div> </div>
</PageLayout> </PageLayout>
</DropFileInput> </DropFileInput>

View File

@ -15,7 +15,7 @@ import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
import { getBackendServiceNamePort } from "../../api/endpoints/ingress.api"; import { getBackendServiceNamePort } from "../../api/endpoints/ingress.api";
import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting"; 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<Ingress> { interface Props extends KubeObjectDetailsProps<Ingress> {
} }
@ -101,6 +101,7 @@ export class IngressDetails extends React.Component<Props> {
if (!ingress) { if (!ingress) {
return null; return null;
} }
const { spec, status } = ingress; const { spec, status } = ingress;
const ingressPoints = status?.loadBalancer?.ingress; const ingressPoints = status?.loadBalancer?.ingress;
const { metrics } = ingressStore; const { metrics } = ingressStore;
@ -108,8 +109,7 @@ export class IngressDetails extends React.Component<Props> {
"Network", "Network",
"Duration", "Duration",
]; ];
const isMetricHidden = clusterStore.isMetricHidden(ResourceType.Ingress); const isMetricHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.Ingress);
const { serviceName, servicePort } = ingress.getServiceNamePort(); const { serviceName, servicePort } = ingress.getServiceNamePort();
return ( return (

View File

@ -6,7 +6,7 @@ import { NoMetrics } from "../resource-metrics/no-metrics";
import { IResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics"; import { IResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { ChartOptions, ChartPoint } from "chart.js"; import { ChartOptions, ChartPoint } from "chart.js";
import { themeStore } from "../../theme.store"; import { ThemeStore } from "../../theme.store";
import { mapValues } from "lodash"; import { mapValues } from "lodash";
type IContext = IResourceMetricsValue<Node, { metrics: IClusterMetrics }>; type IContext = IResourceMetricsValue<Node, { metrics: IClusterMetrics }>;
@ -14,7 +14,7 @@ type IContext = IResourceMetricsValue<Node, { metrics: IClusterMetrics }>;
export const NodeCharts = observer(() => { export const NodeCharts = observer(() => {
const { params: { metrics }, tabId, object } = useContext<IContext>(ResourceMetricsContext); const { params: { metrics }, tabId, object } = useContext<IContext>(ResourceMetricsContext);
const id = object.getId(); const id = object.getId();
const { chartCapacityColor } = themeStore.activeTheme.colors; const { chartCapacityColor } = ThemeStore.getInstance().activeTheme.colors;
if (!metrics) { if (!metrics) {
return null; return null;

View File

@ -18,7 +18,7 @@ import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { KubeEventDetails } from "../+events/kube-event-details"; import { KubeEventDetails } from "../+events/kube-event-details";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting"; 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<Node> { interface Props extends KubeObjectDetailsProps<Node> {
} }
@ -54,7 +54,7 @@ export class NodeDetails extends React.Component<Props> {
"Disk", "Disk",
"Pods", "Pods",
]; ];
const isMetricHidden = clusterStore.isMetricHidden(ResourceType.Node); const isMetricHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.Node);
return ( return (
<div className="NodeDetails"> <div className="NodeDetails">

View File

@ -13,7 +13,7 @@ import { systemName, isUrl, isPath } from "../input/input_validators";
import { SubTitle } from "../layout/sub-title"; import { SubTitle } from "../layout/sub-title";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { Notifications } from "../notifications"; 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<DialogProps> { interface Props extends Partial<DialogProps> {
onAddRepo: Function onAddRepo: Function
@ -79,7 +79,7 @@ export class AddHelmRepoDialog extends React.Component<Props> {
async addCustomRepo() { async addCustomRepo() {
try { try {
await repoManager.addСustomRepo(this.helmRepo); await HelmRepoManager.getInstance().addСustomRepo(this.helmRepo);
Notifications.ok(<>Helm repository <b>{this.helmRepo.name}</b> has added</>); Notifications.ok(<>Helm repository <b>{this.helmRepo.name}</b> has added</>);
this.props.onAddRepo(); this.props.onAddRepo();
this.close(); this.close();

View File

@ -3,7 +3,7 @@ import "./helm-charts.scss";
import React from "react"; import React from "react";
import { action, computed, observable } from "mobx"; 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 { Button } from "../button";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { Notifications } from "../notifications"; import { Notifications } from "../notifications";
@ -34,9 +34,9 @@ export class HelmCharts extends React.Component {
try { try {
if (!this.repos.length) { 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(); this.addedRepos.clear();
repos.forEach(repo => this.addedRepos.set(repo.name, repo)); repos.forEach(repo => this.addedRepos.set(repo.name, repo));
@ -49,7 +49,7 @@ export class HelmCharts extends React.Component {
async addRepo(repo: HelmRepo) { async addRepo(repo: HelmRepo) {
try { try {
await repoManager.addRepo(repo); await HelmRepoManager.getInstance().addRepo(repo);
this.addedRepos.set(repo.name, repo); this.addedRepos.set(repo.name, repo);
} catch (err) { } catch (err) {
Notifications.error(<>Adding helm branch <b>{repo.name}</b> has failed: {String(err)}</>); Notifications.error(<>Adding helm branch <b>{repo.name}</b> has failed: {String(err)}</>);
@ -58,7 +58,7 @@ export class HelmCharts extends React.Component {
async removeRepo(repo: HelmRepo) { async removeRepo(repo: HelmRepo) {
try { try {
await repoManager.removeRepo(repo); await HelmRepoManager.getInstance().removeRepo(repo);
this.addedRepos.delete(repo.name); this.addedRepos.delete(repo.name);
} catch (err) { } catch (err) {
Notifications.error( Notifications.error(

View File

@ -1,7 +1,7 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { Input, InputValidators } from "../input"; import { Input, InputValidators } from "../input";
import { SubTitle } from "../layout/sub-title"; 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 { observer } from "mobx-react";
import { bundledKubectlPath } from "../../../main/kubectl"; import { bundledKubectlPath } from "../../../main/kubectl";
import { SelectOption, Select } from "../select"; import { SelectOption, Select } from "../select";
@ -59,7 +59,7 @@ export const KubectlBinaries = observer(({ preferences }: { preferences: UserPre
<Input <Input
theme="round-black" theme="round-black"
value={downloadPath} value={downloadPath}
placeholder={userStore.getDefaultKubectlPath()} placeholder={getDefaultKubectlPath()}
validators={pathValidator} validators={pathValidator}
onChange={setDownloadPath} onChange={setDownloadPath}
onBlur={save} onBlur={save}

View File

@ -5,10 +5,10 @@ import moment from "moment-timezone";
import { computed, observable, reaction } from "mobx"; import { computed, observable, reaction } from "mobx";
import { disposeOnUnmount, observer } from "mobx-react"; import { disposeOnUnmount, observer } from "mobx-react";
import { userStore } from "../../../common/user-store";
import { isWindows } from "../../../common/vars"; import { isWindows } from "../../../common/vars";
import { appPreferenceRegistry, RegisteredAppPreference } from "../../../extensions/registries/app-preference-registry"; import { appPreferenceRegistry, RegisteredAppPreference } from "../../../extensions/registries/app-preference-registry";
import { themeStore } from "../../theme.store"; import { UserStore } from "../../../common/user-store";
import { ThemeStore } from "../../theme.store";
import { Input } from "../input"; import { Input } from "../input";
import { PageLayout } from "../layout/page-layout"; import { PageLayout } from "../layout/page-layout";
import { SubTitle } from "../layout/sub-title"; import { SubTitle } from "../layout/sub-title";
@ -30,12 +30,12 @@ enum Pages {
@observer @observer
export class Preferences extends React.Component { export class Preferences extends React.Component {
@observable httpProxy = userStore.preferences.httpsProxy || ""; @observable httpProxy = UserStore.getInstance().preferences.httpsProxy || "";
@observable shell = userStore.preferences.shell || ""; @observable shell = UserStore.getInstance().preferences.shell || "";
@observable activeTab = Pages.Application; @observable activeTab = Pages.Application;
@computed get themeOptions(): SelectOption<string>[] { @computed get themeOptions(): SelectOption<string>[] {
return themeStore.themes.map(theme => ({ return ThemeStore.getInstance().themes.map(theme => ({
label: theme.name, label: theme.name,
value: theme.id, value: theme.id,
})); }));
@ -98,18 +98,16 @@ export class Preferences extends React.Component {
} }
render() { render() {
const { preferences } = userStore;
const extensions = appPreferenceRegistry.getItems(); const extensions = appPreferenceRegistry.getItems();
const telemetryExtensions = extensions.filter(e => e.showInPreferencesTab == Pages.Telemetry); const telemetryExtensions = extensions.filter(e => e.showInPreferencesTab == Pages.Telemetry);
let defaultShell = process.env.SHELL || process.env.PTYSHELL; const { preferences } = UserStore.getInstance();
const defaultShell = process.env.SHELL
if (!defaultShell) { || process.env.PTYSHELL
if (isWindows) { || (
defaultShell = "powershell.exe"; isWindows
} else { ? "powershell.exe"
defaultShell = "System default shell"; : "System default shell"
} );
}
return ( return (
<PageLayout <PageLayout
@ -167,7 +165,7 @@ export class Preferences extends React.Component {
<Select <Select
options={this.timezoneOptions} options={this.timezoneOptions}
value={preferences.localeTimezone} value={preferences.localeTimezone}
onChange={({ value }: SelectOption) => userStore.setLocaleTimezone(value)} onChange={({ value }: SelectOption) => UserStore.getInstance().setLocaleTimezone(value)}
themeName="lens" themeName="lens"
/> />
</section> </section>

View File

@ -15,7 +15,7 @@ import { getDetailsUrl, KubeObjectDetailsProps, KubeObjectMeta } from "../kube-o
import { PersistentVolumeClaim } from "../../api/endpoints"; import { PersistentVolumeClaim } from "../../api/endpoints";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting"; 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<PersistentVolumeClaim> { interface Props extends KubeObjectDetailsProps<PersistentVolumeClaim> {
} }
@ -43,7 +43,7 @@ export class PersistentVolumeClaimDetails extends React.Component<Props> {
const metricTabs = [ const metricTabs = [
"Disk" "Disk"
]; ];
const isMetricHidden = clusterStore.isMetricHidden(ResourceType.VolumeClaim); const isMetricHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.VolumeClaim);
return ( return (
<div className="PersistentVolumeClaimDetails"> <div className="PersistentVolumeClaimDetails">

View File

@ -5,13 +5,13 @@ import { BarChart, ChartDataSets, memoryOptions } from "../chart";
import { isMetricsEmpty, normalizeMetrics } from "../../api/endpoints/metrics.api"; import { isMetricsEmpty, normalizeMetrics } from "../../api/endpoints/metrics.api";
import { NoMetrics } from "../resource-metrics/no-metrics"; import { NoMetrics } from "../resource-metrics/no-metrics";
import { IResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics"; import { IResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics";
import { themeStore } from "../../theme.store"; import { ThemeStore } from "../../theme.store";
type IContext = IResourceMetricsValue<PersistentVolumeClaim, { metrics: IPvcMetrics }>; type IContext = IResourceMetricsValue<PersistentVolumeClaim, { metrics: IPvcMetrics }>;
export const VolumeClaimDiskChart = observer(() => { export const VolumeClaimDiskChart = observer(() => {
const { params: { metrics }, object } = useContext<IContext>(ResourceMetricsContext); const { params: { metrics }, object } = useContext<IContext>(ResourceMetricsContext);
const { chartCapacityColor } = themeStore.activeTheme.colors; const { chartCapacityColor } = ThemeStore.getInstance().activeTheme.colors;
const id = object.getId(); const id = object.getId();
if (!metrics) return null; if (!metrics) return null;

View File

@ -3,7 +3,7 @@ import fs from "fs";
import path from "path"; import path from "path";
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { userStore } from "../../../common/user-store"; import { UserStore } from "../../../common/user-store";
import { navigate } from "../../navigation"; import { navigate } from "../../navigation";
import { Button } from "../button"; import { Button } from "../button";
import marked from "marked"; import marked from "marked";
@ -14,7 +14,7 @@ export class WhatsNew extends React.Component {
ok = () => { ok = () => {
navigate("/"); navigate("/");
userStore.saveLastSeenAppVersion(); UserStore.getInstance().saveLastSeenAppVersion();
}; };
render() { render() {

View File

@ -19,7 +19,7 @@ import { PodDetailsList } from "../+workloads-pods/pod-details-list";
import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting"; 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<DaemonSet> { interface Props extends KubeObjectDetailsProps<DaemonSet> {
} }
@ -49,7 +49,7 @@ export class DaemonSetDetails extends React.Component<Props> {
const nodeSelector = daemonSet.getNodeSelectors(); const nodeSelector = daemonSet.getNodeSelectors();
const childPods = daemonSetStore.getChildPods(daemonSet); const childPods = daemonSetStore.getChildPods(daemonSet);
const metrics = daemonSetStore.metrics; const metrics = daemonSetStore.metrics;
const isMetricHidden = clusterStore.isMetricHidden(ResourceType.DaemonSet); const isMetricHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.DaemonSet);
return ( return (
<div className="DaemonSetDetails"> <div className="DaemonSetDetails">

View File

@ -20,7 +20,7 @@ import { PodDetailsList } from "../+workloads-pods/pod-details-list";
import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting"; 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<Deployment> { interface Props extends KubeObjectDetailsProps<Deployment> {
} }
@ -49,7 +49,7 @@ export class DeploymentDetails extends React.Component<Props> {
const selectors = deployment.getSelectors(); const selectors = deployment.getSelectors();
const childPods = deploymentStore.getChildPods(deployment); const childPods = deploymentStore.getChildPods(deployment);
const metrics = deploymentStore.metrics; const metrics = deploymentStore.metrics;
const isMetricHidden = clusterStore.isMetricHidden(ResourceType.Deployment); const isMetricHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.Deployment);
return ( return (
<div className="DeploymentDetails"> <div className="DeploymentDetails">

View File

@ -8,7 +8,7 @@ import { observer } from "mobx-react";
import { PieChart } from "../chart"; import { PieChart } from "../chart";
import { cssVar } from "../../utils"; import { cssVar } from "../../utils";
import { ChartData, ChartDataSets } from "chart.js"; import { ChartData, ChartDataSets } from "chart.js";
import { themeStore } from "../../theme.store"; import { ThemeStore } from "../../theme.store";
interface SimpleChartDataSets extends ChartDataSets { interface SimpleChartDataSets extends ChartDataSets {
backgroundColor?: string[]; backgroundColor?: string[];
@ -41,7 +41,7 @@ export class OverviewWorkloadStatus extends React.Component<Props> {
labels: [] as string[], labels: [] as string[],
datasets: [{ datasets: [{
data: [1], data: [1],
backgroundColor: [themeStore.activeTheme.colors.pieChartDefaultColor], backgroundColor: [ThemeStore.getInstance().activeTheme.colors.pieChartDefaultColor],
label: "Empty" label: "Empty"
}] }]
}; };

View File

@ -5,13 +5,13 @@ import { BarChart, cpuOptions, memoryOptions } from "../chart";
import { isMetricsEmpty, normalizeMetrics } from "../../api/endpoints/metrics.api"; import { isMetricsEmpty, normalizeMetrics } from "../../api/endpoints/metrics.api";
import { NoMetrics } from "../resource-metrics/no-metrics"; import { NoMetrics } from "../resource-metrics/no-metrics";
import { IResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics"; import { IResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics";
import { themeStore } from "../../theme.store"; import { ThemeStore } from "../../theme.store";
type IContext = IResourceMetricsValue<any, { metrics: IPodMetrics }>; type IContext = IResourceMetricsValue<any, { metrics: IPodMetrics }>;
export const ContainerCharts = observer(() => { export const ContainerCharts = observer(() => {
const { params: { metrics }, tabId } = useContext<IContext>(ResourceMetricsContext); const { params: { metrics }, tabId } = useContext<IContext>(ResourceMetricsContext);
const { chartCapacityColor } = themeStore.activeTheme.colors; const { chartCapacityColor } = ThemeStore.getInstance().activeTheme.colors;
if (!metrics) return null; if (!metrics) return null;
if (isMetricsEmpty(metrics)) return <NoMetrics/>; if (isMetricsEmpty(metrics)) return <NoMetrics/>;

View File

@ -6,7 +6,7 @@ import { isMetricsEmpty, normalizeMetrics } from "../../api/endpoints/metrics.ap
import { NoMetrics } from "../resource-metrics/no-metrics"; import { NoMetrics } from "../resource-metrics/no-metrics";
import { IResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics"; import { IResourceMetricsValue, ResourceMetricsContext } from "../resource-metrics";
import { WorkloadKubeObject } from "../../api/workload-kube-object"; import { WorkloadKubeObject } from "../../api/workload-kube-object";
import { themeStore } from "../../theme.store"; import { ThemeStore } from "../../theme.store";
export const podMetricTabs = [ export const podMetricTabs = [
"CPU", "CPU",
@ -19,7 +19,7 @@ type IContext = IResourceMetricsValue<WorkloadKubeObject, { metrics: IPodMetrics
export const PodCharts = observer(() => { export const PodCharts = observer(() => {
const { params: { metrics }, tabId, object } = useContext<IContext>(ResourceMetricsContext); const { params: { metrics }, tabId, object } = useContext<IContext>(ResourceMetricsContext);
const { chartCapacityColor } = themeStore.activeTheme.colors; const { chartCapacityColor } = ThemeStore.getInstance().activeTheme.colors;
const id = object.getId(); const id = object.getId();
if (!metrics) return null; if (!metrics) return null;

View File

@ -12,8 +12,8 @@ import { ResourceMetrics } from "../resource-metrics";
import { IMetrics } from "../../api/endpoints/metrics.api"; import { IMetrics } from "../../api/endpoints/metrics.api";
import { ContainerCharts } from "./container-charts"; import { ContainerCharts } from "./container-charts";
import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting"; import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting";
import { clusterStore } from "../../../common/cluster-store";
import { LocaleDate } from "../locale-date"; import { LocaleDate } from "../locale-date";
import { ClusterStore } from "../../../common/cluster-store";
interface Props { interface Props {
pod: Pod; pod: Pod;
@ -66,7 +66,7 @@ export class PodDetailsContainer extends React.Component<Props> {
"Memory", "Memory",
"Filesystem", "Filesystem",
]; ];
const isMetricHidden = clusterStore.isMetricHidden(ResourceType.Container); const isMetricHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.Container);
return ( return (
<div className="PodDetailsContainer"> <div className="PodDetailsContainer">

View File

@ -23,7 +23,7 @@ import { PodCharts, podMetricTabs } from "./pod-charts";
import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting"; 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<Pod> { interface Props extends KubeObjectDetailsProps<Pod> {
} }
@ -68,7 +68,7 @@ export class PodDetails extends React.Component<Props> {
const nodeSelector = pod.getNodeSelectors(); const nodeSelector = pod.getNodeSelectors();
const volumes = pod.getVolumes(); const volumes = pod.getVolumes();
const metrics = podsStore.metrics; const metrics = podsStore.metrics;
const isMetricHidden = clusterStore.isMetricHidden(ResourceType.Pod); const isMetricHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.Pod);
return ( return (
<div className="PodDetails"> <div className="PodDetails">

View File

@ -18,7 +18,7 @@ import { PodDetailsList } from "../+workloads-pods/pod-details-list";
import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting"; 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<ReplicaSet> { interface Props extends KubeObjectDetailsProps<ReplicaSet> {
} }
@ -49,7 +49,7 @@ export class ReplicaSetDetails extends React.Component<Props> {
const nodeSelector = replicaSet.getNodeSelectors(); const nodeSelector = replicaSet.getNodeSelectors();
const images = replicaSet.getImages(); const images = replicaSet.getImages();
const childPods = replicaSetStore.getChildPods(replicaSet); const childPods = replicaSetStore.getChildPods(replicaSet);
const isMetricHidden = clusterStore.isMetricHidden(ResourceType.ReplicaSet); const isMetricHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.ReplicaSet);
return ( return (
<div className="ReplicaSetDetails"> <div className="ReplicaSetDetails">

View File

@ -19,7 +19,7 @@ import { PodDetailsList } from "../+workloads-pods/pod-details-list";
import { KubeObjectMeta } from "../kube-object/kube-object-meta"; import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry"; import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting"; 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<StatefulSet> { interface Props extends KubeObjectDetailsProps<StatefulSet> {
} }
@ -48,7 +48,7 @@ export class StatefulSetDetails extends React.Component<Props> {
const nodeSelector = statefulSet.getNodeSelectors(); const nodeSelector = statefulSet.getNodeSelectors();
const childPods = statefulSetStore.getChildPods(statefulSet); const childPods = statefulSetStore.getChildPods(statefulSet);
const metrics = statefulSetStore.metrics; const metrics = statefulSetStore.metrics;
const isMetricHidden = clusterStore.isMetricHidden(ResourceType.StatefulSet); const isMetricHidden = ClusterStore.getInstance().isMetricHidden(ResourceType.StatefulSet);
return ( return (
<div className="StatefulSetDetails"> <div className="StatefulSetDetails">

View File

@ -34,7 +34,7 @@ import { getHostedCluster, getHostedClusterId } from "../../common/cluster-store
import logger from "../../main/logger"; import logger from "../../main/logger";
import { webFrame } from "electron"; import { webFrame } from "electron";
import { clusterPageRegistry, getExtensionPageUrl } from "../../extensions/registries/page-registry"; 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 { appEventBus } from "../../common/event-bus";
import { requestMain } from "../../common/ipc"; import { requestMain } from "../../common/ipc";
import whatInput from "what-input"; import whatInput from "what-input";
@ -62,7 +62,7 @@ export class App extends React.Component {
await requestMain(clusterSetFrameIdHandler, clusterId); await requestMain(clusterSetFrameIdHandler, clusterId);
await getHostedCluster().whenReady; // cluster.activate() is done at this point await getHostedCluster().whenReady; // cluster.activate() is done at this point
extensionLoader.loadOnClusterRenderer(); ExtensionLoader.getInstance().loadOnClusterRenderer();
setTimeout(() => { setTimeout(() => {
appEventBus.emit({ appEventBus.emit({
name: "cluster", name: "cluster",

View File

@ -7,7 +7,7 @@ import { ChartData, ChartOptions, ChartPoint, ChartTooltipItem, Scriptable } fro
import { Chart, ChartKind, ChartProps } from "./chart"; import { Chart, ChartKind, ChartProps } from "./chart";
import { bytesToUnits, cssNames } from "../../utils"; import { bytesToUnits, cssNames } from "../../utils";
import { ZebraStripes } from "./zebra-stripes.plugin"; import { ZebraStripes } from "./zebra-stripes.plugin";
import { themeStore } from "../../theme.store"; import { ThemeStore } from "../../theme.store";
import { NoMetrics } from "../resource-metrics/no-metrics"; import { NoMetrics } from "../resource-metrics/no-metrics";
interface Props extends ChartProps { interface Props extends ChartProps {
@ -26,7 +26,7 @@ export class BarChart extends React.Component<Props> {
render() { render() {
const { name, data, className, timeLabelStep, plugins, options: customOptions, ...settings } = this.props; 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<string> = ({ dataset }) => { const getBarColor: Scriptable<string> = ({ dataset }) => {
const color = dataset.borderColor; const color = dataset.borderColor;

View File

@ -4,7 +4,7 @@ import { observer } from "mobx-react";
import ChartJS, { ChartOptions } from "chart.js"; import ChartJS, { ChartOptions } from "chart.js";
import { Chart, ChartProps } from "./chart"; import { Chart, ChartProps } from "./chart";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
import { themeStore } from "../../theme.store"; import { ThemeStore } from "../../theme.store";
interface Props extends ChartProps { interface Props extends ChartProps {
} }
@ -13,7 +13,7 @@ interface Props extends ChartProps {
export class PieChart extends React.Component<Props> { export class PieChart extends React.Component<Props> {
render() { render() {
const { data, className, options, ...chartProps } = this.props; const { data, className, options, ...chartProps } = this.props;
const { contentColor } = themeStore.activeTheme.colors; const { contentColor } = ThemeStore.getInstance().activeTheme.colors;
const cutouts = [88, 76, 63]; const cutouts = [88, 76, 63];
const opts: ChartOptions = this.props.showChart === false ? {} : { const opts: ChartOptions = this.props.showChart === false ? {} : {
maintainAspectRatio: false, maintainAspectRatio: false,

View File

@ -7,7 +7,6 @@ jest.mock("../../../extensions/registries");
import { statusBarRegistry } from "../../../extensions/registries"; import { statusBarRegistry } from "../../../extensions/registries";
describe("<BottomBar />", () => { describe("<BottomBar />", () => {
it("renders w/o errors", () => { it("renders w/o errors", () => {
const { container } = render(<BottomBar />); const { container } = render(<BottomBar />);

View File

@ -10,7 +10,7 @@ import { Preferences, preferencesRoute } from "../+preferences";
import { AddCluster, addClusterRoute } from "../+add-cluster"; import { AddCluster, addClusterRoute } from "../+add-cluster";
import { ClusterView } from "./cluster-view"; import { ClusterView } from "./cluster-view";
import { clusterViewRoute } from "./cluster-view.route"; 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 { hasLoadedView, initView, lensViews, refreshViews } from "./lens-views";
import { globalPageRegistry } from "../../../extensions/registries/page-registry"; import { globalPageRegistry } from "../../../extensions/registries/page-registry";
import { Extensions, extensionsRoute } from "../+extensions"; import { Extensions, extensionsRoute } from "../+extensions";
@ -21,7 +21,7 @@ import { EntitySettings, entitySettingsRoute } from "../+entity-settings";
@observer @observer
export class ClusterManager extends React.Component { export class ClusterManager extends React.Component {
componentDidMount() { componentDidMount() {
const getMatchedCluster = () => clusterStore.getById(getMatchedClusterId()); const getMatchedCluster = () => ClusterStore.getInstance().getById(getMatchedClusterId());
disposeOnUnmount(this, [ disposeOnUnmount(this, [
reaction(getMatchedClusterId, initView, { reaction(getMatchedClusterId, initView, {
@ -59,9 +59,12 @@ export class ClusterManager extends React.Component {
<Route component={AddCluster} {...addClusterRoute} /> <Route component={AddCluster} {...addClusterRoute} />
<Route component={ClusterView} {...clusterViewRoute} /> <Route component={ClusterView} {...clusterViewRoute} />
<Route component={EntitySettings} {...entitySettingsRoute} /> <Route component={EntitySettings} {...entitySettingsRoute} />
{globalPageRegistry.getItems().map(({ url, components: { Page } }) => { {
return <Route key={url} path={url} component={Page}/>; globalPageRegistry.getItems()
})} .map(({ url, components: { Page } }) => (
<Route key={url} path={url} component={Page} />
))
}
<Redirect exact to={this.startUrl}/> <Redirect exact to={this.startUrl}/>
</Switch> </Switch>
</main> </main>

View File

@ -10,7 +10,7 @@ import { Icon } from "../icon";
import { Button } from "../button"; import { Button } from "../button";
import { cssNames, IClassName } from "../../utils"; import { cssNames, IClassName } from "../../utils";
import { Cluster } from "../../../main/cluster"; import { Cluster } from "../../../main/cluster";
import { ClusterId, clusterStore } from "../../../common/cluster-store"; import { ClusterId, ClusterStore } from "../../../common/cluster-store";
import { CubeSpinner } from "../spinner"; import { CubeSpinner } from "../spinner";
import { clusterActivateHandler } from "../../../common/cluster-ipc"; import { clusterActivateHandler } from "../../../common/cluster-ipc";
@ -25,7 +25,7 @@ export class ClusterStatus extends React.Component<Props> {
@observable isReconnecting = false; @observable isReconnecting = false;
get cluster(): Cluster { get cluster(): Cluster {
return clusterStore.getById(this.props.clusterId); return ClusterStore.getInstance().getById(this.props.clusterId);
} }
@computed get hasErrors(): boolean { @computed get hasErrors(): boolean {

View File

@ -7,9 +7,9 @@ import { IClusterViewRouteParams } from "./cluster-view.route";
import { ClusterStatus } from "./cluster-status"; import { ClusterStatus } from "./cluster-status";
import { hasLoadedView } from "./lens-views"; import { hasLoadedView } from "./lens-views";
import { Cluster } from "../../../main/cluster"; import { Cluster } from "../../../main/cluster";
import { clusterStore } from "../../../common/cluster-store";
import { navigate } from "../../navigation"; import { navigate } from "../../navigation";
import { catalogURL } from "../+catalog"; import { catalogURL } from "../+catalog";
import { ClusterStore } from "../../../common/cluster-store";
interface Props extends RouteComponentProps<IClusterViewRouteParams> { interface Props extends RouteComponentProps<IClusterViewRouteParams> {
} }
@ -21,12 +21,12 @@ export class ClusterView extends React.Component<Props> {
} }
get cluster(): Cluster { get cluster(): Cluster {
return clusterStore.getById(this.clusterId); return ClusterStore.getInstance().getById(this.clusterId);
} }
async componentDidMount() { async componentDidMount() {
disposeOnUnmount(this, [ disposeOnUnmount(this, [
reaction(() => this.clusterId, clusterId => clusterStore.setActive(clusterId), { reaction(() => this.clusterId, clusterId => ClusterStore.getInstance().setActive(clusterId), {
fireImmediately: true, fireImmediately: true,
}), }),
reaction(() => this.cluster.online, (online) => { reaction(() => this.cluster.online, (online) => {

View File

@ -1,5 +1,5 @@
import { observable, when } from "mobx"; 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 { getMatchedClusterId } from "../../navigation";
import logger from "../../../main/logger"; import logger from "../../../main/logger";
@ -19,7 +19,7 @@ export async function initView(clusterId: ClusterId) {
if (!clusterId || lensViews.has(clusterId)) { if (!clusterId || lensViews.has(clusterId)) {
return; return;
} }
const cluster = clusterStore.getById(clusterId); const cluster = ClusterStore.getInstance().getById(clusterId);
if (!cluster) { if (!cluster) {
return; return;
@ -44,13 +44,9 @@ export async function initView(clusterId: ClusterId) {
export async function autoCleanOnRemove(clusterId: ClusterId, iframe: HTMLIFrameElement) { export async function autoCleanOnRemove(clusterId: ClusterId, iframe: HTMLIFrameElement) {
await when(() => { await when(() => {
const cluster = clusterStore.getById(clusterId); const cluster = ClusterStore.getInstance().getById(clusterId);
if (!cluster) return true; return !cluster || (cluster.disconnected && lensViews.get(clusterId)?.isLoaded);
const view = lensViews.get(clusterId);
return cluster.disconnected && view?.isLoaded;
}); });
logger.info(`[LENS-VIEW]: remove dashboard, clusterId=${clusterId}`); logger.info(`[LENS-VIEW]: remove dashboard, clusterId=${clusterId}`);
lensViews.delete(clusterId); lensViews.delete(clusterId);
@ -64,7 +60,7 @@ export async function autoCleanOnRemove(clusterId: ClusterId, iframe: HTMLIFrame
} }
export function refreshViews() { export function refreshViews() {
const cluster = clusterStore.getById(getMatchedClusterId()); const cluster = ClusterStore.getInstance().getById(getMatchedClusterId());
lensViews.forEach(({ clusterId, view, isLoaded }) => { lensViews.forEach(({ clusterId, view, isLoaded }) => {
const isCurrent = clusterId === cluster?.id; const isCurrent = clusterId === cluster?.id;

View File

@ -1,7 +1,7 @@
import { navigate } from "../../navigation"; import { navigate } from "../../navigation";
import { commandRegistry } from "../../../extensions/registries/command-registry"; import { commandRegistry } from "../../../extensions/registries/command-registry";
import { clusterStore } from "../../../common/cluster-store";
import { entitySettingsURL } from "../+entity-settings"; import { entitySettingsURL } from "../+entity-settings";
import { ClusterStore } from "../../../common/cluster-store";
commandRegistry.add({ commandRegistry.add({
id: "cluster.viewCurrentClusterSettings", id: "cluster.viewCurrentClusterSettings",
@ -9,7 +9,7 @@ commandRegistry.add({
scope: "global", scope: "global",
action: () => navigate(entitySettingsURL({ action: () => navigate(entitySettingsURL({
params: { params: {
entityId: clusterStore.active.id entityId: ClusterStore.getInstance().active.id
} }
})), })),
isActive: (context) => !!context.entity isActive: (context) => !!context.entity

View File

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { clusterStore } from "../../../common/cluster-store"; import { ClusterStore } from "../../../common/cluster-store";
import { ClusterProxySetting } from "./components/cluster-proxy-setting"; import { ClusterProxySetting } from "./components/cluster-proxy-setting";
import { ClusterNameSetting } from "./components/cluster-name-setting"; import { ClusterNameSetting } from "./components/cluster-name-setting";
import { ClusterHomeDirSetting } from "./components/cluster-home-dir-setting"; import { ClusterHomeDirSetting } from "./components/cluster-home-dir-setting";
@ -13,7 +13,7 @@ import { CatalogEntity } from "../../api/catalog-entity";
function getClusterForEntity(entity: CatalogEntity) { function getClusterForEntity(entity: CatalogEntity) {
const cluster = clusterStore.getById(entity.metadata.uid); const cluster = ClusterStore.getInstance().getById(entity.metadata.uid);
if (!cluster?.enabled) { if (!cluster?.enabled) {
return null; return null;

View File

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { clusterStore } from "../../../../common/cluster-store"; import { ClusterStore } from "../../../../common/cluster-store";
import { Cluster } from "../../../../main/cluster"; import { Cluster } from "../../../../main/cluster";
import { autobind } from "../../../utils"; import { autobind } from "../../../utils";
import { Button } from "../../button"; import { Button } from "../../button";
@ -21,7 +21,7 @@ export class RemoveClusterButton extends React.Component<Props> {
labelOk: "Yes", labelOk: "Yes",
labelCancel: "No", labelCancel: "No",
ok: async () => { ok: async () => {
await clusterStore.removeById(cluster.id); await ClusterStore.getInstance().removeById(cluster.id);
} }
}); });
} }

View File

@ -4,7 +4,7 @@ import { computed, observable, toJS } from "mobx";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import React from "react"; import React from "react";
import { commandRegistry } from "../../../extensions/registries/command-registry"; 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 { CommandOverlay } from "./command-container";
import { broadcastMessage } from "../../../common/ipc"; import { broadcastMessage } from "../../../common/ipc";
import { navigate } from "../../navigation"; import { navigate } from "../../navigation";
@ -20,7 +20,7 @@ export class CommandDialog extends React.Component {
}; };
return commandRegistry.getItems().filter((command) => { return commandRegistry.getItems().filter((command) => {
if (command.scope === "entity" && !clusterStore.active) { if (command.scope === "entity" && !ClusterStore.getInstance().active) {
return false; return false;
} }

View File

@ -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 renderTabs = () => render(getComponent());
const getTabKinds = () => dockStore.tabs.map(tab => tab.kind); const getTabKinds = () => dockStore.tabs.map(tab => tab.kind);

View File

@ -7,6 +7,8 @@ import { Pod } from "../../../api/endpoints";
import { LogResourceSelector } from "../log-resource-selector"; import { LogResourceSelector } from "../log-resource-selector";
import { LogTabData } from "../log-tab.store"; import { LogTabData } from "../log-tab.store";
import { dockerPod, deploymentPod1 } from "./pod.mock"; import { dockerPod, deploymentPod1 } from "./pod.mock";
import { ThemeStore } from "../../../theme.store";
import { UserStore } from "../../../../common/user-store";
const getComponent = (tabData: LogTabData) => { const getComponent = (tabData: LogTabData) => {
return ( return (
@ -41,6 +43,16 @@ const getFewPodsTabData = (): LogTabData => {
}; };
describe("<LogResourceSelector />", () => { describe("<LogResourceSelector />", () => {
beforeEach(() => {
UserStore.getInstanceOrCreate();
ThemeStore.getInstanceOrCreate();
});
afterEach(() => {
UserStore.resetInstance();
ThemeStore.resetInstance();
});
it("renders w/o errors", () => { it("renders w/o errors", () => {
const tabData = getOnePodTabData(); const tabData = getOnePodTabData();
const { container } = render(getComponent(tabData)); const { container } = render(getComponent(tabData));

View File

@ -10,7 +10,7 @@ import moment from "moment-timezone";
import { Align, ListOnScrollProps } from "react-window"; import { Align, ListOnScrollProps } from "react-window";
import { SearchStore, searchStore } from "../../../common/search-store"; import { SearchStore, searchStore } from "../../../common/search-store";
import { userStore } from "../../../common/user-store"; import { UserStore } from "../../../common/user-store";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
import { Button } from "../button"; import { Button } from "../button";
import { Icon } from "../icon"; import { Icon } from "../icon";
@ -81,7 +81,7 @@ export class LogList extends React.Component<Props> {
@computed @computed
get logs() { get logs() {
const showTimestamps = logTabStore.getData(this.props.id).showTimestamps; const showTimestamps = logTabStore.getData(this.props.id).showTimestamps;
const { preferences } = userStore; const { preferences } = UserStore.getInstance();
if (!showTimestamps) { if (!showTimestamps) {
return logStore.logsWithoutTimestamps; return logStore.logsWithoutTimestamps;

View File

@ -7,7 +7,7 @@ import { cssNames } from "../../utils";
import { IDockTab } from "./dock.store"; import { IDockTab } from "./dock.store";
import { Terminal } from "./terminal"; import { Terminal } from "./terminal";
import { terminalStore } from "./terminal.store"; import { terminalStore } from "./terminal.store";
import { themeStore } from "../../theme.store"; import { ThemeStore } from "../../theme.store";
interface Props { interface Props {
className?: string; className?: string;
@ -38,7 +38,7 @@ export class TerminalWindow extends React.Component<Props> {
return ( return (
<div <div
className={cssNames("TerminalWindow", className, themeStore.activeTheme.type)} className={cssNames("TerminalWindow", className, ThemeStore.getInstance().activeTheme.type)}
ref={e => this.elem = e} ref={e => this.elem = e}
/> />
); );

View File

@ -4,9 +4,10 @@ import { Terminal as XTerm } from "xterm";
import { FitAddon } from "xterm-addon-fit"; import { FitAddon } from "xterm-addon-fit";
import { dockStore, TabId } from "./dock.store"; import { dockStore, TabId } from "./dock.store";
import { TerminalApi } from "../../api/terminal-api"; import { TerminalApi } from "../../api/terminal-api";
import { themeStore } from "../../theme.store"; import { ThemeStore } from "../../theme.store";
import { autobind } from "../../utils"; import { autobind } from "../../utils";
import { isMac } from "../../../common/vars"; import { isMac } from "../../../common/vars";
import { camelCase } from "lodash";
export class Terminal { export class Terminal {
static spawningPool: HTMLElement; static spawningPool: HTMLElement;
@ -40,16 +41,10 @@ export class Terminal {
// Replacing keys stored in styles to format accepted by terminal // Replacing keys stored in styles to format accepted by terminal
// E.g. terminalBrightBlack -> brightBlack // E.g. terminalBrightBlack -> brightBlack
const colorPrefix = "terminal"; const colorPrefix = "terminal";
const terminalColors = Object.entries(colors) const terminalColorEntries = Object.entries(colors)
.filter(([name]) => name.startsWith(colorPrefix)) .filter(([name]) => name.startsWith(colorPrefix))
.reduce<any>((colors, [name, color]) => { .map(([name, color]) => [camelCase(name.slice(colorPrefix.length)), color]);
const colorName = name.split("").slice(colorPrefix.length); const terminalColors = Object.fromEntries(terminalColorEntries);
colorName[0] = colorName[0].toLowerCase();
colors[colorName.join("")] = color;
return colors;
}, {});
this.xterm.setOption("theme", terminalColors); this.xterm.setOption("theme", terminalColors);
} }
@ -109,7 +104,7 @@ export class Terminal {
window.addEventListener("resize", this.onResize); window.addEventListener("resize", this.onResize);
this.disposers.push( this.disposers.push(
reaction(() => toJS(themeStore.activeTheme.colors), this.setTheme, { reaction(() => toJS(ThemeStore.getInstance().activeTheme.colors), this.setTheme, {
fireImmediately: true fireImmediately: true
}), }),
dockStore.onResize(this.onResize), dockStore.onResize(this.onResize),

View File

@ -10,7 +10,7 @@ import { Menu, MenuItem } from "../menu";
import { Icon } from "../icon"; import { Icon } from "../icon";
import { observable } from "mobx"; import { observable } from "mobx";
import { navigate } from "../../navigation"; import { navigate } from "../../navigation";
import { hotbarStore } from "../../../common/hotbar-store"; import { HotbarStore } from "../../../common/hotbar-store";
import { ConfirmDialog } from "../confirm-dialog"; import { ConfirmDialog } from "../confirm-dialog";
interface Props extends DOMAttributes<HTMLElement> { interface Props extends DOMAttributes<HTMLElement> {
@ -60,7 +60,7 @@ export class HotbarIcon extends React.Component<Props> {
} }
removeFromHotbar(item: CatalogEntity) { removeFromHotbar(item: CatalogEntity) {
const hotbar = hotbarStore.getByName("default"); // FIXME const hotbar = HotbarStore.getInstance().getByName("default"); // FIXME
if (!hotbar) { if (!hotbar) {
return; return;

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