mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
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:
commit
a0193a2cda
263 files changed
+3401
-31747
No files matched your search
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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>();
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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
@@ -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");
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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,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`);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { getAppVersion } from "../../common/utils";
|
||||
import { extensionsStore } from "../extensions-store";
|
||||
import { ExtensionsStore } from "../extensions-store";
|
||||
|
||||
export const version = getAppVersion();
|
||||
export { isSnap, isWindows, isMac, isLinux, appName, slackUrl, issuesTrackerUrl } from "../../common/vars";
|
||||
|
||||
export function getEnabledExtensions(): string[] {
|
||||
return extensionsStore.enabledExtensions;
|
||||
return ExtensionsStore.getInstance().enabledExtensions;
|
||||
}
|
||||
@@ -1,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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -52,5 +52,3 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const extensionsStore = new ExtensionsStore();
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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 }));
|
||||
}
|
||||
};
|
||||
@@ -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();
|
||||
|
||||
@@ -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>();
|
||||
@@ -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();
|
||||
|
||||
@@ -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: [] }) };
|
||||
}
|
||||
}
|
||||
@@ -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>();
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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; });
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
?? [];
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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't setup <code>{filePath}</code> as kubeconfig: {String(err)}</div>
|
||||
);
|
||||
if (!UserStore.getInstance().isDefaultKubeConfigPath) {
|
||||
Notifications.error(
|
||||
<div>Can'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;
|
||||
|
||||
@@ -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
Reference in new issue
Block a user