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 authored and GitHub committed 2021-04-21 09:59:59 -04:00
1 parent b2a570ce28
commit 9563ead2e6
114 files changed
+924 -736

No files matched your search

+71 -42
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);
});
+4
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", () => {
+7 -3
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);
});
});
});
@@ -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 = [
+17 -8
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");
});
@@ -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);
}
});
+6 -7
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);
+2 -4
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());
}
+1 -3
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>();
+5 -5
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;
+2 -2
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,
+8 -10
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");
}
+2 -2
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);
});
}
+21 -7
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;
@@ -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;
@@ -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"
@@ -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;
+2 -2
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`);
+2 -2
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;
}
+1 -2
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);
}
}
+7 -7
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();
+5 -6
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();
-2
View File
@@ -53,5 +53,3 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
});
}
}
export const extensionsStore = new ExtensionsStore();
+2 -2
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() {
+1 -1
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,
@@ -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;
+2 -2
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;
}
-1
View File
@@ -23,7 +23,6 @@ jest.mock("winston", () => ({
}
}));
jest.mock("../../common/ipc");
jest.mock("../context-handler");
jest.mock("request");
@@ -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 () => {
+2 -6
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();
+14 -14
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;
+5 -4
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 }));
}
};
+7 -3
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();
+1 -3
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>();
+23 -13
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();
+132 -106
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: [] }) };
}
}
-6
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>();
+13 -16
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 };
}
+40 -34
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 }));
});
+9 -9
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;
+8 -9
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);
@@ -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; });
@@ -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":
+2 -2
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
+31 -31
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());
}
}
+2 -2
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);
+23 -13
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";
}
@@ -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 (
@@ -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}
+3 -3
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;
@@ -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>
@@ -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>
@@ -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 = {
@@ -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,14 +124,14 @@ 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(() =>
waitFor(() =>
expect(container.querySelector(".Spinner")).not.toBeInTheDocument()
);
});
@@ -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>();
}
@@ -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>
@@ -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 (
@@ -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;
@@ -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">
@@ -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();
@@ -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(
@@ -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}
@@ -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>
@@ -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">
@@ -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;
@@ -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() {
@@ -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">
@@ -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">
@@ -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"
}]
};
@@ -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/>;
@@ -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;
@@ -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">
@@ -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">
@@ -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">
@@ -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">
+2 -2
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",
+2 -2
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;
+2 -2
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,
@@ -7,7 +7,6 @@ jest.mock("../../../extensions/registries");
import { statusBarRegistry } from "../../../extensions/registries";
describe("<BottomBar />", () => {
it("renders w/o errors", () => {
const { container } = render(<BottomBar />);
@@ -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>
@@ -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 {
@@ -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) => {
@@ -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;
@@ -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
@@ -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;
@@ -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);
}
});
}
@@ -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;
}
@@ -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);
@@ -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));
+2 -2
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;
@@ -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}
/>
);
+6 -11
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),
@@ -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;
@@ -5,7 +5,7 @@ import { observer } from "mobx-react";
import { HotbarIcon } from "./hotbar-icon";
import { cssNames, IClassName } from "../../utils";
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
import { hotbarStore } from "../../../common/hotbar-store";
import { HotbarStore } from "../../../common/hotbar-store";
import { catalogEntityRunContext } from "../../api/catalog-entity";
interface Props {
@@ -16,7 +16,7 @@ interface Props {
export class HotbarMenu extends React.Component<Props> {
get hotbarItems() {
const hotbar = hotbarStore.getByName("default"); // FIXME
const hotbar = HotbarStore.getInstance().getByName("default"); // FIXME
if (!hotbar) {
return [];
@@ -47,4 +47,3 @@ export class HotbarMenu extends React.Component<Props> {
);
}
}
@@ -16,11 +16,11 @@ import { Filter, FilterType, pageFilters } from "./page-filters.store";
import { PageFiltersList } from "./page-filters-list";
import { PageFiltersSelect } from "./page-filters-select";
import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter";
import { themeStore } from "../../theme.store";
import { ThemeStore } from "../../theme.store";
import { MenuActions } from "../menu/menu-actions";
import { MenuItem } from "../menu";
import { Checkbox } from "../checkbox";
import { userStore } from "../../../common/user-store";
import { UserStore } from "../../../common/user-store";
import { namespaceStore } from "../+namespaces/namespace.store";
// todo: refactor, split to small re-usable components
@@ -440,7 +440,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
const { removeItemsDialog, items } = this;
const { selectedItems } = store;
const selectedItemId = detailsItem && detailsItem.getId();
const classNames = cssNames(className, "box", "grow", themeStore.activeTheme.type);
const classNames = cssNames(className, "box", "grow", ThemeStore.getInstance().activeTheme.type);
return (
<div className="items box grow flex column">
@@ -469,7 +469,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
}
@computed get hiddenColumns() {
return userStore.getHiddenTableColumns(this.props.tableId);
return UserStore.getInstance().getHiddenTableColumns(this.props.tableId);
}
isHiddenColumn({ id: columnId, showWithColumn }: TableCellProps): boolean {
@@ -491,7 +491,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
hiddenColumns.delete(columnId);
}
userStore.setHiddenTableColumns(this.props.tableId, hiddenColumns);
UserStore.getInstance().setHiddenTableColumns(this.props.tableId, hiddenColumns);
}
renderColumnVisibilityMenu() {
@@ -6,6 +6,7 @@ import "@testing-library/jest-dom/extend-expect";
import { MainLayoutHeader } from "../main-layout-header";
import { Cluster } from "../../../../main/cluster";
import { ClusterStore } from "../../../../common/cluster-store";
const cluster: Cluster = new Cluster({
id: "foo",
@@ -14,6 +15,14 @@ const cluster: Cluster = new Cluster({
});
describe("<MainLayoutHeader />", () => {
beforeEach(() => {
ClusterStore.getInstanceOrCreate();
});
afterEach(() => {
ClusterStore.resetInstance();
});
it("renders w/o errors", () => {
const { container } = render(<MainLayoutHeader cluster={cluster} />);
@@ -1,7 +1,7 @@
import React from "react";
import { observer } from "mobx-react";
import moment from "moment-timezone";
import { userStore } from "../../../common/user-store";
import { UserStore } from "../../../common/user-store";
interface Props {
date: string
@@ -10,7 +10,7 @@ interface Props {
@observer
export class LocaleDate extends React.Component<Props> {
render() {
const { preferences } = userStore;
const { preferences } = UserStore.getInstance();
const { date } = this.props;
return <>{moment.tz(date, preferences.localeTimezone).format()}</>;
+2 -2
View File
@@ -8,7 +8,7 @@ import { observer } from "mobx-react";
import { autobind, cssNames } from "../../utils";
import ReactSelect, { ActionMeta, components, Props as ReactSelectProps, Styles } from "react-select";
import Creatable, { CreatableProps } from "react-select/creatable";
import { themeStore } from "../../theme.store";
import { ThemeStore } from "../../theme.store";
const { Menu } = components;
@@ -40,7 +40,7 @@ export class Select extends React.Component<SelectProps> {
};
@computed get theme() {
return this.props.themeName || themeStore.activeTheme.type;
return this.props.themeName || ThemeStore.getInstance().activeTheme.type;
}
private styles: Styles = {
+2 -2
View File
@@ -5,7 +5,7 @@ import { Notifications, notificationsStore } from "../components/notifications";
import { Button } from "../components/button";
import { isMac } from "../../common/vars";
import { invalidKubeconfigHandler } from "./invalid-kubeconfig-handler";
import { clusterStore } from "../../common/cluster-store";
import { ClusterStore } from "../../common/cluster-store";
import { navigate } from "../navigation";
import { entitySettingsURL } from "../components/+entity-settings";
@@ -76,7 +76,7 @@ function ListNamespacesForbiddenHandler(event: IpcRendererEvent, ...[clusterId]:
(
<div className="flex column gaps">
<b>Add Accessible Namespaces</b>
<p>Cluster <b>{clusterStore.active.name}</b> does not have permissions to list namespaces. Please add the namespaces you have access to.</p>
<p>Cluster <b>{ClusterStore.getInstance().active.name}</b> does not have permissions to list namespaces. Please add the namespaces you have access to.</p>
<div className="flex gaps row align-left box grow">
<Button active outlined label="Go to Accessible Namespaces Settings" onClick={()=> {
navigate(entitySettingsURL({ params: { entityId: clusterId }, fragment: "accessible-namespaces" }));
@@ -1,6 +1,6 @@
import React from "react";
import { ipcRenderer, IpcRendererEvent, shell } from "electron";
import { clusterStore } from "../../common/cluster-store";
import { ClusterStore } from "../../common/cluster-store";
import { InvalidKubeConfigArgs, InvalidKubeconfigChannel } from "../../common/ipc/invalid-kubeconfig";
import { Notifications, notificationsStore } from "../components/notifications";
import { Button } from "../components/button";
@@ -10,13 +10,13 @@ export const invalidKubeconfigHandler = {
channel: InvalidKubeconfigChannel,
listener: InvalidKubeconfigListener,
verifier: (args: [unknown]): args is InvalidKubeConfigArgs => {
return args.length === 1 && typeof args[0] === "string" && !!clusterStore.getById(args[0]);
return args.length === 1 && typeof args[0] === "string" && !!ClusterStore.getInstance().getById(args[0]);
},
};
function InvalidKubeconfigListener(event: IpcRendererEvent, ...[clusterId]: InvalidKubeConfigArgs): void {
const notificationId = `invalid-kubeconfig:${clusterId}`;
const cluster = clusterStore.getById(clusterId);
const cluster = ClusterStore.getInstance().getById(clusterId);
const contextName = cluster.name !== cluster.contextName ? `(context: ${cluster.contextName})` : "";
Notifications.error(
@@ -29,7 +29,7 @@ function InvalidKubeconfigListener(event: IpcRendererEvent, ...[clusterId]: Inva
<p>Do you want to remove the cluster now?</p>
<div className="flex gaps row align-left box grow">
<Button active outlined label="Remove" onClick={()=> {
clusterStore.removeById(clusterId);
ClusterStore.getInstance().removeById(clusterId);
notificationsStore.remove(notificationId);
}} />
<Button active outlined label="Cancel" onClick={() => notificationsStore.remove(notificationId)} />
@@ -42,5 +42,3 @@ function InvalidKubeconfigListener(event: IpcRendererEvent, ...[clusterId]: Inva
}
);
}
Loaded 100 of 114 files, more files were not shown because too many files have changed in this diff. Show more