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

View File

@ -30,7 +30,7 @@ Each guide or code sample includes the following:
| Sample | APIs |
| ----- | ----- |
[hello-world](https://github.com/lensapp/lens-extension-samples/tree/master/helloworld-sample) | LensMainExtension <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-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 |

View File

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

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.
Use cluster pages to display information about or add functionality to the active cluster.
It is also possible to include custom details from other clusters.
Use your extension to access Kubernetes resources in the active cluster with [`clusterStore`](../stores#clusterstore).
Use your extension to access Kubernetes resources in the active cluster with [`ClusterStore.getInstance()`](../stores#Clusterstore).
Add a cluster page definition to a `LensRendererExtension` subclass with the following example:

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.
However, in the example, the enabled state is not stored anywhere, and it reverts to the default when Lens is restarted.
`Store.ExtensionStore`'s child class will need to be created before being used.
It is recommended to call the inherited static method `getInstanceOrCreate()` only in one place, generally within you extension's `onActivate()` method.
It is also recommenced to delete the instance, using the inherited static method `resetInstance()`, in your extension's `onDeactivate()` method.
Everywhere else in your code you should use the `getInstance()` static method.
This is so that your data is kept up to date and not persisted longer than expected.
The following example code creates a store for the `appPreferences` guide example:
``` typescript
@ -50,8 +56,6 @@ export class ExamplePreferencesStore extends Store.ExtensionStore<ExamplePrefere
});
}
}
export const examplePreferencesStore = ExamplePreferencesStore.getInstance<ExamplePreferencesStore>();
```
First, our example defines the extension's data model using the simple `ExamplePreferencesModel` type.
@ -71,46 +75,51 @@ It is called when the store is being saved.
`toJSON()` must provide a JSON serializable object, facilitating its storage in JSON format.
The `toJS()` function from [`mobx`](https://mobx.js.org/README.html) is convenient for this purpose, and is used here.
Finally, `examplePreferencesStore` is created by calling `ExamplePreferencesStore.getInstance<ExamplePreferencesStore>()`, and exported for use by other parts of the extension.
Note that `examplePreferencesStore` is a singleton.
Calling this function again will not create a new store.
Finally, `ExamplePreferencesStore` is created by calling `ExamplePreferencesStore.getInstanceOrCreate()`, and exported for use by other parts of the extension.
Note that `ExamplePreferencesStore` is a singleton.
Calling this function will create an instance if one has not been made before.
Through normal use you should call `ExamplePreferencesStore.getInstance()` as that will throw an error if an instance does not exist.
This provides some logical safety in that it limits where a new instance can be created.
Thus it prevents an instance from being created when the constructor args are not present at the call site.
If you are doing some cleanup it is recommended to call `ExamplePreferencesStore.getInstance(false)` which returns `undefined` instead of throwing when there is no instance.
The following example code, modified from the [`appPreferences`](../renderer-extension#apppreferences) guide demonstrates how to use the extension store.
`examplePreferencesStore` must be loaded in the main process, where loaded stores are automatically saved when exiting Lens.
`ExamplePreferencesStore` must be loaded in the main process, where loaded stores are automatically saved when exiting Lens.
This can be done in `./main.ts`:
``` typescript
import { LensMainExtension } from "@k8slens/extensions";
import { examplePreferencesStore } from "./src/example-preference-store";
import { ExamplePreferencesStore } from "./src/example-preference-store";
export default class ExampleMainExtension extends LensMainExtension {
async onActivate() {
await examplePreferencesStore.loadExtension(this);
await ExamplePreferencesStore.getInstanceOrCreate().loadExtension(this);
}
}
```
Here, `examplePreferencesStore` loads with `examplePreferencesStore.loadExtension(this)`, which is conveniently called from the `onActivate()` method of `ExampleMainExtension`.
Similarly, `examplePreferencesStore` must load in the renderer process where the `appPreferences` are handled.
Here, `ExamplePreferencesStore` loads with `ExamplePreferencesStore.getInstanceOrCreate().loadExtension(this)`, which is conveniently called from the `onActivate()` method of `ExampleMainExtension`.
Similarly, `ExamplePreferencesStore` must load in the renderer process where the `appPreferences` are handled.
This can be done in `./renderer.ts`:
``` typescript
import { LensRendererExtension } from "@k8slens/extensions";
import { ExamplePreferenceHint, ExamplePreferenceInput } from "./src/example-preference";
import { examplePreferencesStore } from "./src/example-preference-store";
import { ExamplePreferencesStore } from "./src/example-preference-store";
import React from "react";
export default class ExampleRendererExtension extends LensRendererExtension {
async onActivate() {
await examplePreferencesStore.loadExtension(this);
await ExamplePreferencesStore.getInstanceOrCreate().loadExtension(this);
}
appPreferences = [
{
title: "Example Preferences",
components: {
Input: () => <ExamplePreferenceInput preference={examplePreferencesStore}/>,
Input: () => <ExamplePreferenceInput />,
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`.
There is no longer the need for the `preference` field in the `ExampleRendererExtension` class because the props for `ExamplePreferenceInput` is now `examplePreferencesStore`.
Again, `ExamplePreferencesStore.getInstanceOrCreate().loadExtension(this)` is called to load `ExamplePreferencesStore`, this time from the `onActivate()` method of `ExampleRendererExtension`.
`ExamplePreferenceInput` is defined in `./src/example-preference.tsx`:
``` typescript
@ -128,21 +137,15 @@ import { observer } from "mobx-react";
import React from "react";
import { ExamplePreferencesStore } from "./example-preference-store";
export class ExamplePreferenceProps {
preference: ExamplePreferencesStore;
}
@observer
export class ExamplePreferenceInput extends React.Component<ExamplePreferenceProps> {
export class ExamplePreferenceInput extends React.Component {
render() {
const { preference } = this.props;
return (
<Component.Checkbox
label="I understand appPreferences"
value={preference.enabled}
onChange={v => { preference.enabled = v; }}
value={ExamplePreferencesStore.getInstace().enabled}
onChange={v => { ExamplePreferencesStore.getInstace().enabled = v; }}
/>
);
}
@ -159,4 +162,4 @@ export class ExamplePreferenceHint extends React.Component {
The only change here is that `ExamplePreferenceProps` defines its `preference` field as an `ExamplePreferencesStore` type.
Everything else works as before, except that now the `enabled` state persists across Lens restarts because it is managed by the
`examplePreferencesStore`.
`ExamplePreferencesStore`.

View File

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

View File

@ -114,16 +114,14 @@ type HelmRepository = {
url: string;
};
export async function listHelmRepositories(retries = 0): Promise<HelmRepository[]>{
if (retries < 5) {
export async function listHelmRepositories(): Promise<HelmRepository[]>{
for (let i = 0; i < 10; i += 1) {
try {
const { stdout: reposJson } = await promiseExec("helm repo list -o json");
const { stdout } = await promiseExec("helm repo list -o json");
return JSON.parse(reposJson);
return JSON.parse(stdout);
} catch {
await new Promise(r => setTimeout(r, 2000)); // if no repositories, wait for Lens adding bitnami repository
return await listHelmRepositories((retries + 1));
}
}

View File

@ -4,8 +4,9 @@ import yaml from "js-yaml";
import { Cluster } from "../../main/cluster";
import { ClusterStore, getClusterIdFromHost } from "../cluster-store";
import { Console } from "console";
import { stdout, stderr } from "process";
console = new Console(process.stdout, process.stderr); // fix mockFS
console = new Console(stdout, stderr);
const testDataIcon = fs.readFileSync("test-data/cluster-store-migration-icon.png");
const kubeconfig = `
@ -47,10 +48,8 @@ jest.mock("electron", () => {
};
});
let clusterStore: ClusterStore;
describe("empty config", () => {
beforeEach(() => {
beforeEach(async () => {
ClusterStore.resetInstance();
const mockOpts = {
"tmp": {
@ -59,9 +58,8 @@ describe("empty config", () => {
};
mockFs(mockOpts);
clusterStore = ClusterStore.getInstance<ClusterStore>();
return clusterStore.load();
await ClusterStore.getInstanceOrCreate().load();
});
afterEach(() => {
@ -70,7 +68,7 @@ describe("empty config", () => {
describe("with foo cluster added", () => {
beforeEach(() => {
clusterStore.addCluster(
ClusterStore.getInstance().addCluster(
new Cluster({
id: "foo",
contextName: "foo",
@ -85,7 +83,7 @@ describe("empty config", () => {
});
it("adds new cluster to store", async () => {
const storedCluster = clusterStore.getById("foo");
const storedCluster = ClusterStore.getInstance().getById("foo");
expect(storedCluster.id).toBe("foo");
expect(storedCluster.preferences.terminalCWD).toBe("/tmp");
@ -94,19 +92,19 @@ describe("empty config", () => {
});
it("removes cluster from store", async () => {
await clusterStore.removeById("foo");
expect(clusterStore.getById("foo")).toBeNull();
await ClusterStore.getInstance().removeById("foo");
expect(ClusterStore.getInstance().getById("foo")).toBeNull();
});
it("sets active cluster", () => {
clusterStore.setActive("foo");
expect(clusterStore.active.id).toBe("foo");
ClusterStore.getInstance().setActive("foo");
expect(ClusterStore.getInstance().active.id).toBe("foo");
});
});
describe("with prod and dev clusters added", () => {
beforeEach(() => {
clusterStore.addClusters(
ClusterStore.getInstance().addClusters(
new Cluster({
id: "prod",
contextName: "foo",
@ -127,8 +125,8 @@ describe("empty config", () => {
});
it("check if store can contain multiple clusters", () => {
expect(clusterStore.hasClusters()).toBeTruthy();
expect(clusterStore.clusters.size).toBe(2);
expect(ClusterStore.getInstance().hasClusters()).toBeTruthy();
expect(ClusterStore.getInstance().clusters.size).toBe(2);
});
it("check if cluster's kubeconfig file saved", () => {
@ -178,9 +176,8 @@ describe("config with existing clusters", () => {
};
mockFs(mockOpts);
clusterStore = ClusterStore.getInstance<ClusterStore>();
return clusterStore.load();
return ClusterStore.getInstanceOrCreate().load();
});
afterEach(() => {
@ -188,24 +185,24 @@ describe("config with existing clusters", () => {
});
it("allows to retrieve a cluster", () => {
const storedCluster = clusterStore.getById("cluster1");
const storedCluster = ClusterStore.getInstance().getById("cluster1");
expect(storedCluster.id).toBe("cluster1");
expect(storedCluster.preferences.terminalCWD).toBe("/foo");
});
it("allows to delete a cluster", () => {
clusterStore.removeById("cluster2");
const storedCluster = clusterStore.getById("cluster1");
ClusterStore.getInstance().removeById("cluster2");
const storedCluster = ClusterStore.getInstance().getById("cluster1");
expect(storedCluster).toBeTruthy();
const storedCluster2 = clusterStore.getById("cluster2");
const storedCluster2 = ClusterStore.getInstance().getById("cluster2");
expect(storedCluster2).toBeNull();
});
it("allows getting all of the clusters", async () => {
const storedClusters = clusterStore.clustersList;
const storedClusters = ClusterStore.getInstance().clustersList;
expect(storedClusters.length).toBe(3);
expect(storedClusters[0].id).toBe("cluster1");
@ -216,7 +213,7 @@ describe("config with existing clusters", () => {
});
it("marks owned cluster disabled by default", () => {
const storedClusters = clusterStore.clustersList;
const storedClusters = ClusterStore.getInstance().clustersList;
expect(storedClusters[0].enabled).toBe(true);
expect(storedClusters[2].enabled).toBe(false);
@ -276,9 +273,8 @@ users:
};
mockFs(mockOpts);
clusterStore = ClusterStore.getInstance<ClusterStore>();
return clusterStore.load();
return ClusterStore.getInstanceOrCreate().load();
});
afterEach(() => {
@ -286,7 +282,7 @@ users:
});
it("does not enable clusters with invalid kubeconfig", () => {
const storedClusters = clusterStore.clustersList;
const storedClusters = ClusterStore.getInstance().clustersList;
expect(storedClusters.length).toBe(2);
expect(storedClusters[0].enabled).toBeFalsy;
@ -319,9 +315,8 @@ describe("pre 2.0 config with an existing cluster", () => {
};
mockFs(mockOpts);
clusterStore = ClusterStore.getInstance<ClusterStore>();
return clusterStore.load();
return ClusterStore.getInstanceOrCreate().load();
});
afterEach(() => {
@ -329,7 +324,7 @@ describe("pre 2.0 config with an existing cluster", () => {
});
it("migrates to modern format with kubeconfig in a file", async () => {
const config = clusterStore.clustersList[0].kubeConfigPath;
const config = ClusterStore.getInstance().clustersList[0].kubeConfigPath;
expect(fs.readFileSync(config, "utf8")).toContain(`"contexts":[]`);
});
@ -347,16 +342,51 @@ describe("pre 2.6.0 config with a cluster that has arrays in auth config", () =>
}
},
cluster1: {
kubeConfig: "apiVersion: v1\nclusters:\n- cluster:\n server: https://10.211.55.6:8443\n name: minikube\ncontexts:\n- context:\n cluster: minikube\n user: minikube\n name: minikube\ncurrent-context: minikube\nkind: Config\npreferences: {}\nusers:\n- name: minikube\n user:\n client-certificate: /Users/kimmo/.minikube/client.crt\n client-key: /Users/kimmo/.minikube/client.key\n auth-provider:\n config:\n access-token:\n - should be string\n expiry:\n - should be string\n"
kubeConfig: JSON.stringify({
apiVersion: "v1",
clusters: [{
cluster: {
server: "https://10.211.55.6:8443",
},
name: "minikube",
}],
contexts: [{
context: {
cluster: "minikube",
user: "minikube",
name: "minikube",
},
name: "minikube",
}],
"current-context": "minikube",
kind: "Config",
preferences: {},
users: [{
name: "minikube",
user: {
"client-certificate": "/Users/foo/.minikube/client.crt",
"client-key": "/Users/foo/.minikube/client.key",
"auth-provider": {
config: {
"access-token": [
"should be string"
],
expiry: [
"should be string"
],
}
}
},
}]
}),
},
})
}
};
mockFs(mockOpts);
clusterStore = ClusterStore.getInstance<ClusterStore>();
return clusterStore.load();
return ClusterStore.getInstanceOrCreate().load();
});
afterEach(() => {
@ -364,10 +394,12 @@ describe("pre 2.6.0 config with a cluster that has arrays in auth config", () =>
});
it("replaces array format access token and expiry into string", async () => {
const file = clusterStore.clustersList[0].kubeConfigPath;
const file = ClusterStore.getInstance().clustersList[0].kubeConfigPath;
const config = fs.readFileSync(file, "utf8");
const kc = yaml.safeLoad(config);
console.log(kc);
expect(kc.users[0].user["auth-provider"].config["access-token"]).toBe("should be string");
expect(kc.users[0].user["auth-provider"].config["expiry"]).toBe("should be string");
});
@ -397,9 +429,8 @@ describe("pre 2.6.0 config with a cluster icon", () => {
};
mockFs(mockOpts);
clusterStore = ClusterStore.getInstance<ClusterStore>();
return clusterStore.load();
return ClusterStore.getInstanceOrCreate().load();
});
afterEach(() => {
@ -407,7 +438,7 @@ describe("pre 2.6.0 config with a cluster icon", () => {
});
it("moves the icon into preferences", async () => {
const storedClusterData = clusterStore.clustersList[0];
const storedClusterData = ClusterStore.getInstance().clustersList[0];
expect(storedClusterData.hasOwnProperty("icon")).toBe(false);
expect(storedClusterData.preferences.hasOwnProperty("icon")).toBe(true);
@ -437,9 +468,8 @@ describe("for a pre 2.7.0-beta.0 config without a workspace", () => {
};
mockFs(mockOpts);
clusterStore = ClusterStore.getInstance<ClusterStore>();
return clusterStore.load();
return ClusterStore.getInstanceOrCreate().load();
});
afterEach(() => {
@ -474,9 +504,8 @@ describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
};
mockFs(mockOpts);
clusterStore = ClusterStore.getInstance<ClusterStore>();
return clusterStore.load();
return ClusterStore.getInstanceOrCreate().load();
});
afterEach(() => {
@ -484,13 +513,13 @@ describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
});
it("migrates to modern format with kubeconfig in a file", async () => {
const config = clusterStore.clustersList[0].kubeConfigPath;
const config = ClusterStore.getInstance().clustersList[0].kubeConfigPath;
expect(fs.readFileSync(config, "utf8")).toBe(minimalValidKubeConfig);
});
it("migrates to modern format with icon not in file", async () => {
const { icon } = clusterStore.clustersList[0].preferences;
const { icon } = ClusterStore.getInstance().clustersList[0].preferences;
expect(icon.startsWith("data:;base64,")).toBe(true);
});

View File

@ -1,4 +1,8 @@
import { appEventBus, AppEvent } from "../event-bus";
import { Console } from "console";
import { stdout, stderr } from "process";
console = new Console(stdout, stderr);
describe("event bus tests", () => {
describe("emit", () => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,7 +23,7 @@ export interface HotbarStoreModel {
export class HotbarStore extends BaseStore<HotbarStoreModel> {
@observable hotbars: Hotbar[] = [];
private constructor() {
constructor() {
super({
configName: "lens-hotbar-store",
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
@ -67,5 +67,3 @@ export class HotbarStore extends BaseStore<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 Url from "url-parse";
import { RoutingError, RoutingErrorType } from "./error";
import { extensionsStore } from "../../extensions/extensions-store";
import { extensionLoader } from "../../extensions/extension-loader";
import { ExtensionsStore } from "../../extensions/extensions-store";
import { ExtensionLoader } from "../../extensions/extension-loader";
import { LensExtension } from "../../extensions/lens-extension";
import { RouteHandler, RouteParams } from "../../extensions/registries/protocol-handler-registry";
@ -72,7 +72,7 @@ export abstract class LensProtocolRouter extends Singleton {
/**
* find the most specific matching handler and call it
* @param routes the array of (path schemas, handler) paris to match against
* @param routes the array of (path schemas, handler) pairs to match against
* @param url the url (in its current state)
*/
protected _route(routes: [string, RouteHandler][], url: Url, extensionName?: string): void {
@ -124,7 +124,7 @@ export abstract class LensProtocolRouter extends Singleton {
const { [EXTENSION_PUBLISHER_MATCH]: publisher, [EXTENSION_NAME_MATCH]: partialName } = match.params;
const name = [publisher, partialName].filter(Boolean).join("/");
const extension = extensionLoader.userExtensionsByName.get(name);
const extension = ExtensionLoader.getInstance().userExtensionsByName.get(name);
if (!extension) {
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not installed`);
@ -132,7 +132,7 @@ export abstract class LensProtocolRouter extends Singleton {
return name;
}
if (!extensionsStore.isEnabled(extension.id)) {
if (!ExtensionsStore.getInstance().isEnabled(extension.id)) {
logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not enabled`);
return name;

View File

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

View File

@ -38,7 +38,7 @@ export interface UserPreferences {
export class UserStore extends BaseStore<UserStoreModel> {
static readonly defaultTheme: ThemeId = "lens-dark";
private constructor() {
constructor() {
super({
configName: "lens-user-store",
migrations,
@ -163,14 +163,6 @@ export class UserStore extends BaseStore<UserStoreModel> {
this.newContexts.clear();
}
/**
* Getting default directory to download kubectl binaries
* @returns string
*/
getDefaultKubectlPath(): string {
return path.join((app || remote.app).getPath("userData"), "binaries");
}
@action
protected async fromStore(data: Partial<UserStoreModel> = {}) {
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> {
let timer: NodeJS.Timeout;
return (...params: any[]) => new Promise(resolve => {
return (...params: F) => new Promise(resolve => {
clearTimeout(timer);
timer = global.setTimeout(() => resolve(func.apply(this, params)), timeout);
timer = global.setTimeout(() => resolve(func(...params)), timeout);
});
}

View File

@ -5,25 +5,39 @@
* @example
* const usersStore: UsersStore = UsersStore.getInstance();
*/
type StaticThis<T, R extends any[]> = { new(...args: R): T };
type Constructor<T = {}> = new (...args: any[]) => T;
class Singleton {
export class Singleton {
private static instances = new WeakMap<object, Singleton>();
private static creating = "";
// todo: improve types inferring
static getInstance<T>(...args: ConstructorParameters<Constructor<T>>): T {
constructor() {
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)) {
Singleton.instances.set(this, Reflect.construct(this, args));
Singleton.creating = this.name;
Singleton.instances.set(this, new this(...args));
Singleton.creating = "";
}
return Singleton.instances.get(this) as T;
}
static getInstance<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() {
Singleton.instances.delete(this);
}
}
export { Singleton };
export default Singleton;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -36,6 +36,11 @@ import { bundledKubectlPath, Kubectl } from "../kubectl";
import { mock, MockProxy } from "jest-mock-extended";
import { waitUntilUsed } from "tcp-port-used";
import { Readable } from "stream";
import { UserStore } from "../../common/user-store";
import { Console } from "console";
import { stdout, stderr } from "process";
console = new Console(stdout, stderr);
const mockBroadcastIpc = broadcastMessage as jest.MockedFunction<typeof broadcastMessage>;
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", () => {
beforeEach(() => {
jest.clearAllMocks();
UserStore.resetInstance();
UserStore.getInstanceOrCreate();
});
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
describe("kubeconfig manager tests", () => {
beforeEach(() => {
jest.clearAllMocks();
});
beforeEach(() => {
const mockOpts = {
"minikube-config.yml": JSON.stringify({
@ -76,7 +72,7 @@ describe("kubeconfig manager tests", () => {
const cluster = new Cluster({
id: "foo",
contextName: "minikube",
kubeConfigPath: "minikube-config.yml"
kubeConfigPath: "minikube-config.yml",
});
const contextHandler = new ContextHandler(cluster);
const port = await getFreePort();
@ -98,7 +94,7 @@ describe("kubeconfig manager tests", () => {
const cluster = new Cluster({
id: "foo",
contextName: "minikube",
kubeConfigPath: "minikube-config.yml"
kubeConfigPath: "minikube-config.yml",
});
const contextHandler = new ContextHandler(cluster);
const port = await getFreePort();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,15 +15,15 @@ import { getFreePort } from "./port";
import { mangleProxyEnv } from "./proxy-env";
import { registerFileProtocol } from "../common/register-protocol";
import logger from "./logger";
import { clusterStore } from "../common/cluster-store";
import { userStore } from "../common/user-store";
import { ClusterStore } from "../common/cluster-store";
import { UserStore } from "../common/user-store";
import { appEventBus } from "../common/event-bus";
import { extensionLoader } from "../extensions/extension-loader";
import { extensionsStore } from "../extensions/extensions-store";
import { InstalledExtension, extensionDiscovery } from "../extensions/extension-discovery";
import { ExtensionLoader } from "../extensions/extension-loader";
import { ExtensionsStore } from "../extensions/extensions-store";
import { InstalledExtension, ExtensionDiscovery } from "../extensions/extension-discovery";
import type { LensExtensionId } from "../extensions/lens-extension";
import { FilesystemProvisionerStore } from "./extension-filesystem";
import { installDeveloperTools } from "./developer-tools";
import { filesystemProvisionerStore } from "./extension-filesystem";
import { LensProtocolRouterMain } from "./protocol-handler";
import { getAppVersion, getAppVersionFromProxyServer } from "../common/utils";
import { bindBroadcastHandlers } from "../common/ipc";
@ -31,13 +31,9 @@ import { startUpdateChecking } from "./app-updater";
import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
import { CatalogPusher } from "./catalog-pusher";
import { catalogEntityRegistry } from "../common/catalog-entity-registry";
import { hotbarStore } from "../common/hotbar-store";
import { HotbarStore } from "../common/hotbar-store";
const workingDir = path.join(app.getPath("appData"), appName);
let proxyPort: number;
let proxyServer: LensProxy;
let clusterManager: ClusterManager;
let windowManager: WindowManager;
app.setName(appName);
@ -66,7 +62,7 @@ if (app.commandLine.getSwitchValue("proxy-server") !== "") {
if (!app.requestSingleInstanceLock()) {
app.exit();
} else {
const lprm = LensProtocolRouterMain.getInstance<LensProtocolRouterMain>();
const lprm = LensProtocolRouterMain.getInstanceOrCreate();
for (const arg of process.argv) {
if (arg.toLowerCase().startsWith("lens://")) {
@ -77,7 +73,7 @@ if (!app.requestSingleInstanceLock()) {
}
app.on("second-instance", (event, argv) => {
const lprm = LensProtocolRouterMain.getInstance<LensProtocolRouterMain>();
const lprm = LensProtocolRouterMain.getInstanceOrCreate();
for (const arg of argv) {
if (arg.toLowerCase().startsWith("lens://")) {
@ -86,7 +82,7 @@ app.on("second-instance", (event, argv) => {
}
}
windowManager?.ensureMainWindow();
WindowManager.getInstance(false)?.ensureMainWindow();
});
app.on("ready", async () => {
@ -102,7 +98,11 @@ app.on("ready", async () => {
registerFileProtocol("static", __static);
await installDeveloperTools();
const userStore = UserStore.getInstanceOrCreate();
const clusterStore = ClusterStore.getInstanceOrCreate();
const hotbarStore = HotbarStore.getInstanceOrCreate();
const extensionsStore = ExtensionsStore.getInstanceOrCreate();
const filesystemStore = FilesystemProvisionerStore.getInstanceOrCreate();
logger.info("💾 Loading stores");
// preload
@ -111,10 +111,12 @@ app.on("ready", async () => {
clusterStore.load(),
hotbarStore.load(),
extensionsStore.load(),
filesystemProvisionerStore.load(),
filesystemStore.load(),
]);
// find free port
let proxyPort;
try {
logger.info("🔑 Getting free port for LensProxy server");
proxyPort = await getFreePort();
@ -125,13 +127,13 @@ app.on("ready", async () => {
}
// create cluster manager
clusterManager = ClusterManager.getInstance<ClusterManager>(proxyPort);
ClusterManager.getInstanceOrCreate(proxyPort);
// run proxy
try {
logger.info("🔌 Starting LensProxy");
// eslint-disable-next-line unused-imports/no-unused-vars-ts
proxyServer = LensProxy.create(proxyPort, clusterManager);
LensProxy.getInstanceOrCreate(proxyPort).listen();
} catch (error) {
logger.error(`Could not start proxy (127.0.0:${proxyPort}): ${error?.message}`);
dialog.showErrorBox("Lens Error", `Could not start proxy (127.0.0:${proxyPort}): ${error?.message || "unknown error"}`);
@ -151,7 +153,9 @@ app.on("ready", async () => {
logger.error("Checking proxy server connection failed", error);
}
extensionLoader.init();
const extensionDiscovery = ExtensionDiscovery.getInstanceOrCreate();
ExtensionLoader.getInstanceOrCreate().init();
extensionDiscovery.init();
// Start the app without showing the main window when auto starting on login
@ -159,7 +163,9 @@ app.on("ready", async () => {
const startHidden = process.argv.includes("--hidden") || (isMac && app.getLoginItemSettings().wasOpenedAsHidden);
logger.info("🖥️ Starting WindowManager");
windowManager = WindowManager.getInstance<WindowManager>(proxyPort);
const windowManager = WindowManager.getInstanceOrCreate(proxyPort);
installDeveloperTools();
if (!startHidden) {
windowManager.initMainWindow();
@ -169,13 +175,13 @@ app.on("ready", async () => {
CatalogPusher.init(catalogEntityRegistry);
startUpdateChecking();
LensProtocolRouterMain
.getInstance<LensProtocolRouterMain>()
.getInstance()
.rendererLoaded = true;
});
extensionLoader.whenLoaded.then(() => {
ExtensionLoader.getInstance().whenLoaded.then(() => {
LensProtocolRouterMain
.getInstance<LensProtocolRouterMain>()
.getInstance()
.extensionsLoaded = true;
});
@ -189,14 +195,15 @@ app.on("ready", async () => {
extensionDiscovery.watchExtensions();
// Subscribe to extensions that are copied or deleted to/from the extensions folder
extensionDiscovery.events.on("add", (extension: InstalledExtension) => {
extensionLoader.addExtension(extension);
});
extensionDiscovery.events.on("remove", (lensExtensionId: LensExtensionId) => {
extensionLoader.removeExtension(lensExtensionId);
});
extensionDiscovery.events
.on("add", (extension: InstalledExtension) => {
ExtensionLoader.getInstance().addExtension(extension);
})
.on("remove", (lensExtensionId: LensExtensionId) => {
ExtensionLoader.getInstance().removeExtension(lensExtensionId);
});
extensionLoader.initExtensions(extensions);
ExtensionLoader.getInstance().initExtensions(extensions);
} catch (error) {
dialog.showErrorBox("Lens Error", `Could not load extensions${error?.message ? `: ${error.message}` : ""}`);
console.error(error);
@ -212,7 +219,7 @@ app.on("activate", (event, hasVisibleWindows) => {
logger.info("APP:ACTIVATE", { hasVisibleWindows });
if (!hasVisibleWindows) {
windowManager?.initMainWindow(false);
WindowManager.getInstance(false)?.initMainWindow(false);
}
});
@ -227,8 +234,7 @@ app.on("will-quit", (event) => {
// Quit app on Cmd+Q (MacOS)
logger.info("APP:QUIT");
appEventBus.emit({name: "app", action: "close"});
clusterManager?.stop(); // close cluster connections
ClusterManager.getInstance(false)?.stop(); // close cluster connections
if (blockQuit) {
event.preventDefault(); // prevent app's default shutdown (e.g. required for telemetry, etc.)
@ -242,7 +248,7 @@ app.on("open-url", (event, rawUrl) => {
event.preventDefault();
LensProtocolRouterMain
.getInstance<LensProtocolRouterMain>()
.getInstance()
.route(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 * as lockFile from "proper-lockfile";
import { helmCli } from "./helm/helm-cli";
import { userStore } from "../common/user-store";
import { UserStore } from "../common/user-store";
import { customRequest } from "../common/request";
import { getBundledKubectlVersion } from "../common/utils/app-version";
import { isDevelopment, isWindows, isTestEnv } from "../common/vars";
@ -113,12 +113,12 @@ export class Kubectl {
}
public getPathFromPreferences() {
return userStore.preferences?.kubectlBinariesPath || this.getBundledPath();
return UserStore.getInstance().preferences?.kubectlBinariesPath || this.getBundledPath();
}
protected getDownloadDir() {
if (userStore.preferences?.downloadBinariesPath) {
return path.join(userStore.preferences.downloadBinariesPath, "kubectl");
if (UserStore.getInstance().preferences?.downloadBinariesPath) {
return path.join(UserStore.getInstance().preferences.downloadBinariesPath, "kubectl");
}
return Kubectl.kubectlDir;
@ -129,7 +129,7 @@ export class Kubectl {
return this.getBundledPath();
}
if (userStore.preferences?.downloadKubectlBinaries === false) {
if (UserStore.getInstance().preferences?.downloadKubectlBinaries === false) {
return this.getPathFromPreferences();
}
@ -223,7 +223,7 @@ export class Kubectl {
}
public async ensureKubectl(): Promise<boolean> {
if (userStore.preferences?.downloadKubectlBinaries === false) {
if (UserStore.getInstance().preferences?.downloadKubectlBinaries === false) {
return true;
}
@ -273,7 +273,7 @@ export class Kubectl {
logger.info(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`);
return new Promise((resolve, reject) => {
return new Promise<void>((resolve, reject) => {
const stream = customRequest({
url: this.url,
gzip: true,
@ -303,7 +303,7 @@ export class Kubectl {
}
protected async writeInitScripts() {
const kubectlPath = userStore.preferences?.downloadKubectlBinaries ? this.dirname : path.dirname(this.getPathFromPreferences());
const kubectlPath = UserStore.getInstance().preferences?.downloadKubectlBinaries ? this.dirname : path.dirname(this.getPathFromPreferences());
const helmPath = helmCli.getBinaryDir();
const fsPromises = fs.promises;
const bashScriptPath = path.join(this.dirname, ".bash_set_path");
@ -361,7 +361,7 @@ export class Kubectl {
}
protected getDownloadMirror() {
const mirror = packageMirrors.get(userStore.preferences?.downloadMirror);
const mirror = packageMirrors.get(UserStore.getInstance().preferences?.downloadMirror);
if (mirror) {
return mirror;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,10 +2,12 @@ import "@testing-library/jest-dom/extend-expect";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import fse from "fs-extra";
import React from "react";
import { extensionDiscovery } from "../../../../extensions/extension-discovery";
import { UserStore } from "../../../../common/user-store";
import { ExtensionDiscovery } from "../../../../extensions/extension-discovery";
import { ExtensionLoader } from "../../../../extensions/extension-loader";
import { ThemeStore } from "../../../theme.store";
import { ConfirmDialog } from "../../confirm-dialog";
import { Notifications } from "../../notifications";
import { ExtensionStateStore } from "../extension-install.store";
import { Extensions } from "../extensions";
jest.mock("fs-extra");
@ -18,46 +20,52 @@ jest.mock("../../../../common/utils", () => ({
extractTar: jest.fn(() => Promise.resolve())
}));
jest.mock("../../../../extensions/extension-discovery", () => ({
...jest.requireActual("../../../../extensions/extension-discovery"),
extensionDiscovery: {
localFolderPath: "/fake/path",
uninstallExtension: jest.fn(() => Promise.resolve()),
isLoaded: true
}
}));
jest.mock("../../../../extensions/extension-loader", () => ({
...jest.requireActual("../../../../extensions/extension-loader"),
extensionLoader: {
userExtensions: new Map([
["extensionId", {
id: "extensionId",
manifest: {
name: "test",
version: "1.2.3"
},
absolutePath: "/absolute/path",
manifestPath: "/symlinked/path/package.json",
isBundled: false,
isEnabled: true
}]
])
}
}));
jest.mock("../../notifications", () => ({
ok: jest.fn(),
error: jest.fn(),
info: jest.fn()
}));
jest.mock("electron", () => {
return {
app: {
getVersion: () => "99.99.99",
getPath: () => "tmp",
getLocale: () => "en",
setLoginItemSettings: (): void => void 0,
}
};
});
describe("Extensions", () => {
beforeEach(() => {
ExtensionStateStore.resetInstance();
beforeEach(async () => {
UserStore.resetInstance();
ThemeStore.resetInstance();
await UserStore.getInstanceOrCreate().load();
await ThemeStore.getInstanceOrCreate().init();
ExtensionLoader.resetInstance();
ExtensionDiscovery.resetInstance();
Extensions.installStates.clear();
ExtensionDiscovery.getInstanceOrCreate().uninstallExtension = jest.fn(() => Promise.resolve());
ExtensionLoader.getInstanceOrCreate().addExtension({
id: "extensionId",
manifest: {
name: "test",
version: "1.2.3"
},
absolutePath: "/absolute/path",
manifestPath: "/symlinked/path/package.json",
isBundled: false,
isEnabled: true
});
});
it("disables uninstall and disable buttons while uninstalling", async () => {
ExtensionDiscovery.getInstance().isLoaded = true;
render(<><Extensions /><ConfirmDialog/></>);
expect(screen.getByText("Disable").closest("button")).not.toBeDisabled();
@ -68,13 +76,14 @@ describe("Extensions", () => {
// Approve confirm dialog
fireEvent.click(screen.getByText("Yes"));
expect(extensionDiscovery.uninstallExtension).toHaveBeenCalled();
expect(ExtensionDiscovery.getInstance().uninstallExtension).toHaveBeenCalled();
expect(screen.getByText("Disable").closest("button")).toBeDisabled();
expect(screen.getByText("Uninstall").closest("button")).toBeDisabled();
});
it("displays error notification on uninstall error", () => {
(extensionDiscovery.uninstallExtension as any).mockImplementationOnce(() =>
ExtensionDiscovery.getInstance().isLoaded = true;
(ExtensionDiscovery.getInstance().uninstallExtension as any).mockImplementationOnce(() =>
Promise.reject()
);
render(<><Extensions /><ConfirmDialog/></>);
@ -115,12 +124,12 @@ describe("Extensions", () => {
});
it("displays spinner while extensions are loading", () => {
extensionDiscovery.isLoaded = false;
ExtensionDiscovery.getInstance().isLoaded = false;
const { container } = render(<Extensions />);
expect(container.querySelector(".Spinner")).toBeInTheDocument();
extensionDiscovery.isLoaded = true;
ExtensionDiscovery.getInstance().isLoaded = true;
waitFor(() =>
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 { downloadFile, extractTar, listTarEntries, readFileFromTar } from "../../../common/utils";
import { docsUrl } from "../../../common/vars";
import { extensionDiscovery, InstalledExtension, manifestFilename } from "../../../extensions/extension-discovery";
import { extensionLoader } from "../../../extensions/extension-loader";
import { ExtensionDiscovery, InstalledExtension, manifestFilename } from "../../../extensions/extension-discovery";
import { ExtensionLoader } from "../../../extensions/extension-loader";
import { extensionDisplayName, LensExtensionManifest, sanitizeExtensionName } from "../../../extensions/lens-extension";
import logger from "../../../main/logger";
import { prevDefault } from "../../utils";
@ -21,7 +21,6 @@ import { SubTitle } from "../layout/sub-title";
import { Notifications } from "../notifications";
import { Spinner } from "../spinner/spinner";
import { TooltipPosition } from "../tooltip";
import { ExtensionStateStore } from "./extension-install.store";
import "./extensions.scss";
interface InstallRequest {
@ -39,6 +38,12 @@ interface InstallRequestValidated extends InstallRequestPreloaded {
tempFile: string; // temp system path to packed extension for unpacking
}
interface ExtensionState {
displayName: string;
// Possible states the extension can be
state: "installing" | "uninstalling";
}
@observer
export class Extensions extends React.Component {
private static supportedFormats = ["tar", "tgz"];
@ -50,9 +55,7 @@ export class Extensions extends React.Component {
}
};
get extensionStateStore() {
return ExtensionStateStore.getInstance<ExtensionStateStore>();
}
static installStates = observable.map<string, ExtensionState>();
@observable search = "";
@observable installPath = "";
@ -64,7 +67,7 @@ export class Extensions extends React.Component {
* Extensions that were removed from extensions but are still in "uninstalling" state
*/
@computed get removedUninstalling() {
return Array.from(this.extensionStateStore.extensionState.entries())
return Array.from(Extensions.installStates.entries())
.filter(([id, extension]) =>
extension.state === "uninstalling"
&& !this.extensions.find(extension => extension.id === id)
@ -76,7 +79,7 @@ export class Extensions extends React.Component {
* Extensions that were added to extensions but are still in "installing" state
*/
@computed get addedInstalling() {
return Array.from(this.extensionStateStore.extensionState.entries())
return Array.from(Extensions.installStates.entries())
.filter(([id, extension]) =>
extension.state === "installing"
&& this.extensions.find(extension => extension.id === id)
@ -91,7 +94,7 @@ export class Extensions extends React.Component {
Notifications.ok(
<p>Extension <b>{displayName}</b> successfully uninstalled!</p>
);
this.extensionStateStore.extensionState.delete(id);
Extensions.installStates.delete(id);
});
this.addedInstalling.forEach(({ id, displayName }) => {
@ -104,7 +107,7 @@ export class Extensions extends React.Component {
Notifications.ok(
<p>Extension <b>{displayName}</b> successfully installed!</p>
);
this.extensionStateStore.extensionState.delete(id);
Extensions.installStates.delete(id);
this.installPath = "";
// Enable installed extensions by default.
@ -117,7 +120,7 @@ export class Extensions extends React.Component {
@computed get extensions() {
const searchText = this.search.toLowerCase();
return Array.from(extensionLoader.userExtensions.values())
return Array.from(ExtensionLoader.getInstance().userExtensions.values())
.filter(({ manifest: { name, description }}) => (
name.toLowerCase().includes(searchText)
|| description?.toLowerCase().includes(searchText)
@ -125,7 +128,7 @@ export class Extensions extends React.Component {
}
get extensionsPath() {
return extensionDiscovery.localFolderPath;
return ExtensionDiscovery.getInstance().localFolderPath;
}
getExtensionPackageTemp(fileName = "") {
@ -342,11 +345,11 @@ export class Extensions extends React.Component {
async unpackExtension({ fileName, tempFile, manifest: { name, version } }: InstallRequestValidated) {
const displayName = extensionDisplayName(name, version);
const extensionId = path.join(extensionDiscovery.nodeModulesPath, name, "package.json");
const extensionId = path.join(ExtensionDiscovery.getInstance().nodeModulesPath, name, "package.json");
logger.info(`Unpacking extension ${displayName}`, { fileName, tempFile });
this.extensionStateStore.extensionState.set(extensionId, {
Extensions.installStates.set(extensionId, {
state: "installing",
displayName
});
@ -381,8 +384,8 @@ export class Extensions extends React.Component {
);
// Remove install state on install failure
if (this.extensionStateStore.extensionState.get(extensionId)?.state === "installing") {
this.extensionStateStore.extensionState.delete(extensionId);
if (Extensions.installStates.get(extensionId)?.state === "installing") {
Extensions.installStates.delete(extensionId);
}
} finally {
// clean up
@ -406,20 +409,20 @@ export class Extensions extends React.Component {
const displayName = extensionDisplayName(extension.manifest.name, extension.manifest.version);
try {
this.extensionStateStore.extensionState.set(extension.id, {
Extensions.installStates.set(extension.id, {
state: "uninstalling",
displayName
});
await extensionDiscovery.uninstallExtension(extension);
await ExtensionDiscovery.getInstance().uninstallExtension(extension);
} catch (error) {
Notifications.error(
<p>Uninstalling extension <b>{displayName}</b> has failed: <em>{error?.message ?? ""}</em></p>
);
// Remove uninstall state on uninstall failure
if (this.extensionStateStore.extensionState.get(extension.id)?.state === "uninstalling") {
this.extensionStateStore.extensionState.delete(extension.id);
if (Extensions.installStates.get(extension.id)?.state === "uninstalling") {
Extensions.installStates.delete(extension.id);
}
}
}
@ -445,7 +448,7 @@ export class Extensions extends React.Component {
return extensions.map(extension => {
const { id, isEnabled, manifest } = extension;
const { name, description, version } = manifest;
const isUninstalling = this.extensionStateStore.extensionState.get(id)?.state === "uninstalling";
const isUninstalling = Extensions.installStates.get(id)?.state === "uninstalling";
return (
<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
*/
@computed get isInstalling() {
return [...this.extensionStateStore.extensionState.values()].some(extension => extension.state === "installing");
return [...Extensions.installStates.values()].some(extension => extension.state === "installing");
}
render() {
@ -536,7 +539,11 @@ export class Extensions extends React.Component {
value={this.search}
onChange={(value) => this.search = value}
/>
{extensionDiscovery.isLoaded ? this.renderExtensions() : <div className="spinner-wrapper"><Spinner/></div>}
{
ExtensionDiscovery.getInstance().isLoaded
? this.renderExtensions()
: <div className="spinner-wrapper"><Spinner/></div>
}
</div>
</PageLayout>
</DropFileInput>

View File

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

View File

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

View File

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

View File

@ -13,7 +13,7 @@ import { systemName, isUrl, isPath } from "../input/input_validators";
import { SubTitle } from "../layout/sub-title";
import { Icon } from "../icon";
import { Notifications } from "../notifications";
import { HelmRepo, repoManager } from "../../../main/helm/helm-repo-manager";
import { HelmRepo, HelmRepoManager } from "../../../main/helm/helm-repo-manager";
interface Props extends Partial<DialogProps> {
onAddRepo: Function
@ -79,7 +79,7 @@ export class AddHelmRepoDialog extends React.Component<Props> {
async addCustomRepo() {
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</>);
this.props.onAddRepo();
this.close();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import React from "react";
import { observer } from "mobx-react";
import { clusterStore } from "../../../../common/cluster-store";
import { ClusterStore } from "../../../../common/cluster-store";
import { Cluster } from "../../../../main/cluster";
import { autobind } from "../../../utils";
import { Button } from "../../button";
@ -21,7 +21,7 @@ export class RemoveClusterButton extends React.Component<Props> {
labelOk: "Yes",
labelCancel: "No",
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 React from "react";
import { commandRegistry } from "../../../extensions/registries/command-registry";
import { clusterStore } from "../../../common/cluster-store";
import { ClusterStore } from "../../../common/cluster-store";
import { CommandOverlay } from "./command-container";
import { broadcastMessage } from "../../../common/ipc";
import { navigate } from "../../navigation";
@ -20,7 +20,7 @@ export class CommandDialog extends React.Component {
};
return commandRegistry.getItems().filter((command) => {
if (command.scope === "entity" && !clusterStore.active) {
if (command.scope === "entity" && !ClusterStore.getInstance().active) {
return false;
}

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

View File

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

View File

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

View File

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

View File

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

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