Merge remote-tracking branch 'origin/master' into mobx-6.2

# Conflicts:
#	extensions/survey/package-lock.json
#	extensions/survey/package.json
#	extensions/survey/src/survey-preferences-store.ts
#	extensions/telemetry/src/telemetry-preferences-store.ts
#	src/common/user-store.ts
#	src/extensions/core-api/catalog.ts
#	src/extensions/extension-discovery.ts
#	src/extensions/lens-extension.ts
#	src/main/cluster-manager.ts
#	src/renderer/bootstrap.tsx
#	src/renderer/components/+apps-helm-charts/helm-chart.store.ts
#	src/renderer/components/+apps-releases/release.store.ts
#	src/renderer/components/+catalog/catalog.tsx
#	src/renderer/components/+extensions/extension-install.store.ts
#	src/renderer/components/+extensions/extensions.tsx
#	src/renderer/components/+preferences/preferences.tsx
#	src/renderer/components/dock/create-resource.store.ts
#	src/renderer/components/kube-object/kube-object-list-layout.tsx
#	src/renderer/theme.store.ts
#	src/renderer/utils/storageHelper.ts
This commit is contained in:
Roman committed 2021-04-21 17:28:18 +03:00
commit a0193a2cda
263 files changed
+3401 -31747

No files matched your search

+86 -47
View File
@@ -3,6 +3,10 @@ import mockFs from "mock-fs";
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(stdout, stderr);
const testDataIcon = fs.readFileSync("test-data/cluster-store-migration-icon.png");
const kubeconfig = `
@@ -44,10 +48,8 @@ jest.mock("electron", () => {
};
});
let clusterStore: ClusterStore;
describe("empty config", () => {
beforeEach(() => {
beforeEach(async () => {
ClusterStore.resetInstance();
const mockOpts = {
"tmp": {
@@ -56,9 +58,8 @@ describe("empty config", () => {
};
mockFs(mockOpts);
clusterStore = ClusterStore.getInstance<ClusterStore>();
return clusterStore.load();
await ClusterStore.getInstanceOrCreate().load();
});
afterEach(() => {
@@ -67,7 +68,7 @@ describe("empty config", () => {
describe("with foo cluster added", () => {
beforeEach(() => {
clusterStore.addCluster(
ClusterStore.getInstance().addCluster(
new Cluster({
id: "foo",
contextName: "foo",
@@ -82,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");
@@ -91,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",
@@ -124,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", () => {
@@ -175,9 +176,8 @@ describe("config with existing clusters", () => {
};
mockFs(mockOpts);
clusterStore = ClusterStore.getInstance<ClusterStore>();
return clusterStore.load();
return ClusterStore.getInstanceOrCreate().load();
});
afterEach(() => {
@@ -185,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");
@@ -213,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);
@@ -273,9 +273,8 @@ users:
};
mockFs(mockOpts);
clusterStore = ClusterStore.getInstance<ClusterStore>();
return clusterStore.load();
return ClusterStore.getInstanceOrCreate().load();
});
afterEach(() => {
@@ -283,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;
@@ -292,6 +291,13 @@ users:
});
});
const minimalValidKubeConfig = JSON.stringify({
apiVersion: "v1",
clusters: [],
users: [],
contexts: [],
});
describe("pre 2.0 config with an existing cluster", () => {
beforeEach(() => {
ClusterStore.resetInstance();
@@ -303,15 +309,14 @@ describe("pre 2.0 config with an existing cluster", () => {
version: "1.0.0"
}
},
cluster1: "kubeconfig content"
cluster1: minimalValidKubeConfig,
})
}
};
mockFs(mockOpts);
clusterStore = ClusterStore.getInstance<ClusterStore>();
return clusterStore.load();
return ClusterStore.getInstanceOrCreate().load();
});
afterEach(() => {
@@ -319,9 +324,9 @@ 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")).toBe("kubeconfig content");
expect(fs.readFileSync(config, "utf8")).toContain(`"contexts":[]`);
});
});
@@ -337,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(() => {
@@ -354,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");
});
@@ -375,7 +417,7 @@ describe("pre 2.6.0 config with a cluster icon", () => {
}
},
cluster1: {
kubeConfig: "foo",
kubeConfig: minimalValidKubeConfig,
icon: "icon_path",
preferences: {
terminalCWD: "/tmp"
@@ -387,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(() => {
@@ -397,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);
@@ -417,7 +458,7 @@ describe("for a pre 2.7.0-beta.0 config without a workspace", () => {
}
},
cluster1: {
kubeConfig: "foo",
kubeConfig: minimalValidKubeConfig,
preferences: {
terminalCWD: "/tmp"
}
@@ -427,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(() => {
@@ -451,7 +491,7 @@ describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
clusters: [
{
id: "cluster1",
kubeConfig: "kubeconfig content",
kubeConfig: minimalValidKubeConfig,
contextName: "cluster",
preferences: {
icon: "store://icon_path",
@@ -464,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(() => {
@@ -474,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("kubeconfig content");
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", () => {
+24
View File
@@ -0,0 +1,24 @@
import mockFs from "mock-fs";
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": "{}" } });
});
afterEach(() => {
mockFs.restore();
});
describe("load", () => {
it("loads one hotbar by default", () => {
HotbarStore.getInstanceOrCreate().load();
expect(HotbarStore.getInstance().hotbars.length).toEqual(1);
});
});
});
+189 -36
View File
@@ -1,5 +1,5 @@
import { KubeConfig } from "@kubernetes/client-node";
import { validateKubeConfig } from "../kube-helpers";
import { validateKubeConfig, loadConfig } from "../kube-helpers";
const kubeconfig = `
apiVersion: v1
@@ -40,61 +40,214 @@ users:
const kc = new KubeConfig();
describe("validateKubeconfig", () => {
beforeAll(() => {
kc.loadFromString(kubeconfig);
});
describe("with default validation options", () => {
describe("with valid kubeconfig", () => {
it("does not raise exceptions", () => {
expect(() => { validateKubeConfig(kc, "valid");}).not.toThrow();
});
interface kubeconfig {
apiVersion: string,
clusters: [{
name: string,
cluster: {
server: string
}
}],
contexts: [{
context: {
cluster: string,
user: string,
},
name: string
}],
users: [{
name: string
}],
kind: string,
"current-context": string,
preferences: {}
}
let mockKubeConfig: kubeconfig;
describe("kube helpers", () => {
describe("validateKubeconfig", () => {
beforeAll(() => {
kc.loadFromString(kubeconfig);
});
describe("with invalid context object", () => {
it("it raises exception", () => {
expect(() => { validateKubeConfig(kc, "invalid");}).toThrow("No valid context object provided in kubeconfig for context 'invalid'");
describe("with default validation options", () => {
describe("with valid kubeconfig", () => {
it("does not raise exceptions", () => {
expect(() => { validateKubeConfig(kc, "valid");}).not.toThrow();
});
});
describe("with invalid context object", () => {
it("it raises exception", () => {
expect(() => { validateKubeConfig(kc, "invalid");}).toThrow("No valid context object provided in kubeconfig for context 'invalid'");
});
});
describe("with invalid cluster object", () => {
it("it raises exception", () => {
expect(() => { validateKubeConfig(kc, "invalidCluster");}).toThrow("No valid cluster object provided in kubeconfig for context 'invalidCluster'");
});
});
describe("with invalid user object", () => {
it("it raises exception", () => {
expect(() => { validateKubeConfig(kc, "invalidUser");}).toThrow("No valid user object provided in kubeconfig for context 'invalidUser'");
});
});
describe("with invalid exec command", () => {
it("it raises exception", () => {
expect(() => { validateKubeConfig(kc, "invalidExec");}).toThrow("User Exec command \"foo\" not found on host. Please ensure binary is found in PATH or use absolute path to binary in Kubeconfig");
});
});
});
describe("with invalid cluster object", () => {
it("it raises exception", () => {
expect(() => { validateKubeConfig(kc, "invalidCluster");}).toThrow("No valid cluster object provided in kubeconfig for context 'invalidCluster'");
describe("with validateCluster as false", () => {
describe("with invalid cluster object", () => {
it("does not raise exception", () => {
expect(() => { validateKubeConfig(kc, "invalidCluster", { validateCluster: false });}).not.toThrow();
});
});
});
describe("with invalid user object", () => {
it("it raises exception", () => {
expect(() => { validateKubeConfig(kc, "invalidUser");}).toThrow("No valid user object provided in kubeconfig for context 'invalidUser'");
describe("with validateUser as false", () => {
describe("with invalid user object", () => {
it("does not raise exceptions", () => {
expect(() => { validateKubeConfig(kc, "invalidUser", { validateUser: false });}).not.toThrow();
});
});
});
describe("with invalid exec command", () => {
it("it raises exception", () => {
expect(() => { validateKubeConfig(kc, "invalidExec");}).toThrow("User Exec command \"foo\" not found on host. Please ensure binary is found in PATH or use absolute path to binary in Kubeconfig");
describe("with validateExec as false", () => {
describe("with invalid exec object", () => {
it("does not raise exceptions", () => {
expect(() => { validateKubeConfig(kc, "invalidExec", { validateExec: false });}).not.toThrow();
});
});
});
});
describe("with validateCluster as false", () => {
describe("with invalid cluster object", () => {
it("does not raise exception", () => {
expect(() => { validateKubeConfig(kc, "invalidCluster", { validateCluster: false });}).not.toThrow();
describe("pre-validate context object in kubeconfig tests", () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe("Check logger.error() output", () => {
it("invalid yaml string", () => {
const invalidYAMLString = "fancy foo config";
expect(() => loadConfig(invalidYAMLString)).toThrowError("must be an object");
});
it("empty contexts", () => {
const emptyContexts = `apiVersion: v1\ncontexts:`;
expect(() => loadConfig(emptyContexts)).not.toThrow();
});
});
});
describe("with validateUser as false", () => {
describe("with invalid user object", () => {
it("does not raise excpetions", () => {
expect(() => { validateKubeConfig(kc, "invalidUser", { validateUser: false });}).not.toThrow();
describe("Check valid kubeconfigs", () => {
beforeEach(() => {
mockKubeConfig = {
apiVersion: "v1",
clusters: [{
name: "minikube",
cluster: {
server: "https://192.168.64.3:8443",
},
}],
contexts: [{
context: {
cluster: "minikube",
user: "minikube",
},
name: "minikube",
}],
users: [{
name: "minikube",
}],
kind: "Config",
"current-context": "minikube",
preferences: {},
};
});
it("single context is ok", async () => {
const kc:KubeConfig = loadConfig(JSON.stringify(mockKubeConfig));
expect(kc.getCurrentContext()).toBe("minikube");
});
it("multiple context is ok", async () => {
mockKubeConfig.contexts.push({context: {cluster: "cluster-2", user: "cluster-2"}, name: "cluster-2"});
const kc:KubeConfig = loadConfig(JSON.stringify(mockKubeConfig));
expect(kc.getCurrentContext()).toBe("minikube");
expect(kc.contexts.length).toBe(2);
});
});
});
describe("with validateExec as false", () => {
describe("with invalid exec object", () => {
it("does not raise excpetions", () => {
expect(() => { validateKubeConfig(kc, "invalidExec", { validateExec: false });}).not.toThrow();
describe("Check invalid kubeconfigs", () => {
beforeEach(() => {
mockKubeConfig = {
apiVersion: "v1",
clusters: [{
name: "minikube",
cluster: {
server: "https://192.168.64.3:8443",
},
}],
contexts: [{
context: {
cluster: "minikube",
user: "minikube",
},
name: "minikube",
}],
users: [{
name: "minikube",
}],
kind: "Config",
"current-context": "minikube",
preferences: {},
};
});
it("empty name in context causes it to be removed", async () => {
mockKubeConfig.contexts.push({context: {cluster: "cluster-2", user: "cluster-2"}, name: ""});
expect(mockKubeConfig.contexts.length).toBe(2);
const kc:KubeConfig = loadConfig(JSON.stringify(mockKubeConfig));
expect(kc.getCurrentContext()).toBe("minikube");
expect(kc.contexts.length).toBe(1);
});
it("empty cluster in context causes it to be removed", async () => {
mockKubeConfig.contexts.push({context: {cluster: "", user: "cluster-2"}, name: "cluster-2"});
expect(mockKubeConfig.contexts.length).toBe(2);
const kc:KubeConfig = loadConfig(JSON.stringify(mockKubeConfig));
expect(kc.getCurrentContext()).toBe("minikube");
expect(kc.contexts.length).toBe(1);
});
it("empty user in context causes it to be removed", async () => {
mockKubeConfig.contexts.push({context: {cluster: "cluster-2", user: ""}, name: "cluster-2"});
expect(mockKubeConfig.contexts.length).toBe(2);
const kc:KubeConfig = loadConfig(JSON.stringify(mockKubeConfig));
expect(kc.getCurrentContext()).toBe("minikube");
expect(kc.contexts.length).toBe(1);
});
it("invalid context in between valid contexts is removed", async () => {
mockKubeConfig.contexts.push({context: {cluster: "cluster-2", user: ""}, name: "cluster-2"});
mockKubeConfig.contexts.push({context: {cluster: "cluster-3", user: "cluster-3"}, name: "cluster-3"});
expect(mockKubeConfig.contexts.length).toBe(3);
const kc:KubeConfig = loadConfig(JSON.stringify(mockKubeConfig));
expect(kc.getCurrentContext()).toBe("minikube");
expect(kc.contexts.length).toBe(2);
expect(kc.contexts[0].name).toBe("minikube");
expect(kc.contexts[1].name).toBe("cluster-3");
});
});
});
@@ -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");
});
@@ -1,9 +1,9 @@
import { EventEmitter } from "events";
import { observable, makeObservable } from "mobx";
import { catalogCategoryRegistry } from "../catalog-category-registry";
import { CatalogCategory, CatalogEntity, CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityData, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog-entity";
import { CatalogCategory, CatalogEntity, CatalogEntityActionContext, CatalogEntityAddMenuContext, CatalogEntityContextMenuContext, CatalogEntityData, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog-entity";
import { clusterDisconnectHandler } from "../cluster-ipc";
import { clusterStore } from "../cluster-store";
import { ClusterStore } from "../cluster-store";
import { requestMain } from "../ipc";
export type KubernetesClusterSpec = {
@@ -50,12 +50,14 @@ export class KubernetesCluster implements CatalogEntity {
{
icon: "settings",
title: "Settings",
onClick: async () => context.navigate(`/cluster/${this.metadata.uid}/settings`)
onlyVisibleForSource: "local",
onClick: async () => context.navigate(`/entity/${this.metadata.uid}/settings`)
},
{
icon: "delete",
title: "Delete",
onClick: async () => clusterStore.removeById(this.metadata.uid),
onlyVisibleForSource: "local",
onClick: async () => ClusterStore.getInstance().removeById(this.metadata.uid),
confirm: {
message: `Remove Kubernetes Cluster "${this.metadata.name} from Lens?`
}
@@ -67,7 +69,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);
}
});
@@ -98,6 +100,20 @@ export class KubernetesClusterCategory extends EventEmitter implements CatalogCa
}
};
constructor() {
super();
this.on("onCatalogAddMenu", (ctx: CatalogEntityAddMenuContext) => {
ctx.menuItems.push({
icon: "text_snippet",
title: "Add from kubeconfig",
onClick: async () => {
ctx.navigate("/add-cluster");
}
});
});
}
getId() {
return `${this.spec.group}/${this.spec.names.kind}`;
}
+19
View File
@@ -45,17 +45,35 @@ export interface CatalogEntityActionContext {
export type CatalogEntityContextMenu = {
icon: string;
title: string;
onlyVisibleForSource?: string; // show only if empty or if matches with entity source
onClick: () => Promise<void>;
confirm?: {
message: string;
}
};
export type CatalogEntitySettingsMenu = {
group?: string;
title: string;
components: {
View: React.ComponentType<any>
};
};
export interface CatalogEntityContextMenuContext {
navigate: (url: string) => void;
menuItems: CatalogEntityContextMenu[];
}
export interface CatalogEntitySettingsContext {
menuItems: CatalogEntityContextMenu[];
}
export interface CatalogEntityAddMenuContext {
navigate: (url: string) => void;
menuItems: CatalogEntityContextMenu[];
}
export type CatalogEntityData = {
apiVersion: string;
kind: string;
@@ -72,4 +90,5 @@ export interface CatalogEntity extends CatalogEntityData {
onRun: (context: CatalogEntityActionContext) => Promise<void>;
onDetailsOpen: (context: CatalogEntityActionContext) => Promise<void>;
onContextMenuOpen: (context: CatalogEntityContextMenuContext) => Promise<void>;
onSettingsOpen?: (context: CatalogEntitySettingsContext) => Promise<void>;
}
+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);
+3 -5
View File
@@ -11,7 +11,7 @@ import { dumpConfigYaml } from "./kube-helpers";
import { saveToAppFiles } from "./utils/saveToAppFiles";
import { KubeConfig } from "@kubernetes/client-node";
import { handleRequest, requestMain, subscribeToBroadcast, unsubscribeAllFromBroadcast } from "./ipc";
import { ResourceType } from "../renderer/components/+cluster-settings/components/cluster-metrics-setting";
import { ResourceType } from "../renderer/components/cluster-settings/components/cluster-metrics-setting";
export interface ClusterIconUpload {
clusterId: string;
@@ -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());
}
+9 -7
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
@@ -36,10 +36,14 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
}
@action protected async fromStore(data: Partial<HotbarStoreModel> = {}) {
this.hotbars = data.hotbars || [{
name: "default",
items: []
}];
if (data.hotbars?.length === 0) {
this.hotbars = [{
name: "default",
items: []
}];
} else {
this.hotbars = data.hotbars;
}
}
getByName(name: string) {
@@ -60,5 +64,3 @@ export class HotbarStore extends BaseStore<HotbarStoreModel> {
});
}
}
export const hotbarStore = HotbarStore.getInstance<HotbarStore>();
+29 -6
View File
@@ -6,6 +6,7 @@ import yaml from "js-yaml";
import logger from "../main/logger";
import commandExists from "command-exists";
import { ExecValidationNotFoundError } from "./custom-errors";
import { newClusters, newContexts, newUsers } from "@kubernetes/client-node/dist/config_types";
export type KubeConfigValidationOpts = {
validateCluster?: boolean;
@@ -23,14 +24,36 @@ function resolveTilde(filePath: string) {
return filePath;
}
function readResolvedPathSync(filePath: string): string {
return fse.readFileSync(path.resolve(resolveTilde(filePath)), "utf8");
}
function checkRawContext(rawContext: any): boolean {
return rawContext.name && rawContext.context?.cluster && rawContext.context?.user;
}
function loadToOptions(rawYaml: string): any {
const obj = yaml.safeLoad(rawYaml);
if (typeof obj !== "object" || !obj) {
throw new TypeError("KubeConfig root entry must be an object");
}
const { clusters: rawClusters, users: rawUsers, contexts: rawContexts, "current-context": currentContext } = obj;
const clusters = newClusters(rawClusters);
const users = newUsers(rawUsers);
const contexts = newContexts(rawContexts?.filter(checkRawContext));
return { clusters, users, contexts, currentContext };
}
export function loadConfig(pathOrContent?: string): KubeConfig {
const content = fse.pathExistsSync(pathOrContent) ? readResolvedPathSync(pathOrContent) : pathOrContent;
const options = loadToOptions(content);
const kc = new KubeConfig();
if (fse.pathExistsSync(pathOrContent)) {
kc.loadFromFile(path.resolve(resolveTilde(pathOrContent)));
} else {
kc.loadFromString(pathOrContent);
}
// need to load using the kubernetes client to generate a kubeconfig object
kc.loadFromOptions(options);
return kc;
}
@@ -146,7 +169,7 @@ export function podHasIssues(pod: V1Pod) {
return (
notReady ||
pod.status.phase !== "Running" ||
pod.spec.priority > 500000 // We're interested in high prio pods events regardless of their running status
pod.spec.priority > 500000 // We're interested in high priority pods events regardless of their running status
);
}
+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,
+22 -12
View File
@@ -2,7 +2,8 @@ import type { ThemeId } from "../renderer/theme.store";
import { app, remote } from "electron";
import semver from "semver";
import { readFile } from "fs-extra";
import { action, observable, reaction, toJS, makeObservable } from "mobx";
import { action, computed, observable, reaction, toJS, makeObservable } from "mobx";
import moment from "moment-timezone";
import { BaseStore } from "./base-store";
import migrations from "../migrations/user-store";
import { getAppVersion } from "./utils/app-version";
@@ -23,6 +24,7 @@ export interface UserPreferences {
httpsProxy?: string;
shell?: string;
colorTheme?: string;
localeTimezone?: string;
allowUntrustedCAs?: boolean;
allowTelemetry?: boolean;
downloadMirror?: string | "default";
@@ -36,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,
@@ -56,6 +58,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
allowTelemetry: true,
allowUntrustedCAs: false,
colorTheme: UserStore.defaultTheme,
localeTimezone: moment.tz.guess(true) || "UTC",
downloadMirror: "default",
downloadKubectlBinaries: true, // Download kubectl binaries matching cluster version
openAtLogin: false,
@@ -77,7 +80,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
// open at system start-up
reaction(() => this.preferences.openAtLogin, openAtLogin => {
app.setLoginItemSettings({
app.setLoginItemSettings({
openAtLogin,
openAsHidden: true,
args: ["--hidden"]
@@ -116,6 +119,10 @@ export class UserStore extends BaseStore<UserStoreModel> {
this.kubeConfigPath = kubeConfigDefaultPath;
}
@computed get isDefaultKubeConfigPath(): boolean {
return this.kubeConfigPath === kubeConfigDefaultPath;
}
@action
async resetTheme() {
await this.whenLoaded;
@@ -128,6 +135,11 @@ export class UserStore extends BaseStore<UserStoreModel> {
this.lastSeenAppVersion = getAppVersion();
}
@action
setLocaleTimezone(tz: string) {
this.preferences.localeTimezone = tz;
}
protected refreshNewContexts = async () => {
try {
const kubeConfig = await readFile(this.kubeConfigPath, "utf8");
@@ -153,14 +165,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;
@@ -186,4 +190,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");
}
-4
View File
@@ -9,10 +9,6 @@ export function getBundledKubectlVersion(): string {
return packageInfo.config.bundledKubectlVersion;
}
export function getBundledExtensions(): string[] {
return packageInfo.lens?.extensions || [];
}
export async function getAppVersionFromProxyServer(proxyPort: number): Promise<string> {
const response = await requestPromise({
method: "GET",
+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;
+3 -3
View File
@@ -3,12 +3,12 @@ import path from "path";
import hb from "handlebars";
import { observable, makeObservable } from "mobx";
import { ResourceApplier } from "../main/resource-applier";
import { KubernetesCluster } from "./core-api/stores";
import { KubernetesCluster } from "./core-api/catalog";
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 */
@@ -89,7 +89,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;
}
+3
View File
@@ -1,6 +1,9 @@
import { CatalogEntity } from "../../common/catalog-entity";
import { catalogEntityRegistry as registry } from "../../common/catalog-entity-registry";
export { catalogCategoryRegistry as catalogCategories } from "../../common/catalog-category-registry";
export * from "../../common/catalog-entities";
export class CatalogEntityRegistry {
getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
return registry.getItemsForApiKind<T>(apiVersion, kind);
+2
View File
@@ -9,10 +9,12 @@ import * as Store from "./stores";
import * as Util from "./utils";
import * as ClusterFeature from "./cluster-feature";
import * as Interface from "../interfaces";
import * as Catalog from "./catalog";
export {
App,
EventBus,
Catalog,
ClusterFeature,
Interface,
Store,
-3
View File
@@ -1,4 +1 @@
export { ExtensionStore } from "../extension-store";
export { KubernetesCluster, KubernetesClusterCategory } from "../../common/catalog-entities/kubernetes-cluster";
export { catalogCategoryRegistry as catalogCategories } from "../../common/catalog-category-registry";
export { catalogEntities } from "./catalog";
+9 -16
View File
@@ -6,10 +6,10 @@ import { observable, reaction, toJS, when, makeObservable } from "mobx";
import os from "os";
import path from "path";
import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc";
import { getBundledExtensions } from "../common/utils/app-version";
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 {
@@ -51,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;
@@ -67,6 +67,7 @@ export class ExtensionDiscovery {
public events: EventEmitter;
constructor() {
super();
makeObservable(this);
this.events = new EventEmitter();
}
@@ -137,7 +138,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.
@@ -237,7 +238,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}`);
@@ -327,7 +328,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,
@@ -349,7 +350,7 @@ export class ExtensionDiscovery {
await this.installBundledPackages(this.packageJsonPath, bundledExtensions);
const userExtensions = await this.loadFromFolder(this.localFolderPath);
const userExtensions = await this.loadFromFolder(this.localFolderPath, bundledExtensions.map((extension) => extension.manifest.name));
for (const extension of userExtensions) {
if ((await fs.pathExists(extension.manifestPath)) === false) {
@@ -383,14 +384,9 @@ export class ExtensionDiscovery {
async loadBundledExtensions() {
const extensions: InstalledExtension[] = [];
const folderPath = this.bundledFolderPath;
const bundledExtensions = getBundledExtensions();
const paths = await fs.readdir(folderPath);
for (const fileName of paths) {
if (!bundledExtensions.includes(fileName)) {
continue;
}
const absPath = path.resolve(folderPath, fileName);
const extension = await this.loadExtensionFromFolder(absPath, { isBundled: true });
@@ -403,8 +399,7 @@ export class ExtensionDiscovery {
return extensions;
}
async loadFromFolder(folderPath: string): Promise<InstalledExtension[]> {
const bundledExtensions = getBundledExtensions();
async loadFromFolder(folderPath: string, bundledExtensions: string[]): Promise<InstalledExtension[]> {
const extensions: InstalledExtension[] = [];
const paths = await fs.readdir(folderPath);
@@ -461,5 +456,3 @@ export class ExtensionDiscovery {
broadcastMessage(ExtensionDiscovery.extensionDiscoveryChannel, this.toJSON());
}
}
export const extensionDiscovery = new ExtensionDiscovery();
+6 -7
View File
@@ -5,9 +5,10 @@ import { action, computed, observable, reaction, toJS, when, makeObservable } fr
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>();
@@ -99,11 +100,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);
});
}
@@ -215,8 +216,8 @@ export class ExtensionLoader {
this.autoInitExtensions(async (extension: LensRendererExtension) => {
const removeItems = [
registries.globalPageRegistry.add(extension.globalPages, extension),
registries.globalPageMenuRegistry.add(extension.globalPageMenus, extension),
registries.appPreferenceRegistry.add(extension.appPreferences),
registries.entitySettingRegistry.add(extension.entitySettings),
registries.statusBarRegistry.add(extension.statusBarItems),
registries.commandRegistry.add(extension.commands),
];
@@ -330,5 +331,3 @@ export class ExtensionLoader {
broadcastMessage(main ? ExtensionLoader.extensionsMainChannel : ExtensionLoader.extensionsRendererChannel, Array.from(this.toJSON()));
}
}
export const extensionLoader = new ExtensionLoader();
-2
View File
@@ -52,5 +52,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, makeObservable } 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";
@@ -50,7 +50,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,
@@ -3,6 +3,7 @@ import type { Cluster } from "../main/cluster";
import { LensExtension } from "./lens-extension";
import { getExtensionPageUrl } from "./registries/page-registry";
import { CommandRegistration } from "./registries/command-registry";
import { EntitySettingRegistration } from "./registries/entity-setting-registry";
export class LensRendererExtension extends LensExtension {
globalPages: PageRegistration[] = [];
@@ -11,6 +12,7 @@ export class LensRendererExtension extends LensExtension {
clusterPageMenus: ClusterPageMenuRegistration[] = [];
kubeObjectStatusTexts: KubeObjectStatusRegistration[] = [];
appPreferences: AppPreferenceRegistration[] = [];
entitySettings: EntitySettingRegistration[] = [];
statusBarItems: StatusBarRegistration[] = [];
kubeObjectDetailItems: KubeObjectDetailRegistration[] = [];
kubeObjectMenuItems: KubeObjectMenuRegistration[] = [];
@@ -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;
@@ -0,0 +1,49 @@
import type React from "react";
import { CatalogEntity } from "../../common/catalog-entity";
import { BaseRegistry } from "./base-registry";
export interface EntitySettingViewProps {
entity: CatalogEntity;
}
export interface EntitySettingComponents {
View: React.ComponentType<EntitySettingViewProps>;
}
export interface EntitySettingRegistration {
title: string;
kind: string;
apiVersions: string[];
source?: string;
id?: string;
components: EntitySettingComponents;
}
export interface RegisteredEntitySetting extends EntitySettingRegistration {
id: string;
}
export class EntitySettingRegistry extends BaseRegistry<EntitySettingRegistration, RegisteredEntitySetting> {
getRegisteredItem(item: EntitySettingRegistration): RegisteredEntitySetting {
return {
id: item.id || item.title.toLowerCase(),
...item,
};
}
getItemsForKind(kind: string, apiVersion: string, source?: string) {
const items = this.getItems().filter((item) => {
return item.kind === kind && item.apiVersions.includes(apiVersion);
});
if (source) {
return items.filter((item) => {
return !item.source || item.source === source;
});
} else {
return items;
}
}
}
export const entitySettingRegistry = new EntitySettingRegistry();
+1
View File
@@ -9,3 +9,4 @@ export * from "./kube-object-detail-registry";
export * from "./kube-object-menu-registry";
export * from "./kube-object-status-registry";
export * from "./command-registry";
export * from "./entity-setting-registry";
@@ -55,5 +55,4 @@ export class ClusterPageMenuRegistry extends PageMenuRegistry<ClusterPageMenuReg
}
}
export const globalPageMenuRegistry = new PageMenuRegistry();
export const clusterPageMenuRegistry = new ClusterPageMenuRegistry();
@@ -1,5 +1,6 @@
// Common UI components
export * from "../../renderer/components/layout/sub-title";
export * from "../../renderer/components/input/search-input";
// layouts
export * from "../../renderer/components/layout/page-layout";
+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, makeObservable } 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";
@@ -23,7 +23,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());
@@ -33,8 +33,8 @@ export class ClusterManager extends Singleton {
}, { fireImmediately: true });
reaction(() => toJS(clusterStore.enabledClustersList), () => {
this.updateCatalogSource(clusterStore.enabledClustersList);
reaction(() => toJS(ClusterStore.getInstance().enabledClustersList), () => {
this.updateCatalogSource(ClusterStore.getInstance().enabledClustersList);
}, { fireImmediately: true });
reaction(() => catalogEntityRegistry.getItemsForApiKind<KubernetesCluster>("entity.k8slens.dev/v1alpha1", "KubernetesCluster"), (entities) => {
@@ -44,14 +44,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
@@ -92,10 +92,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,
@@ -147,7 +147,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;
@@ -158,7 +158,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);
}
@@ -166,7 +166,7 @@ export class ClusterManager extends Singleton {
}
stop() {
clusterStore.clusters.forEach((cluster: Cluster) => {
ClusterStore.getInstance().clusters.forEach((cluster: Cluster) => {
cluster.disconnect();
});
}
@@ -178,18 +178,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;
+17 -4
View File
@@ -670,6 +670,8 @@ export class Cluster implements ClusterModel, ClusterState {
};
}
protected getAllowedNamespacesErrorCount = 0;
protected async getAllowedNamespaces() {
if (this.accessibleNamespaces.length) {
return this.accessibleNamespaces;
@@ -678,16 +680,27 @@ export class Cluster implements ClusterModel, ClusterState {
const api = (await this.getProxyKubeconfig()).makeApiClient(CoreV1Api);
try {
const namespaceList = await api.listNamespace();
const { body: { items }} = await api.listNamespace();
const namespaces = items.map(ns => ns.metadata.name);
return namespaceList.body.items.map(ns => ns.metadata.name);
this.getAllowedNamespacesErrorCount = 0; // reset on success
return namespaces;
} catch (error) {
const ctx = (await this.getProxyKubeconfig()).getContextObject(this.contextName);
const namespaceList = [ctx.namespace].filter(Boolean);
if (namespaceList.length === 0 && error instanceof HttpError && error.statusCode === 403) {
logger.info("[CLUSTER]: listing namespaces is forbidden, broadcasting", { clusterId: this.id });
broadcastMessage(ClusterListNamespaceForbiddenChannel, this.id);
this.getAllowedNamespacesErrorCount += 1;
if (this.getAllowedNamespacesErrorCount > 3) {
// reset on send
this.getAllowedNamespacesErrorCount = 0;
// then broadcast, make sure it is 3 successive attempts
logger.info("[CLUSTER]: listing namespaces is forbidden, broadcasting", { clusterId: this.id, error });
broadcastMessage(ClusterListNamespaceForbiddenChannel, this.id);
}
}
return namespaceList;
+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
@@ -55,5 +55,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, pathToKubeconfig: string) {
const helm = await helmCli.binaryPath();
const { stdout, } = await promiseExec(`"${helm}" get values ${name} --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>();
+14 -17
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,31 +45,30 @@ 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) {
public async getReleaseValues(cluster: Cluster, releaseName: string, namespace: string, all: boolean) {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
logger.debug("Fetch release values");
return await releaseManager.getValues(releaseName, namespace, 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
@@ -16,15 +16,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";
@@ -32,13 +32,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);
@@ -67,7 +63,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://")) {
@@ -78,7 +74,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://")) {
@@ -87,7 +83,7 @@ app.on("second-instance", (event, argv) => {
}
}
windowManager?.ensureMainWindow();
WindowManager.getInstance(false)?.ensureMainWindow();
});
app.on("ready", async () => {
@@ -103,7 +99,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
@@ -112,10 +112,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();
@@ -126,13 +128,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"}`);
@@ -152,7 +154,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
@@ -160,7 +164,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();
@@ -170,13 +176,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;
});
@@ -190,14 +196,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);
@@ -213,7 +220,7 @@ app.on("activate", (event, hasVisibleWindows) => {
logger.info("APP:ACTIVATE", { hasVisibleWindows });
if (!hasVisibleWindows) {
windowManager?.initMainWindow(false);
WindowManager.getInstance(false)?.initMainWindow(false);
}
});
@@ -228,8 +235,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.)
@@ -243,7 +249,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);
-24
View File
@@ -5,7 +5,6 @@ import { appName, isMac, isWindows, isTestEnv, docsUrl, supportUrl } from "../co
import { addClusterURL } from "../renderer/components/+add-cluster/add-cluster.route";
import { preferencesURL } from "../renderer/components/+preferences/preferences.route";
import { whatsNewURL } from "../renderer/components/+whats-new/whats-new.route";
import { clusterSettingsURL } from "../renderer/components/+cluster-settings/cluster-settings.route";
import { extensionsURL } from "../renderer/components/+extensions/extensions.route";
import { catalogURL } from "../renderer/components/+catalog/catalog.route";
import { menuRegistry } from "../extensions/registries/menu-registry";
@@ -47,16 +46,6 @@ export function buildMenu(windowManager: WindowManager) {
return menuItems;
}
function activeClusterOnly(menuItems: MenuItemConstructorOptions[]) {
if (!windowManager.activeClusterId) {
menuItems.forEach(item => {
item.enabled = false;
});
}
return menuItems;
}
async function navigate(url: string) {
logger.info(`[MENU]: navigating to ${url}`);
await windowManager.navigate(url);
@@ -112,19 +101,6 @@ export function buildMenu(windowManager: WindowManager) {
navigate(addClusterURL());
}
},
...activeClusterOnly([
{
label: "Cluster Settings",
accelerator: "CmdOrCtrl+Shift+S",
click() {
navigate(clusterSettingsURL({
params: {
clusterId: windowManager.activeClusterId
}
}));
}
}
]),
...ignoreOnMac([
{ type: "separator" },
{
@@ -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; });
+2 -2
View File
@@ -101,10 +101,10 @@ class HelmApiRoute extends LensApi {
}
public async getReleaseValues(request: LensApiRequest) {
const { cluster, params, response } = request;
const { cluster, params, response, query } = request;
try {
const result = await helmService.getReleaseValues(cluster, params.release, params.namespace);
const result = await helmService.getReleaseValues(cluster, params.release, params.namespace, query.has("all"));
this.respondText(response, result);
} catch (error) {
@@ -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
@@ -62,35 +62,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 {
@@ -102,8 +101,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);
+91
View File
@@ -0,0 +1,91 @@
import { CustomResourceDefinition } from "../endpoints";
import { IKubeObjectMetadata } from "../kube-object";
describe("Crds", () => {
describe("getVersion", () => {
it("should get the first version name from the list of versions", () => {
const crd = new CustomResourceDefinition({
apiVersion: "foo",
kind: "CustomResourceDefinition",
metadata: {} as IKubeObjectMetadata,
});
crd.spec = {
versions: [
{
name: "123",
served: false,
storage: false,
}
]
} as any;
expect(crd.getVersion()).toBe("123");
});
it("should get the first version name from the list of versions (length 2)", () => {
const crd = new CustomResourceDefinition({
apiVersion: "foo",
kind: "CustomResourceDefinition",
metadata: {} as IKubeObjectMetadata,
});
crd.spec = {
versions: [
{
name: "123",
served: false,
storage: false,
},
{
name: "1234",
served: false,
storage: false,
}
]
} as any;
expect(crd.getVersion()).toBe("123");
});
it("should get the first version name from the list of versions (length 2) even with version field", () => {
const crd = new CustomResourceDefinition({
apiVersion: "foo",
kind: "CustomResourceDefinition",
metadata: {} as IKubeObjectMetadata,
});
crd.spec = {
version: "abc",
versions: [
{
name: "123",
served: false,
storage: false,
},
{
name: "1234",
served: false,
storage: false,
}
]
} as any;
expect(crd.getVersion()).toBe("123");
});
it("should get the first version name from the version field", () => {
const crd = new CustomResourceDefinition({
apiVersion: "foo",
kind: "CustomResourceDefinition",
metadata: {} as IKubeObjectMetadata,
});
crd.spec = {
version: "abc"
} as any;
expect(crd.getVersion()).toBe("abc");
});
});
});
@@ -46,6 +46,10 @@ export class CatalogEntityRegistry {
return this._items;
}
getById(id: string) {
return this._items.find((entity) => entity.metadata.uid === id);
}
getItemsForApiKind<T extends CatalogEntity>(apiVersion: string, kind: string): T[] {
const items = this._items.filter((item) => item.apiVersion === apiVersion && item.kind === kind);
+9 -1
View File
@@ -2,7 +2,15 @@ import { navigate } from "../navigation";
import { commandRegistry } from "../../extensions/registries";
import { CatalogEntity } from "../../common/catalog-entity";
export { CatalogEntity, CatalogEntityData, CatalogEntityActionContext, CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../common/catalog-entity";
export {
CatalogCategory,
CatalogEntity,
CatalogEntityData,
CatalogEntityActionContext,
CatalogEntityAddMenuContext,
CatalogEntityContextMenu,
CatalogEntityContextMenuContext
} from "../../common/catalog-entity";
export const catalogEntityRunContext = {
navigate: (url: string) => navigate(url),
+3 -3
View File
@@ -33,7 +33,7 @@ export class CustomResourceDefinition extends KubeObject {
};
scope: "Namespaced" | "Cluster" | string;
validation?: any;
versions: {
versions?: {
name: string;
served: boolean;
storage: boolean;
@@ -103,7 +103,7 @@ export class CustomResourceDefinition extends KubeObject {
getVersion() {
// v1 has removed the spec.version property, if it is present it must match the first version
return this.spec.versions[0]?.name ?? this.spec.version;
return this.spec.versions?.[0]?.name ?? this.spec.version;
}
isNamespaced() {
@@ -123,7 +123,7 @@ export class CustomResourceDefinition extends KubeObject {
}
getPrinterColumns(ignorePriority = true): AdditionalPrinterColumnsV1[] {
const columns = this.spec.versions.find(a => this.getVersion() == a.name)?.additionalPrinterColumns
const columns = this.spec.versions?.find(a => this.getVersion() == a.name)?.additionalPrinterColumns
?? this.spec.additionalPrinterColumns?.map(({ JSONPath, ...rest }) => ({ ...rest, jsonPath: JSONPath })) // map to V1 shape
?? [];
+48 -50
View File
@@ -6,6 +6,7 @@ import { apiBase } from "../index";
import { helmChartStore } from "../../components/+apps-helm-charts/helm-chart.store";
import { ItemObject } from "../../item.store";
import { KubeObject } from "../kube-object";
import { JsonApiData } from "../json-api";
interface IReleasePayload {
name: string;
@@ -68,73 +69,70 @@ const endpoint = compile(`/v2/releases/:namespace?/:name?`) as (
}
) => string;
export const helmReleasesApi = {
list(namespace?: string) {
return apiBase
.get<HelmRelease[]>(endpoint({ namespace }))
.then(releases => releases.map(HelmRelease.create));
},
export async function listReleases(namespace?: string): Promise<HelmRelease[]> {
const releases = await apiBase.get<HelmRelease[]>(endpoint({ namespace }));
get(name: string, namespace: string) {
const path = endpoint({ name, namespace });
return releases.map(HelmRelease.create);
}
return apiBase.get<IReleaseRawDetails>(path).then(details => {
const items: KubeObject[] = JSON.parse(details.resources).items;
const resources = items.map(item => KubeObject.create(item));
export async function getRelease(name: string, namespace: string): Promise<IReleaseDetails> {
const path = endpoint({ name, namespace });
return {
...details,
resources
};
});
},
const details = await apiBase.get<IReleaseRawDetails>(path);
const items: KubeObject[] = JSON.parse(details.resources).items;
const resources = items.map(item => KubeObject.create(item));
create(payload: IReleaseCreatePayload): Promise<IReleaseUpdateDetails> {
const { repo, ...data } = payload;
return {
...details,
resources
};
}
data.chart = `${repo}/${data.chart}`;
data.values = jsYaml.safeLoad(data.values);
export async function createRelease(payload: IReleaseCreatePayload): Promise<IReleaseUpdateDetails> {
const { repo, ...data } = payload;
return apiBase.post(endpoint(), { data });
},
data.chart = `${repo}/${data.chart}`;
data.values = jsYaml.safeLoad(data.values);
update(name: string, namespace: string, payload: IReleaseUpdatePayload): Promise<IReleaseUpdateDetails> {
const { repo, ...data } = payload;
return apiBase.post(endpoint(), { data });
}
data.chart = `${repo}/${data.chart}`;
data.values = jsYaml.safeLoad(data.values);
export async function updateRelease(name: string, namespace: string, payload: IReleaseUpdatePayload): Promise<IReleaseUpdateDetails> {
const { repo, ...data } = payload;
return apiBase.put(endpoint({ name, namespace }), { data });
},
data.chart = `${repo}/${data.chart}`;
data.values = jsYaml.safeLoad(data.values);
async delete(name: string, namespace: string) {
const path = endpoint({ name, namespace });
return apiBase.put(endpoint({ name, namespace }), { data });
}
return apiBase.del(path);
},
export async function deleteRelease(name: string, namespace: string): Promise<JsonApiData> {
const path = endpoint({ name, namespace });
getValues(name: string, namespace: string) {
const path = `${endpoint({ name, namespace })}/values`;
return apiBase.del(path);
}
return apiBase.get<string>(path);
},
export async function getReleaseValues(name: string, namespace: string, all?: boolean): Promise<string> {
const path = `${endpoint({ name, namespace })}/values${all? "?all": ""}`;
getHistory(name: string, namespace: string): Promise<IReleaseRevision[]> {
const path = `${endpoint({ name, namespace })}/history`;
return apiBase.get<string>(path);
}
return apiBase.get(path);
},
export async function getReleaseHistory(name: string, namespace: string): Promise<IReleaseRevision[]> {
const path = `${endpoint({ name, namespace })}/history`;
rollback(name: string, namespace: string, revision: number) {
const path = `${endpoint({ name, namespace })}/rollback`;
return apiBase.get(path);
}
return apiBase.put(path, {
data: {
revision
}
});
}
};
export async function rollbackRelease(name: string, namespace: string, revision: number): Promise<JsonApiData> {
const path = `${endpoint({ name, namespace })}/rollback`;
return apiBase.put(path, {
data: {
revision
}
});
}
@autobind()
export class HelmRelease implements ItemObject {
+1 -1
View File
@@ -342,7 +342,7 @@ export class Pod extends WorkloadKubeObject {
// Returns pod phase or container error if occurred
getStatusMessage() {
if (this.getReason() === PodStatus.EVICTED) return "Evicted";
if (this.getStatus() === PodStatus.RUNNING && this.metadata.deletionTimestamp) return "Terminating";
if (this.metadata.deletionTimestamp) return "Terminating";
const statuses = this.getContainerStatuses(false); // not including initContainers
+24 -8
View File
@@ -7,10 +7,16 @@ import * as ReactRouter from "react-router";
import * as ReactRouterDom from "react-router-dom";
import * as LensExtensions from "../extensions/extension-api";
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 { FilesystemProvisionerStore } from "../main/extension-filesystem";
import { extensionDiscovery } from "../extensions/extension-discovery";
import { extensionLoader } from "../extensions/extension-loader";
import { extensionsStore } from "../extensions/extensions-store";
@@ -18,7 +24,8 @@ import { hotbarStore } from "../common/hotbar-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 +57,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 +74,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 +88,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";
@@ -49,13 +49,13 @@ export class AddCluster extends React.Component {
}
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
@@ -65,11 +65,13 @@ 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) {
Notifications.error(
<div>Can&apos;t setup <code>{filePath}</code> as kubeconfig: {String(err)}</div>
);
if (!UserStore.getInstance().isDefaultKubeConfigPath) {
Notifications.error(
<div>Can&apos;t setup <code>{filePath}</code> as kubeconfig: {String(err)}</div>
);
}
if (throwError) {
throw err;
@@ -184,7 +186,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)</>
@@ -311,7 +313,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());
@@ -319,7 +321,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
}
}
};
@@ -331,7 +333,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 (
@@ -348,7 +350,7 @@ export class AddCluster extends React.Component {
return (
<DropFileInput onDropFiles={this.onDropKubeConfig}>
<PageLayout className="AddClusters" header={<><Icon svg="logo-lens" big /> <h2>Add Clusters</h2></>} showOnTop={true}>
<PageLayout className="AddClusters" showOnTop={true}>
<h2>Add Clusters from Kubeconfig</h2>
{this.renderInfo()}
{this.renderKubeConfigSource()}
@@ -18,6 +18,19 @@ export class HelmChartStore extends ItemStore<HelmChart> {
makeObservable(this);
}
async loadAll() {
try {
const res = await this.loadItems(() => helmChartsApi.list());
this.failedLoading = false;
return res;
} catch (error) {
this.failedLoading = true;
throw error;
}
loadAll() {
return this.loadItems(() => helmChartsApi.list());
}
@@ -6,7 +6,7 @@ import isEqual from "lodash/isEqual";
import { observable, reaction, toJS, makeObservable } from "mobx";
import { Link } from "react-router-dom";
import kebabCase from "lodash/kebabCase";
import { HelmRelease, helmReleasesApi, IReleaseDetails } from "../../api/endpoints/helm-releases.api";
import { getRelease, getReleaseValues, HelmRelease, IReleaseDetails } from "../../api/endpoints/helm-releases.api";
import { HelmReleaseMenu } from "./release-menu";
import { Drawer, DrawerItem, DrawerTitle } from "../drawer";
import { Badge } from "../badge";
@@ -19,12 +19,13 @@ 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";
import { Secret } from "../../api/endpoints";
import { getDetailsUrl } from "../kube-object";
import { Checkbox } from "../checkbox";
interface Props {
release: HelmRelease;
@@ -35,6 +36,8 @@ interface Props {
export class ReleaseDetails extends Component<Props> {
@observable details: IReleaseDetails;
@observable values = "";
@observable valuesLoading = false;
@observable userSuppliedOnly = false;
@observable saving = false;
@observable releaseSecret: Secret;
@@ -70,14 +73,16 @@ export class ReleaseDetails extends Component<Props> {
const { release } = this.props;
this.details = null;
this.details = await helmReleasesApi.get(release.getName(), release.getNs());
this.details = await getRelease(release.getName(), release.getNs());
}
async loadValues() {
const { release } = this.props;
this.values = "";
this.values = await helmReleasesApi.getValues(release.getName(), release.getNs());
this.valuesLoading = true;
this.values = (await getReleaseValues(release.getName(), release.getNs(), !this.userSuppliedOnly)) ?? "";
this.valuesLoading = false;
}
updateValues = async () => {
@@ -112,21 +117,34 @@ export class ReleaseDetails extends Component<Props> {
};
renderValues() {
const { values, saving } = this;
const { values, valuesLoading, saving } = this;
return (
<div className="values">
<DrawerTitle title="Values"/>
<div className="flex column gaps">
<AceEditor
mode="yaml"
value={values}
onChange={values => this.values = values}
<Checkbox
label="User-supplied values only"
value={this.userSuppliedOnly}
onChange={values => {
this.userSuppliedOnly = values;
this.loadValues();
}}
disabled={valuesLoading}
/>
{valuesLoading
? <Spinner />
: <AceEditor
mode="yaml"
value={values}
onChange={values => this.values = values}
/>
}
<Button
primary
label="Save"
waiting={saving}
disabled={valuesLoading}
onClick={this.updateValues}
/>
</div>
@@ -246,7 +264,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}
@@ -5,7 +5,7 @@ import { observable, makeObservable } from "mobx";
import { observer } from "mobx-react";
import { Dialog, DialogProps } from "../dialog";
import { Wizard, WizardStep } from "../wizard";
import { HelmRelease, helmReleasesApi, IReleaseRevision } from "../../api/endpoints/helm-releases.api";
import { getReleaseHistory, HelmRelease, IReleaseRevision } from "../../api/endpoints/helm-releases.api";
import { releaseStore } from "./release.store";
import { Select, SelectOption } from "../select";
import { Notifications } from "../notifications";
@@ -44,7 +44,7 @@ export class ReleaseRollbackDialog extends React.Component<Props> {
onOpen = async () => {
this.isLoading = true;
const currentRevision = this.release.getRevision();
let releases = await helmReleasesApi.getHistory(this.release.getName(), this.release.getNs());
let releases = await getReleaseHistory(this.release.getName(), this.release.getNs());
releases = releases.filter(item => item.revision !== currentRevision); // remove current
releases = orderBy(releases, "revision", "desc"); // sort
@@ -1,6 +1,8 @@
import isEqual from "lodash/isEqual";
import { action, IReactionDisposer, makeObservable, observable, reaction, toJS, when, } from "mobx";
import { HelmRelease, helmReleasesApi, IReleaseCreatePayload, IReleaseUpdatePayload } from "../../api/endpoints/helm-releases.api";
import { action, observable, reaction, when } from "mobx";
import { createRelease, deleteRelease, HelmRelease, IReleaseCreatePayload, IReleaseUpdatePayload, listReleases, rollbackRelease, updateRelease } from "../../api/endpoints/helm-releases.api";
import { ItemStore } from "../../item.store";
import { Secret } from "../../api/endpoints";
import { secretsStore } from "../+config-secrets/secrets.store";
@@ -8,65 +10,65 @@ import { namespaceStore } from "../+namespaces/namespace.store";
import { Notifications } from "../notifications";
export class ReleaseStore extends ItemStore<HelmRelease> {
@observable releaseSecrets: Secret[] = [];
@observable secretWatcher: IReactionDisposer;
releaseSecrets = observable.map<string, Secret>();
constructor() {
super();
makeObservable(this);
when(() => secretsStore.isLoaded, () => {
this.releaseSecrets = this.getReleaseSecrets();
this.releaseSecrets.replace(this.getReleaseSecrets());
});
}
watch() {
this.secretWatcher = reaction(() => toJS(secretsStore.items), () => {
watchAssociatedSecrets(): (() => void) {
return reaction(() => toJS(secretsStore.items), () => {
if (this.isLoading) return;
const secrets = this.getReleaseSecrets();
const amountChanged = secrets.length !== this.releaseSecrets.length;
const labelsChanged = this.releaseSecrets.some(item => {
const secret = secrets.find(secret => secret.getId() == item.getId());
if (!secret) return;
return !isEqual(item.getLabels(), secret.getLabels());
});
const newSecrets = this.getReleaseSecrets();
const amountChanged = newSecrets.length !== this.releaseSecrets.size;
const labelsChanged = newSecrets.some(([id, secret]) => (
!isEqual(secret.getLabels(), this.releaseSecrets.get(id)?.getLabels())
));
if (amountChanged || labelsChanged) {
this.loadFromContextNamespaces();
}
this.releaseSecrets = [...secrets];
this.releaseSecrets.replace(newSecrets);
});
}
unwatch() {
this.secretWatcher();
watchSelecteNamespaces(): (() => void) {
return reaction(() => namespaceStore.context.contextNamespaces, namespaces => {
this.loadAll(namespaces);
});
}
getReleaseSecrets() {
return secretsStore.getByLabel({ owner: "helm" });
private getReleaseSecrets() {
return secretsStore
.getByLabel({ owner: "helm" })
.map(s => [s.getId(), s] as const);
}
getReleaseSecret(release: HelmRelease) {
const labels = {
return secretsStore.getByLabel({
owner: "helm",
name: release.getName()
};
return secretsStore.getByLabel(labels)
.filter(secret => secret.getNs() == release.getNs())[0];
})
.find(secret => secret.getNs() == release.getNs());
}
@action
async loadAll(namespaces: string[]) {
this.isLoading = true;
this.isLoaded = false;
try {
const items = await this.loadItems(namespaces);
this.items.replace(this.sortItems(items));
this.isLoaded = true;
this.failedLoading = false;
} catch (error) {
this.failedLoading = true;
console.error("Loading Helm Chart releases has failed", error);
if (error.error) {
@@ -78,24 +80,25 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
}
async loadFromContextNamespaces(): Promise<void> {
return this.loadAll(namespaceStore.contextNamespaces);
return this.loadAll(namespaceStore.context.contextNamespaces);
}
async loadItems(namespaces: string[]) {
const isLoadingAll = namespaceStore.allowedNamespaces.every(ns => namespaces.includes(ns));
const noAccessibleNamespaces = namespaceStore.context.cluster.accessibleNamespaces.length === 0;
const isLoadingAll = namespaceStore.context.allNamespaces?.length > 1
&& namespaceStore.context.cluster.accessibleNamespaces.length === 0
&& namespaceStore.context.allNamespaces.every(ns => namespaces.includes(ns));
if (isLoadingAll && noAccessibleNamespaces) {
return helmReleasesApi.list();
} else {
return Promise
.all(namespaces.map(namespace => helmReleasesApi.list(namespace)))
.then(items => items.flat());
if (isLoadingAll) {
return listReleases();
}
return Promise // load resources per namespace
.all(namespaces.map(namespace => listReleases(namespace)))
.then(items => items.flat());
}
async create(payload: IReleaseCreatePayload) {
const response = await helmReleasesApi.create(payload);
const response = await createRelease(payload);
if (this.isLoaded) this.loadFromContextNamespaces();
@@ -103,7 +106,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
}
async update(name: string, namespace: string, payload: IReleaseUpdatePayload) {
const response = await helmReleasesApi.update(name, namespace, payload);
const response = await updateRelease(name, namespace, payload);
if (this.isLoaded) this.loadFromContextNamespaces();
@@ -111,7 +114,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
}
async rollback(name: string, namespace: string, revision: number) {
const response = await helmReleasesApi.rollback(name, namespace, revision);
const response = await rollbackRelease(name, namespace, revision);
if (this.isLoaded) this.loadFromContextNamespaces();
@@ -119,7 +122,7 @@ export class ReleaseStore extends ItemStore<HelmRelease> {
}
async remove(release: HelmRelease) {
return super.removeItem(release, () => helmReleasesApi.delete(release.getName(), release.getNs()));
return super.removeItem(release, () => deleteRelease(release.getName(), release.getNs()));
}
async removeSelectedItems() {
@@ -2,7 +2,7 @@ import "./releases.scss";
import React, { Component } from "react";
import kebabCase from "lodash/kebabCase";
import { observer } from "mobx-react";
import { disposeOnUnmount, observer } from "mobx-react";
import { RouteComponentProps } from "react-router";
import { releaseStore } from "./release.store";
import { IReleaseRouteParams, releaseURL } from "./release.route";
@@ -30,14 +30,11 @@ interface Props extends RouteComponentProps<IReleaseRouteParams> {
@observer
export class HelmReleases extends Component<Props> {
componentDidMount() {
// Watch for secrets associated with releases and react to their changes
releaseStore.watch();
}
componentWillUnmount() {
releaseStore.unwatch();
disposeOnUnmount(this, [
releaseStore.watchAssociatedSecrets(),
releaseStore.watchSelecteNamespaces(),
]);
}
get selectedRelease() {
@@ -49,21 +46,16 @@ export class HelmReleases extends Component<Props> {
}
showDetails = (item: HelmRelease) => {
if (!item) {
navigation.merge(releaseURL());
}
else {
navigation.merge(releaseURL({
params: {
name: item.getName(),
namespace: item.getNs()
}
}));
}
navigation.merge(releaseURL({
params: {
name: item.getName(),
namespace: item.getNs()
}
}));
};
hideDetails = () => {
this.showDetails(null);
navigation.merge(releaseURL());
};
renderRemoveDialogMessage(selectedItems: HelmRelease[]) {
@@ -114,30 +106,22 @@ export class HelmReleases extends Component<Props> {
{ title: "Status", className: "status", sortBy: columnId.status, id: columnId.status },
{ title: "Updated", className: "updated", sortBy: columnId.updated, id: columnId.updated },
]}
renderTableContents={(release: HelmRelease) => {
const version = release.getVersion();
return [
release.getName(),
release.getNs(),
release.getChart(),
release.getRevision(),
<>
{version}
</>,
release.appVersion,
{ title: release.getStatus(), className: kebabCase(release.getStatus()) },
release.getUpdated(),
];
}}
renderItemMenu={(release: HelmRelease) => {
return (
<HelmReleaseMenu
release={release}
removeConfirmationMessage={this.renderRemoveDialogMessage([release])}
/>
);
}}
renderTableContents={(release: HelmRelease) => [
release.getName(),
release.getNs(),
release.getChart(),
release.getRevision(),
release.getVersion(),
release.appVersion,
{ title: release.getStatus(), className: kebabCase(release.getStatus()) },
release.getUpdated(),
]}
renderItemMenu={(release: HelmRelease) => (
<HelmReleaseMenu
release={release}
removeConfirmationMessage={this.renderRemoveDialogMessage([release])}
/>
)}
customizeRemoveDialog={(selectedItems: HelmRelease[]) => ({
message: this.renderRemoveDialogMessage(selectedItems)
})}
@@ -0,0 +1,9 @@
.CatalogAddButton {
position: absolute;
right: 40px;
bottom: 30px;
.MuiFab-primary {
background-color: var(--blue);
}
}
@@ -0,0 +1,74 @@
import "./catalog-add-button.scss";
import React from "react";
import { SpeedDial, SpeedDialAction } from "@material-ui/lab";
import { Icon } from "../icon";
import { disposeOnUnmount, observer } from "mobx-react";
import { observable, reaction } from "mobx";
import { autobind } from "../../../common/utils";
import { CatalogCategory, CatalogEntityAddMenuContext, CatalogEntityContextMenu } from "../../api/catalog-entity";
import { EventEmitter } from "events";
import { navigate } from "../../navigation";
export type CatalogAddButtonProps = {
category: CatalogCategory
};
@observer
export class CatalogAddButton extends React.Component<CatalogAddButtonProps> {
@observable protected isOpen = false;
protected menuItems = observable.array<CatalogEntityContextMenu>([]);
componentDidMount() {
disposeOnUnmount(this, [
reaction(() => this.props.category, (category) => {
this.menuItems.clear();
if (category && category instanceof EventEmitter) {
const context: CatalogEntityAddMenuContext = {
navigate: (url: string) => navigate(url),
menuItems: this.menuItems
};
category.emit("onCatalogAddMenu", context);
}
}, { fireImmediately: true })
]);
}
@autobind()
onOpen() {
this.isOpen = true;
}
@autobind()
onClose() {
this.isOpen = false;
}
render() {
if (this.menuItems.length === 0) {
return null;
}
return (
<SpeedDial
className="CatalogAddButton"
ariaLabel="SpeedDial CatalogAddButton"
open={this.isOpen}
onOpen={this.onOpen}
onClose={this.onClose}
icon={<Icon material="add" />}
direction="up"
>
{ this.menuItems.map((menuItem, index) => {
return <SpeedDialAction
key={index}
icon={<Icon material={menuItem.icon} />}
tooltipTitle={menuItem.title}
onClick={() => menuItem.onClick()}
/>;
})}
</SpeedDial>
);
}
}
@@ -66,8 +66,6 @@ export class CatalogEntityStore extends ItemStore<CatalogEntityItem> {
@computed get entities() {
if (!this.activeCategory) return [];
console.log("computing entities", this.activeCategory);
return catalogEntityRegistry.getItemsForCategory(this.activeCategory).map(entity => new CatalogEntityItem(entity));
}
@@ -21,6 +21,7 @@
> .sidebar {
width: 100%;
padding: 0;
.sidebarTabs {
margin-top: 5px;
+11 -10
View File
@@ -2,7 +2,7 @@ import "./catalog.scss";
import React from "react";
import { disposeOnUnmount, observer } from "mobx-react";
import { ItemListLayout } from "../item-object-list";
import { observable, reaction, makeObservable } from "mobx";
import { action, observable, reaction, makeObservable } from "mobx";
import { CatalogEntityItem, CatalogEntityStore } from "./catalog-entity.store";
import { navigate } from "../../navigation";
import { kebabCase } from "lodash";
@@ -11,13 +11,13 @@ 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 { addClusterURL } from "../+add-cluster";
import { HotbarStore } from "../../../common/hotbar-store";
import { autobind } from "../../utils";
import { Notifications } from "../notifications";
import { ConfirmDialog } from "../confirm-dialog";
import { Tab, Tabs } from "../tabs";
import { catalogCategoryRegistry } from "../../../common/catalog-category-registry";
import { CatalogAddButton } from "./catalog-add-button";
enum sortBy {
name = "name",
@@ -62,7 +62,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;
@@ -72,7 +72,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;
@@ -106,6 +106,7 @@ export class Catalog extends React.Component {
return catalogCategoryRegistry.items;
}
@action
onTabChange = (tabId: string) => {
this.activeTab = tabId;
@@ -131,6 +132,7 @@ export class Catalog extends React.Component {
@autobind()
renderItemMenu(item: CatalogEntityItem) {
const menuItems = this.contextMenu.menuItems.filter((menuItem) => !menuItem.onlyVisibleForSource || menuItem.onlyVisibleForSource === item.entity.metadata.source);
const onOpen = async () => {
await item.onContextMenuOpen(this.contextMenu);
};
@@ -143,7 +145,7 @@ export class Catalog extends React.Component {
<MenuItem key="remove-from-hotbar" onClick={() => this.removeFromHotbar(item) }>
<Icon material="clear" small interactive={true} title="Remove from hotbar"/> Remove from Hotbar
</MenuItem>
{ this.contextMenu.menuItems.map((menuItem, index) => {
{ menuItems.map((menuItem, index) => {
return (
<MenuItem key={index} onClick={() => this.onMenuItemClick(menuItem)}>
<Icon material={menuItem.icon} small interactive={true} title={menuItem.title}/> {menuItem.title}
@@ -154,6 +156,7 @@ export class Catalog extends React.Component {
);
}
render() {
if (!this.catalogEntityStore) {
return null;
@@ -166,6 +169,7 @@ export class Catalog extends React.Component {
provideBackButtonNavigation={false}
contentGaps={false}>
<ItemListLayout
renderHeaderTitle={this.catalogEntityStore.activeCategory?.metadata.name}
isClusterScoped
isSearchable={true}
isSelectable={false}
@@ -191,11 +195,8 @@ export class Catalog extends React.Component {
]}
onDetails={(item: CatalogEntityItem) => this.onDetails(item) }
renderItemMenu={this.renderItemMenu}
addRemoveButtons={{
addTooltip: "Add Kubernetes Cluster",
onAdd: () => navigate(addClusterURL()),
}}
/>
<CatalogAddButton category={this.catalogEntityStore.activeCategory} />
</PageLayout>
);
}
@@ -1,12 +0,0 @@
import type { IClusterViewRouteParams } from "../cluster-manager/cluster-view.route";
import type { RouteProps } from "react-router";
import { buildURL } from "../../../common/utils/buildUrl";
export interface IClusterSettingsRouteParams extends IClusterViewRouteParams {
}
export const clusterSettingsRoute: RouteProps = {
path: `/cluster/:clusterId/settings`,
};
export const clusterSettingsURL = buildURL<IClusterSettingsRouteParams>(clusterSettingsRoute.path);
@@ -1,51 +0,0 @@
.ClusterSettings {
$spacing: $padding * 3;
> .content-wrapper {
--flex-gap: #{$spacing};
}
// TODO: move sub-component styles to separate files
.admin-note {
font-size: small;
opacity: 0.5;
margin-left: $margin;
}
.button-area {
margin-top: $margin * 2;
}
.file-loader {
margin-top: $margin * 2;
}
.status-table {
margin: $spacing 0;
.Table {
border: 1px solid var(--drawerSubtitleBackground);
border-radius: $radius;
.TableRow {
&:not(:last-of-type) {
border-bottom: 1px solid var(--drawerSubtitleBackground);
}
.value {
flex-grow: 2;
word-break: break-word;
color: var(--textColorSecondary);
}
.link {
@include pseudo-link;
}
}
}
}
.Input, .Select {
margin-top: $padding;
}
}
@@ -1,69 +0,0 @@
import "./cluster-settings.scss";
import React from "react";
import { reaction } from "mobx";
import { RouteComponentProps } from "react-router";
import { observer, disposeOnUnmount } from "mobx-react";
import { Status } from "./status";
import { General } from "./general";
import { Cluster } from "../../../main/cluster";
import { IClusterSettingsRouteParams } from "./cluster-settings.route";
import { clusterStore } from "../../../common/cluster-store";
import { PageLayout } from "../layout/page-layout";
import { requestMain } from "../../../common/ipc";
import { clusterActivateHandler, clusterRefreshHandler } from "../../../common/cluster-ipc";
import { navigation } from "../../navigation";
interface Props extends RouteComponentProps<IClusterSettingsRouteParams> {
}
@observer
export class ClusterSettings extends React.Component<Props> {
get clusterId() {
return this.props.match.params.clusterId;
}
get cluster(): Cluster {
return clusterStore.getById(this.clusterId);
}
componentDidMount() {
const { hash } = navigation.location;
document.getElementById(hash.slice(1))?.scrollIntoView();
disposeOnUnmount(this, [
reaction(() => this.cluster, this.refreshCluster, {
fireImmediately: true,
}),
reaction(() => this.clusterId, clusterId => clusterStore.setActive(clusterId), {
fireImmediately: true,
})
]);
}
refreshCluster = async () => {
if (this.cluster) {
await requestMain(clusterActivateHandler, this.cluster.id);
await requestMain(clusterRefreshHandler, this.cluster.id);
}
};
render() {
const cluster = this.cluster;
if (!cluster) return null;
const header = (
<>
<h2>{cluster.preferences.clusterName}</h2>
</>
);
return (
<PageLayout className="ClusterSettings" header={header} showOnTop={true}>
<Status cluster={cluster}></Status>
<General cluster={cluster}></General>
</PageLayout>
);
}
}
@@ -1,28 +0,0 @@
import React from "react";
import { Cluster } from "../../../main/cluster";
import { ClusterNameSetting } from "./components/cluster-name-setting";
import { ClusterProxySetting } from "./components/cluster-proxy-setting";
import { ClusterPrometheusSetting } from "./components/cluster-prometheus-setting";
import { ClusterHomeDirSetting } from "./components/cluster-home-dir-setting";
import { ClusterAccessibleNamespaces } from "./components/cluster-accessible-namespaces";
import { ClusterMetricsSetting } from "./components/cluster-metrics-setting";
import { ShowMetricsSetting } from "./components/show-metrics";
interface Props {
cluster: Cluster;
}
export class General extends React.Component<Props> {
render() {
return <div>
<h2>General</h2>
<ClusterNameSetting cluster={this.props.cluster} />
<ClusterProxySetting cluster={this.props.cluster} />
<ClusterPrometheusSetting cluster={this.props.cluster} />
<ClusterHomeDirSetting cluster={this.props.cluster} />
<ClusterAccessibleNamespaces cluster={this.props.cluster} />
<ClusterMetricsSetting cluster={this.props.cluster}/>
<ShowMetricsSetting cluster={this.props.cluster}/>
</div>;
}
}
@@ -1,61 +0,0 @@
import React from "react";
import { Cluster } from "../../../main/cluster";
import { SubTitle } from "../layout/sub-title";
import { Table, TableCell, TableRow } from "../table";
import { autobind } from "../../utils";
import { shell } from "electron";
interface Props {
cluster: Cluster;
}
export class Status extends React.Component<Props> {
@autobind()
openKubeconfig() {
const { cluster } = this.props;
shell.showItemInFolder(cluster.kubeConfigPath);
}
renderStatusRows() {
const { cluster } = this.props;
const rows = [
["Online Status", cluster.online ? "online" : `offline (${cluster.failureReason || "unknown reason"})`],
["Distribution", cluster.metadata.distribution ? String(cluster.metadata.distribution) : "N/A"],
["Kernel Version", cluster.metadata.version ? String(cluster.metadata.version) : "N/A"],
["API Address", cluster.apiUrl || "N/A"],
["Nodes Count", cluster.metadata.nodes ? String(cluster.metadata.nodes) : "N/A"]
];
return (
<Table scrollable={false}>
{rows.map(([name, value]) => {
return (
<TableRow key={name}>
<TableCell>{name}</TableCell>
<TableCell className="value">{value}</TableCell>
</TableRow>
);
})}
<TableRow>
<TableCell>Kubeconfig</TableCell>
<TableCell className="link value" onClick={this.openKubeconfig}>{cluster.kubeConfigPath}</TableCell>
</TableRow>
</Table>
);
}
render() {
return <div>
<h2>Status</h2>
<SubTitle title="Cluster Status"/>
<p>
Cluster status information including: detected distribution, kernel version, and online status.
</p>
<div className="status-table">
{this.renderStatusRows()}
</div>
</div>;
}
}
@@ -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";
@@ -24,6 +24,7 @@ interface IWarning extends ItemObject {
message: string;
selfLink: string;
age: string | number;
timeDiffFromNow: number;
}
enum sortBy {
@@ -37,7 +38,7 @@ export class ClusterIssues extends React.Component<Props> {
private sortCallbacks = {
[sortBy.type]: (warning: IWarning) => warning.kind,
[sortBy.object]: (warning: IWarning) => warning.getName(),
[sortBy.age]: (warning: IWarning) => warning.age || "",
[sortBy.age]: (warning: IWarning) => warning.timeDiffFromNow,
};
constructor(props: Props) {
@@ -50,13 +51,14 @@ export class ClusterIssues extends React.Component<Props> {
// Node bad conditions
nodesStore.items.forEach(node => {
const { kind, selfLink, getId, getName, getAge } = node;
const { kind, selfLink, getId, getName, getAge, getTimeDiffFromNow } = node;
node.getWarningConditions().forEach(({ message }) => {
warnings.push({
age: getAge(),
getId,
getName,
timeDiffFromNow: getTimeDiffFromNow(),
kind,
message,
selfLink,
@@ -68,12 +70,13 @@ export class ClusterIssues extends React.Component<Props> {
const events = eventStore.getWarnings();
events.forEach(error => {
const { message, involvedObject, getAge } = error;
const { message, involvedObject, getAge, getTimeDiffFromNow } = error;
const { uid, name, kind } = involvedObject;
warnings.push({
getId: () => uid,
getName: () => name,
timeDiffFromNow: getTimeDiffFromNow(),
age: getAge(),
message,
kind,
@@ -147,7 +150,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";
@@ -13,7 +13,7 @@ import { ClusterIssues } from "./cluster-issues";
import { ClusterMetrics } from "./cluster-metrics";
import { clusterOverviewStore } from "./cluster-overview.store";
import { ClusterPieCharts } from "./cluster-pie-charts";
import { ResourceType } from "../+cluster-settings/components/cluster-metrics-setting";
import { ResourceType } from "../cluster-settings/components/cluster-metrics-setting";
@observer
export class ClusterOverview extends React.Component {
@@ -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 = {
@@ -0,0 +1,12 @@
import type { RouteProps } from "react-router";
import { buildURL } from "../../../common/utils/buildUrl";
export interface EntitySettingsRouteParams {
entityId: string;
}
export const entitySettingsRoute: RouteProps = {
path: `/entity/:entityId/settings`,
};
export const entitySettingsURL = buildURL<EntitySettingsRouteParams>(entitySettingsRoute.path);
@@ -0,0 +1,23 @@
.EntitySettings {
$spacing: $padding * 3;
// TODO: move sub-component styles to separate files
.admin-note {
font-size: small;
opacity: 0.5;
margin-left: $margin;
}
.button-area {
margin-top: $margin * 2;
}
.file-loader {
margin-top: $margin * 2;
}
.Input, .Select {
margin-top: $padding;
}
}
@@ -0,0 +1,99 @@
import "./entity-settings.scss";
import React from "react";
import { observable } from "mobx";
import { RouteComponentProps } from "react-router";
import { observer } from "mobx-react";
import { PageLayout } from "../layout/page-layout";
import { navigation } from "../../navigation";
import { Tabs, Tab } from "../tabs";
import { CatalogEntity } from "../../api/catalog-entity";
import { catalogEntityRegistry } from "../../api/catalog-entity-registry";
import { entitySettingRegistry } from "../../../extensions/registries";
import { EntitySettingsRouteParams } from "./entity-settings.route";
interface Props extends RouteComponentProps<EntitySettingsRouteParams> {
}
@observer
export class EntitySettings extends React.Component<Props> {
@observable activeTab: string;
get entityId() {
return this.props.match.params.entityId;
}
get entity(): CatalogEntity {
return catalogEntityRegistry.getById(this.entityId);
}
get menuItems() {
if (!this.entity) return [];
return entitySettingRegistry.getItemsForKind(this.entity.kind, this.entity.apiVersion, this.entity.metadata.source);
}
async componentDidMount() {
const { hash } = navigation.location;
this.ensureActiveTab();
document.getElementById(hash.slice(1))?.scrollIntoView();
}
onTabChange = (tabId: string) => {
this.activeTab = tabId;
};
renderNavigation() {
return (
<>
<h2>{this.entity.metadata.name}</h2>
<Tabs className="flex column" scrollable={false} onChange={this.onTabChange} value={this.activeTab}>
<div className="header">Settings</div>
{ this.menuItems.map((setting) => (
<Tab
key={setting.id}
value={setting.id}
label={setting.title}
data-testid={`${setting.id}-tab`}
/>
))}
</Tabs>
</>
);
}
ensureActiveTab() {
if (!this.activeTab) {
this.activeTab = this.menuItems[0]?.id;
}
}
render() {
if (!this.entity) {
console.error("entity not found", this.entityId);
return null;
}
this.ensureActiveTab();
const activeSetting = this.menuItems.find((setting) => setting.id === this.activeTab);
return (
<PageLayout
className="CatalogEntitySettings"
navigation={this.renderNavigation()}
showOnTop={true}
contentGaps={false}
>
<section>
<h2 data-testid={`${activeSetting.id}-header`}>{activeSetting.title}</h2>
<section>
<activeSetting.components.View entity={this.entity} />
</section>
</section>
</PageLayout>
);
}
}
@@ -0,0 +1,4 @@
import "../cluster-settings";
export * from "./entity-settings.route";
export * from "./entity-settings";
@@ -11,6 +11,7 @@ import { KubeObjectMeta } from "../kube-object/kube-object-meta";
import { Table, TableCell, TableHead, TableRow } from "../table";
import { lookupApiLink } from "../../api/kube-api";
import { kubeObjectDetailRegistry } from "../../api/kube-object-detail-registry";
import { LocaleDate } from "../locale-date";
interface Props extends KubeObjectDetailsProps<KubeEvent> {
}
@@ -38,10 +39,10 @@ export class EventDetails extends React.Component<Props> {
{event.getSource()}
</DrawerItem>
<DrawerItem name="First seen">
{event.getFirstSeenTime()} ago {event.firstTimestamp}
{event.getFirstSeenTime()} ago (<LocaleDate date={event.firstTimestamp} />)
</DrawerItem>
<DrawerItem name="Last seen">
{event.getLastSeenTime()} ago {event.lastTimestamp}
{event.getLastSeenTime()} ago (<LocaleDate date={event.lastTimestamp} />)
</DrawerItem>
<DrawerItem name="Count">
{count}
@@ -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,12 +0,0 @@
import { observable } from "mobx";
import { Singleton } from "../../utils";
interface ExtensionState {
displayName: string;
// Possible states the extension can be
state: "installing" | "uninstalling";
}
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,6 +55,8 @@ export class Extensions extends React.Component {
}
};
static installStates = observable.map<string, ExtensionState>();
constructor(props: object) {
super(props);
makeObservable(this);
@@ -69,7 +76,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)
@@ -81,7 +88,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)
@@ -96,7 +103,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 }) => {
@@ -109,7 +116,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.
@@ -122,7 +129,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)
@@ -130,7 +137,7 @@ export class Extensions extends React.Component {
}
get extensionsPath() {
return extensionDiscovery.localFolderPath;
return ExtensionDiscovery.getInstance().localFolderPath;
}
getExtensionPackageTemp(fileName = "") {
@@ -347,11 +354,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
});
@@ -386,8 +393,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
@@ -411,20 +418,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);
}
}
}
@@ -450,7 +457,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">
@@ -483,16 +490,15 @@ 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() {
const topHeader = <h2>Manage Lens Extensions</h2>;
const { installPath } = this;
return (
<DropFileInput onDropFiles={this.installOnDrop}>
<PageLayout showOnTop className="Extensions" header={topHeader} contentGaps={false}>
<PageLayout showOnTop className="Extensions" contentGaps={false}>
<h2>Lens Extensions</h2>
<div>
Add new features and functionality via Lens Extensions.
@@ -542,7 +548,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>
@@ -14,8 +14,8 @@ import { IngressCharts } from "./ingress-charts";
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 { ResourceType } from "../cluster-settings/components/cluster-metrics-setting";
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;
@@ -17,8 +17,8 @@ import { PodDetailsList } from "../+workloads-pods/pod-details-list";
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 { ResourceType } from "../cluster-settings/components/cluster-metrics-setting";
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
@@ -84,7 +84,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, makeObservable } 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";
@@ -39,9 +39,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));
@@ -54,7 +54,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)}</>);
@@ -63,7 +63,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}
Loaded 100 of 263 files, more files were not shown because too many files have changed in this diff. Show more