diff --git a/.idea/lens.iml b/.idea/lens.iml index fe491887dd..88175e2aaa 100644 --- a/.idea/lens.iml +++ b/.idea/lens.iml @@ -12,6 +12,9 @@ + + + diff --git a/__mocks__/electron-updater.ts b/__mocks__/electron-updater.ts new file mode 100644 index 0000000000..00c83a605c --- /dev/null +++ b/__mocks__/electron-updater.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +// This mock exists because library causes criminal side-effect on import +export const autoUpdater = {}; diff --git a/__mocks__/node-pty.ts b/__mocks__/node-pty.ts new file mode 100644 index 0000000000..0750620e6f --- /dev/null +++ b/__mocks__/node-pty.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +// This mock exists because library causes criminal side-effect on import +export default {}; diff --git a/package.json b/package.json index b3fa600b11..7c81f613a4 100644 --- a/package.json +++ b/package.json @@ -196,8 +196,8 @@ "@hapi/call": "^8.0.1", "@hapi/subtext": "^7.0.3", "@kubernetes/client-node": "^0.16.1", - "@ogre-tools/injectable": "2.0.0", - "@ogre-tools/injectable-react": "2.0.0", + "@ogre-tools/injectable": "3.0.0", + "@ogre-tools/injectable-react": "3.0.0", "@sentry/electron": "^2.5.4", "@sentry/integrations": "^6.15.0", "abort-controller": "^3.0.0", diff --git a/src/common/__tests__/base-store.test.ts b/src/common/__tests__/base-store.test.ts index 0a4f81e8f3..f45769cd85 100644 --- a/src/common/__tests__/base-store.test.ts +++ b/src/common/__tests__/base-store.test.ts @@ -20,29 +20,18 @@ */ import mockFs from "mock-fs"; -import { AppPaths } from "../app-paths"; import { BaseStore } from "../base-store"; import { action, comparer, makeObservable, observable, toJS } from "mobx"; import { readFileSync } from "fs"; +import { getDisForUnitTesting } from "../../test-utils/get-dis-for-unit-testing"; -AppPaths.init(); +import directoryForUserDataInjectable + from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; jest.mock("electron", () => ({ - app: { - getVersion: () => "99.99.99", - getName: () => "lens", - setName: jest.fn(), - setPath: jest.fn(), - getPath: () => "tmp", - getLocale: () => "en", - setLoginItemSettings: jest.fn(), - }, ipcMain: { - handle: jest.fn(), on: jest.fn(), - removeAllListeners: jest.fn(), off: jest.fn(), - send: jest.fn(), }, })); @@ -105,10 +94,17 @@ describe("BaseStore", () => { let store: TestStore; beforeEach(async () => { + const dis = getDisForUnitTesting({ doGeneralOverrides: true }); + + dis.mainDi.override(directoryForUserDataInjectable, () => "some-user-data-directory"); + + await dis.runSetups(); + store = undefined; TestStore.resetInstance(); + const mockOpts = { - "tmp": { + "some-user-data-directory": { "test-store.json": JSON.stringify({}), }, }; @@ -130,7 +126,7 @@ describe("BaseStore", () => { a: "foo", b: "bar", c: "hello", }); - const data = JSON.parse(readFileSync("tmp/test-store.json").toString()); + const data = JSON.parse(readFileSync("some-user-data-directory/test-store.json").toString()); expect(data).toEqual({ a: "foo", b: "bar", c: "hello" }); }); @@ -153,7 +149,7 @@ describe("BaseStore", () => { expect(fileSpy).toHaveBeenCalledTimes(2); - const data = JSON.parse(readFileSync("tmp/test-store.json").toString()); + const data = JSON.parse(readFileSync("some-user-data-directory/test-store.json").toString()); expect(data).toEqual({ a: "a", b: "b", c: "" }); }); diff --git a/src/common/__tests__/cluster-store.test.ts b/src/common/__tests__/cluster-store.test.ts index 1452dfadd5..947cfc2d17 100644 --- a/src/common/__tests__/cluster-store.test.ts +++ b/src/common/__tests__/cluster-store.test.ts @@ -24,17 +24,29 @@ import mockFs from "mock-fs"; import yaml from "js-yaml"; import path from "path"; import fse from "fs-extra"; -import { Cluster } from "../../main/cluster"; -import { ClusterStore } from "../cluster-store"; +import type { Cluster } from "../cluster/cluster"; +import { ClusterStore } from "../cluster-store/cluster-store"; import { Console } from "console"; import { stdout, stderr } from "process"; -import type { ClusterId } from "../cluster-types"; -import { getCustomKubeConfigPath } from "../utils"; -import { AppPaths } from "../app-paths"; +import getCustomKubeConfigDirectoryInjectable from "../app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable"; +import clusterStoreInjectable from "../cluster-store/cluster-store.injectable"; +import type { ClusterModel } from "../cluster-types"; +import type { + DependencyInjectionContainer, +} from "@ogre-tools/injectable"; + + +import { getDisForUnitTesting } from "../../test-utils/get-dis-for-unit-testing"; +import { createClusterInjectionToken } from "../cluster/create-cluster-injection-token"; + +import directoryForUserDataInjectable + from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; console = new Console(stdout, stderr); -const testDataIcon = fs.readFileSync("test-data/cluster-store-migration-icon.png"); +const testDataIcon = fs.readFileSync( + "test-data/cluster-store-migration-icon.png", +); const kubeconfig = ` apiVersion: v1 clusters: @@ -59,25 +71,17 @@ users: token: kubeconfig-user-q4lm4:xxxyyyy `; -function embed(clusterId: ClusterId, contents: any): string { - const absPath = getCustomKubeConfigPath(clusterId); +const embed = (directoryName: string, contents: any): string => { + fse.ensureDirSync(path.dirname(directoryName)); + fse.writeFileSync(directoryName, contents, { + encoding: "utf-8", + mode: 0o600, + }); - fse.ensureDirSync(path.dirname(absPath)); - fse.writeFileSync(absPath, contents, { encoding: "utf-8", mode: 0o600 }); - - return absPath; -} + return directoryName; +}; jest.mock("electron", () => ({ - app: { - getVersion: () => "99.99.99", - getName: () => "lens", - setName: jest.fn(), - setPath: jest.fn(), - getPath: () => "tmp", - getLocale: () => "en", - setLoginItemSettings: jest.fn(), - }, ipcMain: { handle: jest.fn(), on: jest.fn(), @@ -87,157 +91,194 @@ jest.mock("electron", () => ({ }, })); -AppPaths.init(); +describe("cluster-store", () => { + let mainDi: DependencyInjectionContainer; + let clusterStore: ClusterStore; + let createCluster: (model: ClusterModel) => Cluster; -describe("empty config", () => { beforeEach(async () => { - ClusterStore.getInstance(false)?.unregisterIpcListener(); - ClusterStore.resetInstance(); - const mockOpts = { - "tmp": { - "lens-cluster-store.json": JSON.stringify({}), - }, - }; + const dis = getDisForUnitTesting({ doGeneralOverrides: true }); - mockFs(mockOpts); + mockFs(); - ClusterStore.createInstance(); + mainDi = dis.mainDi; + + mainDi.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); + + await dis.runSetups(); + + createCluster = mainDi.inject(createClusterInjectionToken); }); - afterEach(() => { - mockFs.restore(); - }); + describe("empty config", () => { + let getCustomKubeConfigDirectory: (directoryName: string) => string; - describe("with foo cluster added", () => { - beforeEach(() => { - ClusterStore.getInstance().addCluster( - new Cluster({ + beforeEach(async () => { + getCustomKubeConfigDirectory = mainDi.inject( + getCustomKubeConfigDirectoryInjectable, + ); + + // TODO: Remove these by removing Singleton base-class from BaseStore + ClusterStore.getInstance(false)?.unregisterIpcListener(); + ClusterStore.resetInstance(); + + const mockOpts = { + "some-directory-for-user-data": { + "lens-cluster-store.json": JSON.stringify({}), + }, + }; + + mockFs(mockOpts); + + clusterStore = mainDi.inject(clusterStoreInjectable); + }); + + afterEach(() => { + mockFs.restore(); + }); + + describe("with foo cluster added", () => { + beforeEach(() => { + const cluster = createCluster({ id: "foo", contextName: "foo", preferences: { - terminalCWD: "/tmp", + terminalCWD: "/some-directory-for-user-data", icon: "data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5", clusterName: "minikube", }, - kubeConfigPath: embed("foo", kubeconfig), - }), - ); - }); + kubeConfigPath: embed( + getCustomKubeConfigDirectory("foo"), + kubeconfig, + ), + }); - it("adds new cluster to store", async () => { - const storedCluster = ClusterStore.getInstance().getById("foo"); - - expect(storedCluster.id).toBe("foo"); - expect(storedCluster.preferences.terminalCWD).toBe("/tmp"); - expect(storedCluster.preferences.icon).toBe("data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5"); - }); - }); - - describe("with prod and dev clusters added", () => { - beforeEach(() => { - const store = ClusterStore.getInstance(); - - store.addCluster({ - id: "prod", - contextName: "foo", - preferences: { - clusterName: "prod", - }, - kubeConfigPath: embed("prod", kubeconfig), + clusterStore.addCluster(cluster); }); - store.addCluster({ - id: "dev", - contextName: "foo2", - preferences: { - clusterName: "dev", - }, - kubeConfigPath: embed("dev", kubeconfig), + + it("adds new cluster to store", async () => { + const storedCluster = clusterStore.getById("foo"); + + expect(storedCluster.id).toBe("foo"); + expect(storedCluster.preferences.terminalCWD).toBe("/some-directory-for-user-data"); + expect(storedCluster.preferences.icon).toBe( + "data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5", + ); }); }); - it("check if store can contain multiple clusters", () => { - expect(ClusterStore.getInstance().hasClusters()).toBeTruthy(); - expect(ClusterStore.getInstance().clusters.size).toBe(2); - }); + describe("with prod and dev clusters added", () => { + beforeEach(() => { + const store = clusterStore; - it("check if cluster's kubeconfig file saved", () => { - const file = embed("boo", "kubeconfig"); - - expect(fs.readFileSync(file, "utf8")).toBe("kubeconfig"); - }); - }); -}); - -describe("config with existing clusters", () => { - beforeEach(() => { - ClusterStore.resetInstance(); - const mockOpts = { - "temp-kube-config": kubeconfig, - "tmp": { - "lens-cluster-store.json": JSON.stringify({ - __internal__: { - migrations: { - version: "99.99.99", - }, + store.addCluster({ + id: "prod", + contextName: "foo", + preferences: { + clusterName: "prod", }, - clusters: [ - { - id: "cluster1", - kubeConfigPath: "./temp-kube-config", - contextName: "foo", - preferences: { terminalCWD: "/foo" }, - workspace: "default", + kubeConfigPath: embed( + getCustomKubeConfigDirectory("prod"), + kubeconfig, + ), + }); + store.addCluster({ + id: "dev", + contextName: "foo2", + preferences: { + clusterName: "dev", + }, + kubeConfigPath: embed( + getCustomKubeConfigDirectory("dev"), + kubeconfig, + ), + }); + }); + + it("check if store can contain multiple clusters", () => { + expect(clusterStore.hasClusters()).toBeTruthy(); + expect(clusterStore.clusters.size).toBe(2); + }); + + it("check if cluster's kubeconfig file saved", () => { + const file = embed(getCustomKubeConfigDirectory("boo"), "kubeconfig"); + + expect(fs.readFileSync(file, "utf8")).toBe("kubeconfig"); + }); + }); + }); + + describe("config with existing clusters", () => { + beforeEach(() => { + ClusterStore.resetInstance(); + + const mockOpts = { + "temp-kube-config": kubeconfig, + "some-directory-for-user-data": { + "lens-cluster-store.json": JSON.stringify({ + __internal__: { + migrations: { + version: "99.99.99", + }, }, - { - id: "cluster2", - kubeConfigPath: "./temp-kube-config", - contextName: "foo2", - preferences: { terminalCWD: "/foo2" }, - }, - { - id: "cluster3", - kubeConfigPath: "./temp-kube-config", - contextName: "foo", - preferences: { terminalCWD: "/foo" }, - workspace: "foo", - ownerRef: "foo", - }, - ], - }), - }, - }; + clusters: [ + { + id: "cluster1", + kubeConfigPath: "./temp-kube-config", + contextName: "foo", + preferences: { terminalCWD: "/foo" }, + workspace: "default", + }, + { + id: "cluster2", + kubeConfigPath: "./temp-kube-config", + contextName: "foo2", + preferences: { terminalCWD: "/foo2" }, + }, + { + id: "cluster3", + kubeConfigPath: "./temp-kube-config", + contextName: "foo", + preferences: { terminalCWD: "/foo" }, + workspace: "foo", + ownerRef: "foo", + }, + ], + }), + }, + }; - mockFs(mockOpts); + mockFs(mockOpts); - return ClusterStore.createInstance(); + clusterStore = mainDi.inject(clusterStoreInjectable); + }); + + afterEach(() => { + mockFs.restore(); + }); + + it("allows to retrieve a cluster", () => { + const storedCluster = clusterStore.getById("cluster1"); + + expect(storedCluster.id).toBe("cluster1"); + expect(storedCluster.preferences.terminalCWD).toBe("/foo"); + }); + + it("allows getting all of the clusters", async () => { + const storedClusters = clusterStore.clustersList; + + expect(storedClusters.length).toBe(3); + expect(storedClusters[0].id).toBe("cluster1"); + expect(storedClusters[0].preferences.terminalCWD).toBe("/foo"); + expect(storedClusters[1].id).toBe("cluster2"); + expect(storedClusters[1].preferences.terminalCWD).toBe("/foo2"); + expect(storedClusters[2].id).toBe("cluster3"); + }); }); - afterEach(() => { - mockFs.restore(); - }); - - it("allows to retrieve a cluster", () => { - const storedCluster = ClusterStore.getInstance().getById("cluster1"); - - expect(storedCluster.id).toBe("cluster1"); - expect(storedCluster.preferences.terminalCWD).toBe("/foo"); - }); - - it("allows getting all of the clusters", async () => { - const storedClusters = ClusterStore.getInstance().clustersList; - - expect(storedClusters.length).toBe(3); - expect(storedClusters[0].id).toBe("cluster1"); - expect(storedClusters[0].preferences.terminalCWD).toBe("/foo"); - expect(storedClusters[1].id).toBe("cluster2"); - expect(storedClusters[1].preferences.terminalCWD).toBe("/foo2"); - expect(storedClusters[2].id).toBe("cluster3"); - }); -}); - -describe("config with invalid cluster kubeconfig", () => { - beforeEach(() => { - const invalidKubeconfig = ` + describe("config with invalid cluster kubeconfig", () => { + beforeEach(() => { + const invalidKubeconfig = ` apiVersion: v1 clusters: - cluster: @@ -257,302 +298,317 @@ users: token: kubeconfig-user-q4lm4:xxxyyyy `; - ClusterStore.resetInstance(); - const mockOpts = { - "invalid-kube-config": invalidKubeconfig, - "valid-kube-config": kubeconfig, - "tmp": { - "lens-cluster-store.json": JSON.stringify({ - __internal__: { - migrations: { - version: "99.99.99", - }, - }, - clusters: [ - { - id: "cluster1", - kubeConfigPath: "./invalid-kube-config", - contextName: "test", - preferences: { terminalCWD: "/foo" }, - workspace: "foo", - }, - { - id: "cluster2", - kubeConfigPath: "./valid-kube-config", - contextName: "foo", - preferences: { terminalCWD: "/foo" }, - workspace: "default", - }, + ClusterStore.resetInstance(); - ], - }), - }, - }; + const mockOpts = { + "invalid-kube-config": invalidKubeconfig, + "valid-kube-config": kubeconfig, + "some-directory-for-user-data": { + "lens-cluster-store.json": JSON.stringify({ + __internal__: { + migrations: { + version: "99.99.99", + }, + }, + clusters: [ + { + id: "cluster1", + kubeConfigPath: "./invalid-kube-config", + contextName: "test", + preferences: { terminalCWD: "/foo" }, + workspace: "foo", + }, + { + id: "cluster2", + kubeConfigPath: "./valid-kube-config", + contextName: "foo", + preferences: { terminalCWD: "/foo" }, + workspace: "default", + }, + ], + }), + }, + }; - mockFs(mockOpts); + mockFs(mockOpts); - return ClusterStore.createInstance(); + clusterStore = mainDi.inject(clusterStoreInjectable); + }); + + afterEach(() => { + mockFs.restore(); + }); + + it("does not enable clusters with invalid kubeconfig", () => { + const storedClusters = clusterStore.clustersList; + + expect(storedClusters.length).toBe(1); + }); }); - afterEach(() => { - mockFs.restore(); + describe("pre 2.0 config with an existing cluster", () => { + beforeEach(() => { + ClusterStore.resetInstance(); + + const mockOpts = { + "some-directory-for-user-data": { + "lens-cluster-store.json": JSON.stringify({ + __internal__: { + migrations: { + version: "1.0.0", + }, + }, + cluster1: minimalValidKubeConfig, + }), + }, + }; + + mockFs(mockOpts); + + clusterStore = mainDi.inject(clusterStoreInjectable); + }); + + afterEach(() => { + mockFs.restore(); + }); + + it("migrates to modern format with kubeconfig in a file", async () => { + const config = clusterStore.clustersList[0].kubeConfigPath; + + expect(fs.readFileSync(config, "utf8")).toContain(`"contexts":[`); + }); }); - it("does not enable clusters with invalid kubeconfig", () => { - const storedClusters = ClusterStore.getInstance().clustersList; + describe("pre 2.6.0 config with a cluster that has arrays in auth config", () => { + beforeEach(() => { + ClusterStore.resetInstance(); + const mockOpts = { + "some-directory-for-user-data": { + "lens-cluster-store.json": JSON.stringify({ + __internal__: { + migrations: { + version: "2.4.1", + }, + }, + cluster1: { + 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"], + }, + }, + }, + }, + ], + }), + }, + }), + }, + }; - expect(storedClusters.length).toBe(1); + mockFs(mockOpts); + + clusterStore = mainDi.inject(clusterStoreInjectable); + }); + + afterEach(() => { + mockFs.restore(); + }); + + it("replaces array format access token and expiry into string", async () => { + const file = clusterStore.clustersList[0].kubeConfigPath; + const config = fs.readFileSync(file, "utf8"); + const kc = yaml.load(config) as Record; + + 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", + ); + }); + }); + + describe("pre 2.6.0 config with a cluster icon", () => { + beforeEach(() => { + ClusterStore.resetInstance(); + const mockOpts = { + "some-directory-for-user-data": { + "lens-cluster-store.json": JSON.stringify({ + __internal__: { + migrations: { + version: "2.4.1", + }, + }, + cluster1: { + kubeConfig: minimalValidKubeConfig, + icon: "icon_path", + preferences: { + terminalCWD: "/some-directory-for-user-data", + }, + }, + }), + icon_path: testDataIcon, + }, + }; + + mockFs(mockOpts); + + clusterStore = mainDi.inject(clusterStoreInjectable); + }); + + afterEach(() => { + mockFs.restore(); + }); + + it("moves the icon into preferences", async () => { + const storedClusterData = clusterStore.clustersList[0]; + + expect(storedClusterData.hasOwnProperty("icon")).toBe(false); + expect(storedClusterData.preferences.hasOwnProperty("icon")).toBe(true); + expect( + storedClusterData.preferences.icon.startsWith("data:;base64,"), + ).toBe(true); + }); + }); + + describe("for a pre 2.7.0-beta.0 config without a workspace", () => { + beforeEach(() => { + ClusterStore.resetInstance(); + const mockOpts = { + "some-directory-for-user-data": { + "lens-cluster-store.json": JSON.stringify({ + __internal__: { + migrations: { + version: "2.6.6", + }, + }, + cluster1: { + kubeConfig: minimalValidKubeConfig, + preferences: { + terminalCWD: "/some-directory-for-user-data", + }, + }, + }), + }, + }; + + mockFs(mockOpts); + + clusterStore = mainDi.inject(clusterStoreInjectable); + }); + + afterEach(() => { + mockFs.restore(); + }); + }); + + describe("pre 3.6.0-beta.1 config with an existing cluster", () => { + beforeEach(() => { + ClusterStore.resetInstance(); + const mockOpts = { + "some-directory-for-user-data": { + "lens-cluster-store.json": JSON.stringify({ + __internal__: { + migrations: { + version: "3.5.0", + }, + }, + clusters: [ + { + id: "cluster1", + kubeConfig: minimalValidKubeConfig, + contextName: "cluster", + preferences: { + icon: "store://icon_path", + }, + }, + ], + }), + icon_path: testDataIcon, + }, + }; + + mockFs(mockOpts); + + clusterStore = mainDi.inject(clusterStoreInjectable); + }); + + afterEach(() => { + mockFs.restore(); + }); + + it("migrates to modern format with kubeconfig in a file", async () => { + const config = clusterStore.clustersList[0].kubeConfigPath; + + expect(fs.readFileSync(config, "utf8")).toBe(minimalValidKubeConfig); + }); + + it("migrates to modern format with icon not in file", async () => { + const { icon } = clusterStore.clustersList[0].preferences; + + expect(icon.startsWith("data:;base64,")).toBe(true); + }); }); }); - const minimalValidKubeConfig = JSON.stringify({ apiVersion: "v1", - clusters: [{ - name: "minikube", - cluster: { - server: "https://192.168.64.3:8443", + clusters: [ + { + name: "minikube", + cluster: { + server: "https://192.168.64.3:8443", + }, }, - }], + ], "current-context": "minikube", - contexts: [{ - context: { - cluster: "minikube", - user: "minikube", + contexts: [ + { + context: { + cluster: "minikube", + user: "minikube", + }, + name: "minikube", }, - name: "minikube", - }], - users: [{ - name: "minikube", - user: { - "client-certificate": "/Users/foo/.minikube/client.crt", - "client-key": "/Users/foo/.minikube/client.key", + ], + users: [ + { + name: "minikube", + user: { + "client-certificate": "/Users/foo/.minikube/client.crt", + "client-key": "/Users/foo/.minikube/client.key", + }, }, - }], + ], kind: "Config", preferences: {}, }); - -describe("pre 2.0 config with an existing cluster", () => { - beforeEach(() => { - ClusterStore.resetInstance(); - const mockOpts = { - "tmp": { - "lens-cluster-store.json": JSON.stringify({ - __internal__: { - migrations: { - version: "1.0.0", - }, - }, - cluster1: minimalValidKubeConfig, - }), - }, - }; - - mockFs(mockOpts); - - return ClusterStore.createInstance(); - }); - - afterEach(() => { - mockFs.restore(); - }); - - it("migrates to modern format with kubeconfig in a file", async () => { - const config = ClusterStore.getInstance().clustersList[0].kubeConfigPath; - - expect(fs.readFileSync(config, "utf8")).toContain(`"contexts":[`); - }); -}); - -describe("pre 2.6.0 config with a cluster that has arrays in auth config", () => { - beforeEach(() => { - ClusterStore.resetInstance(); - const mockOpts = { - "tmp": { - "lens-cluster-store.json": JSON.stringify({ - __internal__: { - migrations: { - version: "2.4.1", - }, - }, - cluster1: { - 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); - - return ClusterStore.createInstance(); - }); - - afterEach(() => { - mockFs.restore(); - }); - - it("replaces array format access token and expiry into string", async () => { - const file = ClusterStore.getInstance().clustersList[0].kubeConfigPath; - const config = fs.readFileSync(file, "utf8"); - const kc = yaml.load(config) as Record; - - 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"); - }); -}); - -describe("pre 2.6.0 config with a cluster icon", () => { - beforeEach(() => { - ClusterStore.resetInstance(); - const mockOpts = { - "tmp": { - "lens-cluster-store.json": JSON.stringify({ - __internal__: { - migrations: { - version: "2.4.1", - }, - }, - cluster1: { - kubeConfig: minimalValidKubeConfig, - icon: "icon_path", - preferences: { - terminalCWD: "/tmp", - }, - }, - }), - "icon_path": testDataIcon, - }, - }; - - mockFs(mockOpts); - - return ClusterStore.createInstance(); - }); - - afterEach(() => { - mockFs.restore(); - }); - - it("moves the icon into preferences", async () => { - const storedClusterData = ClusterStore.getInstance().clustersList[0]; - - expect(storedClusterData.hasOwnProperty("icon")).toBe(false); - expect(storedClusterData.preferences.hasOwnProperty("icon")).toBe(true); - expect(storedClusterData.preferences.icon.startsWith("data:;base64,")).toBe(true); - }); -}); - -describe("for a pre 2.7.0-beta.0 config without a workspace", () => { - beforeEach(() => { - ClusterStore.resetInstance(); - const mockOpts = { - "tmp": { - "lens-cluster-store.json": JSON.stringify({ - __internal__: { - migrations: { - version: "2.6.6", - }, - }, - cluster1: { - kubeConfig: minimalValidKubeConfig, - preferences: { - terminalCWD: "/tmp", - }, - }, - }), - }, - }; - - mockFs(mockOpts); - - return ClusterStore.createInstance(); - }); - - afterEach(() => { - mockFs.restore(); - }); -}); - -describe("pre 3.6.0-beta.1 config with an existing cluster", () => { - beforeEach(() => { - ClusterStore.resetInstance(); - const mockOpts = { - "tmp": { - "lens-cluster-store.json": JSON.stringify({ - __internal__: { - migrations: { - version: "3.5.0", - }, - }, - clusters: [ - { - id: "cluster1", - kubeConfig: minimalValidKubeConfig, - contextName: "cluster", - preferences: { - icon: "store://icon_path", - }, - }, - ], - }), - "icon_path": testDataIcon, - }, - }; - - mockFs(mockOpts); - - return ClusterStore.createInstance(); - }); - - afterEach(() => { - mockFs.restore(); - }); - - it("migrates to modern format with kubeconfig in a file", async () => { - const config = ClusterStore.getInstance().clustersList[0].kubeConfigPath; - - expect(fs.readFileSync(config, "utf8")).toBe(minimalValidKubeConfig); - }); - - it("migrates to modern format with icon not in file", async () => { - const { icon } = ClusterStore.getInstance().clustersList[0].preferences; - - expect(icon.startsWith("data:;base64,")).toBe(true); - }); -}); diff --git a/src/common/__tests__/event-bus.test.ts b/src/common/__tests__/event-bus.test.ts index 92e4d0628c..3218bfd420 100644 --- a/src/common/__tests__/event-bus.test.ts +++ b/src/common/__tests__/event-bus.test.ts @@ -19,7 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { appEventBus, AppEvent } from "../event-bus"; +import { appEventBus, AppEvent } from "../app-event-bus/event-bus"; import { Console } from "console"; import { stdout, stderr } from "process"; diff --git a/src/common/__tests__/hotbar-store.test.ts b/src/common/__tests__/hotbar-store.test.ts index ffc4e361da..ab69e3ccee 100644 --- a/src/common/__tests__/hotbar-store.test.ts +++ b/src/common/__tests__/hotbar-store.test.ts @@ -23,10 +23,11 @@ import { anyObject } from "jest-mock-extended"; import { merge } from "lodash"; import mockFs from "mock-fs"; import logger from "../../main/logger"; -import { AppPaths } from "../app-paths"; import type { CatalogEntity, CatalogEntityData, CatalogEntityKindData } from "../catalog"; -import { ClusterStore } from "../cluster-store"; import { HotbarStore } from "../hotbar-store"; +import { getDiForUnitTesting } from "../../main/getDiForUnitTesting"; +import directoryForUserDataInjectable + from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; jest.mock("../../main/catalog/catalog-entity-registry", () => ({ catalogEntityRegistry: { @@ -109,37 +110,24 @@ const awsCluster = getMockCatalogEntity({ }, }); -jest.mock("electron", () => ({ - app: { - getVersion: () => "99.99.99", - getName: () => "lens", - setName: jest.fn(), - setPath: jest.fn(), - getPath: () => "tmp", - getLocale: () => "en", - setLoginItemSettings: jest.fn(), - }, - ipcMain: { - on: jest.fn(), - handle: jest.fn(), - }, -})); - -AppPaths.init(); - describe("HotbarStore", () => { - beforeEach(() => { + beforeEach(async () => { + const di = getDiForUnitTesting({ doGeneralOverrides: true }); + + di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); + + await di.runSetups(); + mockFs({ - "tmp": { + "some-directory-for-user-data": { "lens-hotbar-store.json": JSON.stringify({}), }, }); - ClusterStore.createInstance(); + HotbarStore.createInstance(); }); afterEach(() => { - ClusterStore.resetInstance(); HotbarStore.resetInstance(); mockFs.restore(); }); @@ -339,7 +327,7 @@ describe("HotbarStore", () => { beforeEach(() => { HotbarStore.resetInstance(); const mockOpts = { - "tmp": { + "some-directory-for-user-data": { "lens-hotbar-store.json": JSON.stringify({ __internal__: { migrations: { diff --git a/src/common/__tests__/user-store.test.ts b/src/common/__tests__/user-store.test.ts index ba7272f4c9..01e6ea63b9 100644 --- a/src/common/__tests__/user-store.test.ts +++ b/src/common/__tests__/user-store.test.ts @@ -43,18 +43,39 @@ import { SemVer } from "semver"; import electron from "electron"; import { stdout, stderr } from "process"; import { ThemeStore } from "../../renderer/theme.store"; -import type { ClusterStoreModel } from "../cluster-store"; -import { AppPaths } from "../app-paths"; +import type { ClusterStoreModel } from "../cluster-store/cluster-store"; +import { getDisForUnitTesting } from "../../test-utils/get-dis-for-unit-testing"; +import userStoreInjectable from "../user-store/user-store.injectable"; +import type { DependencyInjectionContainer } from "@ogre-tools/injectable"; +import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; console = new Console(stdout, stderr); -AppPaths.init(); describe("user store tests", () => { + let userStore: UserStore; + let mainDi: DependencyInjectionContainer; + + beforeEach(async () => { + const dis = getDisForUnitTesting({ doGeneralOverrides: true }); + + mockFs(); + + mainDi = dis.mainDi; + + mainDi.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); + + await dis.runSetups(); + }); + + afterEach(() => { + mockFs.restore(); + }); + describe("for an empty config", () => { beforeEach(() => { - mockFs({ tmp: { "config.json": "{}", "kube_config": "{}" }}); + mockFs({ "some-directory-for-user-data": { "config.json": "{}", "kube_config": "{}" }}); - (UserStore.createInstance() as any).refreshNewContexts = jest.fn(() => Promise.resolve()); + userStore = mainDi.inject(userStoreInjectable); }); afterEach(() => { @@ -63,46 +84,38 @@ describe("user store tests", () => { }); it("allows setting and retrieving lastSeenAppVersion", () => { - const us = UserStore.getInstance(); - - us.lastSeenAppVersion = "1.2.3"; - expect(us.lastSeenAppVersion).toBe("1.2.3"); + userStore.lastSeenAppVersion = "1.2.3"; + expect(userStore.lastSeenAppVersion).toBe("1.2.3"); }); it("allows setting and getting preferences", () => { - const us = UserStore.getInstance(); + userStore.httpsProxy = "abcd://defg"; - us.httpsProxy = "abcd://defg"; + expect(userStore.httpsProxy).toBe("abcd://defg"); + expect(userStore.colorTheme).toBe(ThemeStore.defaultTheme); - expect(us.httpsProxy).toBe("abcd://defg"); - expect(us.colorTheme).toBe(ThemeStore.defaultTheme); - - us.colorTheme = "light"; - expect(us.colorTheme).toBe("light"); + userStore.colorTheme = "light"; + expect(userStore.colorTheme).toBe("light"); }); it("correctly resets theme to default value", async () => { - const us = UserStore.getInstance(); - - us.colorTheme = "some other theme"; - us.resetTheme(); - expect(us.colorTheme).toBe(ThemeStore.defaultTheme); + userStore.colorTheme = "some other theme"; + userStore.resetTheme(); + expect(userStore.colorTheme).toBe(ThemeStore.defaultTheme); }); it("correctly calculates if the last seen version is an old release", () => { - const us = UserStore.getInstance(); + expect(userStore.isNewVersion).toBe(true); - expect(us.isNewVersion).toBe(true); - - us.lastSeenAppVersion = (new SemVer(electron.app.getVersion())).inc("major").format(); - expect(us.isNewVersion).toBe(false); + userStore.lastSeenAppVersion = (new SemVer(electron.app.getVersion())).inc("major").format(); + expect(userStore.isNewVersion).toBe(false); }); }); describe("migrations", () => { beforeEach(() => { mockFs({ - "tmp": { + "some-directory-for-user-data": { "config.json": JSON.stringify({ user: { username: "foobar" }, preferences: { colorTheme: "light" }, @@ -112,7 +125,7 @@ describe("user store tests", () => { clusters: [ { id: "foobar", - kubeConfigPath: "tmp/extension_data/foo/bar", + kubeConfigPath: "some-directory-for-user-data/extension_data/foo/bar", }, { id: "barfoo", @@ -129,7 +142,7 @@ describe("user store tests", () => { }, }); - UserStore.createInstance(); + userStore = mainDi.inject(userStoreInjectable); }); afterEach(() => { @@ -138,16 +151,12 @@ describe("user store tests", () => { }); it("sets last seen app version to 0.0.0", () => { - const us = UserStore.getInstance(); - - expect(us.lastSeenAppVersion).toBe("0.0.0"); + expect(userStore.lastSeenAppVersion).toBe("0.0.0"); }); it.only("skips clusters for adding to kube-sync with files under extension_data/", () => { - const us = UserStore.getInstance(); - - expect(us.syncKubeconfigEntries.has("tmp/extension_data/foo/bar")).toBe(false); - expect(us.syncKubeconfigEntries.has("some/other/path")).toBe(true); + expect(userStore.syncKubeconfigEntries.has("some-directory-for-user-data/extension_data/foo/bar")).toBe(false); + expect(userStore.syncKubeconfigEntries.has("some/other/path")).toBe(true); }); }); }); diff --git a/src/common/app-event-bus/app-event-bus.injectable.ts b/src/common/app-event-bus/app-event-bus.injectable.ts new file mode 100644 index 0000000000..d8973ed309 --- /dev/null +++ b/src/common/app-event-bus/app-event-bus.injectable.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { appEventBus } from "./event-bus"; + +const appEventBusInjectable = getInjectable({ + instantiate: () => appEventBus, + lifecycle: lifecycleEnum.singleton, +}); + +export default appEventBusInjectable; diff --git a/src/common/event-bus.ts b/src/common/app-event-bus/event-bus.ts similarity index 96% rename from src/common/event-bus.ts rename to src/common/app-event-bus/event-bus.ts index 4c9d087028..30c9896b19 100644 --- a/src/common/event-bus.ts +++ b/src/common/app-event-bus/event-bus.ts @@ -19,7 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { EventEmitter } from "./event-emitter"; +import { EventEmitter } from "../event-emitter"; export type AppEvent = { name: string; diff --git a/src/common/app-paths.ts b/src/common/app-paths.ts deleted file mode 100644 index d23cc8c1a4..0000000000 --- a/src/common/app-paths.ts +++ /dev/null @@ -1,127 +0,0 @@ -/** - * Copyright (c) 2021 OpenLens Authors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -import { app, ipcMain, ipcRenderer } from "electron"; -import { observable, when } from "mobx"; -import path from "path"; -import logger from "./logger"; -import { fromEntries, toJS } from "./utils"; -import { isWindows } from "./vars"; - -export type PathName = Parameters[0]; - -const pathNames: PathName[] = [ - "home", - "appData", - "userData", - "cache", - "temp", - "exe", - "module", - "desktop", - "documents", - "downloads", - "music", - "pictures", - "videos", - "logs", - "crashDumps", -]; - -if (isWindows) { - pathNames.push("recent"); -} - -export class AppPaths { - private static paths = observable.box | undefined>(); - private static readonly ipcChannel = "get-app-paths"; - - /** - * Initializes the local copy of the paths from electron. - */ - static async init(): Promise { - logger.info(`[APP-PATHS]: initializing`); - - if (AppPaths.paths.get()) { - return void logger.error("[APP-PATHS]: init called more than once"); - } - - if (ipcMain) { - AppPaths.initMain(); - } else { - await AppPaths.initRenderer(); - } - } - - private static initMain(): void { - if (process.env.CICD) { - app.setPath("appData", process.env.CICD); - } - - app.setPath("userData", path.join(app.getPath("appData"), app.getName())); - - const getPath = (pathName: PathName) => { - try { - return app.getPath(pathName); - } catch { - logger.debug(`[APP-PATHS] No path found for ${pathName}`); - - return ""; - } - }; - - AppPaths.paths.set(fromEntries(pathNames.map(pathName => [pathName, getPath(pathName)] as const).filter(([, path]) => path))); - ipcMain.handle(AppPaths.ipcChannel, () => toJS(AppPaths.paths.get())); - } - - private static async initRenderer(): Promise { - const paths = await ipcRenderer.invoke(AppPaths.ipcChannel); - - if (!paths || typeof paths !== "object") { - throw Object.assign(new Error("[APP-PATHS]: ipc handler returned unexpected data"), { data: paths }); - } - - AppPaths.paths.set(paths); - } - - /** - * An alternative to `app.getPath()` for use in renderer and common. - * This function throws if called before initialization. - * @param name The name of the path field - */ - static get(name: PathName): string { - if (!AppPaths.paths.get()) { - throw new Error("AppPaths.init() has not been called"); - } - - return AppPaths.paths.get()[name]; - } - - /** - * An async version of `AppPaths.get()` which waits for `AppPaths.init()` to - * be called before returning - */ - static async getAsync(name: PathName): Promise { - await when(() => Boolean(AppPaths.paths.get())); - - return AppPaths.paths.get()[name]; - } -} diff --git a/src/common/app-paths/app-path-injection-token.ts b/src/common/app-paths/app-path-injection-token.ts new file mode 100644 index 0000000000..fcc3d18e3b --- /dev/null +++ b/src/common/app-paths/app-path-injection-token.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectionToken } from "@ogre-tools/injectable"; +import type { PathName } from "./app-path-names"; +import { createChannel } from "../ipc-channel/create-channel/create-channel"; + +export type AppPaths = Record; + +export const appPathsInjectionToken = getInjectionToken(); + +export const appPathsIpcChannel = createChannel("app-paths"); + + diff --git a/src/common/app-paths/app-path-names.ts b/src/common/app-paths/app-path-names.ts new file mode 100644 index 0000000000..3de302c269 --- /dev/null +++ b/src/common/app-paths/app-path-names.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import type { app as electronApp } from "electron"; + +export type PathName = Parameters[0]; + +export const pathNames: PathName[] = [ + "home", + "appData", + "userData", + "cache", + "temp", + "exe", + "module", + "desktop", + "documents", + "downloads", + "music", + "pictures", + "videos", + "logs", + "crashDumps", + "recent", +]; diff --git a/src/common/app-paths/app-paths.test.ts b/src/common/app-paths/app-paths.test.ts new file mode 100644 index 0000000000..bba815f4f5 --- /dev/null +++ b/src/common/app-paths/app-paths.test.ts @@ -0,0 +1,159 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import type { DependencyInjectionContainer } from "@ogre-tools/injectable"; +import { AppPaths, appPathsInjectionToken } from "./app-path-injection-token"; +import getElectronAppPathInjectable from "../../main/app-paths/get-electron-app-path/get-electron-app-path.injectable"; +import { getDisForUnitTesting } from "../../test-utils/get-dis-for-unit-testing"; +import type { PathName } from "./app-path-names"; +import setElectronAppPathInjectable from "../../main/app-paths/set-electron-app-path/set-electron-app-path.injectable"; +import appNameInjectable from "../../main/app-paths/app-name/app-name.injectable"; +import directoryForIntegrationTestingInjectable from "../../main/app-paths/directory-for-integration-testing/directory-for-integration-testing.injectable"; + +describe("app-paths", () => { + let mainDi: DependencyInjectionContainer; + let rendererDi: DependencyInjectionContainer; + let runSetups: () => Promise; + + beforeEach(() => { + const dis = getDisForUnitTesting({ doGeneralOverrides: true }); + + mainDi = dis.mainDi; + rendererDi = dis.rendererDi; + runSetups = dis.runSetups; + + const defaultAppPathsStub: AppPaths = { + appData: "some-app-data", + cache: "some-cache", + crashDumps: "some-crash-dumps", + desktop: "some-desktop", + documents: "some-documents", + downloads: "some-downloads", + exe: "some-exe", + home: "some-home-path", + logs: "some-logs", + module: "some-module", + music: "some-music", + pictures: "some-pictures", + recent: "some-recent", + temp: "some-temp", + videos: "some-videos", + userData: "some-irrelevant", + }; + + mainDi.override( + getElectronAppPathInjectable, + () => + (key: PathName): string | null => + defaultAppPathsStub[key], + ); + + mainDi.override( + setElectronAppPathInjectable, + () => + (key: PathName, path: string): void => { + defaultAppPathsStub[key] = path; + }, + ); + + mainDi.override(appNameInjectable, () => "some-app-name"); + }); + + describe("normally", () => { + beforeEach(async () => { + await runSetups(); + }); + + it("given in renderer, when injecting app paths, returns application specific app paths", () => { + const actual = rendererDi.inject(appPathsInjectionToken); + + expect(actual).toEqual({ + appData: "some-app-data", + cache: "some-cache", + crashDumps: "some-crash-dumps", + desktop: "some-desktop", + documents: "some-documents", + downloads: "some-downloads", + exe: "some-exe", + home: "some-home-path", + logs: "some-logs", + module: "some-module", + music: "some-music", + pictures: "some-pictures", + recent: "some-recent", + temp: "some-temp", + videos: "some-videos", + userData: "some-app-data/some-app-name", + }); + }); + + it("given in main, when injecting app paths, returns application specific app paths", () => { + const actual = mainDi.inject(appPathsInjectionToken); + + expect(actual).toEqual({ + appData: "some-app-data", + cache: "some-cache", + crashDumps: "some-crash-dumps", + desktop: "some-desktop", + documents: "some-documents", + downloads: "some-downloads", + exe: "some-exe", + home: "some-home-path", + logs: "some-logs", + module: "some-module", + music: "some-music", + pictures: "some-pictures", + recent: "some-recent", + temp: "some-temp", + videos: "some-videos", + userData: "some-app-data/some-app-name", + }); + }); + }); + + describe("when running integration tests", () => { + beforeEach(async () => { + mainDi.override( + directoryForIntegrationTestingInjectable, + () => "some-integration-testing-app-data", + ); + + await runSetups(); + }); + + it("given in renderer, when injecting path for app data, has integration specific app data path", () => { + const { appData, userData } = rendererDi.inject(appPathsInjectionToken); + + expect({ appData, userData }).toEqual({ + appData: "some-integration-testing-app-data", + userData: "some-integration-testing-app-data/some-app-name", + }); + }); + + it("given in main, when injecting path for app data, has integration specific app data path", () => { + const { appData, userData } = rendererDi.inject(appPathsInjectionToken); + + expect({ appData, userData }).toEqual({ + appData: "some-integration-testing-app-data", + userData: "some-integration-testing-app-data/some-app-name", + }); + }); + }); +}); diff --git a/src/common/utils/local-kubeconfig.ts b/src/common/app-paths/directory-for-binaries/directory-for-binaries.injectable.ts similarity index 73% rename from src/common/utils/local-kubeconfig.ts rename to src/common/app-paths/directory-for-binaries/directory-for-binaries.injectable.ts index 2b2d6e6241..dd31621681 100644 --- a/src/common/utils/local-kubeconfig.ts +++ b/src/common/app-paths/directory-for-binaries/directory-for-binaries.injectable.ts @@ -18,16 +18,15 @@ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ - +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import path from "path"; -import * as uuid from "uuid"; -import { AppPaths } from "../app-paths"; -import type { ClusterId } from "../cluster-types"; +import directoryForUserDataInjectable from "../directory-for-user-data/directory-for-user-data.injectable"; -export function storedKubeConfigFolder(): string { - return path.resolve(AppPaths.get("userData"), "kubeconfigs"); -} +const directoryForBinariesInjectable = getInjectable({ + instantiate: (di) => + path.join(di.inject(directoryForUserDataInjectable), "binaries"), -export function getCustomKubeConfigPath(clusterId: ClusterId = uuid.v4()): string { - return path.resolve(storedKubeConfigFolder(), clusterId); -} + lifecycle: lifecycleEnum.singleton, +}); + +export default directoryForBinariesInjectable; diff --git a/src/common/app-paths/directory-for-downloads/directory-for-downloads.injectable.ts b/src/common/app-paths/directory-for-downloads/directory-for-downloads.injectable.ts new file mode 100644 index 0000000000..35a4d160ce --- /dev/null +++ b/src/common/app-paths/directory-for-downloads/directory-for-downloads.injectable.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { appPathsInjectionToken } from "../app-path-injection-token"; + +const directoryForDownloadsInjectable = getInjectable({ + instantiate: (di) => di.inject(appPathsInjectionToken).downloads, + lifecycle: lifecycleEnum.singleton, +}); + +export default directoryForDownloadsInjectable; diff --git a/src/common/app-paths/directory-for-exes/directory-for-exes.injectable.ts b/src/common/app-paths/directory-for-exes/directory-for-exes.injectable.ts new file mode 100644 index 0000000000..9a9edda0fa --- /dev/null +++ b/src/common/app-paths/directory-for-exes/directory-for-exes.injectable.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { appPathsInjectionToken } from "../app-path-injection-token"; + +const directoryForExesInjectable = getInjectable({ + instantiate: (di) => di.inject(appPathsInjectionToken).exe, + lifecycle: lifecycleEnum.singleton, +}); + +export default directoryForExesInjectable; diff --git a/src/common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable.ts b/src/common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable.ts new file mode 100644 index 0000000000..db61207efe --- /dev/null +++ b/src/common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable.ts @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import directoryForUserDataInjectable from "../directory-for-user-data/directory-for-user-data.injectable"; +import path from "path"; + +const directoryForKubeConfigsInjectable = getInjectable({ + instantiate: (di) => + path.resolve(di.inject(directoryForUserDataInjectable), "kubeconfigs"), + + lifecycle: lifecycleEnum.singleton, +}); + +export default directoryForKubeConfigsInjectable; diff --git a/src/common/app-paths/directory-for-temp/directory-for-temp.injectable.ts b/src/common/app-paths/directory-for-temp/directory-for-temp.injectable.ts new file mode 100644 index 0000000000..6faef86220 --- /dev/null +++ b/src/common/app-paths/directory-for-temp/directory-for-temp.injectable.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { appPathsInjectionToken } from "../app-path-injection-token"; + +const directoryForTempInjectable = getInjectable({ + instantiate: (di) => di.inject(appPathsInjectionToken).temp, + lifecycle: lifecycleEnum.singleton, +}); + +export default directoryForTempInjectable; diff --git a/src/common/app-paths/directory-for-user-data/directory-for-user-data.injectable.ts b/src/common/app-paths/directory-for-user-data/directory-for-user-data.injectable.ts new file mode 100644 index 0000000000..c4ead5566a --- /dev/null +++ b/src/common/app-paths/directory-for-user-data/directory-for-user-data.injectable.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { appPathsInjectionToken } from "../app-path-injection-token"; + +const directoryForUserDataInjectable = getInjectable({ + instantiate: (di) => di.inject(appPathsInjectionToken).userData, + lifecycle: lifecycleEnum.singleton, +}); + +export default directoryForUserDataInjectable; diff --git a/src/common/app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable.ts b/src/common/app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable.ts new file mode 100644 index 0000000000..c781b610a8 --- /dev/null +++ b/src/common/app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import path from "path"; +import directoryForKubeConfigsInjectable from "../directory-for-kube-configs/directory-for-kube-configs.injectable"; + +const getCustomKubeConfigDirectoryInjectable = getInjectable({ + instantiate: (di) => (directoryName: string) => { + const directoryForKubeConfigs = di.inject(directoryForKubeConfigsInjectable); + + return path.resolve( + directoryForKubeConfigs, + directoryName, + ); + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default getCustomKubeConfigDirectoryInjectable; diff --git a/src/common/base-store.ts b/src/common/base-store.ts index 9f00ec3fbb..6c5cab18e4 100644 --- a/src/common/base-store.ts +++ b/src/common/base-store.ts @@ -30,7 +30,9 @@ import { broadcastMessage, ipcMainOn, ipcRendererOn } from "./ipc"; import isEqual from "lodash/isEqual"; import { isTestEnv } from "./vars"; import { kebabCase } from "lodash"; -import { AppPaths } from "./app-paths"; +import { getLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-global-function-for-extension-api/legacy-global-di-for-extension-api"; +import directoryForUserDataInjectable + from "./app-paths/directory-for-user-data/directory-for-user-data.injectable"; export interface BaseStoreParams extends ConfOptions { syncOptions?: { @@ -102,7 +104,9 @@ export abstract class BaseStore extends Singleton { } protected cwd() { - return AppPaths.get("userData"); + const di = getLegacyGlobalDiForExtensionApi(); + + return di.inject(directoryForUserDataInjectable); } protected saveToFile(model: T) { diff --git a/src/common/catalog-entities/kubernetes-cluster.ts b/src/common/catalog-entities/kubernetes-cluster.ts index 1f4f933620..e589adb91a 100644 --- a/src/common/catalog-entities/kubernetes-cluster.ts +++ b/src/common/catalog-entities/kubernetes-cluster.ts @@ -22,7 +22,7 @@ import { catalogCategoryRegistry } from "../catalog/catalog-category-registry"; import { CatalogEntity, CatalogEntityActionContext, CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog"; import { clusterActivateHandler, clusterDisconnectHandler } from "../cluster-ipc"; -import { ClusterStore } from "../cluster-store"; +import { ClusterStore } from "../cluster-store/cluster-store"; import { broadcastMessage, requestMain } from "../ipc"; import { CatalogCategory, CatalogCategorySpec } from "../catalog"; import { app } from "electron"; diff --git a/src/common/cluster-store/cluster-store.injectable.ts b/src/common/cluster-store/cluster-store.injectable.ts new file mode 100644 index 0000000000..8b1a23ff1d --- /dev/null +++ b/src/common/cluster-store/cluster-store.injectable.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { ClusterStore } from "./cluster-store"; +import { createClusterInjectionToken } from "../cluster/create-cluster-injection-token"; + +const clusterStoreInjectable = getInjectable({ + instantiate: (di) => + ClusterStore.createInstance({ + createCluster: di.inject(createClusterInjectionToken), + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default clusterStoreInjectable; diff --git a/src/common/cluster-store.ts b/src/common/cluster-store/cluster-store.ts similarity index 86% rename from src/common/cluster-store.ts rename to src/common/cluster-store/cluster-store.ts index a3dbc97645..51035254d7 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store/cluster-store.ts @@ -18,17 +18,18 @@ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + import { ipcMain, ipcRenderer, webFrame } from "electron"; import { action, comparer, computed, makeObservable, observable, reaction } from "mobx"; -import { BaseStore } from "./base-store"; -import { Cluster } from "../main/cluster"; -import migrations from "../migrations/cluster-store"; -import logger from "../main/logger"; -import { appEventBus } from "./event-bus"; -import { ipcMainHandle, requestMain } from "./ipc"; -import { disposer, toJS } from "./utils"; -import type { ClusterModel, ClusterId, ClusterState } from "./cluster-types"; +import { BaseStore } from "../base-store"; +import { Cluster } from "../cluster/cluster"; +import migrations from "../../migrations/cluster-store"; +import logger from "../../main/logger"; +import { appEventBus } from "../app-event-bus/event-bus"; +import { ipcMainHandle, requestMain } from "../ipc"; +import { disposer, toJS } from "../utils"; +import type { ClusterModel, ClusterId, ClusterState } from "../cluster-types"; export interface ClusterStoreModel { clusters?: ClusterModel[]; @@ -36,13 +37,17 @@ export interface ClusterStoreModel { const initialStates = "cluster:states"; +interface Dependencies { + createCluster: (model: ClusterModel) => Cluster +} + export class ClusterStore extends BaseStore { readonly displayName = "ClusterStore"; clusters = observable.map(); protected disposer = disposer(); - constructor() { + constructor(private dependencies: Dependencies) { super({ configName: "lens-cluster-store", accessPropertiesByDotNotation: false, // To make dots safe in cluster context names @@ -123,7 +128,7 @@ export class ClusterStore extends BaseStore { const cluster = clusterOrModel instanceof Cluster ? clusterOrModel - : new Cluster(clusterOrModel); + : this.dependencies.createCluster(clusterOrModel); this.clusters.set(cluster.id, cluster); @@ -143,7 +148,7 @@ export class ClusterStore extends BaseStore { if (cluster) { cluster.updateModel(clusterModel); } else { - cluster = new Cluster(clusterModel); + cluster = this.dependencies.createCluster(clusterModel); } newClusters.set(clusterModel.id, cluster); } catch (error) { diff --git a/src/common/cluster-store/hosted-cluster/hosted-cluster.injectable.ts b/src/common/cluster-store/hosted-cluster/hosted-cluster.injectable.ts new file mode 100644 index 0000000000..a3057e7bab --- /dev/null +++ b/src/common/cluster-store/hosted-cluster/hosted-cluster.injectable.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { getHostedClusterId } from "../../utils"; +import clusterStoreInjectable from "../cluster-store.injectable"; + +const hostedClusterInjectable = getInjectable({ + instantiate: (di) => { + const hostedClusterId = getHostedClusterId(); + + return di.inject(clusterStoreInjectable).getById(hostedClusterId); + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default hostedClusterInjectable; diff --git a/src/main/cluster.ts b/src/common/cluster/cluster.ts similarity index 93% rename from src/main/cluster.ts rename to src/common/cluster/cluster.ts index 2ec97c792a..274b47d314 100644 --- a/src/main/cluster.ts +++ b/src/common/cluster/cluster.ts @@ -21,22 +21,29 @@ import { ipcMain } from "electron"; import { action, comparer, computed, makeObservable, observable, reaction, when } from "mobx"; -import { broadcastMessage, ClusterListNamespaceForbiddenChannel } from "../common/ipc"; -import { ContextHandler } from "./context-handler"; +import { broadcastMessage, ClusterListNamespaceForbiddenChannel } from "../ipc"; +import type { ContextHandler } from "../../main/context-handler/context-handler"; import { AuthorizationV1Api, CoreV1Api, HttpError, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"; -import { Kubectl } from "./kubectl"; -import { KubeconfigManager } from "./kubeconfig-manager"; -import { loadConfigFromFile, loadConfigFromFileSync, validateKubeConfig } from "../common/kube-helpers"; -import { apiResourceRecord, apiResources, KubeApiResource, KubeResource } from "../common/rbac"; -import logger from "./logger"; -import { VersionDetector } from "./cluster-detectors/version-detector"; -import { DetectorRegistry } from "./cluster-detectors/detector-registry"; +import type { Kubectl } from "../../main/kubectl/kubectl"; +import type { KubeconfigManager } from "../../main/kubeconfig-manager/kubeconfig-manager"; +import { loadConfigFromFile, loadConfigFromFileSync, validateKubeConfig } from "../kube-helpers"; +import { apiResourceRecord, apiResources, KubeApiResource, KubeResource } from "../rbac"; +import logger from "../../main/logger"; +import { VersionDetector } from "../../main/cluster-detectors/version-detector"; +import { DetectorRegistry } from "../../main/cluster-detectors/detector-registry"; import plimit from "p-limit"; -import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate } from "../common/cluster-types"; -import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "../common/cluster-types"; -import { disposer, storedKubeConfigFolder, toJS } from "../common/utils"; +import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate } from "../cluster-types"; +import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "../cluster-types"; +import { disposer, toJS } from "../utils"; import type { Response } from "request"; +interface Dependencies { + directoryForKubeConfigs: string, + createKubeconfigManager: (cluster: Cluster) => KubeconfigManager, + createContextHandler: (cluster: Cluster) => ContextHandler, + createKubectl: (clusterVersion: string) => Kubectl +} + /** * Cluster * @@ -221,7 +228,7 @@ export class Cluster implements ClusterModel, ClusterState { return this.preferences.defaultNamespace; } - constructor(model: ClusterModel) { + constructor(private dependencies: Dependencies, model: ClusterModel) { makeObservable(this); this.id = model.id; this.updateModel(model); @@ -237,8 +244,8 @@ export class Cluster implements ClusterModel, ClusterState { if (ipcMain) { // for the time being, until renderer gets its own cluster type - this.contextHandler = new ContextHandler(this); - this.proxyKubeconfigManager = new KubeconfigManager(this, this.contextHandler); + this.contextHandler = this.dependencies.createContextHandler(this); + this.proxyKubeconfigManager = this.dependencies.createKubeconfigManager(this); logger.debug(`[CLUSTER]: Cluster init success`, { id: this.id, @@ -362,7 +369,7 @@ export class Cluster implements ClusterModel, ClusterState { * @internal */ async ensureKubectl() { - this.kubeCtl ??= new Kubectl(this.version); + this.kubeCtl ??= this.dependencies.createKubectl(this.version); await this.kubeCtl.ensureKubectl(); @@ -719,6 +726,6 @@ export class Cluster implements ClusterModel, ClusterState { } isInLocalKubeconfig() { - return this.kubeConfigPath.startsWith(storedKubeConfigFolder()); + return this.kubeConfigPath.startsWith(this.dependencies.directoryForKubeConfigs); } } diff --git a/src/common/cluster/create-cluster-injection-token.ts b/src/common/cluster/create-cluster-injection-token.ts new file mode 100644 index 0000000000..86c43302d9 --- /dev/null +++ b/src/common/cluster/create-cluster-injection-token.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectionToken } from "@ogre-tools/injectable"; +import type { ClusterModel } from "../cluster-types"; +import type { Cluster } from "./cluster"; + +export const createClusterInjectionToken = + getInjectionToken<(model: ClusterModel) => Cluster>(); diff --git a/src/common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable.ts b/src/common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable.ts new file mode 100644 index 0000000000..7bbd1017e5 --- /dev/null +++ b/src/common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import path from "path"; +import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; + +const directoryForLensLocalStorageInjectable = getInjectable({ + instantiate: (di) => + path.resolve( + di.inject(directoryForUserDataInjectable), + "lens-local-storage", + ), + + lifecycle: lifecycleEnum.singleton, +}); + +export default directoryForLensLocalStorageInjectable; diff --git a/src/common/event-emitter.ts b/src/common/event-emitter.ts index 4e1fb45151..411bc42f71 100644 --- a/src/common/event-emitter.ts +++ b/src/common/event-emitter.ts @@ -45,7 +45,7 @@ export class EventEmitter { this.listeners.length = 0; } - emit(...data: D) { + emit = (...data: D) => { for (const [callback, { once }] of this.listeners) { if (once) { this.removeListener(callback); @@ -55,5 +55,5 @@ export class EventEmitter { break; } } - } + }; } diff --git a/src/common/fs/fs.injectable.ts b/src/common/fs/fs.injectable.ts new file mode 100644 index 0000000000..e63c0137cc --- /dev/null +++ b/src/common/fs/fs.injectable.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import fse from "fs-extra"; + +const fsInjectable = getInjectable({ + instantiate: () => fse, + causesSideEffects: true, + lifecycle: lifecycleEnum.singleton, +}); + +export default fsInjectable; diff --git a/src/common/fs/read-json-file/read-json-file.injectable.ts b/src/common/fs/read-json-file/read-json-file.injectable.ts new file mode 100644 index 0000000000..ae801fc9e5 --- /dev/null +++ b/src/common/fs/read-json-file/read-json-file.injectable.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { readJsonFile } from "./read-json-file"; +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import fsInjectable from "../fs.injectable"; + +const readJsonFileInjectable = getInjectable({ + instantiate: (di) => readJsonFile({ + fs: di.inject(fsInjectable), + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default readJsonFileInjectable; diff --git a/src/common/fs/read-json-file/read-json-file.ts b/src/common/fs/read-json-file/read-json-file.ts new file mode 100644 index 0000000000..9f9bd4acdc --- /dev/null +++ b/src/common/fs/read-json-file/read-json-file.ts @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import type { JsonObject } from "type-fest"; + +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +interface Dependencies { + fs: { + readJson: (filePath: string) => Promise; + }; +} + +export const readJsonFile = + ({ fs }: Dependencies) => + (filePath: string) => + fs.readJson(filePath); diff --git a/src/common/fs/write-json-file/write-json-file.injectable.ts b/src/common/fs/write-json-file/write-json-file.injectable.ts new file mode 100644 index 0000000000..569be976b0 --- /dev/null +++ b/src/common/fs/write-json-file/write-json-file.injectable.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { writeJsonFile } from "./write-json-file"; +import fsInjectable from "../fs.injectable"; + +const writeJsonFileInjectable = getInjectable({ + instantiate: (di) => writeJsonFile({ fs: di.inject(fsInjectable) }), + lifecycle: lifecycleEnum.singleton, +}); + +export default writeJsonFileInjectable; diff --git a/src/common/fs/write-json-file/write-json-file.ts b/src/common/fs/write-json-file/write-json-file.ts new file mode 100644 index 0000000000..8aea56b508 --- /dev/null +++ b/src/common/fs/write-json-file/write-json-file.ts @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import path from "path"; +import type { JsonObject } from "type-fest"; + +interface Dependencies { + fs: { + ensureDir: ( + directoryName: string, + options: { mode: number } + ) => Promise; + + writeJson: ( + filePath: string, + contentObject: JsonObject, + options: { spaces: number } + ) => Promise; + }; +} + +export const writeJsonFile = + ({ fs }: Dependencies) => + async (filePath: string, contentObject: JsonObject) => { + const directoryName = path.dirname(filePath); + + await fs.ensureDir(directoryName, { mode: 0o755 }); + + await fs.writeJson(filePath, contentObject, { spaces: 2 }); + }; diff --git a/src/common/ipc-channel/channel.d.ts b/src/common/ipc-channel/channel.d.ts new file mode 100644 index 0000000000..bb426ada77 --- /dev/null +++ b/src/common/ipc-channel/channel.d.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +export interface Channel { + name: string; + _template: TInstance; +} diff --git a/src/common/ipc-channel/create-channel/create-channel.ts b/src/common/ipc-channel/create-channel/create-channel.ts new file mode 100644 index 0000000000..4c68af128a --- /dev/null +++ b/src/common/ipc-channel/create-channel/create-channel.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import type { Channel } from "../channel"; + +export const createChannel = (name: string): Channel => ({ + name, + _template: null, +}); diff --git a/src/common/k8s-api/cluster-context.ts b/src/common/k8s-api/cluster-context.ts index af892a2ce3..361a854ccb 100644 --- a/src/common/k8s-api/cluster-context.ts +++ b/src/common/k8s-api/cluster-context.ts @@ -19,7 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import type { Cluster } from "../../main/cluster"; +import type { Cluster } from "../cluster/cluster"; export interface ClusterContext { cluster?: Cluster; diff --git a/src/common/k8s-api/index.ts b/src/common/k8s-api/index.ts index c4fcb9a1c1..29e05b3b49 100644 --- a/src/common/k8s-api/index.ts +++ b/src/common/k8s-api/index.ts @@ -23,7 +23,7 @@ import { JsonApi } from "./json-api"; import { KubeJsonApi } from "./kube-json-api"; import { apiKubePrefix, apiPrefix, isDebugging, isDevelopment } from "../../common/vars"; import { isClusterPageContext } from "../utils/cluster-id-url-parsing"; -import { appEventBus } from "../event-bus"; +import { appEventBus } from "../app-event-bus/event-bus"; let apiBase: JsonApi; let apiKube: KubeJsonApi; diff --git a/src/common/k8s-api/kube-api.ts b/src/common/k8s-api/kube-api.ts index 0f31758ab2..8911aa7a9f 100644 --- a/src/common/k8s-api/kube-api.ts +++ b/src/common/k8s-api/kube-api.ts @@ -30,7 +30,7 @@ import { apiBase, apiKube } from "./index"; import { createKubeApiURL, parseKubeApi } from "./kube-api-parse"; import { KubeObjectConstructor, KubeObject, KubeStatus } from "./kube-object"; import byline from "byline"; -import type { IKubeWatchEvent } from "./kube-watch-api"; +import type { IKubeWatchEvent } from "./kube-watch-event"; import { KubeJsonApi, KubeJsonApiData } from "./kube-json-api"; import { noop } from "../utils"; import type { RequestInit } from "node-fetch"; diff --git a/src/common/k8s-api/kube-object.store.ts b/src/common/k8s-api/kube-object.store.ts index 1832cdfa10..b67d24547e 100644 --- a/src/common/k8s-api/kube-object.store.ts +++ b/src/common/k8s-api/kube-object.store.ts @@ -24,7 +24,7 @@ import type { ClusterContext } from "./cluster-context"; import { action, computed, makeObservable, observable, reaction, when } from "mobx"; import { autoBind, noop, rejectPromiseBy } from "../utils"; import { KubeObject, KubeStatus } from "./kube-object"; -import type { IKubeWatchEvent } from "./kube-watch-api"; +import type { IKubeWatchEvent } from "./kube-watch-event"; import { ItemStore } from "../item.store"; import { ensureObjectSelfLink, IKubeApiQueryParams, KubeApi } from "./kube-api"; import { parseKubeApi } from "./kube-api-parse"; @@ -323,14 +323,14 @@ export abstract class KubeObjectStore extends ItemStore return this.api.create(params, data); } - async create(params: { name: string; namespace?: string }, data?: Partial): Promise { + create = async (params: { name: string; namespace?: string }, data?: Partial): Promise => { const newItem = await this.createItem(params, data); const items = this.sortItems([...this.items, newItem]); this.items.replace(items); return newItem; - } + }; private postUpdate(rawItem: KubeJsonApiData): T { const newItem = new this.api.objectConstructor(rawItem); diff --git a/src/common/k8s-api/kube-watch-event.d.ts b/src/common/k8s-api/kube-watch-event.d.ts new file mode 100644 index 0000000000..56a4f0c6a8 --- /dev/null +++ b/src/common/k8s-api/kube-watch-event.d.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import type { KubeJsonApiData } from "./kube-json-api"; + +export interface IKubeWatchEvent { + type: "ADDED" | "MODIFIED" | "DELETED" | "ERROR"; + object?: T; +} + diff --git a/src/common/k8s/resource-stack.ts b/src/common/k8s/resource-stack.ts index 483dbeeb9b..f5e4e9a7e4 100644 --- a/src/common/k8s/resource-stack.ts +++ b/src/common/k8s/resource-stack.ts @@ -27,7 +27,7 @@ import logger from "../../main/logger"; import { app } from "electron"; import { requestMain } from "../ipc"; import { clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler } from "../cluster-ipc"; -import { ClusterStore } from "../cluster-store"; +import { ClusterStore } from "../cluster-store/cluster-store"; import yaml from "js-yaml"; import { productName } from "../vars"; diff --git a/src/renderer/components/layout/sidebar-storage.ts b/src/common/user-store/user-store.injectable.ts similarity index 78% rename from src/renderer/components/layout/sidebar-storage.ts rename to src/common/user-store/user-store.injectable.ts index 162ca3e6b7..ad73efe949 100644 --- a/src/renderer/components/layout/sidebar-storage.ts +++ b/src/common/user-store/user-store.injectable.ts @@ -18,19 +18,13 @@ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { UserStore } from "./user-store"; -import { createStorage } from "../../utils"; +const userStoreInjectable = getInjectable({ + instantiate: () => UserStore.createInstance(), -export interface SidebarStorageState { - width: number; - expanded: { - [itemId: string]: boolean; - } -} - -export const defaultSidebarWidth = 200; - -export const sidebarStorage = createStorage("sidebar", { - width: defaultSidebarWidth, - expanded: {}, + lifecycle: lifecycleEnum.singleton, }); + +export default userStoreInjectable; diff --git a/src/common/user-store/user-store.ts b/src/common/user-store/user-store.ts index 31238f0ded..6f0203e151 100644 --- a/src/common/user-store/user-store.ts +++ b/src/common/user-store/user-store.ts @@ -26,12 +26,10 @@ import { BaseStore } from "../base-store"; import migrations, { fileNameMigration } from "../../migrations/user-store"; import { getAppVersion } from "../utils/app-version"; import { kubeConfigDefaultPath } from "../kube-helpers"; -import { appEventBus } from "../event-bus"; -import path from "path"; +import { appEventBus } from "../app-event-bus/event-bus"; import { ObservableToggleSet, toJS } from "../../renderer/utils"; import { DESCRIPTORS, EditorConfiguration, ExtensionRegistry, KubeconfigSyncValue, UserPreferencesModel } from "./preferences-helpers"; import logger from "../../main/logger"; -import { AppPaths } from "../app-paths"; export interface UserStoreModel { lastSeenAppVersion: string; @@ -233,11 +231,3 @@ export class UserStore extends BaseStore /* implements UserStore return toJS(model); } } - -/** - * Getting default directory to download kubectl binaries - * @returns string - */ -export function getDefaultKubectlDownloadPath(): string { - return path.join(AppPaths.get("userData"), "binaries"); -} diff --git a/src/common/utils/allowed-resource.ts b/src/common/utils/allowed-resource.ts index d8d3676f17..b924a36712 100644 --- a/src/common/utils/allowed-resource.ts +++ b/src/common/utils/allowed-resource.ts @@ -19,7 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { ClusterStore } from "../cluster-store"; +import { ClusterStore } from "../cluster-store/cluster-store"; import type { KubeResource } from "../rbac"; import { getHostedClusterId } from "./cluster-id-url-parsing"; diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index ea2f742bc7..74deb61f90 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -43,7 +43,6 @@ export * from "./extended-map"; export * from "./formatDuration"; export * from "./getRandId"; export * from "./hash-set"; -export * from "./local-kubeconfig"; export * from "./n-fircate"; export * from "./objects"; export * from "./openExternal"; diff --git a/src/extensions/__tests__/extension-loader.test.ts b/src/extensions/__tests__/extension-loader.test.ts index c0b233ca4b..1426a174d4 100644 --- a/src/extensions/__tests__/extension-loader.test.ts +++ b/src/extensions/__tests__/extension-loader.test.ts @@ -22,12 +22,12 @@ import type { ExtensionLoader } from "../extension-loader"; import { Console } from "console"; import { stdout, stderr } from "process"; -import { getDiForUnitTesting } from "../getDiForUnitTesting"; import extensionLoaderInjectable from "../extension-loader/extension-loader.injectable"; -import { AppPaths } from "../../common/app-paths"; import { runInAction } from "mobx"; import updateExtensionsStateInjectable from "../extension-loader/update-extensions-state/update-extensions-state.injectable"; +import { getDisForUnitTesting } from "../../test-utils/get-dis-for-unit-testing"; +import mockFs from "mock-fs"; console = new Console(stdout, stderr); @@ -122,21 +122,26 @@ jest.mock( }, ); -// TODO: Remove explicit global initialization at unclear time window -AppPaths.init(); - describe("ExtensionLoader", () => { let extensionLoader: ExtensionLoader; let updateExtensionStateMock: jest.Mock; - beforeEach(() => { - const di = getDiForUnitTesting(); + beforeEach(async () => { + const dis = getDisForUnitTesting({ doGeneralOverrides: true }); + + mockFs(); updateExtensionStateMock = jest.fn(); - di.override(updateExtensionsStateInjectable, () => updateExtensionStateMock); + dis.mainDi.override(updateExtensionsStateInjectable, () => updateExtensionStateMock); - extensionLoader = di.inject(extensionLoaderInjectable); + await dis.runSetups(); + + extensionLoader = dis.mainDi.inject(extensionLoaderInjectable); + }); + + afterEach(() => { + mockFs.restore(); }); it("renderer updates extension after ipc broadcast", async done => { diff --git a/src/extensions/common-api/event-bus.ts b/src/extensions/common-api/event-bus.ts index 1d296e2992..30654a0568 100644 --- a/src/extensions/common-api/event-bus.ts +++ b/src/extensions/common-api/event-bus.ts @@ -19,5 +19,5 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -export { appEventBus } from "../../common/event-bus"; -export type { AppEvent } from "../../common/event-bus"; +export { appEventBus } from "../../common/app-event-bus/event-bus"; +export type { AppEvent } from "../../common/app-event-bus/event-bus"; diff --git a/src/extensions/extension-discovery/extension-discovery.injectable.ts b/src/extensions/extension-discovery/extension-discovery.injectable.ts index e0e86d6192..ecf9769ea0 100644 --- a/src/extensions/extension-discovery/extension-discovery.injectable.ts +++ b/src/extensions/extension-discovery/extension-discovery.injectable.ts @@ -26,12 +26,9 @@ import isCompatibleExtensionInjectable from "./is-compatible-extension/is-compat import isCompatibleBundledExtensionInjectable from "./is-compatible-bundled-extension/is-compatible-bundled-extension.injectable"; import extensionsStoreInjectable from "../extensions-store/extensions-store.injectable"; import extensionInstallationStateStoreInjectable from "../extension-installation-state-store/extension-installation-state-store.injectable"; -import installExtensionInjectable - from "../extension-installer/install-extension/install-extension.injectable"; -import extensionPackageRootDirectoryInjectable - from "../extension-installer/extension-package-root-directory/extension-package-root-directory.injectable"; -import installExtensionsInjectable - from "../extension-installer/install-extensions/install-extensions.injectable"; +import installExtensionInjectable from "../extension-installer/install-extension/install-extension.injectable"; +import extensionPackageRootDirectoryInjectable from "../extension-installer/extension-package-root-directory/extension-package-root-directory.injectable"; +import installExtensionsInjectable from "../extension-installer/install-extensions/install-extensions.injectable"; const extensionDiscoveryInjectable = getInjectable({ instantiate: (di) => @@ -51,7 +48,10 @@ const extensionDiscoveryInjectable = getInjectable({ installExtension: di.inject(installExtensionInjectable), installExtensions: di.inject(installExtensionsInjectable), - extensionPackageRootDirectory: di.inject(extensionPackageRootDirectoryInjectable), + + extensionPackageRootDirectory: di.inject( + extensionPackageRootDirectoryInjectable, + ), }), lifecycle: lifecycleEnum.singleton, diff --git a/src/extensions/extension-discovery/extension-discovery.test.ts b/src/extensions/extension-discovery/extension-discovery.test.ts index f9665127aa..7eb648e312 100644 --- a/src/extensions/extension-discovery/extension-discovery.test.ts +++ b/src/extensions/extension-discovery/extension-discovery.test.ts @@ -25,11 +25,10 @@ import path from "path"; import type { ExtensionDiscovery } from "./extension-discovery"; import os from "os"; import { Console } from "console"; -import { AppPaths } from "../../common/app-paths"; -import { getDiForUnitTesting } from "../getDiForUnitTesting"; import extensionDiscoveryInjectable from "./extension-discovery.injectable"; import extensionPackageRootDirectoryInjectable from "../extension-installer/extension-package-root-directory/extension-package-root-directory.injectable"; import installExtensionInjectable from "../extension-installer/install-extension/install-extension.injectable"; +import { getDiForUnitTesting } from "../../main/getDiForUnitTesting"; jest.setTimeout(60_000); @@ -53,16 +52,22 @@ jest.mock("electron", () => ({ }, })); -AppPaths.init(); - console = new Console(process.stdout, process.stderr); // fix mockFS const mockedWatch = watch as jest.MockedFunction; describe("ExtensionDiscovery", () => { let extensionDiscovery: ExtensionDiscovery; - beforeEach(() => { - const di = getDiForUnitTesting(); + beforeEach(async () => { + const di = getDiForUnitTesting({ doGeneralOverrides: true }); + + mockFs({ + [`${os.homedir()}/.k8slens/extensions/my-extension/package.json`]: + JSON.stringify({ + name: "my-extension", + }), + }); + di.override(installExtensionInjectable, () => () => Promise.resolve()); @@ -71,70 +76,62 @@ describe("ExtensionDiscovery", () => { () => "some-extension-packages-root", ); + await di.runSetups(); + extensionDiscovery = di.inject(extensionDiscoveryInjectable); }); - describe("with mockFs", () => { - beforeEach(() => { - mockFs({ - [`${os.homedir()}/.k8slens/extensions/my-extension/package.json`]: - JSON.stringify({ - name: "my-extension", - }), - }); - }); + afterEach(() => { + mockFs.restore(); + }); - afterEach(() => { - mockFs.restore(); - }); - it("emits add for added extension", async (done) => { - let addHandler: (filePath: string) => void; + it("emits add for added extension", async (done) => { + let addHandler: (filePath: string) => void; - const mockWatchInstance: any = { - on: jest.fn((event: string, handler: typeof addHandler) => { - if (event === "add") { - addHandler = handler; - } + const mockWatchInstance: any = { + on: jest.fn((event: string, handler: typeof addHandler) => { + if (event === "add") { + addHandler = handler; + } - return mockWatchInstance; - }), - }; + return mockWatchInstance; + }), + }; - mockedWatch.mockImplementationOnce(() => mockWatchInstance as any); + mockedWatch.mockImplementationOnce(() => mockWatchInstance as any); - // Need to force isLoaded to be true so that the file watching is started - extensionDiscovery.isLoaded = true; + // Need to force isLoaded to be true so that the file watching is started + extensionDiscovery.isLoaded = true; - await extensionDiscovery.watchExtensions(); + await extensionDiscovery.watchExtensions(); - extensionDiscovery.events.on("add", (extension) => { - expect(extension).toEqual({ - absolutePath: expect.any(String), - id: path.normalize( - "some-extension-packages-root/node_modules/my-extension/package.json", - ), - isBundled: false, - isEnabled: false, - isCompatible: false, - manifest: { - name: "my-extension", - }, - manifestPath: path.normalize( - "some-extension-packages-root/node_modules/my-extension/package.json", - ), - }); - - done(); - }); - - addHandler( - path.join( - extensionDiscovery.localFolderPath, - "/my-extension/package.json", + extensionDiscovery.events.on("add", (extension) => { + expect(extension).toEqual({ + absolutePath: expect.any(String), + id: path.normalize( + "some-extension-packages-root/node_modules/my-extension/package.json", ), - ); + isBundled: false, + isEnabled: false, + isCompatible: false, + manifest: { + name: "my-extension", + }, + manifestPath: path.normalize( + "some-extension-packages-root/node_modules/my-extension/package.json", + ), + }); + + done(); }); + + addHandler( + path.join( + extensionDiscovery.localFolderPath, + "/my-extension/package.json", + ), + ); }); it("doesn't emit add for added file under extension", async (done) => { diff --git a/src/extensions/extension-installer/extension-installer.injectable.ts b/src/extensions/extension-installer/extension-installer.injectable.ts index 8d3cb8942c..390f751e7e 100644 --- a/src/extensions/extension-installer/extension-installer.injectable.ts +++ b/src/extensions/extension-installer/extension-installer.injectable.ts @@ -20,9 +20,16 @@ */ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { ExtensionInstaller } from "./extension-installer"; +import extensionPackageRootDirectoryInjectable from "./extension-package-root-directory/extension-package-root-directory.injectable"; const extensionInstallerInjectable = getInjectable({ - instantiate: () => new ExtensionInstaller(), + instantiate: (di) => + new ExtensionInstaller({ + extensionPackageRootDirectory: di.inject( + extensionPackageRootDirectoryInjectable, + ), + }), + lifecycle: lifecycleEnum.singleton, }); diff --git a/src/extensions/extension-installer/extension-installer.ts b/src/extensions/extension-installer/extension-installer.ts index 234e476b6d..84f096afab 100644 --- a/src/extensions/extension-installer/extension-installer.ts +++ b/src/extensions/extension-installer/extension-installer.ts @@ -24,20 +24,21 @@ import child_process from "child_process"; import fs from "fs-extra"; import path from "path"; import logger from "../../main/logger"; -import { extensionPackagesRoot } from "../extension-loader"; import type { PackageJson } from "type-fest"; const logModule = "[EXTENSION-INSTALLER]"; +interface Dependencies { + extensionPackageRootDirectory: string +} + /** * Installs dependencies for extensions */ export class ExtensionInstaller { private installLock = new AwaitLock(); - get extensionPackagesRoot() { - return extensionPackagesRoot(); - } + constructor(private dependencies: Dependencies) {} get npmPath() { return __non_webpack_require__.resolve("npm/bin/npm-cli"); @@ -46,7 +47,7 @@ export class ExtensionInstaller { /** * Write package.json to the file system and execute npm install for it. */ - async installPackages(packageJsonPath: string, packagesJson: PackageJson): Promise { + installPackages = async (packageJsonPath: string, packagesJson: PackageJson): Promise => { // Mutual exclusion to install packages in sequence await this.installLock.acquireAsync(); @@ -56,34 +57,34 @@ export class ExtensionInstaller { mode: 0o600, }); - logger.info(`${logModule} installing dependencies at ${extensionPackagesRoot()}`); + logger.info(`${logModule} installing dependencies at ${this.dependencies.extensionPackageRootDirectory}`); await this.npm(["install", "--no-audit", "--only=prod", "--prefer-offline", "--no-package-lock"]); - logger.info(`${logModule} dependencies installed at ${extensionPackagesRoot()}`); + logger.info(`${logModule} dependencies installed at ${this.dependencies.extensionPackageRootDirectory}`); } finally { this.installLock.release(); } - } + }; /** * Install single package using npm */ - async installPackage(name: string): Promise { + installPackage = async (name: string): Promise => { // Mutual exclusion to install packages in sequence await this.installLock.acquireAsync(); try { - logger.info(`${logModule} installing package from ${name} to ${extensionPackagesRoot()}`); + logger.info(`${logModule} installing package from ${name} to ${this.dependencies.extensionPackageRootDirectory}`); await this.npm(["install", "--no-audit", "--only=prod", "--prefer-offline", "--no-package-lock", "--no-save", name]); - logger.info(`${logModule} package ${name} installed to ${extensionPackagesRoot()}`); + logger.info(`${logModule} package ${name} installed to ${this.dependencies.extensionPackageRootDirectory}`); } finally { this.installLock.release(); } - } + }; private npm(args: string[]): Promise { return new Promise((resolve, reject) => { const child = child_process.fork(this.npmPath, args, { - cwd: extensionPackagesRoot(), + cwd: this.dependencies.extensionPackageRootDirectory, silent: true, env: {}, }); diff --git a/src/extensions/extension-installer/extension-package-root-directory/extension-package-root-directory.injectable.ts b/src/extensions/extension-installer/extension-package-root-directory/extension-package-root-directory.injectable.ts index 89deea93e5..a5ee898706 100644 --- a/src/extensions/extension-installer/extension-package-root-directory/extension-package-root-directory.injectable.ts +++ b/src/extensions/extension-installer/extension-package-root-directory/extension-package-root-directory.injectable.ts @@ -19,12 +19,12 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; -import extensionInstallerInjectable from "../extension-installer.injectable"; +import directoryForUserDataInjectable + from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; const extensionPackageRootDirectoryInjectable = getInjectable({ - instantiate: (di) => - di.inject(extensionInstallerInjectable).extensionPackagesRoot, - + instantiate: (di) => di.inject(directoryForUserDataInjectable), + lifecycle: lifecycleEnum.singleton, }); diff --git a/src/extensions/extension-installer/install-extensions/install-extensions.injectable.ts b/src/extensions/extension-installer/install-extensions/install-extensions.injectable.ts index 857f60568b..3c370b4690 100644 --- a/src/extensions/extension-installer/install-extensions/install-extensions.injectable.ts +++ b/src/extensions/extension-installer/install-extensions/install-extensions.injectable.ts @@ -23,7 +23,7 @@ import extensionInstallerInjectable from "../extension-installer.injectable"; const installExtensionsInjectable = getInjectable({ instantiate: (di) => di.inject(extensionInstallerInjectable).installPackages, - + lifecycle: lifecycleEnum.singleton, }); diff --git a/src/extensions/extension-loader/create-extension-instance/create-extension-instance.injectable.ts b/src/extensions/extension-loader/create-extension-instance/create-extension-instance.injectable.ts new file mode 100644 index 0000000000..b507be0c36 --- /dev/null +++ b/src/extensions/extension-loader/create-extension-instance/create-extension-instance.injectable.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { createExtensionInstance } from "./create-extension-instance"; +import fileSystemProvisionerStoreInjectable from "./file-system-provisioner-store/file-system-provisioner-store.injectable"; + +const createExtensionInstanceInjectable = getInjectable({ + instantiate: (di) => createExtensionInstance({ + fileSystemProvisionerStore: di.inject(fileSystemProvisionerStoreInjectable), + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default createExtensionInstanceInjectable; diff --git a/src/extensions/extension-loader/create-extension-instance/create-extension-instance.ts b/src/extensions/extension-loader/create-extension-instance/create-extension-instance.ts new file mode 100644 index 0000000000..105c9f6872 --- /dev/null +++ b/src/extensions/extension-loader/create-extension-instance/create-extension-instance.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import type { LensExtensionConstructor } from "../../lens-extension"; +import type { InstalledExtension } from "../../extension-discovery/extension-discovery"; +import { + LensExtensionDependencies, + setLensExtensionDependencies, +} from "../../lens-extension-set-dependencies"; + +export const createExtensionInstance = + (dependencies: LensExtensionDependencies) => + (ExtensionClass: LensExtensionConstructor, extension: InstalledExtension) => { + const instance = new ExtensionClass(extension); + + instance[setLensExtensionDependencies](dependencies); + + return instance; + }; diff --git a/src/extensions/extension-loader/create-extension-instance/file-system-provisioner-store/directory-for-extension-data/directory-for-extension-data.injectable.ts b/src/extensions/extension-loader/create-extension-instance/file-system-provisioner-store/directory-for-extension-data/directory-for-extension-data.injectable.ts new file mode 100644 index 0000000000..cd2a63085a --- /dev/null +++ b/src/extensions/extension-loader/create-extension-instance/file-system-provisioner-store/directory-for-extension-data/directory-for-extension-data.injectable.ts @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import path from "path"; +import directoryForUserDataInjectable from "../../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; + +const directoryForExtensionDataInjectable = getInjectable({ + instantiate: (di) => + path.join(di.inject(directoryForUserDataInjectable), "extension_data"), + + lifecycle: lifecycleEnum.singleton, +}); + +export default directoryForExtensionDataInjectable; diff --git a/src/extensions/extension-loader/create-extension-instance/file-system-provisioner-store/file-system-provisioner-store.injectable.ts b/src/extensions/extension-loader/create-extension-instance/file-system-provisioner-store/file-system-provisioner-store.injectable.ts new file mode 100644 index 0000000000..4ed7728e3b --- /dev/null +++ b/src/extensions/extension-loader/create-extension-instance/file-system-provisioner-store/file-system-provisioner-store.injectable.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { FileSystemProvisionerStore } from "./file-system-provisioner-store"; +import directoryForExtensionDataInjectable from "./directory-for-extension-data/directory-for-extension-data.injectable"; + +const fileSystemProvisionerStoreInjectable = getInjectable({ + instantiate: (di) => + FileSystemProvisionerStore.createInstance({ + directoryForExtensionData: di.inject( + directoryForExtensionDataInjectable, + ), + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default fileSystemProvisionerStoreInjectable; diff --git a/src/main/extension-filesystem.ts b/src/extensions/extension-loader/create-extension-instance/file-system-provisioner-store/file-system-provisioner-store.ts similarity index 86% rename from src/main/extension-filesystem.ts rename to src/extensions/extension-loader/create-extension-instance/file-system-provisioner-store/file-system-provisioner-store.ts index cc49a0b706..5cbb05cb43 100644 --- a/src/main/extension-filesystem.ts +++ b/src/extensions/extension-loader/create-extension-instance/file-system-provisioner-store/file-system-provisioner-store.ts @@ -24,24 +24,28 @@ import { SHA256 } from "crypto-js"; import fse from "fs-extra"; import { action, makeObservable, observable } from "mobx"; import path from "path"; -import { BaseStore } from "../common/base-store"; -import type { LensExtensionId } from "../extensions/lens-extension"; -import { toJS } from "../common/utils"; -import { AppPaths } from "../common/app-paths"; +import { BaseStore } from "../../../../common/base-store"; +import type { LensExtensionId } from "../../../lens-extension"; +import { toJS } from "../../../../common/utils"; interface FSProvisionModel { extensions: Record; // extension names to paths } -export class FilesystemProvisionerStore extends BaseStore { +interface Dependencies { + directoryForExtensionData: string +} + +export class FileSystemProvisionerStore extends BaseStore { readonly displayName = "FilesystemProvisionerStore"; registeredExtensions = observable.map(); - constructor() { + constructor(private dependencies: Dependencies) { super({ configName: "lens-filesystem-provisioner-store", accessPropertiesByDotNotation: false, // To make dots safe in cluster context names }); + makeObservable(this); this.load(); } @@ -56,7 +60,8 @@ export class FilesystemProvisionerStore extends BaseStore { if (!this.registeredExtensions.has(extensionName)) { const salt = randomBytes(32).toString("hex"); const hashedName = SHA256(`${extensionName}/${salt}`).toString(); - const dirPath = path.resolve(AppPaths.get("userData"), "extension_data", hashedName); + + const dirPath = path.resolve(this.dependencies.directoryForExtensionData, hashedName); this.registeredExtensions.set(extensionName, dirPath); } diff --git a/src/extensions/extension-loader/extension-loader.injectable.ts b/src/extensions/extension-loader/extension-loader.injectable.ts index 66f613f8e0..87014dd9ee 100644 --- a/src/extensions/extension-loader/extension-loader.injectable.ts +++ b/src/extensions/extension-loader/extension-loader.injectable.ts @@ -21,11 +21,14 @@ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { ExtensionLoader } from "./extension-loader"; import updateExtensionsStateInjectable from "./update-extensions-state/update-extensions-state.injectable"; +import createExtensionInstanceInjectable + from "./create-extension-instance/create-extension-instance.injectable"; const extensionLoaderInjectable = getInjectable({ instantiate: (di) => new ExtensionLoader({ updateExtensionsState: di.inject(updateExtensionsStateInjectable), + createExtensionInstance: di.inject(createExtensionInstanceInjectable), }), lifecycle: lifecycleEnum.singleton, diff --git a/src/extensions/extension-loader/extension-loader.ts b/src/extensions/extension-loader/extension-loader.ts index bee23ccd0e..a076a895c6 100644 --- a/src/extensions/extension-loader/extension-loader.ts +++ b/src/extensions/extension-loader/extension-loader.ts @@ -24,7 +24,6 @@ import { EventEmitter } from "events"; import { isEqual } from "lodash"; import { action, computed, makeObservable, observable, observe, reaction, when } from "mobx"; import path from "path"; -import { AppPaths } from "../../common/app-paths"; import { broadcastMessage, ipcMainOn, ipcRendererOn, requestMain, ipcMainHandle } from "../../common/ipc"; import { Disposer, toJS } from "../../common/utils"; import logger from "../../main/logger"; @@ -35,14 +34,16 @@ import type { LensRendererExtension } from "../lens-renderer-extension"; import * as registries from "../registries"; import type { LensExtensionState } from "../extensions-store/extensions-store"; -export function extensionPackagesRoot() { - return path.join(AppPaths.get("userData")); -} - const logModule = "[EXTENSIONS-LOADER]"; interface Dependencies { updateExtensionsState: (extensionsState: Record) => void + createExtensionInstance: (ExtensionClass: LensExtensionConstructor, extension: InstalledExtension) => LensExtension, +} + +export interface ExtensionLoading { + isBundled: boolean, + loaded: Promise } /** @@ -81,6 +82,7 @@ export class ExtensionLoader { constructor(protected dependencies : Dependencies) { makeObservable(this); + observe(this.instances, change => { switch (change.type) { case "add": @@ -260,7 +262,7 @@ export class ExtensionLoader { this.autoInitExtensions(() => Promise.resolve([])); } - loadOnClusterManagerRenderer() { + loadOnClusterManagerRenderer = () => { logger.debug(`${logModule}: load on main renderer (cluster manager)`); return this.autoInitExtensions(async (extension: LensRendererExtension) => { @@ -286,9 +288,9 @@ export class ExtensionLoader { return removeItems; }); - } + }; - loadOnClusterRenderer(entity: KubernetesCluster) { + loadOnClusterRenderer = (entity: KubernetesCluster) => { logger.debug(`${logModule}: load on cluster renderer (dashboard)`); this.autoInitExtensions(async (extension: LensRendererExtension) => { @@ -316,12 +318,12 @@ export class ExtensionLoader { return removeItems; }); - } + }; protected autoInitExtensions(register: (ext: LensExtension) => Promise) { - const loadingExtensions: { isBundled: boolean, loaded: Promise }[] = []; + const loadingExtensions: ExtensionLoading[] = []; - reaction(() => this.toJSON(), installedExtensions => { + reaction(() => this.toJSON(), async installedExtensions => { for (const [extId, extension] of installedExtensions) { const alreadyInit = this.instances.has(extId) || this.nonInstancesByName.has(extension.manifest.name); @@ -334,7 +336,10 @@ export class ExtensionLoader { continue; } - const instance = new LensExtensionClass(extension); + const instance = this.dependencies.createExtensionInstance( + LensExtensionClass, + extension, + ); const loaded = instance.enable(register).catch((err) => { logger.error(`${logModule}: failed to enable`, { ext: extension, err }); diff --git a/src/extensions/extension-packages-root/extension-packages-root.injectable.ts b/src/extensions/extension-packages-root/extension-packages-root.injectable.ts new file mode 100644 index 0000000000..a926660e68 --- /dev/null +++ b/src/extensions/extension-packages-root/extension-packages-root.injectable.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import directoryForUserDataInjectable + from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; + +const extensionPackagesRootInjectable = getInjectable({ + instantiate: (di) => di.inject(directoryForUserDataInjectable), + lifecycle: lifecycleEnum.singleton, +}); + +export default extensionPackagesRootInjectable; diff --git a/src/extensions/extensions-store/extensions-store.ts b/src/extensions/extensions-store/extensions-store.ts index edbf3ce018..07bb7bed49 100644 --- a/src/extensions/extensions-store/extensions-store.ts +++ b/src/extensions/extensions-store/extensions-store.ts @@ -59,9 +59,9 @@ export class ExtensionsStore extends BaseStore { } @action - mergeState(extensionsState: Record) { + mergeState = (extensionsState: Record) => { this.state.merge(extensionsState); - } + }; @action protected fromStore({ extensions }: LensExtensionsStoreModel) { diff --git a/src/extensions/lens-extension-set-dependencies.ts b/src/extensions/lens-extension-set-dependencies.ts new file mode 100644 index 0000000000..500b198354 --- /dev/null +++ b/src/extensions/lens-extension-set-dependencies.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import type { FileSystemProvisionerStore } from "./extension-loader/create-extension-instance/file-system-provisioner-store/file-system-provisioner-store"; + +// This symbol encapsulates setting of dependencies to only happen locally in Lens Core +// and not by e.g. authors of extensions +export const setLensExtensionDependencies = Symbol("set-lens-extension-dependencies"); + +export interface LensExtensionDependencies { + fileSystemProvisionerStore: FileSystemProvisionerStore +} diff --git a/src/extensions/lens-extension.ts b/src/extensions/lens-extension.ts index 0c6026d22d..2c32b984a6 100644 --- a/src/extensions/lens-extension.ts +++ b/src/extensions/lens-extension.ts @@ -21,11 +21,14 @@ import type { InstalledExtension } from "./extension-discovery/extension-discovery"; import { action, observable, makeObservable, computed } from "mobx"; -import { FilesystemProvisionerStore } from "../main/extension-filesystem"; import logger from "../main/logger"; import type { ProtocolHandlerRegistration } from "./registries"; import type { PackageJson } from "type-fest"; import { Disposer, disposer } from "../common/utils"; +import { + LensExtensionDependencies, + setLensExtensionDependencies, +} from "./lens-extension-set-dependencies"; export type LensExtensionId = string; // path to manifest (package.json) export type LensExtensionConstructor = new (...args: ConstructorParameters) => LensExtension; @@ -75,6 +78,12 @@ export class LensExtension { return this.manifest.description; } + private dependencies: LensExtensionDependencies; + + [setLensExtensionDependencies] = (dependencies: LensExtensionDependencies) => { + this.dependencies = dependencies; + }; + /** * getExtensionFileFolder returns the path to an already created folder. This * folder is for the sole use of this extension. @@ -83,7 +92,7 @@ export class LensExtension { * folder name. */ async getExtensionFileFolder(): Promise { - return FilesystemProvisionerStore.getInstance().requestDirectory(this.id); + return this.dependencies.fileSystemProvisionerStore.requestDirectory(this.id); } @action diff --git a/src/extensions/registries/__tests__/page-registry.test.ts b/src/extensions/registries/__tests__/page-registry.test.ts index 00d0096197..51d82da23c 100644 --- a/src/extensions/registries/__tests__/page-registry.test.ts +++ b/src/extensions/registries/__tests__/page-registry.test.ts @@ -26,10 +26,10 @@ import React from "react"; import fse from "fs-extra"; import { Console } from "console"; import { stderr, stdout } from "process"; -import { TerminalStore } from "../../../renderer/components/dock/terminal.store"; import { ThemeStore } from "../../../renderer/theme.store"; import { UserStore } from "../../../common/user-store"; -import { AppPaths } from "../../../common/app-paths"; +import { getDisForUnitTesting } from "../../../test-utils/get-dis-for-unit-testing"; +import mockFs from "mock-fs"; jest.mock("electron", () => ({ app: { @@ -47,14 +47,18 @@ jest.mock("electron", () => ({ }, })); -AppPaths.init(); - console = new Console(stdout, stderr); let ext: LensExtension = null; describe("page registry tests", () => { beforeEach(async () => { + const dis = getDisForUnitTesting({ doGeneralOverrides: true }); + + mockFs(); + + await dis.runSetups(); + ext = new LensExtension({ manifest: { name: "foo-bar", @@ -69,7 +73,6 @@ describe("page registry tests", () => { }); UserStore.createInstance(); ThemeStore.createInstance(); - TerminalStore.createInstance(); ClusterPageRegistry.createInstance(); GlobalPageRegistry.createInstance().add({ id: "page-with-params", @@ -105,10 +108,10 @@ describe("page registry tests", () => { afterEach(() => { GlobalPageRegistry.resetInstance(); ClusterPageRegistry.resetInstance(); - TerminalStore.resetInstance(); ThemeStore.resetInstance(); UserStore.resetInstance(); fse.remove("tmp"); + mockFs.restore(); }); describe("getPageUrl", () => { diff --git a/src/extensions/renderer-api/components.ts b/src/extensions/renderer-api/components.ts index 7173b592b3..b4c714035d 100644 --- a/src/extensions/renderer-api/components.ts +++ b/src/extensions/renderer-api/components.ts @@ -71,5 +71,12 @@ export * from "../../renderer/components/+events/kube-event-details"; // specific exports export * from "../../renderer/components/status-brick"; -export { terminalStore, createTerminalTab, TerminalStore } from "../../renderer/components/dock/terminal.store"; -export { logTabStore } from "../../renderer/components/dock/log-tab.store"; + +// Mikko +// export { terminalStore, TerminalStore } from "../../renderer/components/dock/terminal-store/terminal.store"; +// +// // Mikko +// export { createTerminalTab } from "../../renderer/components/dock/terminal-store/terminal.store"; +// +// // Mikko +// export { logTabStore } from "../../renderer/components/dock/log-tab-store/log-tab.store"; diff --git a/src/extensions/renderer-api/k8s-api.ts b/src/extensions/renderer-api/k8s-api.ts index ecae80fb5a..6ce8e62074 100644 --- a/src/extensions/renderer-api/k8s-api.ts +++ b/src/extensions/renderer-api/k8s-api.ts @@ -86,7 +86,7 @@ export type { NetworkPolicyStore } from "../../renderer/components/+network-poli export type { PersistentVolumesStore } from "../../renderer/components/+storage-volumes/volumes.store"; export type { VolumeClaimStore } from "../../renderer/components/+storage-volume-claims/volume-claim.store"; export type { StorageClassStore } from "../../renderer/components/+storage-classes/storage-class.store"; -export type { NamespaceStore } from "../../renderer/components/+namespaces/namespace.store"; +export type { NamespaceStore } from "../../renderer/components/+namespaces/namespace-store/namespace.store"; export type { ServiceAccountsStore } from "../../renderer/components/+user-management/+service-accounts/store"; export type { RolesStore } from "../../renderer/components/+user-management/+roles/store"; export type { RoleBindingsStore } from "../../renderer/components/+user-management/+role-bindings/store"; diff --git a/src/main/__test__/cluster.test.ts b/src/main/__test__/cluster.test.ts index ffd77da3d2..1379937c49 100644 --- a/src/main/__test__/cluster.test.ts +++ b/src/main/__test__/cluster.test.ts @@ -45,25 +45,29 @@ jest.mock("winston", () => ({ })); jest.mock("../../common/ipc"); -jest.mock("../context-handler"); jest.mock("request"); jest.mock("request-promise-native"); import { Console } from "console"; import mockFs from "mock-fs"; -import { Cluster } from "../cluster"; -import { Kubectl } from "../kubectl"; +import type { Cluster } from "../../common/cluster/cluster"; +import { Kubectl } from "../kubectl/kubectl"; +import { getDiForUnitTesting } from "../getDiForUnitTesting"; +import type { ClusterModel } from "../../common/cluster-types"; +import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token"; console = new Console(process.stdout, process.stderr); // fix mockFS describe("create clusters", () => { - beforeEach(() => { + let cluster: Cluster; + let createCluster: (model: ClusterModel) => Cluster; + + beforeEach(async () => { jest.clearAllMocks(); - }); - let c: Cluster; + const di = getDiForUnitTesting({ doGeneralOverrides: true }); + - beforeEach(() => { const mockOpts = { "minikube-config.yml": JSON.stringify({ apiVersion: "v1", @@ -89,8 +93,14 @@ describe("create clusters", () => { }; mockFs(mockOpts); + + await di.runSetups(); + + createCluster = di.inject(createClusterInjectionToken); + jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValue(Promise.resolve(true)); - c = new Cluster({ + + cluster = createCluster({ id: "foo", contextName: "minikube", kubeConfigPath: "minikube-config.yml", @@ -102,48 +112,39 @@ describe("create clusters", () => { }); it("should be able to create a cluster from a cluster model and apiURL should be decoded", () => { - expect(c.apiUrl).toBe("https://192.168.64.3:8443"); + expect(cluster.apiUrl).toBe("https://192.168.64.3:8443"); }); it("reconnect should not throw if contextHandler is missing", () => { - expect(() => c.reconnect()).not.toThrowError(); + expect(() => cluster.reconnect()).not.toThrowError(); }); it("disconnect should not throw if contextHandler is missing", () => { - expect(() => c.disconnect()).not.toThrowError(); + expect(() => cluster.disconnect()).not.toThrowError(); }); it("activating cluster should try to connect to cluster and do a refresh", async () => { - - const c = new class extends Cluster { - // only way to mock protected methods, without these we leak promises - protected bindEvents() { - return; - } - async ensureKubectl() { - return Promise.resolve(null); - } - }({ + const cluster = createCluster({ id: "foo", contextName: "minikube", kubeConfigPath: "minikube-config.yml", }); - c.contextHandler = { + cluster.contextHandler = { ensureServer: jest.fn(), stopServer: jest.fn(), } as any; - jest.spyOn(c, "reconnect"); - jest.spyOn(c, "canI"); - jest.spyOn(c, "refreshConnectionStatus"); + jest.spyOn(cluster, "reconnect"); + jest.spyOn(cluster, "canI"); + jest.spyOn(cluster, "refreshConnectionStatus"); - await c.activate(); + await cluster.activate(); - expect(c.reconnect).toBeCalled(); - expect(c.refreshConnectionStatus).toBeCalled(); + expect(cluster.reconnect).toBeCalled(); + expect(cluster.refreshConnectionStatus).toBeCalled(); - c.disconnect(); + cluster.disconnect(); jest.resetAllMocks(); }); }); diff --git a/src/main/__test__/context-handler.test.ts b/src/main/__test__/context-handler.test.ts index 330195f22e..03f54a3a2a 100644 --- a/src/main/__test__/context-handler.test.ts +++ b/src/main/__test__/context-handler.test.ts @@ -20,10 +20,12 @@ */ import { UserStore } from "../../common/user-store"; -import { ContextHandler } from "../context-handler"; +import type { ContextHandler } from "../context-handler/context-handler"; import { PrometheusProvider, PrometheusProviderRegistry, PrometheusService } from "../prometheus"; import mockFs from "mock-fs"; -import { AppPaths } from "../../common/app-paths"; +import { getDiForUnitTesting } from "../getDiForUnitTesting"; +import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable"; +import type { Cluster } from "../../common/cluster/cluster"; jest.mock("electron", () => ({ app: { @@ -77,25 +79,28 @@ class TestProvider extends PrometheusProvider { } } -function getHandler() { - return new ContextHandler(({ - getProxyKubeconfig: (): any => ({ - makeApiClient: (): any => undefined, - }), - apiUrl: "http://localhost:81", - }) as any); -} - -AppPaths.init(); +const clusterStub = { + getProxyKubeconfig: (): any => ({ + makeApiClient: (): any => undefined, + }), + apiUrl: "http://localhost:81", +} as Cluster; describe("ContextHandler", () => { - beforeEach(() => { + let createContextHandler: (cluster: Cluster) => ContextHandler; + + beforeEach(async () => { + const di = getDiForUnitTesting({ doGeneralOverrides: true }); + mockFs({ "tmp": {}, }); + await di.runSetups(); + + createContextHandler = di.inject(createContextHandlerInjectable); + PrometheusProviderRegistry.createInstance(); - UserStore.createInstance(); }); afterEach(() => { @@ -124,7 +129,12 @@ describe("ContextHandler", () => { reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success)); } - expect(() => (getHandler() as any).getPrometheusService()).rejects.toBeDefined(); + expect(() => { + // TODO: Unit test shouldn't access protected or private methods + const contextHandler = createContextHandler(clusterStub) as any; + + return contextHandler.getPrometheusService(); + }).rejects.toBeDefined(); }); it.each([ @@ -150,7 +160,10 @@ describe("ContextHandler", () => { reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success)); } - const service = await (getHandler() as any).getPrometheusService(); + // TODO: Unit test shouldn't access protected or private methods + const contextHandler = createContextHandler(clusterStub) as any; + + const service = await contextHandler.getPrometheusService(); expect(service.id === `id_${failures}`); }); @@ -178,7 +191,10 @@ describe("ContextHandler", () => { reg.registerProvider(new TestProvider(`id_${count++}`, serviceResult)); } - const service = await (getHandler() as any).getPrometheusService(); + // TODO: Unit test shouldn't access protected or private methods + const contextHandler = createContextHandler(clusterStub) as any; + + const service = await contextHandler.getPrometheusService(); expect(service.id === "id_0"); }); @@ -211,8 +227,11 @@ describe("ContextHandler", () => { for (let i = 0; i < afterSuccesses; i += 1) { reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success)); } - - const service = await (getHandler() as any).getPrometheusService(); + + // TODO: Unit test shouldn't access protected or private methods + const contextHandler = createContextHandler(clusterStub) as any; + + const service = await contextHandler.getPrometheusService(); expect(service.id === "id_0"); }); @@ -224,8 +243,11 @@ describe("ContextHandler", () => { reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Failure)); reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success)); reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success)); + + // TODO: Unit test shouldn't access protected or private methods + const contextHandler = createContextHandler(clusterStub) as any; - const service = await (getHandler() as any).getPrometheusService(); + const service = await contextHandler.getPrometheusService(); expect(service.id).not.toBe("id_2"); }); diff --git a/src/main/__test__/kube-auth-proxy.test.ts b/src/main/__test__/kube-auth-proxy.test.ts index a2ba4cb453..b56250e34f 100644 --- a/src/main/__test__/kube-auth-proxy.test.ts +++ b/src/main/__test__/kube-auth-proxy.test.ts @@ -19,6 +19,8 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +import type { ClusterModel } from "../../common/cluster-types"; + jest.mock("winston", () => ({ format: { colorize: jest.fn(), @@ -48,11 +50,11 @@ jest.mock("../../common/ipc"); jest.mock("child_process"); jest.mock("tcp-port-used"); -import { Cluster } from "../cluster"; -import { KubeAuthProxy } from "../kube-auth-proxy"; +import type { Cluster } from "../../common/cluster/cluster"; +import type { KubeAuthProxy } from "../kube-auth-proxy/kube-auth-proxy"; import { broadcastMessage } from "../../common/ipc"; import { ChildProcess, spawn } from "child_process"; -import { bundledKubectlPath, Kubectl } from "../kubectl"; +import { bundledKubectlPath, Kubectl } from "../kubectl/kubectl"; import { mock, MockProxy } from "jest-mock-extended"; import { waitUntilUsed } from "tcp-port-used"; import { EventEmitter, Readable } from "stream"; @@ -60,7 +62,9 @@ import { UserStore } from "../../common/user-store"; import { Console } from "console"; import { stdout, stderr } from "process"; import mockFs from "mock-fs"; -import { AppPaths } from "../../common/app-paths"; +import { getDiForUnitTesting } from "../getDiForUnitTesting"; +import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable"; +import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token"; console = new Console(stdout, stderr); @@ -68,25 +72,11 @@ const mockBroadcastIpc = broadcastMessage as jest.MockedFunction; const mockWaitUntilUsed = waitUntilUsed as jest.MockedFunction; -jest.mock("electron", () => ({ - app: { - getVersion: () => "99.99.99", - getName: () => "lens", - setName: jest.fn(), - setPath: jest.fn(), - getPath: () => "tmp", - getLocale: () => "en", - setLoginItemSettings: jest.fn(), - }, - ipcMain: { - on: jest.fn(), - handle: jest.fn(), - }, -})); -AppPaths.init(); - describe("kube auth proxy tests", () => { - beforeEach(() => { + let createCluster: (model: ClusterModel) => Cluster; + let createKubeAuthProxy: (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => KubeAuthProxy; + + beforeEach(async () => { jest.clearAllMocks(); const mockMinikubeConfig = { @@ -115,7 +105,16 @@ describe("kube auth proxy tests", () => { "tmp": {}, }; + const di = getDiForUnitTesting({ doGeneralOverrides: true }); + mockFs(mockMinikubeConfig); + + await di.runSetups(); + + createCluster = di.inject(createClusterInjectionToken); + + createKubeAuthProxy = di.inject(createKubeAuthProxyInjectable); + UserStore.createInstance(); }); @@ -125,7 +124,13 @@ describe("kube auth proxy tests", () => { }); it("calling exit multiple times shouldn't throw", async () => { - const kap = new KubeAuthProxy(new Cluster({ id: "foobar", kubeConfigPath: "minikube-config.yml", contextName: "minikube" }), {}); + const cluster = createCluster({ + id: "foobar", + kubeConfigPath: "minikube-config.yml", + contextName: "minikube", + }); + + const kap = createKubeAuthProxy(cluster, {}); kap.exit(); kap.exit(); @@ -211,9 +216,13 @@ describe("kube auth proxy tests", () => { }); mockWaitUntilUsed.mockReturnValueOnce(Promise.resolve()); - const cluster = new Cluster({ id: "foobar", kubeConfigPath: "minikube-config.yml", contextName: "minikube" }); + const cluster = createCluster({ + id: "foobar", + kubeConfigPath: "minikube-config.yml", + contextName: "minikube", + }); - proxy = new KubeAuthProxy(cluster, {}); + proxy = createKubeAuthProxy(cluster, {}); }); it("should call spawn and broadcast errors", async () => { diff --git a/src/main/__test__/kubeconfig-manager.test.ts b/src/main/__test__/kubeconfig-manager.test.ts index 081beed63e..c0a0086e32 100644 --- a/src/main/__test__/kubeconfig-manager.test.ts +++ b/src/main/__test__/kubeconfig-manager.test.ts @@ -18,6 +18,7 @@ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +import { getDiForUnitTesting } from "../getDiForUnitTesting"; const logger = { silly: jest.fn(), @@ -46,41 +47,28 @@ jest.mock("winston", () => ({ }, })); -import { KubeconfigManager } from "../kubeconfig-manager"; +import { KubeconfigManager } from "../kubeconfig-manager/kubeconfig-manager"; import mockFs from "mock-fs"; -import { Cluster } from "../cluster"; -import type { ContextHandler } from "../context-handler"; +import type { Cluster } from "../../common/cluster/cluster"; import fse from "fs-extra"; import { loadYaml } from "@kubernetes/client-node"; import { Console } from "console"; import * as path from "path"; -import { AppPaths } from "../../common/app-paths"; - -jest.mock("electron", () => ({ - app: { - getVersion: () => "99.99.99", - getName: () => "lens", - setName: jest.fn(), - setPath: jest.fn(), - getPath: () => "tmp", - getLocale: () => "en", - setLoginItemSettings: jest.fn(), - }, - ipcMain: { - on: jest.fn(), - handle: jest.fn(), - }, -})); - -AppPaths.init(); +import createKubeconfigManagerInjectable from "../kubeconfig-manager/create-kubeconfig-manager.injectable"; +import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token"; +import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable"; console = new Console(process.stdout, process.stderr); // fix mockFS describe("kubeconfig manager tests", () => { let cluster: Cluster; - let contextHandler: ContextHandler; + let createKubeconfigManager: (cluster: Cluster) => KubeconfigManager; + + beforeEach(async () => { + const di = getDiForUnitTesting({ doGeneralOverrides: true }); + + di.override(directoryForTempInjectable, () => "some-directory-for-temp"); - beforeEach(() => { const mockOpts = { "minikube-config.yml": JSON.stringify({ apiVersion: "v1", @@ -107,14 +95,22 @@ describe("kubeconfig manager tests", () => { mockFs(mockOpts); - cluster = new Cluster({ + await di.runSetups(); + + const createCluster = di.inject(createClusterInjectionToken); + + createKubeconfigManager = di.inject(createKubeconfigManagerInjectable); + + cluster = createCluster({ id: "foo", contextName: "minikube", kubeConfigPath: "minikube-config.yml", }); - contextHandler = { + + cluster.contextHandler = { ensureServer: () => Promise.resolve(), } as any; + jest.spyOn(KubeconfigManager.prototype, "resolveProxyUrl", "get").mockReturnValue("http://127.0.0.1:9191/foo"); }); @@ -123,10 +119,10 @@ describe("kubeconfig manager tests", () => { }); it("should create 'temp' kube config with proxy", async () => { - const kubeConfManager = new KubeconfigManager(cluster, contextHandler); + const kubeConfManager = createKubeconfigManager(cluster); expect(logger.error).not.toBeCalled(); - expect(await kubeConfManager.getPath()).toBe(`tmp${path.sep}kubeconfig-foo`); + expect(await kubeConfManager.getPath()).toBe(`some-directory-for-temp${path.sep}kubeconfig-foo`); // this causes an intermittent "ENXIO: no such device or address, read" error // const file = await fse.readFile(await kubeConfManager.getPath()); const file = fse.readFileSync(await kubeConfManager.getPath()); @@ -138,7 +134,8 @@ describe("kubeconfig manager tests", () => { }); it("should remove 'temp' kube config on unlink and remove reference from inside class", async () => { - const kubeConfManager = new KubeconfigManager(cluster, contextHandler); + const kubeConfManager = createKubeconfigManager(cluster); + const configPath = await kubeConfManager.getPath(); expect(await fse.pathExists(configPath)).toBe(true); diff --git a/src/main/__test__/router.test.ts b/src/main/__test__/router.test.ts index 278467eca0..9dd87a9bf6 100644 --- a/src/main/__test__/router.test.ts +++ b/src/main/__test__/router.test.ts @@ -19,7 +19,6 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { AppPaths } from "../../common/app-paths"; import { Router } from "../router"; jest.mock("electron", () => ({ @@ -38,8 +37,6 @@ jest.mock("electron", () => ({ }, })); -AppPaths.init(); - describe("Router", () => { it("blocks path traversal attacks", async () => { const response: any = { diff --git a/src/main/app-paths/app-name/app-name.injectable.ts b/src/main/app-paths/app-name/app-name.injectable.ts new file mode 100644 index 0000000000..60edfac709 --- /dev/null +++ b/src/main/app-paths/app-name/app-name.injectable.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import electronAppInjectable from "../get-electron-app-path/electron-app/electron-app.injectable"; + +const appNameInjectable = getInjectable({ + instantiate: (di) => di.inject(electronAppInjectable).name, + lifecycle: lifecycleEnum.singleton, +}); + +export default appNameInjectable; diff --git a/src/main/app-paths/app-paths.injectable.ts b/src/main/app-paths/app-paths.injectable.ts new file mode 100644 index 0000000000..a4a194ea94 --- /dev/null +++ b/src/main/app-paths/app-paths.injectable.ts @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { + DependencyInjectionContainer, + getInjectable, + lifecycleEnum, +} from "@ogre-tools/injectable"; + +import { + appPathsInjectionToken, + appPathsIpcChannel, +} from "../../common/app-paths/app-path-injection-token"; + +import registerChannelInjectable from "./register-channel/register-channel.injectable"; +import { getAppPaths } from "./get-app-paths"; +import getElectronAppPathInjectable from "./get-electron-app-path/get-electron-app-path.injectable"; +import setElectronAppPathInjectable from "./set-electron-app-path/set-electron-app-path.injectable"; +import path from "path"; +import appNameInjectable from "./app-name/app-name.injectable"; +import directoryForIntegrationTestingInjectable from "./directory-for-integration-testing/directory-for-integration-testing.injectable"; + +const appPathsInjectable = getInjectable({ + setup: (di) => { + const directoryForIntegrationTesting = di.inject( + directoryForIntegrationTestingInjectable, + ); + + if (directoryForIntegrationTesting) { + setupPathForAppDataInIntegrationTesting(di, directoryForIntegrationTesting); + } + + setupPathForUserData(di); + registerAppPathsChannel(di); + }, + + instantiate: (di) => + getAppPaths({ getAppPath: di.inject(getElectronAppPathInjectable) }), + + injectionToken: appPathsInjectionToken, + lifecycle: lifecycleEnum.singleton, +}); + +export default appPathsInjectable; + +const registerAppPathsChannel = (di: DependencyInjectionContainer) => { + const registerChannel = di.inject(registerChannelInjectable); + + registerChannel(appPathsIpcChannel, () => di.inject(appPathsInjectable)); +}; + +const setupPathForUserData = (di: DependencyInjectionContainer) => { + const setElectronAppPath = di.inject(setElectronAppPathInjectable); + const appName = di.inject(appNameInjectable); + const getAppPath = di.inject(getElectronAppPathInjectable); + + const appDataPath = getAppPath("appData"); + + setElectronAppPath("userData", path.join(appDataPath, appName)); +}; + +const setupPathForAppDataInIntegrationTesting = (di: DependencyInjectionContainer, appDataPath: string) => { + const setElectronAppPath = di.inject(setElectronAppPathInjectable); + + setElectronAppPath("appData", appDataPath); +}; diff --git a/src/main/app-paths/directory-for-integration-testing/directory-for-integration-testing.injectable.ts b/src/main/app-paths/directory-for-integration-testing/directory-for-integration-testing.injectable.ts new file mode 100644 index 0000000000..0588fc9f82 --- /dev/null +++ b/src/main/app-paths/directory-for-integration-testing/directory-for-integration-testing.injectable.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; + +const directoryForIntegrationTestingInjectable = getInjectable({ + instantiate: () => process.env.CICD, + lifecycle: lifecycleEnum.singleton, +}); + +export default directoryForIntegrationTestingInjectable; diff --git a/src/main/app-paths/get-app-paths.ts b/src/main/app-paths/get-app-paths.ts new file mode 100644 index 0000000000..f2f8f50b65 --- /dev/null +++ b/src/main/app-paths/get-app-paths.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { fromPairs } from "lodash/fp"; +import { pathNames, PathName } from "../../common/app-paths/app-path-names"; +import type { AppPaths } from "../../common/app-paths/app-path-injection-token"; + +interface Dependencies { + getAppPath: (name: PathName) => string +} + +export const getAppPaths = ({ getAppPath }: Dependencies) => + fromPairs(pathNames.map((name) => [name, getAppPath(name)])) as AppPaths; diff --git a/src/main/app-paths/get-electron-app-path/electron-app/electron-app.injectable.ts b/src/main/app-paths/get-electron-app-path/electron-app/electron-app.injectable.ts new file mode 100644 index 0000000000..15da37c645 --- /dev/null +++ b/src/main/app-paths/get-electron-app-path/electron-app/electron-app.injectable.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { app } from "electron"; + +const electronAppInjectable = getInjectable({ + instantiate: () => app, + lifecycle: lifecycleEnum.singleton, + causesSideEffects: false, +}); + +export default electronAppInjectable; diff --git a/src/main/app-paths/get-electron-app-path/get-electron-app-path.injectable.ts b/src/main/app-paths/get-electron-app-path/get-electron-app-path.injectable.ts new file mode 100644 index 0000000000..216daec6dd --- /dev/null +++ b/src/main/app-paths/get-electron-app-path/get-electron-app-path.injectable.ts @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import electronAppInjectable from "./electron-app/electron-app.injectable"; +import { getElectronAppPath } from "./get-electron-app-path"; + +const getElectronAppPathInjectable = getInjectable({ + instantiate: (di) => + getElectronAppPath({ app: di.inject(electronAppInjectable) }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default getElectronAppPathInjectable; diff --git a/src/main/app-paths/get-electron-app-path/get-electron-app-path.test.ts b/src/main/app-paths/get-electron-app-path/get-electron-app-path.test.ts new file mode 100644 index 0000000000..6ad163fad1 --- /dev/null +++ b/src/main/app-paths/get-electron-app-path/get-electron-app-path.test.ts @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import electronAppInjectable from "./electron-app/electron-app.injectable"; +import getElectronAppPathInjectable from "./get-electron-app-path.injectable"; +import { getDiForUnitTesting } from "../../getDiForUnitTesting"; +import type { App } from "electron"; + +describe("get-electron-app-path", () => { + let getElectronAppPath: (name: string) => string | null; + + beforeEach(() => { + const di = getDiForUnitTesting(); + + const appStub = { + getPath: (name: string) => { + if (name !== "some-existing-name") { + throw new Error("irrelevant"); + } + + return "some-existing-app-path"; + + }, + } as App; + + di.override(electronAppInjectable, () => appStub); + + getElectronAppPath = di.inject(getElectronAppPathInjectable); + }); + + it("given app path exists, when called, returns app path", () => { + const actual = getElectronAppPath("some-existing-name"); + + expect(actual).toBe("some-existing-app-path"); + }); + + it("given app path does not exist, when called, returns null", () => { + const actual = getElectronAppPath("some-non-existing-name"); + + expect(actual).toBe(null); + }); +}); diff --git a/src/main/app-paths/get-electron-app-path/get-electron-app-path.ts b/src/main/app-paths/get-electron-app-path/get-electron-app-path.ts new file mode 100644 index 0000000000..7fe409378d --- /dev/null +++ b/src/main/app-paths/get-electron-app-path/get-electron-app-path.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import type { App } from "electron"; +import type { PathName } from "../../../common/app-paths/app-path-names"; + +interface Dependencies { + app: App; +} + +export const getElectronAppPath = + ({ app }: Dependencies) => + (name: PathName) : string | null => { + try { + return app.getPath(name); + } catch (e) { + return null; + } + }; diff --git a/src/main/app-paths/register-channel/ipc-main/ipc-main.injectable.ts b/src/main/app-paths/register-channel/ipc-main/ipc-main.injectable.ts new file mode 100644 index 0000000000..200dfbad92 --- /dev/null +++ b/src/main/app-paths/register-channel/ipc-main/ipc-main.injectable.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { ipcMain } from "electron"; + +const ipcMainInjectable = getInjectable({ + instantiate: () => ipcMain, + lifecycle: lifecycleEnum.singleton, + causesSideEffects: true, +}); + +export default ipcMainInjectable; diff --git a/src/main/app-paths/register-channel/register-channel.injectable.ts b/src/main/app-paths/register-channel/register-channel.injectable.ts new file mode 100644 index 0000000000..e79d115758 --- /dev/null +++ b/src/main/app-paths/register-channel/register-channel.injectable.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import ipcMainInjectable from "./ipc-main/ipc-main.injectable"; +import { registerChannel } from "./register-channel"; + +const registerChannelInjectable = getInjectable({ + instantiate: (di) => registerChannel({ + ipcMain: di.inject(ipcMainInjectable), + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default registerChannelInjectable; diff --git a/src/main/app-paths/register-channel/register-channel.ts b/src/main/app-paths/register-channel/register-channel.ts new file mode 100644 index 0000000000..712f6b9473 --- /dev/null +++ b/src/main/app-paths/register-channel/register-channel.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import type { IpcMain } from "electron"; +import type { Channel } from "../../../common/ipc-channel/channel"; + +interface Dependencies { + ipcMain: IpcMain; +} + +export const registerChannel = + ({ ipcMain }: Dependencies) => + , TInstance>( + channel: TChannel, + getValue: () => TInstance, + ) => + ipcMain.handle(channel.name, getValue); diff --git a/src/main/app-paths/set-electron-app-path/set-electron-app-path.injectable.ts b/src/main/app-paths/set-electron-app-path/set-electron-app-path.injectable.ts new file mode 100644 index 0000000000..4dfe123cb4 --- /dev/null +++ b/src/main/app-paths/set-electron-app-path/set-electron-app-path.injectable.ts @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { PathName } from "../../../common/app-paths/app-path-names"; +import electronAppInjectable from "../get-electron-app-path/electron-app/electron-app.injectable"; + +const setElectronAppPathInjectable = getInjectable({ + instantiate: (di) => (name: PathName, path: string) : void => + di.inject(electronAppInjectable).setPath(name, path), + + lifecycle: lifecycleEnum.singleton, +}); + +export default setElectronAppPathInjectable; diff --git a/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts b/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts index 5c3a7094ae..2c0ea4c4e3 100644 --- a/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts +++ b/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts @@ -22,13 +22,17 @@ import { ObservableMap } from "mobx"; import type { CatalogEntity } from "../../../common/catalog"; import { loadFromOptions } from "../../../common/kube-helpers"; -import type { Cluster } from "../../cluster"; -import { computeDiff, configToModels } from "../kubeconfig-sync"; +import type { Cluster } from "../../../common/cluster/cluster"; +import { computeDiff as computeDiffFor, configToModels } from "../kubeconfig-sync-manager/kubeconfig-sync-manager"; import mockFs from "mock-fs"; import fs from "fs"; -import { ClusterStore } from "../../../common/cluster-store"; import { ClusterManager } from "../../cluster-manager"; -import { AppPaths } from "../../../common/app-paths"; +import clusterStoreInjectable from "../../../common/cluster-store/cluster-store.injectable"; +import { getDiForUnitTesting } from "../../getDiForUnitTesting"; +import { createClusterInjectionToken } from "../../../common/cluster/create-cluster-injection-token"; +import directoryForKubeConfigsInjectable + from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; + jest.mock("electron", () => ({ app: { @@ -46,18 +50,28 @@ jest.mock("electron", () => ({ }, })); -AppPaths.init(); - describe("kubeconfig-sync.source tests", () => { - beforeEach(() => { + let computeDiff: ReturnType; + + beforeEach(async () => { + const di = getDiForUnitTesting({ doGeneralOverrides: true }); + mockFs(); - ClusterStore.createInstance(); + + await di.runSetups(); + + computeDiff = computeDiffFor({ + directoryForKubeConfigs: di.inject(directoryForKubeConfigsInjectable), + createCluster: di.inject(createClusterInjectionToken), + }); + + di.inject(clusterStoreInjectable); + ClusterManager.createInstance(); }); afterEach(() => { mockFs.restore(); - ClusterStore.resetInstance(); ClusterManager.resetInstance(); }); diff --git a/src/main/catalog-sources/index.ts b/src/main/catalog-sources/index.ts index 70c2d073fb..348f03bbd3 100644 --- a/src/main/catalog-sources/index.ts +++ b/src/main/catalog-sources/index.ts @@ -20,5 +20,4 @@ */ export { syncWeblinks } from "./weblinks"; -export { KubeconfigSyncManager } from "./kubeconfig-sync"; export { syncGeneralEntities } from "./general"; diff --git a/src/main/catalog-sources/kubeconfig-sync-manager/kubeconfig-sync-manager.injectable.ts b/src/main/catalog-sources/kubeconfig-sync-manager/kubeconfig-sync-manager.injectable.ts new file mode 100644 index 0000000000..c2b8b4818c --- /dev/null +++ b/src/main/catalog-sources/kubeconfig-sync-manager/kubeconfig-sync-manager.injectable.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; +import { KubeconfigSyncManager } from "./kubeconfig-sync-manager"; +import { createClusterInjectionToken } from "../../../common/cluster/create-cluster-injection-token"; + +const kubeconfigSyncManagerInjectable = getInjectable({ + instantiate: (di) => new KubeconfigSyncManager({ + directoryForKubeConfigs: di.inject(directoryForKubeConfigsInjectable), + createCluster: di.inject(createClusterInjectionToken), + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default kubeconfigSyncManagerInjectable; diff --git a/src/main/catalog-sources/kubeconfig-sync.ts b/src/main/catalog-sources/kubeconfig-sync-manager/kubeconfig-sync-manager.ts similarity index 86% rename from src/main/catalog-sources/kubeconfig-sync.ts rename to src/main/catalog-sources/kubeconfig-sync-manager/kubeconfig-sync-manager.ts index e011ff6860..90639fbd45 100644 --- a/src/main/catalog-sources/kubeconfig-sync.ts +++ b/src/main/catalog-sources/kubeconfig-sync-manager/kubeconfig-sync-manager.ts @@ -20,25 +20,25 @@ */ import { action, observable, IComputedValue, computed, ObservableMap, runInAction, makeObservable, observe } from "mobx"; -import type { CatalogEntity } from "../../common/catalog"; -import { catalogEntityRegistry } from "../../main/catalog"; +import type { CatalogEntity } from "../../../common/catalog"; +import { catalogEntityRegistry } from "../../catalog"; import { FSWatcher, watch } from "chokidar"; import fs from "fs"; import path from "path"; import type stream from "stream"; -import { bytesToUnits, Disposer, ExtendedObservableMap, iter, noop, Singleton, storedKubeConfigFolder } from "../../common/utils"; -import logger from "../logger"; +import { bytesToUnits, Disposer, ExtendedObservableMap, iter, noop } from "../../../common/utils"; +import logger from "../../logger"; import type { KubeConfig } from "@kubernetes/client-node"; -import { loadConfigFromString, splitConfig } from "../../common/kube-helpers"; -import { Cluster } from "../cluster"; -import { catalogEntityFromCluster, ClusterManager } from "../cluster-manager"; -import { UserStore } from "../../common/user-store"; -import { ClusterStore } from "../../common/cluster-store"; +import { loadConfigFromString, splitConfig } from "../../../common/kube-helpers"; +import { catalogEntityFromCluster, ClusterManager } from "../../cluster-manager"; +import { UserStore } from "../../../common/user-store"; +import { ClusterStore } from "../../../common/cluster-store/cluster-store"; import { createHash } from "crypto"; import { homedir } from "os"; import globToRegExp from "glob-to-regexp"; import { inspect } from "util"; -import type { UpdateClusterModel } from "../../common/cluster-types"; +import type { ClusterModel, UpdateClusterModel } from "../../../common/cluster-types"; +import type { Cluster } from "../../../common/cluster/cluster"; const logPrefix = "[KUBECONFIG-SYNC]:"; @@ -63,16 +63,19 @@ const ignoreGlobs = [ const folderSyncMaxAllowedFileReadSize = 2 * 1024 * 1024; // 2 MiB const fileSyncMaxAllowedFileReadSize = 16 * folderSyncMaxAllowedFileReadSize; // 32 MiB -export class KubeconfigSyncManager extends Singleton { +interface Dependencies { + directoryForKubeConfigs: string + createCluster: (model: ClusterModel) => Cluster +} + +const kubeConfigSyncName = "lens:kube-sync"; + +export class KubeconfigSyncManager { protected sources = observable.map, Disposer]>(); protected syncing = false; protected syncListDisposer?: Disposer; - protected static readonly syncName = "lens:kube-sync"; - - constructor() { - super(); - + constructor(private dependencies: Dependencies) { makeObservable(this); } @@ -86,7 +89,7 @@ export class KubeconfigSyncManager extends Singleton { logger.info(`${logPrefix} starting requested syncs`); - catalogEntityRegistry.addComputedSource(KubeconfigSyncManager.syncName, computed(() => ( + catalogEntityRegistry.addComputedSource(kubeConfigSyncName, computed(() => ( Array.from(iter.flatMap( this.sources.values(), ([entities]) => entities.get(), @@ -94,7 +97,7 @@ export class KubeconfigSyncManager extends Singleton { ))); // This must be done so that c&p-ed clusters are visible - this.startNewSync(storedKubeConfigFolder()); + this.startNewSync(this.dependencies.directoryForKubeConfigs); for (const filePath of UserStore.getInstance().syncKubeconfigEntries.keys()) { this.startNewSync(filePath); @@ -120,7 +123,7 @@ export class KubeconfigSyncManager extends Singleton { this.stopOldSync(filePath); } - catalogEntityRegistry.removeSource(KubeconfigSyncManager.syncName); + catalogEntityRegistry.removeSource(kubeConfigSyncName); this.syncing = false; } @@ -131,7 +134,11 @@ export class KubeconfigSyncManager extends Singleton { return void logger.debug(`${logPrefix} already syncing file/folder`, { filePath }); } - this.sources.set(filePath, watchFileChanges(filePath)); + this.sources.set( + filePath, + watchFileChanges(filePath, this.dependencies), + ); + logger.info(`${logPrefix} starting sync of file/folder`, { filePath }); logger.debug(`${logPrefix} ${this.sources.size} files/folders watched`, { files: Array.from(this.sources.keys()) }); } @@ -170,7 +177,7 @@ type RootSourceValue = [Cluster, CatalogEntity]; type RootSource = ObservableMap; // exported for testing -export function computeDiff(contents: string, source: RootSource, filePath: string): void { +export const computeDiff = ({ directoryForKubeConfigs, createCluster }: Dependencies) => (contents: string, source: RootSource, filePath: string): void => { runInAction(() => { try { const { config, error } = loadConfigFromString(contents); @@ -212,7 +219,8 @@ export function computeDiff(contents: string, source: RootSource, filePath: stri // add new clusters to the source try { const clusterId = createHash("md5").update(`${filePath}:${contextName}`).digest("hex"); - const cluster = ClusterStore.getInstance().getById(clusterId) || new Cluster({ ...model, id: clusterId }); + + const cluster = ClusterStore.getInstance().getById(clusterId) || createCluster({ ...model, id: clusterId }); if (!cluster.apiUrl) { throw new Error("Cluster constructor failed, see above error"); @@ -220,7 +228,7 @@ export function computeDiff(contents: string, source: RootSource, filePath: stri const entity = catalogEntityFromCluster(cluster); - if (!filePath.startsWith(storedKubeConfigFolder())) { + if (!filePath.startsWith(directoryForKubeConfigs)) { entity.metadata.labels.file = filePath.replace(homedir(), "~"); } source.set(contextName, [cluster, entity]); @@ -231,11 +239,12 @@ export function computeDiff(contents: string, source: RootSource, filePath: stri } } } catch (error) { + console.log(error); logger.warn(`${logPrefix} Failed to compute diff: ${error}`, { filePath }); source.clear(); // clear source if we have failed so as to not show outdated information } }); -} +}; interface DiffChangedConfigArgs { filePath: string; @@ -244,7 +253,7 @@ interface DiffChangedConfigArgs { maxAllowedFileReadSize: number; } -function diffChangedConfig({ filePath, source, stats, maxAllowedFileReadSize }: DiffChangedConfigArgs): Disposer { +const diffChangedConfigFor = (dependencies: Dependencies) => ({ filePath, source, stats, maxAllowedFileReadSize }: DiffChangedConfigArgs): Disposer => { logger.debug(`${logPrefix} file changed`, { filePath }); if (stats.size >= maxAllowedFileReadSize) { @@ -293,14 +302,14 @@ function diffChangedConfig({ filePath, source, stats, maxAllowedFileReadSize }: }) .on("end", () => { if (!closed) { - computeDiff(fileString, source, filePath); + computeDiff(dependencies)(fileString, source, filePath); } }); return cleanup; -} +}; -function watchFileChanges(filePath: string): [IComputedValue, Disposer] { +const watchFileChanges = (filePath: string, dependencies: Dependencies): [IComputedValue, Disposer] => { const rootSource = new ExtendedObservableMap>(); const derivedSource = computed(() => Array.from(iter.flatMap(rootSource.values(), from => iter.map(from.values(), child => child[1])))); @@ -328,6 +337,8 @@ function watchFileChanges(filePath: string): [IComputedValue, D atomic: 150, // for "atomic writes" }); + const diffChangedConfig = diffChangedConfigFor(dependencies); + watcher .on("change", (childFilePath, stats) => { const cleanup = cleanupFns.get(childFilePath); @@ -378,4 +389,4 @@ function watchFileChanges(filePath: string): [IComputedValue, D return [derivedSource, () => { watcher?.close(); }]; -} +}; diff --git a/src/main/cluster-detectors/base-cluster-detector.ts b/src/main/cluster-detectors/base-cluster-detector.ts index 1edd971cfd..8f60fbe112 100644 --- a/src/main/cluster-detectors/base-cluster-detector.ts +++ b/src/main/cluster-detectors/base-cluster-detector.ts @@ -20,7 +20,7 @@ */ import type { RequestPromiseOptions } from "request-promise-native"; -import type { Cluster } from "../cluster"; +import type { Cluster } from "../../common/cluster/cluster"; import { k8sRequest } from "../k8s-request"; export type ClusterDetectionResult = { diff --git a/src/main/cluster-detectors/detector-registry.ts b/src/main/cluster-detectors/detector-registry.ts index 5f4f12175e..75d63f5b2a 100644 --- a/src/main/cluster-detectors/detector-registry.ts +++ b/src/main/cluster-detectors/detector-registry.ts @@ -22,7 +22,7 @@ import { observable } from "mobx"; import type { ClusterMetadata } from "../../common/cluster-types"; import { Singleton } from "../../common/utils"; -import type { Cluster } from "../cluster"; +import type { Cluster } from "../../common/cluster/cluster"; import type { BaseClusterDetector, ClusterDetectionResult } from "./base-cluster-detector"; export class DetectorRegistry extends Singleton { diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts index 850b87c7b8..ecf06c975e 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster-manager.ts @@ -22,7 +22,7 @@ import "../common/cluster-ipc"; import type http from "http"; import { action, makeObservable, observable, observe, reaction, toJS } from "mobx"; -import { Cluster } from "./cluster"; +import { Cluster } from "../common/cluster/cluster"; import logger from "./logger"; import { apiKubePrefix } from "../common/vars"; import { getClusterIdFromHost, Singleton } from "../common/utils"; @@ -30,7 +30,7 @@ import { catalogEntityRegistry } from "./catalog"; import { KubernetesCluster, KubernetesClusterPrometheusMetrics, LensKubernetesClusterStatus } from "../common/catalog-entities/kubernetes-cluster"; import { ipcMainOn } from "../common/ipc"; import { once } from "lodash"; -import { ClusterStore } from "../common/cluster-store"; +import { ClusterStore } from "../common/cluster-store/cluster-store"; import type { ClusterId } from "../common/cluster-types"; const logPrefix = "[CLUSTER-MANAGER]:"; diff --git a/src/main/context-handler.ts b/src/main/context-handler/context-handler.ts similarity index 88% rename from src/main/context-handler.ts rename to src/main/context-handler/context-handler.ts index 3042c0c4f3..f5b240589a 100644 --- a/src/main/context-handler.ts +++ b/src/main/context-handler/context-handler.ts @@ -19,15 +19,15 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import type { PrometheusProvider, PrometheusService } from "./prometheus/provider-registry"; -import { PrometheusProviderRegistry } from "./prometheus/provider-registry"; -import type { ClusterPrometheusPreferences } from "../common/cluster-types"; -import type { Cluster } from "./cluster"; +import type { PrometheusProvider, PrometheusService } from "../prometheus/provider-registry"; +import { PrometheusProviderRegistry } from "../prometheus/provider-registry"; +import type { ClusterPrometheusPreferences } from "../../common/cluster-types"; +import type { Cluster } from "../../common/cluster/cluster"; import type httpProxy from "http-proxy"; import url, { UrlWithStringQuery } from "url"; import { CoreV1Api } from "@kubernetes/client-node"; -import logger from "./logger"; -import { KubeAuthProxy } from "./kube-auth-proxy"; +import logger from "../logger"; +import type { KubeAuthProxy } from "../kube-auth-proxy/kube-auth-proxy"; export interface PrometheusDetails { prometheusPath: string; @@ -41,6 +41,10 @@ interface PrometheusServicePreferences { prefix: string; } +interface Dependencies { + createKubeAuthProxy: (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => KubeAuthProxy +} + export class ContextHandler { public clusterUrl: UrlWithStringQuery; protected kubeAuthProxy?: KubeAuthProxy; @@ -48,7 +52,7 @@ export class ContextHandler { protected prometheusProvider?: string; protected prometheus?: PrometheusServicePreferences; - constructor(protected cluster: Cluster) { + constructor(private dependencies: Dependencies, protected cluster: Cluster) { this.clusterUrl = url.parse(cluster.apiUrl); this.setupPrometheus(cluster.preferences); } @@ -161,7 +165,7 @@ export class ContextHandler { if (this.cluster.preferences.httpsProxy) { proxyEnv.HTTPS_PROXY = this.cluster.preferences.httpsProxy; } - this.kubeAuthProxy = new KubeAuthProxy(this.cluster, proxyEnv); + this.kubeAuthProxy = this.dependencies.createKubeAuthProxy(this.cluster, proxyEnv); await this.kubeAuthProxy.run(); } diff --git a/src/main/context-handler/create-context-handler.injectable.ts b/src/main/context-handler/create-context-handler.injectable.ts new file mode 100644 index 0000000000..3c92d75fb9 --- /dev/null +++ b/src/main/context-handler/create-context-handler.injectable.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { Cluster } from "../../common/cluster/cluster"; +import { ContextHandler } from "./context-handler"; +import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable"; + +const createContextHandlerInjectable = getInjectable({ + instantiate: (di) => { + const dependencies = { + createKubeAuthProxy: di.inject(createKubeAuthProxyInjectable), + }; + + return (cluster: Cluster) => new ContextHandler(dependencies, cluster); + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default createContextHandlerInjectable; diff --git a/src/main/create-cluster/create-cluster.injectable.ts b/src/main/create-cluster/create-cluster.injectable.ts new file mode 100644 index 0000000000..7c98bea3eb --- /dev/null +++ b/src/main/create-cluster/create-cluster.injectable.ts @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { ClusterModel } from "../../common/cluster-types"; +import { Cluster } from "../../common/cluster/cluster"; +import directoryForKubeConfigsInjectable from "../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; +import createKubeconfigManagerInjectable from "../kubeconfig-manager/create-kubeconfig-manager.injectable"; +import createKubectlInjectable from "../kubectl/create-kubectl.injectable"; +import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable"; +import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token"; + +const createClusterInjectable = getInjectable({ + instantiate: (di) => { + const dependencies = { + directoryForKubeConfigs: di.inject(directoryForKubeConfigsInjectable), + createKubeconfigManager: di.inject(createKubeconfigManagerInjectable), + createKubectl: di.inject(createKubectlInjectable), + createContextHandler: di.inject(createContextHandlerInjectable), + }; + + return (model: ClusterModel) => new Cluster(dependencies, model); + }, + + injectionToken: createClusterInjectionToken, + + lifecycle: lifecycleEnum.singleton, +}); + +export default createClusterInjectable; diff --git a/src/main/exit-app.ts b/src/main/exit-app.ts index 8f9f85d2a4..3f0320506c 100644 --- a/src/main/exit-app.ts +++ b/src/main/exit-app.ts @@ -21,7 +21,7 @@ import { app } from "electron"; import { WindowManager } from "./window-manager"; -import { appEventBus } from "../common/event-bus"; +import { appEventBus } from "../common/app-event-bus/event-bus"; import { ClusterManager } from "./cluster-manager"; import logger from "./logger"; diff --git a/src/main/getDi.ts b/src/main/getDi.ts index 2eafe7e93d..13f0aa7c93 100644 --- a/src/main/getDi.ts +++ b/src/main/getDi.ts @@ -26,6 +26,7 @@ export const getDi = () => { const di = createContainer( getRequireContextForMainCode, getRequireContextForCommonExtensionCode, + getRequireContextForCommonCode, ); setLegacyGlobalDiForExtensionApi(di); @@ -38,3 +39,6 @@ const getRequireContextForMainCode = () => const getRequireContextForCommonExtensionCode = () => require.context("../extensions", true, /\.injectable\.(ts|tsx)$/); + +const getRequireContextForCommonCode = () => + require.context("../common", true, /\.injectable\.(ts|tsx)$/); diff --git a/src/main/getDiForUnitTesting.ts b/src/main/getDiForUnitTesting.ts index 97f0f777a3..9de67b892f 100644 --- a/src/main/getDiForUnitTesting.ts +++ b/src/main/getDiForUnitTesting.ts @@ -21,20 +21,32 @@ import glob from "glob"; import { memoize } from "lodash/fp"; +import { kebabCase } from "lodash/fp"; import { createContainer, ConfigurableDependencyInjectionContainer, } from "@ogre-tools/injectable"; -import { setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-global-function-for-extension-api/legacy-global-di-for-extension-api"; -export const getDiForUnitTesting = () => { +import { setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-global-function-for-extension-api/legacy-global-di-for-extension-api"; +import getElectronAppPathInjectable from "./app-paths/get-electron-app-path/get-electron-app-path.injectable"; +import setElectronAppPathInjectable from "./app-paths/set-electron-app-path/set-electron-app-path.injectable"; +import appNameInjectable from "./app-paths/app-name/app-name.injectable"; +import registerChannelInjectable from "./app-paths/register-channel/register-channel.injectable"; +import writeJsonFileInjectable + from "../common/fs/write-json-file/write-json-file.injectable"; +import readJsonFileInjectable + from "../common/fs/read-json-file/read-json-file.injectable"; + +export const getDiForUnitTesting = ( + { doGeneralOverrides } = { doGeneralOverrides: false }, +) => { const di: ConfigurableDependencyInjectionContainer = createContainer(); setLegacyGlobalDiForExtensionApi(di); getInjectableFilePaths() - .map(key => { + .map((key) => { // eslint-disable-next-line @typescript-eslint/no-var-requires const injectable = require(key).default; @@ -45,14 +57,34 @@ export const getDiForUnitTesting = () => { }; }) - .forEach(injectable => di.register(injectable)); + .forEach((injectable) => di.register(injectable)); di.preventSideEffects(); + if (doGeneralOverrides) { + di.override( + getElectronAppPathInjectable, + () => (name: string) => `some-electron-app-path-for-${kebabCase(name)}`, + ); + + di.override(setElectronAppPathInjectable, () => () => undefined); + di.override(appNameInjectable, () => "some-electron-app-name"); + di.override(registerChannelInjectable, () => () => undefined); + + di.override(writeJsonFileInjectable, () => () => { + throw new Error("Tried to write JSON file to file system without specifying explicit override."); + }); + + di.override(readJsonFileInjectable, () => () => { + throw new Error("Tried to read JSON file from file system without specifying explicit override."); + }); + } + return di; }; const getInjectableFilePaths = memoize(() => [ ...glob.sync("./**/*.injectable.{ts,tsx}", { cwd: __dirname }), ...glob.sync("../extensions/**/*.injectable.{ts,tsx}", { cwd: __dirname }), + ...glob.sync("../common/**/*.injectable.{ts,tsx}", { cwd: __dirname }), ]); diff --git a/src/main/helm/helm-service.ts b/src/main/helm/helm-service.ts index 4d4637b3b1..4357db4f85 100644 --- a/src/main/helm/helm-service.ts +++ b/src/main/helm/helm-service.ts @@ -19,7 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import type { Cluster } from "../cluster"; +import type { Cluster } from "../../common/cluster/cluster"; import logger from "../logger"; import { HelmRepoManager } from "./helm-repo-manager"; import { HelmChartManager } from "./helm-chart-manager"; diff --git a/src/main/index.ts b/src/main/index.ts index c0b86f7f4f..a80e4f190a 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -35,315 +35,343 @@ import { shellSync } from "./shell-sync"; import { mangleProxyEnv } from "./proxy-env"; import { registerFileProtocol } from "../common/register-protocol"; import logger from "./logger"; -import { appEventBus } from "../common/event-bus"; +import { appEventBus } from "../common/app-event-bus/event-bus"; import type { InstalledExtension } from "../extensions/extension-discovery/extension-discovery"; import type { LensExtensionId } from "../extensions/lens-extension"; import { installDeveloperTools } from "./developer-tools"; -import { disposer, getAppVersion, getAppVersionFromProxyServer, storedKubeConfigFolder } from "../common/utils"; +import { disposer, getAppVersion, getAppVersionFromProxyServer } from "../common/utils"; import { bindBroadcastHandlers, ipcMainOn } from "../common/ipc"; import { startUpdateChecking } from "./app-updater"; import { IpcRendererNavigationEvents } from "../renderer/navigation/events"; import { pushCatalogToRenderer } from "./catalog-pusher"; import { catalogEntityRegistry } from "./catalog"; import { HelmRepoManager } from "./helm/helm-repo-manager"; -import { syncGeneralEntities, syncWeblinks, KubeconfigSyncManager } from "./catalog-sources"; +import { syncGeneralEntities, syncWeblinks } from "./catalog-sources"; import configurePackages from "../common/configure-packages"; import { PrometheusProviderRegistry } from "./prometheus"; import * as initializers from "./initializers"; -import { ClusterStore } from "../common/cluster-store"; import { HotbarStore } from "../common/hotbar-store"; -import { UserStore } from "../common/user-store"; import { WeblinkStore } from "../common/weblink-store"; -import { FilesystemProvisionerStore } from "./extension-filesystem"; import { SentryInit } from "../common/sentry"; import { ensureDir } from "fs-extra"; -import { Router } from "./router"; import { initMenu } from "./menu/menu"; import { initTray } from "./tray"; -import { kubeApiRequest, shellApiRequest, ShellRequestAuthenticator } from "./proxy-functions"; -import { AppPaths } from "../common/app-paths"; +import { kubeApiRequest } from "./proxy-functions"; import { ShellSession } from "./shell-session/shell-session"; import { getDi } from "./getDi"; -import electronMenuItemsInjectable from "./menu/electron-menu-items.injectable"; import extensionLoaderInjectable from "../extensions/extension-loader/extension-loader.injectable"; import lensProtocolRouterMainInjectable from "./protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable"; import extensionDiscoveryInjectable from "../extensions/extension-discovery/extension-discovery.injectable"; +import directoryForExesInjectable + from "../common/app-paths/directory-for-exes/directory-for-exes.injectable"; +import initIpcMainHandlersInjectable + from "./initializers/init-ipc-main-handlers/init-ipc-main-handlers.injectable"; +import electronMenuItemsInjectable from "./menu/electron-menu-items.injectable"; +import directoryForKubeConfigsInjectable + from "../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; +import kubeconfigSyncManagerInjectable + from "./catalog-sources/kubeconfig-sync-manager/kubeconfig-sync-manager.injectable"; +import clusterStoreInjectable from "../common/cluster-store/cluster-store.injectable"; +import routerInjectable from "./router/router.injectable"; +import shellApiRequestInjectable + from "./proxy-functions/shell-api-request/shell-api-request.injectable"; +import userStoreInjectable from "../common/user-store/user-store.injectable"; const di = getDi(); -injectSystemCAs(); +di.runSetups().then(() => { + injectSystemCAs(); -const onCloseCleanup = disposer(); -const onQuitCleanup = disposer(); + const onCloseCleanup = disposer(); + const onQuitCleanup = disposer(); -SentryInit(); -app.setName(appName); + SentryInit(); + app.setName(appName); -logger.info(`📟 Setting ${productName} as protocol client for lens://`); + logger.info(`📟 Setting ${productName} as protocol client for lens://`); -if (app.setAsDefaultProtocolClient("lens")) { - logger.info("📟 Protocol client register succeeded ✅"); -} else { - logger.info("📟 Protocol client register failed ❗"); -} - -AppPaths.init(); - -if (process.env.LENS_DISABLE_GPU) { - app.disableHardwareAcceleration(); -} - -logger.debug("[APP-MAIN] initializing remote"); -initializeRemote(); - -logger.debug("[APP-MAIN] configuring packages"); -configurePackages(); - -mangleProxyEnv(); - -logger.debug("[APP-MAIN] initializing ipc main handlers"); - -const menuItems = di.inject(electronMenuItemsInjectable); - -initializers.initIpcMainHandlers(menuItems); - -if (app.commandLine.getSwitchValue("proxy-server") !== "") { - process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server"); -} - -logger.debug("[APP-MAIN] Lens protocol routing main"); - -const lensProtocolRouterMain = di.inject(lensProtocolRouterMainInjectable); - -if (!app.requestSingleInstanceLock()) { - app.exit(); -} else { - for (const arg of process.argv) { - if (arg.toLowerCase().startsWith("lens://")) { - lensProtocolRouterMain.route(arg); - } + if (app.setAsDefaultProtocolClient("lens")) { + logger.info("📟 Protocol client register succeeded ✅"); + } else { + logger.info("📟 Protocol client register failed ❗"); } -} -app.on("second-instance", (event, argv) => { - logger.debug("second-instance message"); + if (process.env.LENS_DISABLE_GPU) { + app.disableHardwareAcceleration(); + } - for (const arg of argv) { - if (arg.toLowerCase().startsWith("lens://")) { - lensProtocolRouterMain.route(arg); + logger.debug("[APP-MAIN] initializing remote"); + initializeRemote(); + + logger.debug("[APP-MAIN] configuring packages"); + configurePackages(); + + mangleProxyEnv(); + + const initIpcMainHandlers = di.inject(initIpcMainHandlersInjectable); + + logger.debug("[APP-MAIN] initializing ipc main handlers"); + initIpcMainHandlers(); + + if (app.commandLine.getSwitchValue("proxy-server") !== "") { + process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server"); + } + + logger.debug("[APP-MAIN] Lens protocol routing main"); + + const lensProtocolRouterMain = di.inject(lensProtocolRouterMainInjectable); + + if (!app.requestSingleInstanceLock()) { + app.exit(); + } else { + for (const arg of process.argv) { + if (arg.toLowerCase().startsWith("lens://")) { + lensProtocolRouterMain.route(arg); + } } } - WindowManager.getInstance(false)?.ensureMainWindow(); -}); + app.on("second-instance", (event, argv) => { + logger.debug("second-instance message"); -app.on("ready", async () => { - logger.info(`🚀 Starting ${productName} from "${AppPaths.get("exe")}"`); - logger.info("🐚 Syncing shell environment"); - await shellSync(); + for (const arg of argv) { + if (arg.toLowerCase().startsWith("lens://")) { + lensProtocolRouterMain.route(arg); + } + } - bindBroadcastHandlers(); - - powerMonitor.on("shutdown", () => app.exit()); - - registerFileProtocol("static", __static); - - PrometheusProviderRegistry.createInstance(); - ShellRequestAuthenticator.createInstance().init(); - initializers.initPrometheusProviderRegistry(); - - /** - * The following sync MUST be done before HotbarStore creation, because that - * store has migrations that will remove items that previous migrations add - * if this is not present - */ - syncGeneralEntities(); - - logger.info("💾 Loading stores"); - - UserStore.createInstance().startMainReactions(); - - // ClusterStore depends on: UserStore - ClusterStore.createInstance().provideInitialFromMain(); - - // HotbarStore depends on: ClusterStore - HotbarStore.createInstance(); - - FilesystemProvisionerStore.createInstance(); - WeblinkStore.createInstance(); - - syncWeblinks(); - - HelmRepoManager.createInstance(); // create the instance - - const lensProxy = LensProxy.createInstance(new Router(), { - getClusterForRequest: req => ClusterManager.getInstance().getClusterForRequest(req), - kubeApiRequest, - shellApiRequest, + WindowManager.getInstance(false)?.ensureMainWindow(); }); - ClusterManager.createInstance().init(); - KubeconfigSyncManager.createInstance(); + app.on("ready", async () => { + const directoryForExes = di.inject(directoryForExesInjectable); - initializers.initClusterMetadataDetectors(); + logger.info(`🚀 Starting ${productName} from "${directoryForExes}"`); + logger.info("🐚 Syncing shell environment"); + await shellSync(); - try { - logger.info("🔌 Starting LensProxy"); - await lensProxy.listen(); - } catch (error) { - dialog.showErrorBox("Lens Error", `Could not start proxy: ${error?.message || "unknown error"}`); + bindBroadcastHandlers(); - return app.exit(); - } + powerMonitor.on("shutdown", () => app.exit()); - // test proxy connection - try { - logger.info("🔎 Testing LensProxy connection ..."); - const versionFromProxy = await getAppVersionFromProxyServer(lensProxy.port); + registerFileProtocol("static", __static); - if (getAppVersion() !== versionFromProxy) { - logger.error("Proxy server responded with invalid response"); + PrometheusProviderRegistry.createInstance(); + initializers.initPrometheusProviderRegistry(); + + /** + * The following sync MUST be done before HotbarStore creation, because that + * store has migrations that will remove items that previous migrations add + * if this is not present + */ + syncGeneralEntities(); + + logger.info("💾 Loading stores"); + + const userStore = di.inject(userStoreInjectable); + + userStore.startMainReactions(); + + // ClusterStore depends on: UserStore + const clusterStore = di.inject(clusterStoreInjectable); + + clusterStore.provideInitialFromMain(); + + // HotbarStore depends on: ClusterStore + HotbarStore.createInstance(); + + WeblinkStore.createInstance(); + + syncWeblinks(); + + HelmRepoManager.createInstance(); // create the instance + + const router = di.inject(routerInjectable); + const shellApiRequest = di.inject(shellApiRequestInjectable); + + const lensProxy = LensProxy.createInstance(router, { + getClusterForRequest: (req) => ClusterManager.getInstance().getClusterForRequest(req), + kubeApiRequest, + shellApiRequest, + }); + + ClusterManager.createInstance().init(); + + initializers.initClusterMetadataDetectors(); + + try { + logger.info("🔌 Starting LensProxy"); + await lensProxy.listen(); + } catch (error) { + dialog.showErrorBox("Lens Error", `Could not start proxy: ${error?.message || "unknown error"}`); return app.exit(); } - logger.info("⚡ LensProxy connection OK"); - } catch (error) { - logger.error(`🛑 LensProxy: failed connection test: ${error}`); + // test proxy connection + try { + logger.info("🔎 Testing LensProxy connection ..."); + const versionFromProxy = await getAppVersionFromProxyServer(lensProxy.port); - const hostsPath = isWindows - ? "C:\\windows\\system32\\drivers\\etc\\hosts" - : "/etc/hosts"; - const message = [ - `Failed connection test: ${error}`, - "Check to make sure that no other versions of Lens are running", - `Check ${hostsPath} to make sure that it is clean and that the localhost loopback is at the top and set to 127.0.0.1`, - "If you have HTTP_PROXY or http_proxy set in your environment, make sure that the localhost and the ipv4 loopback address 127.0.0.1 are added to the NO_PROXY environment variable.", - ]; + if (getAppVersion() !== versionFromProxy) { + logger.error("Proxy server responded with invalid response"); - dialog.showErrorBox("Lens Proxy Error", message.join("\n\n")); + return app.exit(); + } - return app.exit(); - } + logger.info("⚡ LensProxy connection OK"); + } catch (error) { + logger.error(`🛑 LensProxy: failed connection test: ${error}`); - const extensionLoader = di.inject(extensionLoaderInjectable); + const hostsPath = isWindows + ? "C:\\windows\\system32\\drivers\\etc\\hosts" + : "/etc/hosts"; + const message = [ + `Failed connection test: ${error}`, + "Check to make sure that no other versions of Lens are running", + `Check ${hostsPath} to make sure that it is clean and that the localhost loopback is at the top and set to 127.0.0.1`, + "If you have HTTP_PROXY or http_proxy set in your environment, make sure that the localhost and the ipv4 loopback address 127.0.0.1 are added to the NO_PROXY environment variable.", + ]; - extensionLoader.init(); + dialog.showErrorBox("Lens Proxy Error", message.join("\n\n")); - const extensionDiscovery = di.inject(extensionDiscoveryInjectable); + return app.exit(); + } - extensionDiscovery.init(); + const extensionLoader = di.inject(extensionLoaderInjectable); - // Start the app without showing the main window when auto starting on login - // (On Windows and Linux, we get a flag. On MacOS, we get special API.) - const startHidden = process.argv.includes("--hidden") || (isMac && app.getLoginItemSettings().wasOpenedAsHidden); + extensionLoader.init(); - logger.info("🖥️ Starting WindowManager"); - const windowManager = WindowManager.createInstance(); + const extensionDiscovery = di.inject(extensionDiscoveryInjectable); - onQuitCleanup.push( - initMenu(windowManager, menuItems), - initTray(windowManager), - () => ShellSession.cleanup(), - ); + extensionDiscovery.init(); - installDeveloperTools(); + // Start the app without showing the main window when auto starting on login + // (On Windows and Linux, we get a flag. On MacOS, we get special API.) + const startHidden = process.argv.includes("--hidden") || (isMac && app.getLoginItemSettings().wasOpenedAsHidden); - if (!startHidden) { - windowManager.ensureMainWindow(); - } + logger.info("🖥️ Starting WindowManager"); + const windowManager = WindowManager.createInstance(); - ipcMainOn(IpcRendererNavigationEvents.LOADED, async () => { - onCloseCleanup.push(pushCatalogToRenderer(catalogEntityRegistry)); - await ensureDir(storedKubeConfigFolder()); - KubeconfigSyncManager.getInstance().startSync(); - startUpdateChecking(); - lensProtocolRouterMain.rendererLoaded = true; + const menuItems = di.inject(electronMenuItemsInjectable); + + onQuitCleanup.push( + initMenu(windowManager, menuItems), + initTray(windowManager), + () => ShellSession.cleanup(), + ); + + installDeveloperTools(); + + if (!startHidden) { + windowManager.ensureMainWindow(); + } + + ipcMainOn(IpcRendererNavigationEvents.LOADED, async () => { + onCloseCleanup.push(pushCatalogToRenderer(catalogEntityRegistry)); + + const directoryForKubeConfigs = di.inject(directoryForKubeConfigsInjectable); + + await ensureDir(directoryForKubeConfigs); + + const kubeConfigSyncManager = di.inject(kubeconfigSyncManagerInjectable); + + kubeConfigSyncManager.startSync(); + + startUpdateChecking(); + lensProtocolRouterMain.rendererLoaded = true; + }); + + logger.info("🧩 Initializing extensions"); + + // call after windowManager to see splash earlier + try { + const extensions = await extensionDiscovery.load(); + + // Start watching after bundled extensions are loaded + extensionDiscovery.watchExtensions(); + + // Subscribe to extensions that are copied or deleted to/from the extensions folder + extensionDiscovery.events + .on("add", (extension: InstalledExtension) => { + extensionLoader.addExtension(extension); + }) + .on("remove", (lensExtensionId: LensExtensionId) => { + extensionLoader.removeExtension(lensExtensionId); + }); + + extensionLoader.initExtensions(extensions); + } catch (error) { + dialog.showErrorBox("Lens Error", `Could not load extensions${error?.message ? `: ${error.message}` : ""}`); + console.error(error); + console.trace(); + } + + setTimeout(() => { + appEventBus.emit({ name: "service", action: "start" }); + }, 1000); }); - logger.info("🧩 Initializing extensions"); + app.on("activate", (event, hasVisibleWindows) => { + logger.info("APP:ACTIVATE", { hasVisibleWindows }); - // call after windowManager to see splash earlier - try { - const extensions = await extensionDiscovery.load(); + if (!hasVisibleWindows) { + WindowManager.getInstance(false)?.ensureMainWindow(false); + } + }); - // Start watching after bundled extensions are loaded - extensionDiscovery.watchExtensions(); + /** + * This variable should is used so that `autoUpdater.installAndQuit()` works + */ + let blockQuit = !isIntegrationTesting; - // Subscribe to extensions that are copied or deleted to/from the extensions folder - extensionDiscovery.events - .on("add", (extension: InstalledExtension) => { - extensionLoader.addExtension(extension); - }) - .on("remove", (lensExtensionId: LensExtensionId) => { - extensionLoader.removeExtension(lensExtensionId); - }); + autoUpdater.on("before-quit-for-update", () => { + logger.debug("Unblocking quit for update"); + blockQuit = false; + }); - extensionLoader.initExtensions(extensions); - } catch (error) { - dialog.showErrorBox("Lens Error", `Could not load extensions${error?.message ? `: ${error.message}` : ""}`); - console.error(error); - console.trace(); - } + app.on("will-quit", (event) => { + logger.debug("will-quit message"); - setTimeout(() => { - appEventBus.emit({ name: "service", action: "start" }); - }, 1000); -}); - -app.on("activate", (event, hasVisibleWindows) => { - logger.info("APP:ACTIVATE", { hasVisibleWindows }); - - if (!hasVisibleWindows) { - WindowManager.getInstance(false)?.ensureMainWindow(false); - } -}); - -/** - * This variable should is used so that `autoUpdater.installAndQuit()` works - */ -let blockQuit = !isIntegrationTesting; - -autoUpdater.on("before-quit-for-update", () => { - logger.debug("Unblocking quit for update"); - blockQuit = false; -}); - -app.on("will-quit", (event) => { - logger.debug("will-quit message"); - - // This is called when the close button of the main window is clicked + // This is called when the close button of the main window is clicked - logger.info("APP:QUIT"); - appEventBus.emit({ name: "app", action: "close" }); - ClusterManager.getInstance(false)?.stop(); // close cluster connections - KubeconfigSyncManager.getInstance(false)?.stopSync(); - onCloseCleanup(); + logger.info("APP:QUIT"); + appEventBus.emit({ name: "app", action: "close" }); + ClusterManager.getInstance(false)?.stop(); // close cluster connections - // This is set to false here so that LPRM can wait to send future lens:// - // requests until after it loads again - lensProtocolRouterMain.rendererLoaded = false; + const kubeConfigSyncManager = di.inject(kubeconfigSyncManagerInjectable); - if (blockQuit) { - // Quit app on Cmd+Q (MacOS) + kubeConfigSyncManager.stopSync(); - event.preventDefault(); // prevent app's default shutdown (e.g. required for telemetry, etc.) + onCloseCleanup(); - return; // skip exit to make tray work, to quit go to app's global menu or tray's menu - } + // This is set to false here so that LPRM can wait to send future lens:// + // requests until after it loads again + lensProtocolRouterMain.rendererLoaded = false; - lensProtocolRouterMain.cleanup(); - onQuitCleanup(); -}); + if (blockQuit) { + // Quit app on Cmd+Q (MacOS) -app.on("open-url", (event, rawUrl) => { - logger.debug("open-url message"); + event.preventDefault(); // prevent app's default shutdown (e.g. required for telemetry, etc.) - // lens:// protocol handler - event.preventDefault(); - lensProtocolRouterMain.route(rawUrl); + return; // skip exit to make tray work, to quit go to app's global menu or tray's menu + } + + lensProtocolRouterMain.cleanup(); + onQuitCleanup(); + }); + + app.on("open-url", (event, rawUrl) => { + logger.debug("open-url message"); + + // lens:// protocol handler + event.preventDefault(); + lensProtocolRouterMain.route(rawUrl); + }); + + logger.debug("[APP-MAIN] waiting for 'ready' and other messages"); }); /** @@ -360,5 +388,3 @@ export { Mobx, LensExtensions, }; - -logger.debug("[APP-MAIN] waiting for 'ready' and other messages"); diff --git a/src/main/initializers/index.ts b/src/main/initializers/index.ts index 44c994789a..e660a1277f 100644 --- a/src/main/initializers/index.ts +++ b/src/main/initializers/index.ts @@ -19,5 +19,4 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ export * from "./metrics-providers"; -export * from "./ipc"; export * from "./cluster-metadata-detectors"; diff --git a/src/main/initializers/init-ipc-main-handlers/init-ipc-main-handlers.injectable.ts b/src/main/initializers/init-ipc-main-handlers/init-ipc-main-handlers.injectable.ts new file mode 100644 index 0000000000..6c1a783659 --- /dev/null +++ b/src/main/initializers/init-ipc-main-handlers/init-ipc-main-handlers.injectable.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import electronMenuItemsInjectable from "../../menu/electron-menu-items.injectable"; +import directoryForLensLocalStorageInjectable + from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable"; +import { initIpcMainHandlers } from "./init-ipc-main-handlers"; + +const initIpcMainHandlersInjectable = getInjectable({ + instantiate: (di) => initIpcMainHandlers({ + electronMenuItems: di.inject(electronMenuItemsInjectable), + directoryForLensLocalStorage: di.inject(directoryForLensLocalStorageInjectable), + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default initIpcMainHandlersInjectable; diff --git a/src/main/initializers/ipc.ts b/src/main/initializers/init-ipc-main-handlers/init-ipc-main-handlers.ts similarity index 83% rename from src/main/initializers/ipc.ts rename to src/main/initializers/init-ipc-main-handlers/init-ipc-main-handlers.ts index e3d4553935..9fd2bf8d0c 100644 --- a/src/main/initializers/ipc.ts +++ b/src/main/initializers/init-ipc-main-handlers/init-ipc-main-handlers.ts @@ -20,25 +20,29 @@ */ import { BrowserWindow, dialog, IpcMainInvokeEvent, Menu } from "electron"; -import { clusterFrameMap } from "../../common/cluster-frames"; -import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler, clusterSetDeletingHandler, clusterClearDeletingHandler } from "../../common/cluster-ipc"; -import type { ClusterId } from "../../common/cluster-types"; -import { ClusterStore } from "../../common/cluster-store"; -import { appEventBus } from "../../common/event-bus"; -import { dialogShowOpenDialogHandler, ipcMainHandle, ipcMainOn } from "../../common/ipc"; -import { catalogEntityRegistry } from "../catalog"; -import { pushCatalogToRenderer } from "../catalog-pusher"; -import { ClusterManager } from "../cluster-manager"; -import { ResourceApplier } from "../resource-applier"; -import { IpcMainWindowEvents, WindowManager } from "../window-manager"; +import { clusterFrameMap } from "../../../common/cluster-frames"; +import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler, clusterSetDeletingHandler, clusterClearDeletingHandler } from "../../../common/cluster-ipc"; +import type { ClusterId } from "../../../common/cluster-types"; +import { ClusterStore } from "../../../common/cluster-store/cluster-store"; +import { appEventBus } from "../../../common/app-event-bus/event-bus"; +import { dialogShowOpenDialogHandler, ipcMainHandle, ipcMainOn } from "../../../common/ipc"; +import { catalogEntityRegistry } from "../../catalog"; +import { pushCatalogToRenderer } from "../../catalog-pusher"; +import { ClusterManager } from "../../cluster-manager"; +import { ResourceApplier } from "../../resource-applier"; +import { IpcMainWindowEvents, WindowManager } from "../../window-manager"; import path from "path"; import { remove } from "fs-extra"; -import { AppPaths } from "../../common/app-paths"; -import { getAppMenu } from "../menu/menu"; -import type { MenuRegistration } from "../menu/menu-registration"; +import { getAppMenu } from "../../menu/menu"; +import type { MenuRegistration } from "../../menu/menu-registration"; import type { IComputedValue } from "mobx"; -export function initIpcMainHandlers(electronMenuItems: IComputedValue) { +interface Dependencies { + electronMenuItems: IComputedValue, + directoryForLensLocalStorage: string; +} + +export const initIpcMainHandlers = ({ electronMenuItems, directoryForLensLocalStorage }: Dependencies) => () => { ipcMainHandle(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => { return ClusterStore.getInstance() .getById(clusterId) @@ -94,7 +98,7 @@ export function initIpcMainHandlers(electronMenuItems: IComputedValue(cluster: Cluster, path: string, options: RequestPromiseOptions = {}): Promise { const kubeProxyUrl = `http://localhost:${LensProxy.getInstance().port}${apiKubePrefix}`; diff --git a/src/main/kube-auth-proxy/create-kube-auth-proxy.injectable.ts b/src/main/kube-auth-proxy/create-kube-auth-proxy.injectable.ts new file mode 100644 index 0000000000..b7dc8b0b4b --- /dev/null +++ b/src/main/kube-auth-proxy/create-kube-auth-proxy.injectable.ts @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { KubeAuthProxy } from "./kube-auth-proxy"; +import type { Cluster } from "../../common/cluster/cluster"; +import bundledKubectlInjectable from "../kubectl/bundled-kubectl.injectable"; + +const createKubeAuthProxyInjectable = getInjectable({ + instantiate: (di) => { + const bundledKubectl = di.inject(bundledKubectlInjectable); + + const dependencies = { + getProxyBinPath: bundledKubectl.getPath, + }; + + return (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => + new KubeAuthProxy(dependencies, cluster, environmentVariables); + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default createKubeAuthProxyInjectable; diff --git a/src/main/kube-auth-proxy.ts b/src/main/kube-auth-proxy/kube-auth-proxy.ts similarity index 90% rename from src/main/kube-auth-proxy.ts rename to src/main/kube-auth-proxy/kube-auth-proxy.ts index 8627570138..62062125d0 100644 --- a/src/main/kube-auth-proxy.ts +++ b/src/main/kube-auth-proxy/kube-auth-proxy.ts @@ -22,15 +22,18 @@ import { ChildProcess, spawn } from "child_process"; import { waitUntilUsed } from "tcp-port-used"; import { randomBytes } from "crypto"; -import type { Cluster } from "./cluster"; -import { Kubectl } from "./kubectl"; -import logger from "./logger"; +import type { Cluster } from "../../common/cluster/cluster"; +import logger from "../logger"; import * as url from "url"; -import { getPortFrom } from "./utils/get-port"; +import { getPortFrom } from "../utils/get-port"; import { makeObservable, observable, when } from "mobx"; const startingServeRegex = /^starting to serve on (?
.+)/i; +interface Dependencies { + getProxyBinPath: () => Promise; +} + export class KubeAuthProxy { public readonly apiPrefix = `/${randomBytes(8).toString("hex")}`; @@ -43,7 +46,7 @@ export class KubeAuthProxy { protected readonly acceptHosts: string; @observable protected ready = false; - constructor(protected readonly cluster: Cluster, protected readonly env: NodeJS.ProcessEnv) { + constructor(private dependencies: Dependencies, protected readonly cluster: Cluster, protected readonly env: NodeJS.ProcessEnv) { makeObservable(this); this.acceptHosts = url.parse(this.cluster.apiUrl).hostname; @@ -58,7 +61,7 @@ export class KubeAuthProxy { return this.whenReady; } - const proxyBin = await Kubectl.bundled().getPath(); + const proxyBin = await this.dependencies.getProxyBinPath(); const args = [ "proxy", "-p", "0", diff --git a/src/main/kubeconfig-manager/create-kubeconfig-manager.injectable.ts b/src/main/kubeconfig-manager/create-kubeconfig-manager.injectable.ts new file mode 100644 index 0000000000..5caf47d540 --- /dev/null +++ b/src/main/kubeconfig-manager/create-kubeconfig-manager.injectable.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { Cluster } from "../../common/cluster/cluster"; +import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable"; +import { KubeconfigManager } from "./kubeconfig-manager"; + +export interface KubeConfigManagerInstantiationParameter { + cluster: Cluster; +} + +const createKubeconfigManagerInjectable = getInjectable({ + instantiate: (di) => { + const dependencies = { + directoryForTemp: di.inject(directoryForTempInjectable), + }; + + return (cluster: Cluster) => new KubeconfigManager(dependencies, cluster); + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default createKubeconfigManagerInjectable; diff --git a/src/main/kubeconfig-manager.ts b/src/main/kubeconfig-manager/kubeconfig-manager.ts similarity index 86% rename from src/main/kubeconfig-manager.ts rename to src/main/kubeconfig-manager/kubeconfig-manager.ts index 7f0cfb25ee..1cc2798158 100644 --- a/src/main/kubeconfig-manager.ts +++ b/src/main/kubeconfig-manager/kubeconfig-manager.ts @@ -20,14 +20,17 @@ */ import type { KubeConfig } from "@kubernetes/client-node"; -import type { Cluster } from "./cluster"; -import type { ContextHandler } from "./context-handler"; +import type { Cluster } from "../../common/cluster/cluster"; +import type { ContextHandler } from "../context-handler/context-handler"; import path from "path"; import fs from "fs-extra"; -import { dumpConfigYaml } from "../common/kube-helpers"; -import logger from "./logger"; -import { LensProxy } from "./lens-proxy"; -import { AppPaths } from "../common/app-paths"; +import { dumpConfigYaml } from "../../common/kube-helpers"; +import logger from "../logger"; +import { LensProxy } from "../lens-proxy"; + +interface Dependencies { + directoryForTemp: string +} export class KubeconfigManager { /** @@ -39,7 +42,11 @@ export class KubeconfigManager { */ protected tempFilePath: string | null | undefined = null; - constructor(protected cluster: Cluster, protected contextHandler: ContextHandler) { } + protected contextHandler: ContextHandler; + + constructor(private dependencies: Dependencies, protected cluster: Cluster) { + this.contextHandler = cluster.contextHandler; + } /** * @@ -98,7 +105,10 @@ export class KubeconfigManager { protected async createProxyKubeconfig(): Promise { const { cluster } = this; const { contextName, id } = cluster; - const tempFile = path.join(AppPaths.get("temp"), `kubeconfig-${id}`); + const tempFile = path.join( + this.dependencies.directoryForTemp, + `kubeconfig-${id}`, + ); const kubeConfig = await cluster.getKubeconfig(); const proxyConfig: Partial = { currentContext: contextName, diff --git a/src/main/kubectl/bundled-kubectl.injectable.ts b/src/main/kubectl/bundled-kubectl.injectable.ts new file mode 100644 index 0000000000..25199485de --- /dev/null +++ b/src/main/kubectl/bundled-kubectl.injectable.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { getBundledKubectlVersion } from "../../common/utils"; +import createKubectlInjectable from "./create-kubectl.injectable"; + +const bundledKubectlInjectable = getInjectable({ + instantiate: (di) => { + const createKubectl = di.inject(createKubectlInjectable); + + const bundledKubectlVersion = getBundledKubectlVersion(); + + return createKubectl(bundledKubectlVersion); + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default bundledKubectlInjectable; diff --git a/src/main/kubectl/create-kubectl.injectable.ts b/src/main/kubectl/create-kubectl.injectable.ts new file mode 100644 index 0000000000..cb1de7e93d --- /dev/null +++ b/src/main/kubectl/create-kubectl.injectable.ts @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { Kubectl } from "./kubectl"; +import directoryForKubectlBinariesInjectable from "./directory-for-kubectl-binaries/directory-for-kubectl-binaries.injectable"; +import userStoreInjectable from "../../common/user-store/user-store.injectable"; + +const createKubectlInjectable = getInjectable({ + instantiate: (di) => { + const dependencies = { + userStore: di.inject(userStoreInjectable), + + directoryForKubectlBinaries: di.inject( + directoryForKubectlBinariesInjectable, + ), + }; + + return (clusterVersion: string) => + new Kubectl(dependencies, clusterVersion); + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default createKubectlInjectable; diff --git a/src/main/kubectl/directory-for-kubectl-binaries/directory-for-kubectl-binaries.injectable.ts b/src/main/kubectl/directory-for-kubectl-binaries/directory-for-kubectl-binaries.injectable.ts new file mode 100644 index 0000000000..8b6e708601 --- /dev/null +++ b/src/main/kubectl/directory-for-kubectl-binaries/directory-for-kubectl-binaries.injectable.ts @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import directoryForBinariesInjectable from "../../../common/app-paths/directory-for-binaries/directory-for-binaries.injectable"; +import path from "path"; + +const directoryForKubectlBinariesInjectable = getInjectable({ + instantiate: (di) => + path.join(di.inject(directoryForBinariesInjectable), "kubectl"), + + lifecycle: lifecycleEnum.singleton, +}); + +export default directoryForKubectlBinariesInjectable; diff --git a/src/main/kubectl.ts b/src/main/kubectl/kubectl.ts similarity index 88% rename from src/main/kubectl.ts rename to src/main/kubectl/kubectl.ts index 2ac1831c41..d37a798b58 100644 --- a/src/main/kubectl.ts +++ b/src/main/kubectl/kubectl.ts @@ -21,18 +21,16 @@ import path from "path"; import fs from "fs"; -import { promiseExecFile } from "../common/utils/promise-exec"; -import logger from "./logger"; +import { promiseExecFile } from "../../common/utils/promise-exec"; +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 { customRequest } from "../common/request"; -import { getBundledKubectlVersion } from "../common/utils/app-version"; -import { isDevelopment, isWindows, isTestEnv } from "../common/vars"; +import { helmCli } from "../helm/helm-cli"; +import { customRequest } from "../../common/request"; +import { getBundledKubectlVersion } from "../../common/utils/app-version"; +import { isDevelopment, isWindows, isTestEnv } from "../../common/vars"; import { SemVer } from "semver"; -import { defaultPackageMirror, packageMirrors } from "../common/user-store/preferences-helpers"; -import { AppPaths } from "../common/app-paths"; +import { defaultPackageMirror, packageMirrors } from "../../common/user-store/preferences-helpers"; const bundledVersion = getBundledKubectlVersion(); const kubectlMap: Map = new Map([ @@ -73,6 +71,17 @@ export function bundledKubectlPath(): string { return bundledPath; } +interface Dependencies { + directoryForKubectlBinaries: string; + + userStore: { + kubectlBinariesPath?: string + downloadBinariesPath?: string + downloadKubectlBinaries: boolean + downloadMirror: string + }; +} + export class Kubectl { public kubectlVersion: string; protected directory: string; @@ -80,20 +89,10 @@ export class Kubectl { protected path: string; protected dirname: string; - static get kubectlDir() { - return path.join(AppPaths.get("userData"), "binaries", "kubectl"); - } - public static readonly bundledKubectlVersion: string = bundledVersion; public static invalidBundle = false; - private static bundledInstance: Kubectl; - // Returns the single bundled Kubectl instance - public static bundled() { - return Kubectl.bundledInstance ??= new Kubectl(Kubectl.bundledKubectlVersion); - } - - constructor(clusterVersion: string) { + constructor(private dependencies: Dependencies, clusterVersion: string) { let version: SemVer; try { @@ -138,23 +137,23 @@ export class Kubectl { } public getPathFromPreferences() { - return UserStore.getInstance().kubectlBinariesPath || this.getBundledPath(); + return this.dependencies.userStore.kubectlBinariesPath || this.getBundledPath(); } protected getDownloadDir() { - if (UserStore.getInstance().downloadBinariesPath) { - return path.join(UserStore.getInstance().downloadBinariesPath, "kubectl"); + if (this.dependencies.userStore.downloadBinariesPath) { + return path.join(this.dependencies.userStore.downloadBinariesPath, "kubectl"); } - return Kubectl.kubectlDir; + return this.dependencies.directoryForKubectlBinaries; } - public async getPath(bundled = false): Promise { + public getPath = async (bundled = false): Promise => { if (bundled) { return this.getBundledPath(); } - if (UserStore.getInstance().downloadKubectlBinaries === false) { + if (this.dependencies.userStore.downloadKubectlBinaries === false) { return this.getPathFromPreferences(); } @@ -179,7 +178,7 @@ export class Kubectl { return this.getBundledPath(); } - } + }; public async binDir() { try { @@ -253,7 +252,7 @@ export class Kubectl { } public async ensureKubectl(): Promise { - if (UserStore.getInstance().downloadKubectlBinaries === false) { + if (this.dependencies.userStore.downloadKubectlBinaries === false) { return true; } @@ -339,7 +338,7 @@ export class Kubectl { } protected async writeInitScripts() { - const kubectlPath = UserStore.getInstance().downloadKubectlBinaries ? this.dirname : path.dirname(this.getPathFromPreferences()); + const kubectlPath = this.dependencies.userStore.downloadKubectlBinaries ? this.dirname : path.dirname(this.getPathFromPreferences()); const helmPath = helmCli.getBinaryDir(); const fsPromises = fs.promises; const bashScriptPath = path.join(this.dirname, ".bash_set_path"); @@ -399,7 +398,7 @@ export class Kubectl { protected getDownloadMirror(): string { // MacOS packages are only available from default - const mirror = packageMirrors.get(UserStore.getInstance().downloadMirror) + const mirror = packageMirrors.get(this.dependencies.userStore.downloadMirror) ?? packageMirrors.get(defaultPackageMirror); return mirror.url; diff --git a/src/main/kubectl_spec.ts b/src/main/kubectl_spec.ts deleted file mode 100644 index 974ced9f73..0000000000 --- a/src/main/kubectl_spec.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * Copyright (c) 2021 OpenLens Authors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -import packageInfo from "../../package.json"; -import path from "path"; -import { Kubectl } from "../../src/main/kubectl"; -import { isWindows } from "../common/vars"; - -jest.mock("../common/user-store"); - -describe("kubectlVersion", () => { - it("returns bundled version if exactly same version used", async () => { - const kubectl = new Kubectl(Kubectl.bundled().kubectlVersion); - - expect(kubectl.kubectlVersion).toBe(Kubectl.bundled().kubectlVersion); - }); - - it("returns bundled version if same major.minor version is used", async () => { - const { bundledKubectlVersion } = packageInfo.config; - const kubectl = new Kubectl(bundledKubectlVersion); - - expect(kubectl.kubectlVersion).toBe(Kubectl.bundled().kubectlVersion); - }); -}); - -describe("getPath()", () => { - it("returns path to downloaded kubectl binary", async () => { - const { bundledKubectlVersion } = packageInfo.config; - const kubectl = new Kubectl(bundledKubectlVersion); - const kubectlPath = await kubectl.getPath(); - let binaryName = "kubectl"; - - if (isWindows) { - binaryName += ".exe"; - } - const expectedPath = path.join(Kubectl.kubectlDir, Kubectl.bundledKubectlVersion, binaryName); - - expect(kubectlPath).toBe(expectedPath); - }); - - it("returns plain binary name if bundled kubectl is non-functional", async () => { - const { bundledKubectlVersion } = packageInfo.config; - const kubectl = new Kubectl(bundledKubectlVersion); - - jest.spyOn(kubectl, "getBundledPath").mockReturnValue("/invalid/path/kubectl"); - const kubectlPath = await kubectl.getPath(); - let binaryName = "kubectl"; - - if (isWindows) { - binaryName += ".exe"; - } - expect(kubectlPath).toBe(binaryName); - }); -}); diff --git a/src/main/lens-proxy.ts b/src/main/lens-proxy.ts index a9982d5cb7..f5f37732b0 100644 --- a/src/main/lens-proxy.ts +++ b/src/main/lens-proxy.ts @@ -25,12 +25,12 @@ import spdy from "spdy"; import httpProxy from "http-proxy"; import { apiPrefix, apiKubePrefix } from "../common/vars"; import type { Router } from "./router"; -import type { ContextHandler } from "./context-handler"; +import type { ContextHandler } from "./context-handler/context-handler"; import logger from "./logger"; import { Singleton } from "../common/utils"; -import type { Cluster } from "./cluster"; +import type { Cluster } from "../common/cluster/cluster"; import type { ProxyApiRequestArgs } from "./proxy-functions"; -import { appEventBus } from "../common/event-bus"; +import { appEventBus } from "../common/app-event-bus/event-bus"; import { getBoolean } from "./utils/parse-query"; type GetClusterForRequest = (req: http.IncomingMessage) => Cluster | null; diff --git a/src/main/menu/electron-menu-items.test.ts b/src/main/menu/electron-menu-items.test.ts index edd885d1c6..d4837b3b61 100644 --- a/src/main/menu/electron-menu-items.test.ts +++ b/src/main/menu/electron-menu-items.test.ts @@ -32,8 +32,8 @@ describe("electron-menu-items", () => { let electronMenuItems: IComputedValue; let extensionsStub: ObservableMap; - beforeEach(() => { - di = getDiForUnitTesting(); + beforeEach(async () => { + di = getDiForUnitTesting({ doGeneralOverrides: true }); extensionsStub = new ObservableMap(); @@ -42,6 +42,8 @@ describe("electron-menu-items", () => { () => computed(() => [...extensionsStub.values()]), ); + await di.runSetups(); + electronMenuItems = di.inject(electronMenuItemsInjectable); }); diff --git a/src/main/protocol-handler/__test__/router.test.ts b/src/main/protocol-handler/__test__/router.test.ts index 7c81a78711..fc287c4bc7 100644 --- a/src/main/protocol-handler/__test__/router.test.ts +++ b/src/main/protocol-handler/__test__/router.test.ts @@ -28,7 +28,6 @@ import { LensExtension } from "../../../extensions/main-api"; import { ExtensionsStore } from "../../../extensions/extensions-store/extensions-store"; import type { LensProtocolRouterMain } from "../lens-protocol-router-main/lens-protocol-router-main"; import mockFs from "mock-fs"; -import { AppPaths } from "../../../common/app-paths"; import { getDiForUnitTesting } from "../../getDiForUnitTesting"; import extensionLoaderInjectable from "../../../extensions/extension-loader/extension-loader.injectable"; @@ -39,24 +38,6 @@ import extensionsStoreInjectable jest.mock("../../../common/ipc"); -jest.mock("electron", () => ({ - app: { - getVersion: () => "99.99.99", - getName: () => "lens", - setName: jest.fn(), - setPath: jest.fn(), - getPath: () => "tmp", - getLocale: () => "en", - setLoginItemSettings: jest.fn(), - }, - ipcMain: { - on: jest.fn(), - handle: jest.fn(), - }, -})); - -AppPaths.init(); - function throwIfDefined(val: any): void { if (val != null) { throw val; @@ -70,16 +51,19 @@ describe("protocol router tests", () => { let lpr: LensProtocolRouterMain; let extensionsStore: ExtensionsStore; - beforeEach(() => { - const di = getDiForUnitTesting(); - - extensionLoader = di.inject(extensionLoaderInjectable); - extensionsStore = di.inject(extensionsStoreInjectable); + beforeEach(async () => { + const di = getDiForUnitTesting({ doGeneralOverrides: true }); mockFs({ "tmp": {}, }); + await di.runSetups(); + + extensionLoader = di.inject(extensionLoaderInjectable); + extensionsStore = di.inject(extensionsStoreInjectable); + + lpr = di.inject(lensProtocolRouterMainInjectable); lpr.rendererLoaded = true; diff --git a/src/main/proxy-functions/index.ts b/src/main/proxy-functions/index.ts index 2154d81014..3aca4e4d29 100644 --- a/src/main/proxy-functions/index.ts +++ b/src/main/proxy-functions/index.ts @@ -18,7 +18,5 @@ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ - -export * from "./shell-api-request"; export * from "./kube-api-request"; export * from "./types"; diff --git a/src/main/proxy-functions/shell-api-request/shell-api-request.injectable.ts b/src/main/proxy-functions/shell-api-request/shell-api-request.injectable.ts new file mode 100644 index 0000000000..a67e58d26d --- /dev/null +++ b/src/main/proxy-functions/shell-api-request/shell-api-request.injectable.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { shellApiRequest } from "./shell-api-request"; +import createShellSessionInjectable from "../../shell-session/create-shell-session.injectable"; +import shellRequestAuthenticatorInjectable + from "./shell-request-authenticator/shell-request-authenticator.injectable"; + +const shellApiRequestInjectable = getInjectable({ + instantiate: (di) => shellApiRequest({ + createShellSession: di.inject(createShellSessionInjectable), + authenticateRequest: di.inject(shellRequestAuthenticatorInjectable).authenticate, + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default shellApiRequestInjectable; diff --git a/src/main/proxy-functions/shell-api-request/shell-api-request.ts b/src/main/proxy-functions/shell-api-request/shell-api-request.ts new file mode 100644 index 0000000000..5226b03631 --- /dev/null +++ b/src/main/proxy-functions/shell-api-request/shell-api-request.ts @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import logger from "../../logger"; +import WebSocket, { Server as WebSocketServer } from "ws"; +import type { ProxyApiRequestArgs } from "../types"; +import { ClusterManager } from "../../cluster-manager"; +import URLParse from "url-parse"; +import type { Cluster } from "../../../common/cluster/cluster"; +import type { ClusterId } from "../../../common/cluster-types"; + +interface Dependencies { + authenticateRequest: (clusterId: ClusterId, tabId: string, shellToken: string) => boolean, + + createShellSession: (args: { + webSocket: WebSocket; + cluster: Cluster; + tabId: string; + nodeName?: string; + }) => { open: () => Promise }; +} + +export const shellApiRequest = ({ createShellSession, authenticateRequest }: Dependencies) => ({ req, socket, head }: ProxyApiRequestArgs): void => { + const cluster = ClusterManager.getInstance().getClusterForRequest(req); + const { query: { node: nodeName, shellToken, id: tabId }} = new URLParse(req.url, true); + + if (!cluster || !authenticateRequest(cluster.id, tabId, shellToken)) { + socket.write("Invalid shell request"); + + return void socket.end(); + } + + const ws = new WebSocketServer({ noServer: true }); + + ws.handleUpgrade(req, socket, head, (webSocket) => { + const shell = createShellSession({ webSocket, cluster, tabId, nodeName }); + + shell.open() + .catch(error => logger.error(`[SHELL-SESSION]: failed to open a ${nodeName ? "node" : "local"} shell`, error)); + }); +}; diff --git a/src/main/proxy-functions/shell-api-request/shell-request-authenticator/shell-request-authenticator.injectable.ts b/src/main/proxy-functions/shell-api-request/shell-request-authenticator/shell-request-authenticator.injectable.ts new file mode 100644 index 0000000000..c8d2797d4e --- /dev/null +++ b/src/main/proxy-functions/shell-api-request/shell-request-authenticator/shell-request-authenticator.injectable.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { ShellRequestAuthenticator } from "./shell-request-authenticator"; + +const shellRequestAuthenticatorInjectable = getInjectable({ + instantiate: () => { + const authenticator = new ShellRequestAuthenticator(); + + authenticator.init(); + + return authenticator; + }, + lifecycle: lifecycleEnum.singleton, +}); + +export default shellRequestAuthenticatorInjectable; diff --git a/src/main/proxy-functions/shell-api-request.ts b/src/main/proxy-functions/shell-api-request/shell-request-authenticator/shell-request-authenticator.ts similarity index 64% rename from src/main/proxy-functions/shell-api-request.ts rename to src/main/proxy-functions/shell-api-request/shell-request-authenticator/shell-request-authenticator.ts index 76340585e1..4259f98435 100644 --- a/src/main/proxy-functions/shell-api-request.ts +++ b/src/main/proxy-functions/shell-api-request/shell-request-authenticator/shell-request-authenticator.ts @@ -18,22 +18,15 @@ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ - -import logger from "../logger"; -import { Server as WebSocketServer } from "ws"; -import { NodeShellSession, LocalShellSession } from "../shell-session"; -import type { ProxyApiRequestArgs } from "./types"; -import { ClusterManager } from "../cluster-manager"; -import URLParse from "url-parse"; -import { ExtendedMap, Singleton } from "../../common/utils"; -import type { ClusterId } from "../../common/cluster-types"; -import { ipcMainHandle } from "../../common/ipc"; +import { ExtendedMap } from "../../../../common/utils"; +import type { ClusterId } from "../../../../common/cluster-types"; +import { ipcMainHandle } from "../../../../common/ipc"; import crypto from "crypto"; import { promisify } from "util"; const randomBytes = promisify(crypto.randomBytes); -export class ShellRequestAuthenticator extends Singleton { +export class ShellRequestAuthenticator { private tokens = new ExtendedMap>(); init() { @@ -75,25 +68,3 @@ export class ShellRequestAuthenticator extends Singleton { return false; } } - -export function shellApiRequest({ req, socket, head }: ProxyApiRequestArgs): void { - const cluster = ClusterManager.getInstance().getClusterForRequest(req); - const { query: { node, shellToken, id: tabId }} = new URLParse(req.url, true); - - if (!cluster || !ShellRequestAuthenticator.getInstance().authenticate(cluster.id, tabId, shellToken)) { - socket.write("Invalid shell request"); - - return void socket.end(); - } - - const ws = new WebSocketServer({ noServer: true }); - - ws.handleUpgrade(req, socket, head, (webSocket) => { - const shell = node - ? new NodeShellSession(webSocket, cluster, node, tabId) - : new LocalShellSession(webSocket, cluster, tabId); - - shell.open() - .catch(error => logger.error(`[SHELL-SESSION]: failed to open a ${node ? "node" : "local"} shell`, error)); - }); -} diff --git a/src/main/proxy-functions/types.ts b/src/main/proxy-functions/types.ts index 2a41b9b97f..c9f8f78958 100644 --- a/src/main/proxy-functions/types.ts +++ b/src/main/proxy-functions/types.ts @@ -21,7 +21,7 @@ import type http from "http"; import type net from "net"; -import type { Cluster } from "../cluster"; +import type { Cluster } from "../../common/cluster/cluster"; export interface ProxyApiRequestArgs { req: http.IncomingMessage, diff --git a/src/main/resource-applier.ts b/src/main/resource-applier.ts index 7be1eec94d..b414cecc4c 100644 --- a/src/main/resource-applier.ts +++ b/src/main/resource-applier.ts @@ -19,7 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import type { Cluster } from "./cluster"; +import type { Cluster } from "../common/cluster/cluster"; import type { KubernetesObject } from "@kubernetes/client-node"; import { exec } from "child_process"; import fs from "fs-extra"; @@ -27,7 +27,7 @@ import * as yaml from "js-yaml"; import path from "path"; import * as tempy from "tempy"; import logger from "./logger"; -import { appEventBus } from "../common/event-bus"; +import { appEventBus } from "../common/app-event-bus/event-bus"; import { cloneJsonObject } from "../common/utils"; import type { Patch } from "rfc6902"; import { promiseExecFile } from "../common/utils/promise-exec"; diff --git a/src/main/router.ts b/src/main/router.ts index 96ed7cdd02..affe92eecd 100644 --- a/src/main/router.ts +++ b/src/main/router.ts @@ -24,7 +24,7 @@ import Subtext from "@hapi/subtext"; import type http from "http"; import path from "path"; import { readFile } from "fs-extra"; -import type { Cluster } from "./cluster"; +import type { Cluster } from "../common/cluster/cluster"; import { apiPrefix, appName, publicPath, isDevelopment, webpackDevServerPort } from "../common/vars"; import { HelmApiRoute, KubeconfigRoute, MetricsRoute, PortForwardRoute, ResourceApplierApiRoute, VersionRoute } from "./routes"; import logger from "./logger"; @@ -76,11 +76,15 @@ function getMimeType(filename: string) { return mimeTypes[path.extname(filename).slice(1)] || "text/plain"; } +interface Dependencies { + routePortForward: (request: LensApiRequest) => Promise +} + export class Router { protected router = new Call.Router(); protected static rootPath = path.resolve(__static); - public constructor() { + public constructor(private dependencies: Dependencies) { this.addRoutes(); } @@ -180,7 +184,7 @@ export class Router { this.router.add({ method: "get", path: `${apiPrefix}/metrics/providers` }, MetricsRoute.routeMetricsProviders); // Port-forward API (the container port and local forwarding port are obtained from the query parameters) - this.router.add({ method: "post", path: `${apiPrefix}/pods/port-forward/{namespace}/{resourceType}/{resourceName}` }, PortForwardRoute.routePortForward); + this.router.add({ method: "post", path: `${apiPrefix}/pods/port-forward/{namespace}/{resourceType}/{resourceName}` }, this.dependencies.routePortForward); this.router.add({ method: "get", path: `${apiPrefix}/pods/port-forward/{namespace}/{resourceType}/{resourceName}` }, PortForwardRoute.routeCurrentPortForward); this.router.add({ method: "get", path: `${apiPrefix}/pods/port-forwards` }, PortForwardRoute.routeAllPortForwards); this.router.add({ method: "delete", path: `${apiPrefix}/pods/port-forward/{namespace}/{resourceType}/{resourceName}` }, PortForwardRoute.routeCurrentPortForwardStop); diff --git a/src/main/router/router.injectable.ts b/src/main/router/router.injectable.ts new file mode 100644 index 0000000000..98d3690233 --- /dev/null +++ b/src/main/router/router.injectable.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { Router } from "../router"; +import routePortForwardInjectable + from "../routes/port-forward/route-port-forward/route-port-forward.injectable"; + +const routerInjectable = getInjectable({ + instantiate: (di) => new Router({ + routePortForward: di.inject(routePortForwardInjectable), + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default routerInjectable; diff --git a/src/main/routes/kubeconfig-route.ts b/src/main/routes/kubeconfig-route.ts index 14b09489f0..78a520244c 100644 --- a/src/main/routes/kubeconfig-route.ts +++ b/src/main/routes/kubeconfig-route.ts @@ -21,7 +21,7 @@ import type { LensApiRequest } from "../router"; import { respondJson } from "../utils/http-responses"; -import type { Cluster } from "../cluster"; +import type { Cluster } from "../../common/cluster/cluster"; import { CoreV1Api, V1Secret } from "@kubernetes/client-node"; function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster) { diff --git a/src/main/routes/metrics-route.ts b/src/main/routes/metrics-route.ts index c323c06be9..bd00d58d18 100644 --- a/src/main/routes/metrics-route.ts +++ b/src/main/routes/metrics-route.ts @@ -21,7 +21,7 @@ import type { LensApiRequest } from "../router"; import { respondJson } from "../utils/http-responses"; -import type { Cluster } from "../cluster"; +import type { Cluster } from "../../common/cluster/cluster"; import { ClusterMetadataKey, ClusterPrometheusMetadata } from "../../common/cluster-types"; import logger from "../logger"; import { getMetrics } from "../k8s-request"; diff --git a/src/main/routes/port-forward-route.ts b/src/main/routes/port-forward-route.ts index 6392c70ed9..c8a97a0436 100644 --- a/src/main/routes/port-forward-route.ts +++ b/src/main/routes/port-forward-route.ts @@ -20,159 +20,11 @@ */ import type { LensApiRequest } from "../router"; -import { spawn, ChildProcessWithoutNullStreams } from "child_process"; -import { Kubectl } from "../kubectl"; -import * as tcpPortUsed from "tcp-port-used"; import logger from "../logger"; -import { getPortFrom } from "../utils/get-port"; import { respondJson } from "../utils/http-responses"; - -interface PortForwardArgs { - clusterId: string; - kind: string; - namespace: string; - name: string; - port: number; - forwardPort: number; - protocol?: string; -} - -const internalPortRegex = /^forwarding from (?
.+) ->/i; - -class PortForward { - public static portForwards: PortForward[] = []; - - static getPortforward(forward: PortForwardArgs) { - return PortForward.portForwards.find((pf) => ( - pf.clusterId == forward.clusterId && - pf.kind == forward.kind && - pf.name == forward.name && - pf.namespace == forward.namespace && - pf.port == forward.port && - (!forward.protocol || pf.protocol == forward.protocol) - )); - } - - public process: ChildProcessWithoutNullStreams; - public clusterId: string; - public kind: string; - public namespace: string; - public name: string; - public port: number; - public forwardPort: number; - public protocol: string; - - constructor(public kubeConfig: string, args: PortForwardArgs) { - this.clusterId = args.clusterId; - this.kind = args.kind; - this.namespace = args.namespace; - this.name = args.name; - this.port = args.port; - this.forwardPort = args.forwardPort; - this.protocol = args.protocol ?? "http"; - } - - public async start() { - const kubectlBin = await Kubectl.bundled().getPath(true); - const args = [ - "--kubeconfig", this.kubeConfig, - "port-forward", - "-n", this.namespace, - `${this.kind}/${this.name}`, - `${this.forwardPort ?? ""}:${this.port}`, - ]; - - this.process = spawn(kubectlBin, args, { - env: process.env, - }); - PortForward.portForwards.push(this); - this.process.on("exit", () => { - const index = PortForward.portForwards.indexOf(this); - - if (index > -1) { - PortForward.portForwards.splice(index, 1); - } - }); - - this.process.stderr.on("data", (data) => { - logger.debug(`[PORT-FORWARD-ROUTE]: kubectl port-forward process stderr: ${data}`); - }); - - const internalPort = await getPortFrom(this.process.stdout, { - lineRegex: internalPortRegex, - }); - - try { - await tcpPortUsed.waitUntilUsed(internalPort, 500, 15000); - - // make sure this.forwardPort is set to the actual port used (if it was 0 then an available port is found by 'kubectl port-forward') - this.forwardPort = internalPort; - - return true; - } catch (error) { - this.process.kill(); - - return false; - } - } - - public async stop() { - this.process.kill(); - } -} +import { PortForward, PortForwardArgs } from "./port-forward/port-forward"; export class PortForwardRoute { - static async routePortForward(request: LensApiRequest) { - const { params, query, response, cluster } = request; - const { namespace, resourceType, resourceName } = params; - const port = Number(query.get("port")); - const forwardPort = Number(query.get("forwardPort")); - const protocol = query.get("protocol"); - - try { - let portForward = PortForward.getPortforward({ - clusterId: cluster.id, kind: resourceType, name: resourceName, - namespace, port, forwardPort, protocol, - }); - - if (!portForward) { - logger.info(`Creating a new port-forward ${namespace}/${resourceType}/${resourceName}:${port}`); - - const thePort = 0 < forwardPort && forwardPort < 65536 - ? forwardPort - : 0; - - portForward = new PortForward(await cluster.getProxyKubeconfigPath(), { - clusterId: cluster.id, - kind: resourceType, - namespace, - name: resourceName, - port, - forwardPort: thePort, - protocol, - }); - - const started = await portForward.start(); - - if (!started) { - logger.error("[PORT-FORWARD-ROUTE]: failed to start a port-forward", { namespace, port, resourceType, resourceName }); - - return respondJson(response, { - message: `Failed to forward port ${port} to ${thePort ? forwardPort : "random port"}`, - }, 400); - } - } - - respondJson(response, { port: portForward.forwardPort }); - } catch (error) { - logger.error(`[PORT-FORWARD-ROUTE]: failed to open a port-forward: ${error}`, { namespace, port, resourceType, resourceName }); - - return respondJson(response, { - message: `Failed to forward port ${port}`, - }, 400); - } - } - static async routeCurrentPortForward(request: LensApiRequest) { const { params, query, response, cluster } = request; const { namespace, resourceType, resourceName } = params; diff --git a/src/main/routes/port-forward/create-port-forward.injectable.ts b/src/main/routes/port-forward/create-port-forward.injectable.ts new file mode 100644 index 0000000000..40deb16641 --- /dev/null +++ b/src/main/routes/port-forward/create-port-forward.injectable.ts @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { PortForward, PortForwardArgs } from "./port-forward"; +import bundledKubectlInjectable from "../../kubectl/bundled-kubectl.injectable"; + +const createPortForwardInjectable = getInjectable({ + instantiate: (di) => { + const bundledKubectl = di.inject(bundledKubectlInjectable); + + const dependencies = { + getKubectlBinPath: bundledKubectl.getPath, + }; + + return (pathToKubeConfig: string, args: PortForwardArgs) => + new PortForward(dependencies, pathToKubeConfig, args); + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default createPortForwardInjectable; diff --git a/src/main/routes/port-forward/port-forward.ts b/src/main/routes/port-forward/port-forward.ts new file mode 100644 index 0000000000..4b973d261c --- /dev/null +++ b/src/main/routes/port-forward/port-forward.ts @@ -0,0 +1,122 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import logger from "../../logger"; +import { getPortFrom } from "../../utils/get-port"; +import { spawn, ChildProcessWithoutNullStreams } from "child_process"; +import * as tcpPortUsed from "tcp-port-used"; + +const internalPortRegex = /^forwarding from (?
.+) ->/i; + +export interface PortForwardArgs { + clusterId: string; + kind: string; + namespace: string; + name: string; + port: number; + forwardPort: number; + protocol?: string; +} + +interface Dependencies { + getKubectlBinPath: (bundled: boolean) => Promise +} + +export class PortForward { + public static portForwards: PortForward[] = []; + + static getPortforward(forward: PortForwardArgs) { + return PortForward.portForwards.find((pf) => ( + pf.clusterId == forward.clusterId && + pf.kind == forward.kind && + pf.name == forward.name && + pf.namespace == forward.namespace && + pf.port == forward.port && + (!forward.protocol || pf.protocol == forward.protocol) + )); + } + + public process: ChildProcessWithoutNullStreams; + public clusterId: string; + public kind: string; + public namespace: string; + public name: string; + public port: number; + public forwardPort: number; + public protocol: string; + + constructor(private dependencies: Dependencies, public pathToKubeConfig: string, args: PortForwardArgs) { + this.clusterId = args.clusterId; + this.kind = args.kind; + this.namespace = args.namespace; + this.name = args.name; + this.port = args.port; + this.forwardPort = args.forwardPort; + this.protocol = args.protocol ?? "http"; + } + + public async start() { + const kubectlBin = await this.dependencies.getKubectlBinPath(true); + const args = [ + "--kubeconfig", this.pathToKubeConfig, + "port-forward", + "-n", this.namespace, + `${this.kind}/${this.name}`, + `${this.forwardPort ?? ""}:${this.port}`, + ]; + + this.process = spawn(kubectlBin, args, { + env: process.env, + }); + PortForward.portForwards.push(this); + this.process.on("exit", () => { + const index = PortForward.portForwards.indexOf(this); + + if (index > -1) { + PortForward.portForwards.splice(index, 1); + } + }); + + this.process.stderr.on("data", (data) => { + logger.debug(`[PORT-FORWARD-ROUTE]: kubectl port-forward process stderr: ${data}`); + }); + + const internalPort = await getPortFrom(this.process.stdout, { + lineRegex: internalPortRegex, + }); + + try { + await tcpPortUsed.waitUntilUsed(internalPort, 500, 15000); + + // make sure this.forwardPort is set to the actual port used (if it was 0 then an available port is found by 'kubectl port-forward') + this.forwardPort = internalPort; + + return true; + } catch (error) { + this.process.kill(); + + return false; + } + } + + public async stop() { + this.process.kill(); + } +} diff --git a/src/main/routes/port-forward/route-port-forward/route-port-forward.injectable.ts b/src/main/routes/port-forward/route-port-forward/route-port-forward.injectable.ts new file mode 100644 index 0000000000..db1130500f --- /dev/null +++ b/src/main/routes/port-forward/route-port-forward/route-port-forward.injectable.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { routePortForward } from "./route-port-forward"; +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import createPortForwardInjectable from "../create-port-forward.injectable"; + +const routePortForwardInjectable = getInjectable({ + instantiate: (di) => routePortForward({ + createPortForward: di.inject(createPortForwardInjectable), + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default routePortForwardInjectable; diff --git a/src/main/routes/port-forward/route-port-forward/route-port-forward.ts b/src/main/routes/port-forward/route-port-forward/route-port-forward.ts new file mode 100644 index 0000000000..738cb6679f --- /dev/null +++ b/src/main/routes/port-forward/route-port-forward/route-port-forward.ts @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import type { LensApiRequest } from "../../../router"; +import logger from "../../../logger"; +import { respondJson } from "../../../utils/http-responses"; +import { PortForward, PortForwardArgs } from "../port-forward"; + +interface Dependencies { + createPortForward: (pathToKubeConfig: string, args: PortForwardArgs) => PortForward; +} + +export const routePortForward = + ({ createPortForward }: Dependencies) => + async (request: LensApiRequest) => { + const { params, query, response, cluster } = request; + const { namespace, resourceType, resourceName } = params; + const port = Number(query.get("port")); + const forwardPort = Number(query.get("forwardPort")); + const protocol = query.get("protocol"); + + try { + let portForward = PortForward.getPortforward({ + clusterId: cluster.id, + kind: resourceType, + name: resourceName, + namespace, + port, + forwardPort, + protocol, + }); + + if (!portForward) { + logger.info( + `Creating a new port-forward ${namespace}/${resourceType}/${resourceName}:${port}`, + ); + + const thePort = + 0 < forwardPort && forwardPort < 65536 ? forwardPort : 0; + + portForward = createPortForward(await cluster.getProxyKubeconfigPath(), { + clusterId: cluster.id, + kind: resourceType, + namespace, + name: resourceName, + port, + forwardPort: thePort, + protocol, + }); + + const started = await portForward.start(); + + if (!started) { + logger.error("[PORT-FORWARD-ROUTE]: failed to start a port-forward", { + namespace, + port, + resourceType, + resourceName, + }); + + return respondJson( + response, + { + message: `Failed to forward port ${port} to ${ + thePort ? forwardPort : "random port" + }`, + }, + 400, + ); + } + } + + respondJson(response, { port: portForward.forwardPort }); + } catch (error) { + logger.error( + `[PORT-FORWARD-ROUTE]: failed to open a port-forward: ${error}`, + { namespace, port, resourceType, resourceName }, + ); + + return respondJson( + response, + { + message: `Failed to forward port ${port}`, + }, + 400, + ); + } + }; diff --git a/src/main/shell-session/create-shell-session.injectable.ts b/src/main/shell-session/create-shell-session.injectable.ts new file mode 100644 index 0000000000..be600ca2f2 --- /dev/null +++ b/src/main/shell-session/create-shell-session.injectable.ts @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { Cluster } from "../../common/cluster/cluster"; +import type WebSocket from "ws"; +import localShellSessionInjectable from "./local-shell-session/local-shell-session.injectable"; +import nodeShellSessionInjectable from "./node-shell-session/node-shell-session.injectable"; + +interface Args { + webSocket: WebSocket; + cluster: Cluster; + tabId: string; + nodeName?: string; +} + +const createShellSessionInjectable = getInjectable({ + instantiate: + (di) => + ({ nodeName, ...rest }: Args) => + !nodeName + ? di.inject(localShellSessionInjectable, rest) + : di.inject(nodeShellSessionInjectable, { nodeName, ...rest }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default createShellSessionInjectable; diff --git a/src/main/shell-session/index.ts b/src/main/shell-session/index.ts index 1db6cb9217..9311fc63c0 100644 --- a/src/main/shell-session/index.ts +++ b/src/main/shell-session/index.ts @@ -19,5 +19,5 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -export * from "./node-shell-session"; -export * from "./local-shell-session"; +export * from "./node-shell-session/node-shell-session"; +export * from "./local-shell-session/local-shell-session"; diff --git a/src/main/shell-session/local-shell-session/local-shell-session.injectable.ts b/src/main/shell-session/local-shell-session/local-shell-session.injectable.ts new file mode 100644 index 0000000000..b08a009b5f --- /dev/null +++ b/src/main/shell-session/local-shell-session/local-shell-session.injectable.ts @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { LocalShellSession } from "./local-shell-session"; +import type { Cluster } from "../../../common/cluster/cluster"; +import type WebSocket from "ws"; +import createKubectlInjectable from "../../kubectl/create-kubectl.injectable"; + +interface InstantiationParameter { + webSocket: WebSocket; + cluster: Cluster; + tabId: string; +} + +const localShellSessionInjectable = getInjectable({ + instantiate: (di, { cluster, tabId, webSocket }: InstantiationParameter) => { + const createKubectl = di.inject(createKubectlInjectable); + + const kubectl = createKubectl(cluster.version); + + return new LocalShellSession(kubectl, webSocket, cluster, tabId); + }, + + lifecycle: lifecycleEnum.transient, +}); + +export default localShellSessionInjectable; diff --git a/src/main/shell-session/local-shell-session.ts b/src/main/shell-session/local-shell-session/local-shell-session.ts similarity index 94% rename from src/main/shell-session/local-shell-session.ts rename to src/main/shell-session/local-shell-session/local-shell-session.ts index 855dfd3dc9..c5f2418394 100644 --- a/src/main/shell-session/local-shell-session.ts +++ b/src/main/shell-session/local-shell-session/local-shell-session.ts @@ -20,9 +20,9 @@ */ import path from "path"; -import { helmCli } from "../helm/helm-cli"; -import { UserStore } from "../../common/user-store"; -import { ShellSession } from "./shell-session"; +import { helmCli } from "../../helm/helm-cli"; +import { UserStore } from "../../../common/user-store"; +import { ShellSession } from "../shell-session"; export class LocalShellSession extends ShellSession { ShellType = "shell"; diff --git a/src/main/shell-session/node-shell-session/node-shell-session.injectable.ts b/src/main/shell-session/node-shell-session/node-shell-session.injectable.ts new file mode 100644 index 0000000000..6356800ce6 --- /dev/null +++ b/src/main/shell-session/node-shell-session/node-shell-session.injectable.ts @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { Cluster } from "../../../common/cluster/cluster"; +import type WebSocket from "ws"; +import createKubectlInjectable from "../../kubectl/create-kubectl.injectable"; +import { NodeShellSession } from "./node-shell-session"; + +interface InstantiationParameter { + webSocket: WebSocket; + cluster: Cluster; + tabId: string; + nodeName: string; +} + +const nodeShellSessionInjectable = getInjectable({ + instantiate: (di, { cluster, tabId, webSocket, nodeName }: InstantiationParameter) => { + const createKubectl = di.inject(createKubectlInjectable); + + const kubectl = createKubectl(cluster.version); + + return new NodeShellSession(nodeName, kubectl, webSocket, cluster, tabId); + }, + + lifecycle: lifecycleEnum.transient, +}); + +export default nodeShellSessionInjectable; diff --git a/src/main/shell-session/node-shell-session.ts b/src/main/shell-session/node-shell-session/node-shell-session.ts similarity index 90% rename from src/main/shell-session/node-shell-session.ts rename to src/main/shell-session/node-shell-session/node-shell-session.ts index b865fc411d..339ecd4933 100644 --- a/src/main/shell-session/node-shell-session.ts +++ b/src/main/shell-session/node-shell-session/node-shell-session.ts @@ -23,13 +23,14 @@ import type WebSocket from "ws"; import { v4 as uuid } from "uuid"; import * as k8s from "@kubernetes/client-node"; import type { KubeConfig } from "@kubernetes/client-node"; -import type { Cluster } from "../cluster"; -import { ShellOpenError, ShellSession } from "./shell-session"; +import type { Cluster } from "../../../common/cluster/cluster"; +import { ShellOpenError, ShellSession } from "../shell-session"; import { get } from "lodash"; -import { Node, NodesApi } from "../../common/k8s-api/endpoints"; -import { KubeJsonApi } from "../../common/k8s-api/kube-json-api"; -import logger from "../logger"; -import { TerminalChannels } from "../../renderer/api/terminal-api"; +import { Node, NodesApi } from "../../../common/k8s-api/endpoints"; +import { KubeJsonApi } from "../../../common/k8s-api/kube-json-api"; +import logger from "../../logger"; +import { TerminalChannels } from "../../../renderer/api/terminal-api"; +import type { Kubectl } from "../../kubectl/kubectl"; export class NodeShellSession extends ShellSession { ShellType = "node-shell"; @@ -39,8 +40,8 @@ export class NodeShellSession extends ShellSession { protected readonly cwd: string | undefined = undefined; - constructor(socket: WebSocket, cluster: Cluster, protected nodeName: string, terminalId: string) { - super(socket, cluster, terminalId); + constructor(protected nodeName: string, kubectl: Kubectl, socket: WebSocket, cluster: Cluster, terminalId: string) { + super(kubectl, socket, cluster, terminalId); } public async open() { diff --git a/src/main/shell-session/shell-session.ts b/src/main/shell-session/shell-session.ts index 677a637cc0..8614fab3cb 100644 --- a/src/main/shell-session/shell-session.ts +++ b/src/main/shell-session/shell-session.ts @@ -19,8 +19,8 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import type { Cluster } from "../cluster"; -import { Kubectl } from "../kubectl"; +import type { Cluster } from "../../common/cluster/cluster"; +import type { Kubectl } from "../kubectl/kubectl"; import type WebSocket from "ws"; import { shellEnv } from "../utils/shell-env"; import { app } from "electron"; @@ -30,7 +30,7 @@ import os from "os"; import { isMac, isWindows } from "../../common/vars"; import { UserStore } from "../../common/user-store"; import * as pty from "node-pty"; -import { appEventBus } from "../../common/event-bus"; +import { appEventBus } from "../../common/app-event-bus/event-bus"; import logger from "../logger"; import { TerminalChannels, TerminalMessage } from "../../renderer/api/terminal-api"; import { deserialize, serialize } from "v8"; @@ -140,7 +140,6 @@ export abstract class ShellSession { this.processes.clear(); } - protected kubectl: Kubectl; protected running = false; protected kubectlBinDirP: Promise; protected kubeconfigPathP: Promise; @@ -170,8 +169,7 @@ export abstract class ShellSession { return { shellProcess, resume }; } - constructor(protected websocket: WebSocket, protected cluster: Cluster, terminalId: string) { - this.kubectl = new Kubectl(cluster.version); + constructor(protected kubectl: Kubectl, protected websocket: WebSocket, protected cluster: Cluster, terminalId: string) { this.kubeconfigPathP = this.cluster.getProxyKubeconfigPath(); this.kubectlBinDirP = this.kubectl.binDir(); this.terminalId = `${cluster.id}:${terminalId}`; diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts index 3987e29824..f435f4a83b 100644 --- a/src/main/window-manager.ts +++ b/src/main/window-manager.ts @@ -23,7 +23,7 @@ import type { ClusterId } from "../common/cluster-types"; import { makeObservable, observable } from "mobx"; import { app, BrowserWindow, dialog, ipcMain, shell, webContents } from "electron"; import windowStateKeeper from "electron-window-state"; -import { appEventBus } from "../common/event-bus"; +import { appEventBus } from "../common/app-event-bus/event-bus"; import { BundledExtensionsLoaded, ipcMainOn } from "../common/ipc"; import { delay, iter, Singleton } from "../common/utils"; import { ClusterFrameInfo, clusterFrameMap } from "../common/cluster-frames"; diff --git a/src/migrations/cluster-store/3.6.0-beta.1.ts b/src/migrations/cluster-store/3.6.0-beta.1.ts index 55e2812b9e..3d42158ea8 100644 --- a/src/migrations/cluster-store/3.6.0-beta.1.ts +++ b/src/migrations/cluster-store/3.6.0-beta.1.ts @@ -27,8 +27,13 @@ import fse from "fs-extra"; import { loadConfigFromFileSync } from "../../common/kube-helpers"; import { MigrationDeclaration, migrationLog } from "../helpers"; import type { ClusterModel } from "../../common/cluster-types"; -import { getCustomKubeConfigPath, storedKubeConfigFolder } from "../../common/utils"; -import { AppPaths } from "../../common/app-paths"; +import { getLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-global-function-for-extension-api/legacy-global-di-for-extension-api"; +import directoryForUserDataInjectable + from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; +import directoryForKubeConfigsInjectable + from "../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; +import getCustomKubeConfigDirectoryInjectable + from "../../common/app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable"; interface Pre360ClusterModel extends ClusterModel { kubeConfig: string; @@ -37,11 +42,16 @@ interface Pre360ClusterModel extends ClusterModel { export default { version: "3.6.0-beta.1", run(store) { - const userDataPath = AppPaths.get("userData"); + const di = getLegacyGlobalDiForExtensionApi(); + + const userDataPath = di.inject(directoryForUserDataInjectable); + const kubeConfigsPath = di.inject(directoryForKubeConfigsInjectable); + const getCustomKubeConfigDirectory = di.inject(getCustomKubeConfigDirectoryInjectable); + const storedClusters: Pre360ClusterModel[] = store.get("clusters") ?? []; const migratedClusters: ClusterModel[] = []; - fse.ensureDirSync(storedKubeConfigFolder()); + fse.ensureDirSync(kubeConfigsPath); migrationLog("Number of clusters to migrate: ", storedClusters.length); @@ -50,7 +60,7 @@ export default { * migrate kubeconfig */ try { - const absPath = getCustomKubeConfigPath(clusterModel.id); + const absPath = getCustomKubeConfigDirectory(clusterModel.id); // take the embedded kubeconfig and dump it into a file fse.writeFileSync(absPath, clusterModel.kubeConfig, { encoding: "utf-8", mode: 0o600 }); diff --git a/src/migrations/cluster-store/5.0.0-beta.10.ts b/src/migrations/cluster-store/5.0.0-beta.10.ts index 287f2ecd79..5f12f3fe49 100644 --- a/src/migrations/cluster-store/5.0.0-beta.10.ts +++ b/src/migrations/cluster-store/5.0.0-beta.10.ts @@ -23,7 +23,9 @@ import path from "path"; import fse from "fs-extra"; import type { ClusterModel } from "../../common/cluster-types"; import type { MigrationDeclaration } from "../helpers"; -import { AppPaths } from "../../common/app-paths"; +import { getLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-global-function-for-extension-api/legacy-global-di-for-extension-api"; +import directoryForUserDataInjectable + from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; interface Pre500WorkspaceStoreModel { workspaces: { @@ -35,7 +37,9 @@ interface Pre500WorkspaceStoreModel { export default { version: "5.0.0-beta.10", run(store) { - const userDataPath = AppPaths.get("userData"); + const di = getLegacyGlobalDiForExtensionApi(); + + const userDataPath = di.inject(directoryForUserDataInjectable); try { const workspaceData: Pre500WorkspaceStoreModel = fse.readJsonSync(path.join(userDataPath, "lens-workspace-store.json")); diff --git a/src/migrations/cluster-store/5.0.0-beta.13.ts b/src/migrations/cluster-store/5.0.0-beta.13.ts index e5c81e90bc..128e6e88ed 100644 --- a/src/migrations/cluster-store/5.0.0-beta.13.ts +++ b/src/migrations/cluster-store/5.0.0-beta.13.ts @@ -24,7 +24,9 @@ import { MigrationDeclaration, migrationLog } from "../helpers"; import { generateNewIdFor } from "../utils"; import path from "path"; import { moveSync, removeSync } from "fs-extra"; -import { AppPaths } from "../../common/app-paths"; +import { getLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-global-function-for-extension-api/legacy-global-di-for-extension-api"; +import directoryForUserDataInjectable + from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; function mergePrometheusPreferences(left: ClusterPrometheusPreferences, right: ClusterPrometheusPreferences): ClusterPrometheusPreferences { if (left.prometheus && left.prometheusProvider) { @@ -106,7 +108,11 @@ function moveStorageFolder({ folder, newId, oldId }: { folder: string, newId: st export default { version: "5.0.0-beta.13", run(store) { - const folder = path.resolve(AppPaths.get("userData"), "lens-local-storage"); + const di = getLegacyGlobalDiForExtensionApi(); + + const userDataPath = di.inject(directoryForUserDataInjectable); + + const folder = path.resolve(userDataPath, "lens-local-storage"); const oldClusters: ClusterModel[] = store.get("clusters") ?? []; const clusters = new Map(); diff --git a/src/migrations/hotbar-store/5.0.0-beta.10.ts b/src/migrations/hotbar-store/5.0.0-beta.10.ts index 882685e1b3..f0f5ee7ec2 100644 --- a/src/migrations/hotbar-store/5.0.0-beta.10.ts +++ b/src/migrations/hotbar-store/5.0.0-beta.10.ts @@ -23,12 +23,14 @@ import fse from "fs-extra"; import { isNull } from "lodash"; import path from "path"; import * as uuid from "uuid"; -import { AppPaths } from "../../common/app-paths"; -import type { ClusterStoreModel } from "../../common/cluster-store"; +import type { ClusterStoreModel } from "../../common/cluster-store/cluster-store"; import { defaultHotbarCells, getEmptyHotbar, Hotbar, HotbarItem } from "../../common/hotbar-types"; import { catalogEntity } from "../../main/catalog-sources/general"; import { MigrationDeclaration, migrationLog } from "../helpers"; import { generateNewIdFor } from "../utils"; +import { getLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-global-function-for-extension-api/legacy-global-di-for-extension-api"; +import directoryForUserDataInjectable + from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; interface Pre500WorkspaceStoreModel { workspaces: { @@ -48,7 +50,10 @@ export default { run(store) { const rawHotbars = store.get("hotbars"); const hotbars: Hotbar[] = Array.isArray(rawHotbars) ? rawHotbars.filter(h => h && typeof h === "object") : []; - const userDataPath = AppPaths.get("userData"); + + const di = getLegacyGlobalDiForExtensionApi(); + + const userDataPath = di.inject(directoryForUserDataInjectable); // Hotbars might be empty, if some of the previous migrations weren't run if (hotbars.length === 0) { diff --git a/src/migrations/user-store/5.0.3-beta.1.ts b/src/migrations/user-store/5.0.3-beta.1.ts index ec5de34d66..a9d1b86de5 100644 --- a/src/migrations/user-store/5.0.3-beta.1.ts +++ b/src/migrations/user-store/5.0.3-beta.1.ts @@ -22,20 +22,29 @@ import { existsSync, readFileSync } from "fs"; import path from "path"; import os from "os"; -import type { ClusterStoreModel } from "../../common/cluster-store"; +import type { ClusterStoreModel } from "../../common/cluster-store/cluster-store"; import type { KubeconfigSyncEntry, UserPreferencesModel } from "../../common/user-store"; import { MigrationDeclaration, migrationLog } from "../helpers"; -import { isLogicalChildPath, storedKubeConfigFolder } from "../../common/utils"; -import { AppPaths } from "../../common/app-paths"; +import { isLogicalChildPath } from "../../common/utils"; +import { getLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-global-function-for-extension-api/legacy-global-di-for-extension-api"; +import directoryForUserDataInjectable + from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; +import directoryForKubeConfigsInjectable + from "../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; export default { version: "5.0.3-beta.1", run(store) { try { const { syncKubeconfigEntries = [], ...preferences }: UserPreferencesModel = store.get("preferences") ?? {}; - const userData = AppPaths.get("userData"); - const { clusters = [] }: ClusterStoreModel = JSON.parse(readFileSync(path.resolve(userData, "lens-cluster-store.json"), "utf-8")) ?? {}; - const extensionDataDir = path.resolve(userData, "extension_data"); + + const di = getLegacyGlobalDiForExtensionApi(); + + const userDataPath = di.inject(directoryForUserDataInjectable); + const kubeConfigsPath = di.inject(directoryForKubeConfigsInjectable); + + const { clusters = [] }: ClusterStoreModel = JSON.parse(readFileSync(path.resolve(userDataPath, "lens-cluster-store.json"), "utf-8")) ?? {}; + const extensionDataDir = path.resolve(userDataPath, "extension_data"); const syncPaths = new Set(syncKubeconfigEntries.map(s => s.filePath)); syncPaths.add(path.join(os.homedir(), ".kube")); @@ -46,7 +55,7 @@ export default { } const dirOfKubeconfig = path.dirname(cluster.kubeConfigPath); - if (dirOfKubeconfig === storedKubeConfigFolder()) { + if (dirOfKubeconfig === kubeConfigsPath) { migrationLog(`Skipping ${cluster.id} because kubeConfigPath is under the stored KubeConfig folder`); continue; } diff --git a/src/migrations/user-store/file-name-migration.ts b/src/migrations/user-store/file-name-migration.ts index 80bff67302..5f9035fbc7 100644 --- a/src/migrations/user-store/file-name-migration.ts +++ b/src/migrations/user-store/file-name-migration.ts @@ -21,10 +21,14 @@ import fse from "fs-extra"; import path from "path"; -import { AppPaths } from "../../common/app-paths"; +import { getLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-global-function-for-extension-api/legacy-global-di-for-extension-api"; +import directoryForUserDataInjectable + from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; export function fileNameMigration() { - const userDataPath = AppPaths.get("userData"); + const di = getLegacyGlobalDiForExtensionApi(); + + const userDataPath = di.inject(directoryForUserDataInjectable); const configJsonPath = path.join(userDataPath, "config.json"); const lensUserStoreJsonPath = path.join(userDataPath, "lens-user-store.json"); diff --git a/src/renderer/api/catalog-entity-registry.ts b/src/renderer/api/catalog-entity-registry.ts index 4885c8f265..83cfd96b01 100644 --- a/src/renderer/api/catalog-entity-registry.ts +++ b/src/renderer/api/catalog-entity-registry.ts @@ -23,8 +23,8 @@ import { computed, observable, makeObservable, action } from "mobx"; import { ipcRendererOn } from "../../common/ipc"; import { CatalogCategory, CatalogEntity, CatalogEntityData, catalogCategoryRegistry, CatalogCategoryRegistry, CatalogEntityKindData } from "../../common/catalog"; import "../../common/catalog-entities"; -import type { Cluster } from "../../main/cluster"; -import { ClusterStore } from "../../common/cluster-store"; +import type { Cluster } from "../../common/cluster/cluster"; +import { ClusterStore } from "../../common/cluster-store/cluster-store"; import { Disposer, iter } from "../utils"; import { once } from "lodash"; import logger from "../../common/logger"; diff --git a/src/renderer/api/catalog-entity-registry/catalog-entity-registry.injectable.ts b/src/renderer/api/catalog-entity-registry/catalog-entity-registry.injectable.ts new file mode 100644 index 0000000000..69894a6d9e --- /dev/null +++ b/src/renderer/api/catalog-entity-registry/catalog-entity-registry.injectable.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { catalogEntityRegistry } from "../catalog-entity-registry"; + +const catalogEntityRegistryInjectable = getInjectable({ + instantiate: () => catalogEntityRegistry, + lifecycle: lifecycleEnum.singleton, +}); + +export default catalogEntityRegistryInjectable; diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index fccbb902b6..3eb4283897 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -31,8 +31,6 @@ import * as LensExtensionsRendererApi from "../extensions/renderer-api"; import { render } from "react-dom"; import { delay } from "../common/utils"; import { isMac, isDevelopment } from "../common/vars"; -import { ClusterStore } from "../common/cluster-store"; -import { UserStore } from "../common/user-store"; import { HelmRepoManager } from "../main/helm/helm-repo-manager"; import { DefaultProps } from "./mui-base-theme"; import configurePackages from "../common/configure-packages"; @@ -40,26 +38,27 @@ import * as initializers from "./initializers"; import logger from "../common/logger"; import { HotbarStore } from "../common/hotbar-store"; import { WeblinkStore } from "../common/weblink-store"; -import { FilesystemProvisionerStore } from "../main/extension-filesystem"; import { ThemeStore } from "./theme.store"; import { SentryInit } from "../common/sentry"; -import { TerminalStore } from "./components/dock/terminal.store"; -import { AppPaths } from "../common/app-paths"; import { registerCustomThemes } from "./components/monaco-editor"; import { getDi } from "./components/getDi"; import { DiContextProvider } from "@ogre-tools/injectable-react"; import type { DependencyInjectionContainer } from "@ogre-tools/injectable"; import extensionLoaderInjectable from "../extensions/extension-loader/extension-loader.injectable"; -import type { ExtensionLoader } from "../extensions/extension-loader"; -import bindProtocolAddRouteHandlersInjectable - from "./protocol-handler/bind-protocol-add-route-handlers/bind-protocol-add-route-handlers.injectable"; -import type { LensProtocolRouterRenderer } from "./protocol-handler"; -import lensProtocolRouterRendererInjectable - from "./protocol-handler/lens-protocol-router-renderer/lens-protocol-router-renderer.injectable"; + + import extensionDiscoveryInjectable from "../extensions/extension-discovery/extension-discovery.injectable"; import extensionInstallationStateStoreInjectable from "../extensions/extension-installation-state-store/extension-installation-state-store.injectable"; +import clusterStoreInjectable from "../common/cluster-store/cluster-store.injectable"; +import userStoreInjectable from "../common/user-store/user-store.injectable"; + +import initRootFrameInjectable from "./frames/root-frame/init-root-frame/init-root-frame.injectable"; +import initClusterFrameInjectable + from "./frames/cluster-frame/init-cluster-frame/init-cluster-frame.injectable"; +import createTerminalTabInjectable + from "./components/dock/create-terminal-tab/create-terminal-tab.injectable"; if (process.isMainFrame) { SentryInit(); @@ -79,23 +78,14 @@ async function attachChromeDebugger() { } } -type AppComponent = React.ComponentType & { - - // TODO: This static method is criminal as it has no direct relation with component - init( - rootElem: HTMLElement, - extensionLoader: ExtensionLoader, - bindProtocolAddRouteHandlers?: () => void, - lensProtocolRouterRendererInjectable?: LensProtocolRouterRenderer - ): Promise; -}; - -export async function bootstrap(comp: () => Promise, di: DependencyInjectionContainer) { +export async function bootstrap(di: DependencyInjectionContainer) { + await di.runSetups(); + const rootElem = document.getElementById("app"); const logPrefix = `[BOOTSTRAP-${process.isMainFrame ? "ROOT" : "CLUSTER"}-FRAME]:`; - await AppPaths.init(); - UserStore.createInstance(); + // TODO: Remove temporal dependencies to make timing of initialization not important + di.inject(userStoreInjectable); await attachChromeDebugger(); rootElem.classList.toggle("is-mac", isMac); @@ -103,8 +93,10 @@ export async function bootstrap(comp: () => Promise, di: Dependenc logger.info(`${logPrefix} initializing Registries`); initializers.initRegistries(); + const createTerminalTab = di.inject(createTerminalTabInjectable); + logger.info(`${logPrefix} initializing CommandRegistry`); - initializers.initCommandRegistry(); + initializers.initCommandRegistry(createTerminalTab); logger.info(`${logPrefix} initializing EntitySettingsRegistry`); initializers.initEntitySettingsRegistry(); @@ -145,19 +137,16 @@ export async function bootstrap(comp: () => Promise, di: Dependenc extensionDiscovery.init(); // ClusterStore depends on: UserStore - const clusterStore = ClusterStore.createInstance(); + const clusterStore = di.inject(clusterStoreInjectable); await clusterStore.loadInitialOnRenderer(); // HotbarStore depends on: ClusterStore HotbarStore.createInstance(); - FilesystemProvisionerStore.createInstance(); // ThemeStore depends on: UserStore ThemeStore.createInstance(); - // TerminalStore depends on: ThemeStore - TerminalStore.createInstance(); WeblinkStore.createInstance(); const extensionInstallationStateStore = di.inject(extensionInstallationStateStoreInjectable); @@ -169,13 +158,20 @@ export async function bootstrap(comp: () => Promise, di: Dependenc // Register additional store listeners clusterStore.registerIpcListener(); - // init app's dependencies if any - const App = await comp(); + let App; + let initializeApp; - const bindProtocolAddRouteHandlers = di.inject(bindProtocolAddRouteHandlersInjectable); - const lensProtocolRouterRenderer = di.inject(lensProtocolRouterRendererInjectable); + // TODO: Introduce proper architectural boundaries between root and cluster iframes + if (process.isMainFrame) { + initializeApp = di.inject(initRootFrameInjectable); - await App.init(rootElem, extensionLoader, bindProtocolAddRouteHandlers, lensProtocolRouterRenderer); + App = (await import("./frames/root-frame/root-frame")).RootFrame; + } else { + initializeApp = di.inject(initClusterFrameInjectable); + App = (await import("./frames/cluster-frame/cluster-frame")).ClusterFrame; + } + + await initializeApp(rootElem); render( @@ -189,14 +185,7 @@ export async function bootstrap(comp: () => Promise, di: Dependenc const di = getDi(); // run -bootstrap( - async () => - process.isMainFrame - ? (await import("./root-frame")).RootFrame - : (await import("./cluster-frame")).ClusterFrame, - di, -); - +bootstrap(di); /** * Exports for virtual package "@k8slens/extensions" for renderer-process. diff --git a/src/renderer/cluster-frame-context/cluster-frame-context.injectable.ts b/src/renderer/cluster-frame-context/cluster-frame-context.injectable.ts new file mode 100644 index 0000000000..1d2e58b82b --- /dev/null +++ b/src/renderer/cluster-frame-context/cluster-frame-context.injectable.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { ClusterFrameContext } from "./cluster-frame-context"; +import namespaceStoreInjectable from "../components/+namespaces/namespace-store/namespace-store.injectable"; +import hostedClusterInjectable from "../../common/cluster-store/hosted-cluster/hosted-cluster.injectable"; + +const clusterFrameContextInjectable = getInjectable({ + instantiate: (di) => { + const cluster = di.inject(hostedClusterInjectable); + + return new ClusterFrameContext( + cluster, + + { + namespaceStore: di.inject(namespaceStoreInjectable), + }, + ); + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default clusterFrameContextInjectable; diff --git a/src/renderer/components/context.ts b/src/renderer/cluster-frame-context/cluster-frame-context.ts similarity index 77% rename from src/renderer/components/context.ts rename to src/renderer/cluster-frame-context/cluster-frame-context.ts index fc494f5f37..104f58a854 100755 --- a/src/renderer/components/context.ts +++ b/src/renderer/cluster-frame-context/cluster-frame-context.ts @@ -19,13 +19,17 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import type { Cluster } from "../../main/cluster"; -import { namespaceStore } from "./+namespaces/namespace.store"; +import type { Cluster } from "../../common/cluster/cluster"; +import type { NamespaceStore } from "../components/+namespaces/namespace-store/namespace.store"; import type { ClusterContext } from "../../common/k8s-api/cluster-context"; import { computed, makeObservable } from "mobx"; -export class FrameContext implements ClusterContext { - constructor(public cluster: Cluster) { +interface Dependencies { + namespaceStore: NamespaceStore +} + +export class ClusterFrameContext implements ClusterContext { + constructor(public cluster: Cluster, private dependencies: Dependencies) { makeObservable(this); } @@ -35,9 +39,9 @@ export class FrameContext implements ClusterContext { return this.cluster.accessibleNamespaces; } - if (namespaceStore.items.length > 0) { + if (this.dependencies.namespaceStore.items.length > 0) { // namespaces from kubernetes api - return namespaceStore.items.map((namespace) => namespace.getName()); + return this.dependencies.namespaceStore.items.map((namespace) => namespace.getName()); } else { // fallback to cluster resolved namespaces because we could not load list return this.cluster.allowedNamespaces || []; @@ -45,7 +49,7 @@ export class FrameContext implements ClusterContext { } @computed get contextNamespaces(): string[] { - return namespaceStore.contextNamespaces; + return this.dependencies.namespaceStore.contextNamespaces; } @computed get hasSelectedAll(): boolean { diff --git a/src/renderer/cluster-frame.tsx b/src/renderer/cluster-frame.tsx deleted file mode 100755 index 5113377116..0000000000 --- a/src/renderer/cluster-frame.tsx +++ /dev/null @@ -1,251 +0,0 @@ -/** - * Copyright (c) 2021 OpenLens Authors - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of - * the Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER - * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN - * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -import React from "react"; -import { observable, makeObservable, when } from "mobx"; -import { disposeOnUnmount, observer } from "mobx-react"; -import { Redirect, Route, Router, Switch } from "react-router"; -import { history } from "./navigation"; -import { NotFound } from "./components/+404"; -import { UserManagement } from "./components/+user-management/user-management"; -import { ConfirmDialog } from "./components/confirm-dialog"; -import { ClusterOverview } from "./components/+cluster/cluster-overview"; -import { Events } from "./components/+events/events"; -import { DeploymentScaleDialog } from "./components/+workloads-deployments/deployment-scale-dialog"; -import { CronJobTriggerDialog } from "./components/+workloads-cronjobs/cronjob-trigger-dialog"; -import { CustomResources } from "./components/+custom-resources/custom-resources"; -import { isAllowedResource } from "../common/utils/allowed-resource"; -import logger from "../main/logger"; -import { webFrame } from "electron"; -import { ClusterPageRegistry, getExtensionPageUrl } from "../extensions/registries/page-registry"; -import type { ExtensionLoader } from "../extensions/extension-loader"; -import { appEventBus } from "../common/event-bus"; -import { requestMain } from "../common/ipc"; -import { clusterSetFrameIdHandler } from "../common/cluster-ipc"; -import { ClusterPageMenuRegistration, ClusterPageMenuRegistry } from "../extensions/registries"; -import { StatefulSetScaleDialog } from "./components/+workloads-statefulsets/statefulset-scale-dialog"; -import { KubeWatchApi, kubeWatchApi } from "../common/k8s-api/kube-watch-api"; -import { ReplicaSetScaleDialog } from "./components/+workloads-replicasets/replicaset-scale-dialog"; -import { CommandContainer } from "./components/command-palette/command-container"; -import { KubeObjectStore } from "../common/k8s-api/kube-object.store"; -import { FrameContext } from "./components/context"; -import * as routes from "../common/routes"; -import { TabLayout, TabLayoutRoute } from "./components/layout/tab-layout"; -import { ErrorBoundary } from "./components/error-boundary"; -import { MainLayout } from "./components/layout/main-layout"; -import { Notifications } from "./components/notifications"; -import { KubeObjectDetails } from "./components/kube-object-details"; -import { KubeConfigDialog } from "./components/kubeconfig-dialog"; -import { Terminal } from "./components/dock/terminal"; -import { namespaceStore } from "./components/+namespaces/namespace.store"; -import { Sidebar } from "./components/layout/sidebar"; -import { Dock } from "./components/dock"; -import { Apps } from "./components/+apps"; -import { Namespaces } from "./components/+namespaces"; -import { Network } from "./components/+network"; -import { Nodes } from "./components/+nodes"; -import { Workloads } from "./components/+workloads"; -import { Config } from "./components/+config"; -import { Storage } from "./components/+storage"; -import { catalogEntityRegistry } from "./api/catalog-entity-registry"; -import { getHostedClusterId } from "./utils"; -import { ClusterStore } from "../common/cluster-store"; -import type { ClusterId } from "../common/cluster-types"; -import { watchHistoryState } from "./remote-helpers/history-updater"; -import { unmountComponentAtNode } from "react-dom"; -import { PortForwardDialog } from "./port-forward"; -import { DeleteClusterDialog } from "./components/delete-cluster-dialog"; -import { WorkloadsOverview } from "./components/+workloads-overview/overview"; -import { KubeObjectListLayout } from "./components/kube-object-list-layout"; -import type { KubernetesCluster } from "../common/catalog-entities"; - -@observer -export class ClusterFrame extends React.Component { - static clusterId: ClusterId; - static readonly logPrefix = "[CLUSTER-FRAME]:"; - static displayName = "ClusterFrame"; - - constructor(props: {}) { - super(props); - makeObservable(this); - } - - static async init(rootElem: HTMLElement, extensionLoader: ExtensionLoader) { - catalogEntityRegistry.init(); - const frameId = webFrame.routingId; - - ClusterFrame.clusterId = getHostedClusterId(); - - const cluster = ClusterStore.getInstance().getById(ClusterFrame.clusterId); - - logger.info(`${ClusterFrame.logPrefix} Init dashboard, clusterId=${ClusterFrame.clusterId}, frameId=${frameId}`); - await Terminal.preloadFonts(); - await requestMain(clusterSetFrameIdHandler, ClusterFrame.clusterId); - await cluster.whenReady; // cluster.activate() is done at this point - - catalogEntityRegistry.activeEntity = ClusterFrame.clusterId; - - // Only load the extensions once the catalog has been populated - when( - () => Boolean(catalogEntityRegistry.activeEntity), - () => extensionLoader.loadOnClusterRenderer(catalogEntityRegistry.activeEntity as KubernetesCluster), - { - timeout: 15_000, - onError: (error) => { - console.warn("[CLUSTER-FRAME]: error from activeEntity when()", error); - Notifications.error("Failed to get KubernetesCluster for this view. Extensions will not be loaded."); - }, - }, - ); - - setTimeout(() => { - appEventBus.emit({ - name: "cluster", - action: "open", - params: { - clusterId: ClusterFrame.clusterId, - }, - }); - }); - window.addEventListener("online", () => { - window.location.reload(); - }); - - window.onbeforeunload = () => { - logger.info(`${ClusterFrame.logPrefix} Unload dashboard, clusterId=${ClusterFrame.clusterId}, frameId=${frameId}`); - - unmountComponentAtNode(rootElem); - }; - - const clusterContext = new FrameContext(cluster); - - // Setup hosted cluster context - KubeObjectStore.defaultContext.set(clusterContext); - WorkloadsOverview.clusterContext - = KubeObjectListLayout.clusterContext - = KubeWatchApi.context - = clusterContext; - } - - componentDidMount() { - disposeOnUnmount(this, [ - kubeWatchApi.subscribeStores([ - namespaceStore, - ]), - - watchHistoryState(), - ]); - } - - @observable startUrl = isAllowedResource(["events", "nodes", "pods"]) ? routes.clusterURL() : routes.workloadsURL(); - - getTabLayoutRoutes(menuItem: ClusterPageMenuRegistration) { - const routes: TabLayoutRoute[] = []; - - if (!menuItem.id) { - return routes; - } - ClusterPageMenuRegistry.getInstance().getSubItems(menuItem).forEach((subMenu) => { - const page = ClusterPageRegistry.getInstance().getByPageTarget(subMenu.target); - - if (page) { - routes.push({ - routePath: page.url, - url: getExtensionPageUrl(subMenu.target), - title: subMenu.title, - component: page.components.Page, - }); - } - }); - - return routes; - } - - renderExtensionTabLayoutRoutes() { - return ClusterPageMenuRegistry.getInstance().getRootItems().map((menu, index) => { - const tabRoutes = this.getTabLayoutRoutes(menu); - - if (tabRoutes.length > 0) { - const pageComponent = () => ; - - return tab.routePath)}/>; - } else { - const page = ClusterPageRegistry.getInstance().getByPageTarget(menu.target); - - if (page) { - return ; - } - } - - return null; - }); - } - - renderExtensionRoutes() { - return ClusterPageRegistry.getInstance().getItems().map((page, index) => { - const menu = ClusterPageMenuRegistry.getInstance().getByPage(page); - - if (!menu) { - return ; - } - - return null; - }); - } - - render() { - return ( - - - } footer={}> - - - - - - - - - - - - - {this.renderExtensionTabLayoutRoutes()} - {this.renderExtensionRoutes()} - - - - - - - - - - - - - - - - - - ); - } -} diff --git a/src/renderer/components/+add-cluster/add-cluster.tsx b/src/renderer/components/+add-cluster/add-cluster.tsx index 40257f6518..e9bd599618 100644 --- a/src/renderer/components/+add-cluster/add-cluster.tsx +++ b/src/renderer/components/+add-cluster/add-cluster.tsx @@ -28,23 +28,31 @@ import { action, computed, makeObservable, observable } from "mobx"; import { observer } from "mobx-react"; import path from "path"; import React from "react"; +import * as uuid from "uuid"; import { catalogURL } from "../../../common/routes"; -import { appEventBus } from "../../../common/event-bus"; +import { appEventBus } from "../../../common/app-event-bus/event-bus"; import { loadConfigFromString, splitConfig } from "../../../common/kube-helpers"; import { docsUrl } from "../../../common/vars"; import { navigate } from "../../navigation"; -import { getCustomKubeConfigPath, iter } from "../../utils"; +import { iter } from "../../utils"; import { Button } from "../button"; import { Notifications } from "../notifications"; import { SettingLayout } from "../layout/setting-layout"; import { MonacoEditor } from "../monaco-editor"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import getCustomKubeConfigDirectoryInjectable + from "../../../common/app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable"; interface Option { config: KubeConfig; error?: string; } +interface Dependencies { + getCustomKubeConfigDirectory: (directoryName: string) => string +} + function getContexts(config: KubeConfig): Map { return new Map( splitConfig(config) @@ -56,14 +64,14 @@ function getContexts(config: KubeConfig): Map { } @observer -export class AddCluster extends React.Component { +class NonInjectedAddCluster extends React.Component { @observable kubeContexts = observable.map(); @observable customConfig = ""; @observable isWaiting = false; @observable errors: string[] = []; - constructor(props: {}) { - super(props); + constructor(dependencies: Dependencies) { + super(dependencies); makeObservable(this); } @@ -99,7 +107,7 @@ export class AddCluster extends React.Component { appEventBus.emit({ name: "cluster-add", action: "click" }); try { - const absPath = getCustomKubeConfigPath(); + const absPath = this.props.getCustomKubeConfigDirectory(uuid.v4()); await fse.ensureDir(path.dirname(absPath)); await fse.writeFile(absPath, this.customConfig.trim(), { encoding: "utf-8", mode: 0o600 }); @@ -153,3 +161,11 @@ export class AddCluster extends React.Component { ); } } + +export const AddCluster = withInjectables(NonInjectedAddCluster, { + getProps: (di) => ({ + getCustomKubeConfigDirectory: di.inject( + getCustomKubeConfigDirectoryInjectable, + ), + }), +}); diff --git a/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx b/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx index 739d8082e3..b0b3e1b44d 100644 --- a/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx +++ b/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx @@ -31,9 +31,11 @@ import { MarkdownViewer } from "../markdown-viewer"; import { Spinner } from "../spinner"; import { Button } from "../button"; import { Select, SelectOption } from "../select"; -import { createInstallChartTab } from "../dock/install-chart.store"; import { Badge } from "../badge"; import { Tooltip, withStyles } from "@material-ui/core"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import createInstallChartTabInjectable + from "../dock/create-install-chart-tab/create-install-chart-tab.injectable"; interface Props { chart: HelmChart; @@ -46,8 +48,12 @@ const LargeTooltip = withStyles({ }, })(Tooltip); +interface Dependencies { + createInstallChartTab: (helmChart: HelmChart) => void +} + @observer -export class HelmChartDetails extends Component { +class NonInjectedHelmChartDetails extends Component { @observable chartVersions: HelmChart[]; @observable selectedChart?: HelmChart; @observable readme?: string; @@ -55,7 +61,7 @@ export class HelmChartDetails extends Component { private abortController?: AbortController; - constructor(props: Props) { + constructor(props: Props & Dependencies) { super(props); makeObservable(this); } @@ -106,7 +112,7 @@ export class HelmChartDetails extends Component { @boundMethod install() { - createInstallChartTab(this.selectedChart); + this.props.createInstallChartTab(this.selectedChart); this.props.hideDetails(); } @@ -215,3 +221,14 @@ export class HelmChartDetails extends Component { ); } } + +export const HelmChartDetails = withInjectables( + NonInjectedHelmChartDetails, + + { + getProps: (di, props) => ({ + createInstallChartTab: di.inject(createInstallChartTabInjectable), + ...props, + }), + }, +); diff --git a/src/renderer/components/+apps-releases/release-details.tsx b/src/renderer/components/+apps-releases/release-details.tsx index 0a3d947411..e0b2926d99 100644 --- a/src/renderer/components/+apps-releases/release-details.tsx +++ b/src/renderer/components/+apps-releases/release-details.tsx @@ -36,9 +36,8 @@ import { disposeOnUnmount, observer } from "mobx-react"; import { Spinner } from "../spinner"; import { Table, TableCell, TableHead, TableRow } from "../table"; import { Button } from "../button"; -import { releaseStore } from "./release.store"; +import type { ReleaseStore } from "./release.store"; import { Notifications } from "../notifications"; -import { createUpgradeChartTab } from "../dock/upgrade-chart.store"; import { ThemeStore } from "../../theme.store"; import { apiManager } from "../../../common/k8s-api/api-manager"; import { SubTitle } from "../layout/sub-title"; @@ -47,14 +46,23 @@ import { Secret } from "../../../common/k8s-api/endpoints"; import { getDetailsUrl } from "../kube-detail-params"; import { Checkbox } from "../checkbox"; import { MonacoEditor } from "../monaco-editor"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import releaseStoreInjectable from "./release-store.injectable"; +import createUpgradeChartTabInjectable + from "../dock/create-upgrade-chart-tab/create-upgrade-chart-tab.injectable"; interface Props { release: HelmRelease; hideDetails(): void; } +interface Dependencies { + releaseStore: ReleaseStore + createUpgradeChartTab: (release: HelmRelease) => void +} + @observer -export class ReleaseDetails extends Component { +class NonInjectedReleaseDetails extends Component { @observable details: IReleaseDetails | null = null; @observable values = ""; @observable valuesLoading = false; @@ -73,7 +81,7 @@ export class ReleaseDetails extends Component { }), reaction(() => secretsStore.getItems(), () => { if (!this.props.release) return; - const { getReleaseSecret } = releaseStore; + const { getReleaseSecret } = this.props.releaseStore; const { release } = this.props; const secret = getReleaseSecret(release); @@ -89,7 +97,7 @@ export class ReleaseDetails extends Component { ]); } - constructor(props: Props) { + constructor(props: Props & Dependencies) { super(props); makeObservable(this); } @@ -133,7 +141,7 @@ export class ReleaseDetails extends Component { this.saving = true; try { - await releaseStore.update(name, namespace, data); + await this.props.releaseStore.update(name, namespace, data); Notifications.ok(

Release {name} successfully updated!

, ); @@ -146,7 +154,7 @@ export class ReleaseDetails extends Component { upgradeVersion = () => { const { release, hideDetails } = this.props; - createUpgradeChartTab(release); + this.props.createUpgradeChartTab(release); hideDetails(); }; @@ -315,3 +323,15 @@ export class ReleaseDetails extends Component { ); } } + +export const ReleaseDetails = withInjectables( + NonInjectedReleaseDetails, + + { + getProps: (di, props) => ({ + releaseStore: di.inject(releaseStoreInjectable), + createUpgradeChartTab: di.inject(createUpgradeChartTabInjectable), + ...props, + }), + }, +); diff --git a/src/renderer/components/+apps-releases/release-menu.tsx b/src/renderer/components/+apps-releases/release-menu.tsx index 13532c22e4..fc92fd7546 100644 --- a/src/renderer/components/+apps-releases/release-menu.tsx +++ b/src/renderer/components/+apps-releases/release-menu.tsx @@ -22,32 +22,42 @@ import React from "react"; import type { HelmRelease } from "../../../common/k8s-api/endpoints/helm-releases.api"; import { cssNames } from "../../utils"; -import { releaseStore } from "./release.store"; +import type { ReleaseStore } from "./release.store"; import { MenuActions, MenuActionsProps } from "../menu/menu-actions"; import { MenuItem } from "../menu"; import { Icon } from "../icon"; -import { ReleaseRollbackDialog } from "./release-rollback-dialog"; -import { createUpgradeChartTab } from "../dock/upgrade-chart.store"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import releaseStoreInjectable from "./release-store.injectable"; +import createUpgradeChartTabInjectable + from "../dock/create-upgrade-chart-tab/create-upgrade-chart-tab.injectable"; +import releaseRollbackDialogModelInjectable + from "./release-rollback-dialog-model/release-rollback-dialog-model.injectable"; interface Props extends MenuActionsProps { release: HelmRelease; hideDetails?(): void; } -export class HelmReleaseMenu extends React.Component { +interface Dependencies { + releaseStore: ReleaseStore + createUpgradeChartTab: (release: HelmRelease) => void + openRollbackDialog: (release: HelmRelease) => void +} + +class NonInjectedHelmReleaseMenu extends React.Component { remove = () => { - return releaseStore.remove(this.props.release); + return this.props.releaseStore.remove(this.props.release); }; upgrade = () => { const { release, hideDetails } = this.props; - createUpgradeChartTab(release); + this.props.createUpgradeChartTab(release); hideDetails?.(); }; rollback = () => { - ReleaseRollbackDialog.open(this.props.release); + this.props.openRollbackDialog(this.props.release); }; renderContent() { @@ -87,3 +97,17 @@ export class HelmReleaseMenu extends React.Component { ); } } + +export const HelmReleaseMenu = withInjectables( + NonInjectedHelmReleaseMenu, + + { + getProps: (di, props) => ({ + releaseStore: di.inject(releaseStoreInjectable), + createUpgradeChartTab: di.inject(createUpgradeChartTabInjectable), + openRollbackDialog: di.inject(releaseRollbackDialogModelInjectable).open, + + ...props, + }), + }, +); diff --git a/src/renderer/components/+apps-releases/release-rollback-dialog-model/release-rollback-dialog-model.injectable.ts b/src/renderer/components/+apps-releases/release-rollback-dialog-model/release-rollback-dialog-model.injectable.ts new file mode 100644 index 0000000000..5ae114a106 --- /dev/null +++ b/src/renderer/components/+apps-releases/release-rollback-dialog-model/release-rollback-dialog-model.injectable.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { ReleaseRollbackDialogModel } from "./release-rollback-dialog-model"; + +const releaseRollbackDialogModelInjectable = getInjectable({ + instantiate: () => new ReleaseRollbackDialogModel(), + lifecycle: lifecycleEnum.singleton, +}); + +export default releaseRollbackDialogModelInjectable; diff --git a/src/renderer/components/+apps-releases/release-rollback-dialog-model/release-rollback-dialog-model.ts b/src/renderer/components/+apps-releases/release-rollback-dialog-model/release-rollback-dialog-model.ts new file mode 100644 index 0000000000..bdd4a82625 --- /dev/null +++ b/src/renderer/components/+apps-releases/release-rollback-dialog-model/release-rollback-dialog-model.ts @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { computed, observable, makeObservable, action } from "mobx"; +import type { HelmRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api"; + +export class ReleaseRollbackDialogModel { + release: HelmRelease | null = null; + + constructor() { + makeObservable(this, { + isOpen: computed, + release: observable, + open: action, + close: action, + }); + } + + get isOpen() { + return !!this.release; + } + + open = (release: HelmRelease) => { + this.release = release; + }; + + close = () => { + this.release = null; + }; +} diff --git a/src/renderer/components/+apps-releases/release-rollback-dialog.tsx b/src/renderer/components/+apps-releases/release-rollback-dialog.tsx index 07de38232f..2db946ea6b 100644 --- a/src/renderer/components/+apps-releases/release-rollback-dialog.tsx +++ b/src/renderer/components/+apps-releases/release-rollback-dialog.tsx @@ -27,41 +27,36 @@ import { observer } from "mobx-react"; import { Dialog, DialogProps } from "../dialog"; import { Wizard, WizardStep } from "../wizard"; import { getReleaseHistory, HelmRelease, IReleaseRevision } from "../../../common/k8s-api/endpoints/helm-releases.api"; -import { releaseStore } from "./release.store"; import { Select, SelectOption } from "../select"; import { Notifications } from "../notifications"; import orderBy from "lodash/orderBy"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import releaseStoreInjectable from "./release-store.injectable"; +import releaseRollbackDialogModelInjectable + from "./release-rollback-dialog-model/release-rollback-dialog-model.injectable"; +import type { ReleaseRollbackDialogModel } from "./release-rollback-dialog-model/release-rollback-dialog-model"; interface Props extends DialogProps { } -const dialogState = observable.object({ - isOpen: false, - release: null as HelmRelease, -}); +interface Dependencies { + rollbackRelease: (releaseName: string, namespace: string, revisionNumber: number) => Promise + model: ReleaseRollbackDialogModel +} @observer -export class ReleaseRollbackDialog extends React.Component { +class NonInjectedReleaseRollbackDialog extends React.Component { @observable isLoading = false; @observable revision: IReleaseRevision; @observable revisions = observable.array(); - constructor(props: Props) { + constructor(props: Props & Dependencies) { super(props); makeObservable(this); } - static open(release: HelmRelease) { - dialogState.isOpen = true; - dialogState.release = release; - } - - static close() { - dialogState.isOpen = false; - } - get release(): HelmRelease { - return dialogState.release; + return this.props.model.release; } onOpen = async () => { @@ -78,17 +73,13 @@ export class ReleaseRollbackDialog extends React.Component { const revisionNumber = this.revision.revision; try { - await releaseStore.rollback(this.release.getName(), this.release.getNs(), revisionNumber); - this.close(); + await this.props.rollbackRelease(this.release.getName(), this.release.getNs(), revisionNumber); + this.props.model.close(); } catch (err) { Notifications.error(err); } }; - close = () => { - ReleaseRollbackDialog.close(); - }; - renderContent() { const { revision, revisions } = this; @@ -120,11 +111,11 @@ export class ReleaseRollbackDialog extends React.Component { - + { ); } } + +export const ReleaseRollbackDialog = withInjectables( + NonInjectedReleaseRollbackDialog, + + { + getProps: (di, props) => ({ + rollbackRelease: di.inject(releaseStoreInjectable).rollback, + model: di.inject(releaseRollbackDialogModelInjectable), + ...props, + }), + }, +); diff --git a/src/renderer/components/+apps-releases/release-store.injectable.ts b/src/renderer/components/+apps-releases/release-store.injectable.ts new file mode 100644 index 0000000000..3d64da383b --- /dev/null +++ b/src/renderer/components/+apps-releases/release-store.injectable.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { ReleaseStore } from "./release.store"; +import namespaceStoreInjectable from "../+namespaces/namespace-store/namespace-store.injectable"; + +const releaseStoreInjectable = getInjectable({ + instantiate: (di) => new ReleaseStore({ + namespaceStore: di.inject(namespaceStoreInjectable), + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default releaseStoreInjectable; diff --git a/src/renderer/components/+apps-releases/release.store.ts b/src/renderer/components/+apps-releases/release.store.ts index 081b637447..db90b077a8 100644 --- a/src/renderer/components/+apps-releases/release.store.ts +++ b/src/renderer/components/+apps-releases/release.store.ts @@ -26,13 +26,17 @@ import { createRelease, deleteRelease, HelmRelease, IReleaseCreatePayload, IRele import { ItemStore } from "../../../common/item.store"; import type { Secret } from "../../../common/k8s-api/endpoints"; import { secretsStore } from "../+config-secrets/secrets.store"; -import { namespaceStore } from "../+namespaces/namespace.store"; +import type { NamespaceStore } from "../+namespaces/namespace-store/namespace.store"; import { Notifications } from "../notifications"; +interface Dependencies { + namespaceStore: NamespaceStore +} + export class ReleaseStore extends ItemStore { releaseSecrets = observable.map(); - constructor() { + constructor(private dependencies: Dependencies ) { super(); makeObservable(this); autoBind(this); @@ -61,7 +65,7 @@ export class ReleaseStore extends ItemStore { } watchSelectedNamespaces(): (() => void) { - return reaction(() => namespaceStore.context.contextNamespaces, namespaces => { + return reaction(() => this.dependencies.namespaceStore.context.contextNamespaces, namespaces => { this.loadAll(namespaces); }, { fireImmediately: true, @@ -106,13 +110,13 @@ export class ReleaseStore extends ItemStore { } async loadFromContextNamespaces(): Promise { - return this.loadAll(namespaceStore.context.contextNamespaces); + return this.loadAll(this.dependencies.namespaceStore.context.contextNamespaces); } async loadItems(namespaces: string[]) { - const isLoadingAll = namespaceStore.context.allNamespaces?.length > 1 - && namespaceStore.context.cluster.accessibleNamespaces.length === 0 - && namespaceStore.context.allNamespaces.every(ns => namespaces.includes(ns)); + const isLoadingAll = this.dependencies.namespaceStore.context.allNamespaces?.length > 1 + && this.dependencies.namespaceStore.context.cluster.accessibleNamespaces.length === 0 + && this.dependencies.namespaceStore.context.allNamespaces.every(ns => namespaces.includes(ns)); if (isLoadingAll) { return listReleases(); @@ -123,13 +127,13 @@ export class ReleaseStore extends ItemStore { .then(items => items.flat()); } - async create(payload: IReleaseCreatePayload) { + create = async (payload: IReleaseCreatePayload) => { const response = await createRelease(payload); if (this.isLoaded) this.loadFromContextNamespaces(); return response; - } + }; async update(name: string, namespace: string, payload: IReleaseUpdatePayload) { const response = await updateRelease(name, namespace, payload); @@ -139,13 +143,13 @@ export class ReleaseStore extends ItemStore { return response; } - async rollback(name: string, namespace: string, revision: number) { + rollback = async (name: string, namespace: string, revision: number) => { const response = await rollbackRelease(name, namespace, revision); if (this.isLoaded) this.loadFromContextNamespaces(); return response; - } + }; async remove(release: HelmRelease) { return super.removeItem(release, () => deleteRelease(release.getName(), release.getNs())); @@ -155,5 +159,3 @@ export class ReleaseStore extends ItemStore { return Promise.all(this.selectedItems.map(this.remove)); } } - -export const releaseStore = new ReleaseStore(); diff --git a/src/renderer/components/+apps-releases/releases.tsx b/src/renderer/components/+apps-releases/releases.tsx index 8b01baf476..3d8f7b7ae4 100644 --- a/src/renderer/components/+apps-releases/releases.tsx +++ b/src/renderer/components/+apps-releases/releases.tsx @@ -25,7 +25,7 @@ import React, { Component } from "react"; import kebabCase from "lodash/kebabCase"; import { disposeOnUnmount, observer } from "mobx-react"; import type { RouteComponentProps } from "react-router"; -import { releaseStore } from "./release.store"; +import type { ReleaseStore } from "./release.store"; import type { HelmRelease } from "../../../common/k8s-api/endpoints/helm-releases.api"; import { ReleaseDetails } from "./release-details"; import { ReleaseRollbackDialog } from "./release-rollback-dialog"; @@ -36,7 +36,9 @@ import { secretsStore } from "../+config-secrets/secrets.store"; import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter"; import type { ReleaseRouteParams } from "../../../common/routes"; import { releaseURL } from "../../../common/routes"; -import { namespaceStore } from "../+namespaces/namespace.store"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import releaseStoreInjectable from "./release-store.injectable"; +import namespaceStoreInjectable from "../+namespaces/namespace-store/namespace-store.injectable"; enum columnId { name = "name", @@ -52,25 +54,30 @@ enum columnId { interface Props extends RouteComponentProps { } +interface Dependencies { + releaseStore: ReleaseStore + selectNamespace: (namespace: string) => void +} + @observer -export class HelmReleases extends Component { +class NonInjectedHelmReleases extends Component { componentDidMount() { const { match: { params: { namespace }}} = this.props; if (namespace) { - namespaceStore.selectNamespaces(namespace); + this.props.selectNamespace(namespace); } disposeOnUnmount(this, [ - releaseStore.watchAssociatedSecrets(), - releaseStore.watchSelectedNamespaces(), + this.props.releaseStore.watchAssociatedSecrets(), + this.props.releaseStore.watchSelectedNamespaces(), ]); } get selectedRelease() { const { match: { params: { name, namespace }}} = this.props; - return releaseStore.items.find(release => { + return this.props.releaseStore.items.find(release => { return release.getName() == name && release.getNs() == namespace; }); } @@ -116,7 +123,7 @@ export class HelmReleases extends Component { isConfigurable tableId="helm_releases" className="HelmReleases" - store={releaseStore} + store={this.props.releaseStore} dependentStores={[secretsStore]} sortingCallbacks={{ [columnId.name]: release => release.getName(), @@ -188,3 +195,15 @@ export class HelmReleases extends Component { ); } } + +export const HelmReleases = withInjectables( + NonInjectedHelmReleases, + + { + getProps: (di, props) => ({ + releaseStore: di.inject(releaseStoreInjectable), + selectNamespace: di.inject(namespaceStoreInjectable).selectNamespaces, + ...props, + }), + }, +); diff --git a/src/renderer/components/+catalog/catalog-entity-store/catalog-entity-store.injectable.ts b/src/renderer/components/+catalog/catalog-entity-store/catalog-entity-store.injectable.ts new file mode 100644 index 0000000000..6b559aca11 --- /dev/null +++ b/src/renderer/components/+catalog/catalog-entity-store/catalog-entity-store.injectable.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { CatalogEntityStore } from "./catalog-entity.store"; +import catalogEntityRegistryInjectable from "../../../api/catalog-entity-registry/catalog-entity-registry.injectable"; + +const catalogEntityStoreInjectable = getInjectable({ + instantiate: (di) => new CatalogEntityStore({ + registry: di.inject(catalogEntityRegistryInjectable), + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default catalogEntityStoreInjectable; diff --git a/src/renderer/components/+catalog/catalog-entity.store.tsx b/src/renderer/components/+catalog/catalog-entity-store/catalog-entity.store.tsx similarity index 78% rename from src/renderer/components/+catalog/catalog-entity.store.tsx rename to src/renderer/components/+catalog/catalog-entity-store/catalog-entity.store.tsx index 8b7743f470..f833a9c310 100644 --- a/src/renderer/components/+catalog/catalog-entity.store.tsx +++ b/src/renderer/components/+catalog/catalog-entity-store/catalog-entity.store.tsx @@ -20,14 +20,18 @@ */ import { computed, makeObservable, observable, reaction } from "mobx"; -import { catalogEntityRegistry, CatalogEntityRegistry } from "../../api/catalog-entity-registry"; -import type { CatalogEntity } from "../../api/catalog-entity"; -import { ItemStore } from "../../../common/item.store"; -import { CatalogCategory, catalogCategoryRegistry } from "../../../common/catalog"; -import { autoBind, disposer } from "../../../common/utils"; +import type { CatalogEntityRegistry } from "../../../api/catalog-entity-registry"; +import type { CatalogEntity } from "../../../api/catalog-entity"; +import { ItemStore } from "../../../../common/item.store"; +import { CatalogCategory, catalogCategoryRegistry } from "../../../../common/catalog"; +import { autoBind, disposer } from "../../../../common/utils"; + +interface Dependencies { + registry: CatalogEntityRegistry +} export class CatalogEntityStore extends ItemStore { - constructor(private registry: CatalogEntityRegistry = catalogEntityRegistry) { + constructor(private dependencies: Dependencies) { super(); makeObservable(this); autoBind(this); @@ -38,10 +42,10 @@ export class CatalogEntityStore extends ItemStore { @computed get entities() { if (!this.activeCategory) { - return this.registry.filteredItems; + return this.dependencies.registry.filteredItems; } - return this.registry.getItemsForCategory(this.activeCategory, { filtered: true }); + return this.dependencies.registry.getItemsForCategory(this.activeCategory, { filtered: true }); } @computed get selectedItem() { @@ -69,6 +73,6 @@ export class CatalogEntityStore extends ItemStore { } onRun(entity: CatalogEntity): void { - this.registry.onRun(entity); + this.dependencies.registry.onRun(entity); } } diff --git a/src/renderer/components/+catalog/catalog-previous-active-tab-storage/catalog-previous-active-tab-storage.injectable.ts b/src/renderer/components/+catalog/catalog-previous-active-tab-storage/catalog-previous-active-tab-storage.injectable.ts new file mode 100644 index 0000000000..4f778fbea4 --- /dev/null +++ b/src/renderer/components/+catalog/catalog-previous-active-tab-storage/catalog-previous-active-tab-storage.injectable.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { browseCatalogTab } from "../../../../common/routes"; +import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable"; + +const catalogPreviousActiveTabStorageInjectable = getInjectable({ + instantiate: (di) => { + const createStorage = di.inject(createStorageInjectable); + + return createStorage( + "catalog-previous-active-tab", + browseCatalogTab, + ); + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default catalogPreviousActiveTabStorageInjectable; diff --git a/src/renderer/components/+catalog/catalog.test.tsx b/src/renderer/components/+catalog/catalog.test.tsx index 0f80aa475b..f7496aa24b 100644 --- a/src/renderer/components/+catalog/catalog.test.tsx +++ b/src/renderer/components/+catalog/catalog.test.tsx @@ -20,17 +20,27 @@ */ import React from "react"; -import { render, screen } from "@testing-library/react"; +import { screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { Catalog } from "./catalog"; import { createMemoryHistory } from "history"; import { mockWindow } from "../../../../__mocks__/windowMock"; -import { kubernetesClusterCategory } from "../../../common/catalog-entities/kubernetes-cluster"; -import { catalogCategoryRegistry, CatalogCategoryRegistry, CatalogEntity, CatalogEntityActionContext, CatalogEntityData } from "../../../common/catalog"; -import { CatalogEntityRegistry } from "../../../renderer/api/catalog-entity-registry"; +import { CatalogCategoryRegistry, CatalogEntity, CatalogEntityActionContext, CatalogEntityData } from "../../../common/catalog"; +import { CatalogEntityRegistry } from "../../api/catalog-entity-registry"; import { CatalogEntityDetailRegistry } from "../../../extensions/registries"; -import { CatalogEntityStore } from "./catalog-entity.store"; -import { AppPaths } from "../../../common/app-paths"; +import type { CatalogEntityStore } from "./catalog-entity-store/catalog-entity.store"; +import { getDiForUnitTesting } from "../getDiForUnitTesting"; +import type { DependencyInjectionContainer } from "@ogre-tools/injectable"; +import catalogEntityStoreInjectable from "./catalog-entity-store/catalog-entity-store.injectable"; +import catalogEntityRegistryInjectable + from "../../api/catalog-entity-registry/catalog-entity-registry.injectable"; +import type { DiRender } from "../test-utils/renderFor"; +import { renderFor } from "../test-utils/renderFor"; +import { ThemeStore } from "../../theme.store"; +import { UserStore } from "../../../common/user-store"; +import mockFs from "mock-fs"; +import directoryForUserDataInjectable + from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; mockWindow(); jest.mock("electron", () => ({ @@ -49,8 +59,6 @@ jest.mock("electron", () => ({ }, })); -AppPaths.init(); - jest.mock("./hotbar-toggle-menu-item", () => ({ HotbarToggleMenuItem: () =>
menu item
, })); @@ -103,31 +111,46 @@ describe("", () => { }, onRun); } - beforeEach(() => { - CatalogEntityDetailRegistry.createInstance(); - // mock the return of getting CatalogCategoryRegistry.filteredItems - jest - .spyOn(catalogCategoryRegistry, "filteredItems", "get") - .mockImplementation(() => { - return [kubernetesClusterCategory]; - }); + let di: DependencyInjectionContainer; + let catalogEntityStore: CatalogEntityStore; + let catalogEntityRegistry: CatalogEntityRegistry; + let render: DiRender; - // we don't care what this.renderList renders in this test case. - jest.spyOn(Catalog.prototype, "renderList").mockImplementation(() => { - return empty renderList; - }); + beforeEach(async () => { + di = getDiForUnitTesting({ doGeneralOverrides: true }); + + di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); + + await di.runSetups(); + + mockFs(); + + UserStore.createInstance(); + ThemeStore.createInstance(); + CatalogEntityDetailRegistry.createInstance(); + + render = renderFor(di); + + const catalogCategoryRegistry = new CatalogCategoryRegistry(); + + catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry); + + di.override(catalogEntityRegistryInjectable, () => catalogEntityRegistry); + + catalogEntityStore = di.inject(catalogEntityStoreInjectable); }); afterEach(() => { + UserStore.resetInstance(); + ThemeStore.resetInstance(); CatalogEntityDetailRegistry.resetInstance(); + jest.clearAllMocks(); jest.restoreAllMocks(); + mockFs.restore(); }); it("can use catalogEntityRegistry.addOnBeforeRun to add hooks for catalog entities", (done) => { - const catalogCategoryRegistry = new CatalogCategoryRegistry(); - const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry); - const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry); const onRun = jest.fn(); const catalogEntityItem = createMockCatalogEntity(onRun); @@ -153,7 +176,6 @@ describe("", () => { history={history} location={mockLocation} match={mockMatch} - catalogEntityStore={catalogEntityStore} />, ); @@ -161,9 +183,6 @@ describe("", () => { }); it("onBeforeRun prevents event => onRun wont be triggered", (done) => { - const catalogCategoryRegistry = new CatalogCategoryRegistry(); - const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry); - const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry); const onRun = jest.fn(); const catalogEntityItem = createMockCatalogEntity(onRun); @@ -187,7 +206,6 @@ describe("", () => { history={history} location={mockLocation} match={mockMatch} - catalogEntityStore={catalogEntityStore} />, ); @@ -195,9 +213,6 @@ describe("", () => { }); it("addOnBeforeRun throw an exception => onRun will be triggered", (done) => { - const catalogCategoryRegistry = new CatalogCategoryRegistry(); - const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry); - const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry); const onRun = jest.fn(); const catalogEntityItem = createMockCatalogEntity(onRun); @@ -222,7 +237,6 @@ describe("", () => { history={history} location={mockLocation} match={mockMatch} - catalogEntityStore={catalogEntityStore} />, ); @@ -230,9 +244,6 @@ describe("", () => { }); it("addOnRunHook return a promise and does not prevent run event => onRun()", (done) => { - const catalogCategoryRegistry = new CatalogCategoryRegistry(); - const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry); - const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry); const onRun = jest.fn(() => done()); const catalogEntityItem = createMockCatalogEntity(onRun); @@ -252,7 +263,6 @@ describe("", () => { history={history} location={mockLocation} match={mockMatch} - catalogEntityStore={catalogEntityStore} />, ); @@ -260,9 +270,6 @@ describe("", () => { }); it("addOnRunHook return a promise and prevents event wont be triggered", (done) => { - const catalogCategoryRegistry = new CatalogCategoryRegistry(); - const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry); - const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry); const onRun = jest.fn(); const catalogEntityItem = createMockCatalogEntity(onRun); @@ -289,7 +296,6 @@ describe("", () => { history={history} location={mockLocation} match={mockMatch} - catalogEntityStore={catalogEntityStore} />, ); @@ -297,9 +303,6 @@ describe("", () => { }); it("addOnRunHook return a promise and reject => onRun will be triggered", (done) => { - const catalogCategoryRegistry = new CatalogCategoryRegistry(); - const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry); - const catalogEntityStore = new CatalogEntityStore(catalogEntityRegistry); const onRun = jest.fn(); const catalogEntityItem = createMockCatalogEntity(onRun); @@ -324,7 +327,6 @@ describe("", () => { history={history} location={mockLocation} match={mockMatch} - catalogEntityStore={catalogEntityStore} />, ); diff --git a/src/renderer/components/+catalog/catalog.tsx b/src/renderer/components/+catalog/catalog.tsx index 595ba6a81c..583a4e3c0e 100644 --- a/src/renderer/components/+catalog/catalog.tsx +++ b/src/renderer/components/+catalog/catalog.tsx @@ -25,7 +25,7 @@ import React from "react"; import { disposeOnUnmount, observer } from "mobx-react"; import { ItemListLayout } from "../item-object-list"; import { action, makeObservable, observable, reaction, runInAction, when } from "mobx"; -import { CatalogEntityStore } from "./catalog-entity.store"; +import type { CatalogEntityStore } from "./catalog-entity-store/catalog-entity.store"; import { navigate } from "../../navigation"; import { MenuItem, MenuActions } from "../menu"; import type { CatalogEntityContextMenu, CatalogEntityContextMenuContext } from "../../api/catalog-entity"; @@ -36,7 +36,7 @@ import { CatalogAddButton } from "./catalog-add-button"; import type { RouteComponentProps } from "react-router"; import { Notifications } from "../notifications"; import { MainLayout } from "../layout/main-layout"; -import { createStorage, prevDefault } from "../../utils"; +import { prevDefault } from "../../utils"; import { CatalogEntityDetails } from "./catalog-entity-details"; import { browseCatalogTab, catalogURL, CatalogViewRouteParam } from "../../../common/routes"; import { CatalogMenu } from "./catalog-menu"; @@ -46,8 +46,10 @@ import { HotbarToggleMenuItem } from "./hotbar-toggle-menu-item"; import { Avatar } from "../avatar"; import { KubeObject } from "../../../common/k8s-api/kube-object"; import { getLabelBadges } from "./helpers"; - -export const previousActiveTab = createStorage("catalog-previous-active-tab", browseCatalogTab); +import { withInjectables } from "@ogre-tools/injectable-react"; +import catalogPreviousActiveTabStorageInjectable + from "./catalog-previous-active-tab-storage/catalog-previous-active-tab-storage.injectable"; +import catalogEntityStoreInjectable from "./catalog-entity-store/catalog-entity-store.injectable"; enum sortBy { name = "name", @@ -56,26 +58,23 @@ enum sortBy { status = "status", } -interface Props extends RouteComponentProps { - catalogEntityStore?: CatalogEntityStore; +interface Props extends RouteComponentProps {} + +interface Dependencies { + catalogPreviousActiveTabStorage: { set: (value: string ) => void } + catalogEntityStore: CatalogEntityStore } @observer -export class Catalog extends React.Component { - @observable private catalogEntityStore?: CatalogEntityStore; +class NonInjectedCatalog extends React.Component { @observable private contextMenu: CatalogEntityContextMenuContext; @observable activeTab?: string; - constructor(props: Props) { + constructor(props: Props & Dependencies) { super(props); makeObservable(this); - this.catalogEntityStore = props.catalogEntityStore; } - - static defaultProps = { - catalogEntityStore: new CatalogEntityStore(), - }; - + get routeActiveTab(): string { const { group, kind } = this.props.match.params ?? {}; @@ -92,9 +91,9 @@ export class Catalog extends React.Component { navigate: (url: string) => navigate(url), }; disposeOnUnmount(this, [ - this.catalogEntityStore.watch(), + this.props.catalogEntityStore.watch(), reaction(() => this.routeActiveTab, async (routeTab) => { - previousActiveTab.set(this.routeActiveTab); + this.props.catalogPreviousActiveTabStorage.set(this.routeActiveTab); try { await when(() => (routeTab === browseCatalogTab || !!catalogCategoryRegistry.filteredItems.find(i => i.getId() === routeTab)), { timeout: 5_000 }); // we need to wait because extensions might take a while to load @@ -102,7 +101,7 @@ export class Catalog extends React.Component { runInAction(() => { this.activeTab = routeTab; - this.catalogEntityStore.activeCategory = item; + this.props.catalogEntityStore.activeCategory = item; }); } catch (error) { console.error(error); @@ -113,13 +112,13 @@ export class Catalog extends React.Component { // If active category is filtered out, automatically switch to the first category disposeOnUnmount(this, reaction(() => catalogCategoryRegistry.filteredItems, () => { - if (!catalogCategoryRegistry.filteredItems.find(item => item.getId() === this.catalogEntityStore.activeCategory.getId())) { + if (!catalogCategoryRegistry.filteredItems.find(item => item.getId() === this.props.catalogEntityStore.activeCategory.getId())) { const item = catalogCategoryRegistry.filteredItems[0]; runInAction(() => { if (item) { this.activeTab = item.getId(); - this.catalogEntityStore.activeCategory = item; + this.props.catalogEntityStore.activeCategory = item; } }); } @@ -135,10 +134,10 @@ export class Catalog extends React.Component { } onDetails = (entity: CatalogEntity) => { - if (this.catalogEntityStore.selectedItemId) { - this.catalogEntityStore.selectedItemId = null; + if (this.props.catalogEntityStore.selectedItemId) { + this.props.catalogEntityStore.selectedItemId = null; } else { - this.catalogEntityStore.onRun(entity); + this.props.catalogEntityStore.onRun(entity); } }; @@ -189,7 +188,7 @@ export class Catalog extends React.Component { return ( - this.catalogEntityStore.selectedItemId = entity.getId()}> + this.props.catalogEntityStore.selectedItemId = entity.getId()}> View Details { @@ -238,7 +237,7 @@ export class Catalog extends React.Component { } renderList() { - const { activeCategory } = this.catalogEntityStore; + const { activeCategory } = this.props.catalogEntityStore; const tableId = activeCategory ? `catalog-items-${activeCategory.metadata.name.replace(" ", "")}` : "catalog-items"; if (this.activeTab === undefined) { @@ -252,7 +251,7 @@ export class Catalog extends React.Component { renderHeaderTitle={activeCategory?.metadata.name || "Browse All"} isSelectable={false} isConfigurable={true} - store={this.catalogEntityStore} + store={this.props.catalogEntityStore} sortingCallbacks={{ [sortBy.name]: entity => entity.getName(), [sortBy.source]: entity => entity.getSource(), @@ -292,11 +291,11 @@ export class Catalog extends React.Component { } render() { - if (!this.catalogEntityStore) { + if (!this.props.catalogEntityStore) { return null; } - const selectedEntity = this.catalogEntityStore.selectedItem; + const selectedEntity = this.props.catalogEntityStore.selectedItem; return ( @@ -307,13 +306,13 @@ export class Catalog extends React.Component { selectedEntity ? this.catalogEntityStore.selectedItemId = null} - onRun={() => this.catalogEntityStore.onRun(selectedEntity)} + hideDetails={() => this.props.catalogEntityStore.selectedItemId = null} + onRun={() => this.props.catalogEntityStore.onRun(selectedEntity)} /> : ( ) @@ -322,3 +321,18 @@ export class Catalog extends React.Component { ); } } + +export const Catalog = withInjectables( + NonInjectedCatalog, + { + getProps: (di, props) => ({ + catalogEntityStore: di.inject(catalogEntityStoreInjectable), + + catalogPreviousActiveTabStorage: di.inject( + catalogPreviousActiveTabStorageInjectable, + ), + + ...props, + }), + }, +); diff --git a/src/renderer/components/+cluster/cluster-metric-switchers.tsx b/src/renderer/components/+cluster/cluster-metric-switchers.tsx index aa2ff2985f..6581452907 100644 --- a/src/renderer/components/+cluster/cluster-metric-switchers.tsx +++ b/src/renderer/components/+cluster/cluster-metric-switchers.tsx @@ -24,10 +24,15 @@ import { observer } from "mobx-react"; import { nodesStore } from "../+nodes/nodes.store"; import { cssNames } from "../../utils"; import { Radio, RadioGroup } from "../radio"; -import { clusterOverviewStore, MetricNodeRole, MetricType } from "./cluster-overview.store"; +import { ClusterOverviewStore, MetricNodeRole, MetricType } from "./cluster-overview-store/cluster-overview-store"; +import clusterOverviewStoreInjectable from "./cluster-overview-store/cluster-overview-store.injectable"; +import { withInjectables } from "@ogre-tools/injectable-react"; -export const ClusterMetricSwitchers = observer(() => { - const { metricType, metricNodeRole, getMetricsValues, metrics } = clusterOverviewStore; +interface Dependencies { + clusterOverviewStore: ClusterOverviewStore +} + +const NonInjectedClusterMetricSwitchers = observer(({ clusterOverviewStore: { metricType, metricNodeRole, getMetricsValues, metrics }}: Dependencies) => { const { masterNodes, workerNodes } = nodesStore; const metricsValues = getMetricsValues(metrics); const disableRoles = !masterNodes.length || !workerNodes.length; @@ -40,7 +45,7 @@ export const ClusterMetricSwitchers = observer(() => { asButtons className={cssNames("RadioGroup flex gaps", { disabled: disableRoles })} value={metricNodeRole} - onChange={(metric: MetricNodeRole) => clusterOverviewStore.metricNodeRole = metric} + onChange={(metric: MetricNodeRole) => metricNodeRole = metric} > @@ -51,7 +56,7 @@ export const ClusterMetricSwitchers = observer(() => { asButtons className={cssNames("RadioGroup flex gaps", { disabled: disableMetrics })} value={metricType} - onChange={(value: MetricType) => clusterOverviewStore.metricType = value} + onChange={(value: MetricType) => metricType = value} > @@ -60,3 +65,14 @@ export const ClusterMetricSwitchers = observer(() => { ); }); + +export const ClusterMetricSwitchers = withInjectables( + NonInjectedClusterMetricSwitchers, + + { + getProps: (di) => ({ + clusterOverviewStore: di.inject(clusterOverviewStoreInjectable), + }), + }, +); + diff --git a/src/renderer/components/+cluster/cluster-metrics.tsx b/src/renderer/components/+cluster/cluster-metrics.tsx index 919a96e774..654300db55 100644 --- a/src/renderer/components/+cluster/cluster-metrics.tsx +++ b/src/renderer/components/+cluster/cluster-metrics.tsx @@ -24,7 +24,7 @@ import styles from "./cluster-metrics.module.scss"; import React from "react"; import { observer } from "mobx-react"; import type { ChartOptions, ChartPoint } from "chart.js"; -import { clusterOverviewStore, MetricType } from "./cluster-overview.store"; +import { ClusterOverviewStore, MetricType } from "./cluster-overview-store/cluster-overview-store"; import { BarChart } from "../chart"; import { bytesToUnits, cssNames } from "../../utils"; import { Spinner } from "../spinner"; @@ -32,10 +32,16 @@ import { ZebraStripes } from "../chart/zebra-stripes.plugin"; import { ClusterNoMetrics } from "./cluster-no-metrics"; import { ClusterMetricSwitchers } from "./cluster-metric-switchers"; import { getMetricLastPoints } from "../../../common/k8s-api/endpoints/metrics.api"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import clusterOverviewStoreInjectable + from "./cluster-overview-store/cluster-overview-store.injectable"; -export const ClusterMetrics = observer(() => { - const { metricType, metricNodeRole, getMetricsValues, metricsLoaded, metrics } = clusterOverviewStore; - const { memoryCapacity, cpuCapacity } = getMetricLastPoints(clusterOverviewStore.metrics); +interface Dependencies { + clusterOverviewStore: ClusterOverviewStore +} + +const NonInjectedClusterMetrics = observer(({ clusterOverviewStore: { metricType, metricNodeRole, getMetricsValues, metricsLoaded, metrics }}: Dependencies) => { + const { memoryCapacity, cpuCapacity } = getMetricLastPoints(metrics); const metricValues = getMetricsValues(metrics); const colors = { cpu: "#3D90CE", memory: "#C93DCE" }; const data = metricValues.map(value => ({ @@ -118,3 +124,13 @@ export const ClusterMetrics = observer(() => { ); }); + +export const ClusterMetrics = withInjectables( + NonInjectedClusterMetrics, + + { + getProps: (di) => ({ + clusterOverviewStore: di.inject(clusterOverviewStoreInjectable), + }), + }, +); diff --git a/src/renderer/components/+cluster/cluster-overview-store/cluster-overview-store.injectable.ts b/src/renderer/components/+cluster/cluster-overview-store/cluster-overview-store.injectable.ts new file mode 100644 index 0000000000..7df5e3f6bb --- /dev/null +++ b/src/renderer/components/+cluster/cluster-overview-store/cluster-overview-store.injectable.ts @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { + ClusterOverviewStorageState, + ClusterOverviewStore, + MetricNodeRole, + MetricType, +} from "./cluster-overview-store"; +import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable"; +import apiManagerInjectable from "../../kube-object-menu/dependencies/api-manager.injectable"; + +const clusterOverviewStoreInjectable = getInjectable({ + instantiate: (di) => { + const createStorage = di.inject(createStorageInjectable); + + const storage = createStorage( + "cluster_overview", + { + metricType: MetricType.CPU, // setup defaults + metricNodeRole: MetricNodeRole.WORKER, + }, + ); + + const store = new ClusterOverviewStore({ + storage, + }); + + const apiManager = di.inject(apiManagerInjectable); + + apiManager.registerStore(store); + + return store; + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default clusterOverviewStoreInjectable; diff --git a/src/renderer/components/+cluster/cluster-overview.store.ts b/src/renderer/components/+cluster/cluster-overview-store/cluster-overview-store.ts similarity index 78% rename from src/renderer/components/+cluster/cluster-overview.store.ts rename to src/renderer/components/+cluster/cluster-overview-store/cluster-overview-store.ts index 130797c4f8..c57437fc8c 100644 --- a/src/renderer/components/+cluster/cluster-overview.store.ts +++ b/src/renderer/components/+cluster/cluster-overview-store/cluster-overview-store.ts @@ -20,12 +20,11 @@ */ import { action, observable, reaction, when, makeObservable } from "mobx"; -import { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; -import { Cluster, clusterApi, getMetricsByNodeNames, IClusterMetrics } from "../../../common/k8s-api/endpoints"; -import { autoBind, createStorage } from "../../utils"; -import { IMetricsReqParams, normalizeMetrics } from "../../../common/k8s-api/endpoints/metrics.api"; -import { nodesStore } from "../+nodes/nodes.store"; -import { apiManager } from "../../../common/k8s-api/api-manager"; +import { KubeObjectStore } from "../../../../common/k8s-api/kube-object.store"; +import { Cluster, clusterApi, getMetricsByNodeNames, IClusterMetrics } from "../../../../common/k8s-api/endpoints"; +import { autoBind, StorageHelper } from "../../../utils"; +import { IMetricsReqParams, normalizeMetrics } from "../../../../common/k8s-api/endpoints/metrics.api"; +import { nodesStore } from "../../+nodes/nodes.store"; export enum MetricType { MEMORY = "memory", @@ -42,34 +41,33 @@ export interface ClusterOverviewStorageState { metricNodeRole: MetricNodeRole, } +interface Dependencies { + storage: StorageHelper +} + export class ClusterOverviewStore extends KubeObjectStore implements ClusterOverviewStorageState { api = clusterApi; @observable metrics: Partial = {}; @observable metricsLoaded = false; - private storage = createStorage("cluster_overview", { - metricType: MetricType.CPU, // setup defaults - metricNodeRole: MetricNodeRole.WORKER, - }); - get metricType(): MetricType { - return this.storage.get().metricType; + return this.dependencies.storage.get().metricType; } set metricType(value: MetricType) { - this.storage.merge({ metricType: value }); + this.dependencies.storage.merge({ metricType: value }); } get metricNodeRole(): MetricNodeRole { - return this.storage.get().metricNodeRole; + return this.dependencies.storage.get().metricNodeRole; } set metricNodeRole(value: MetricNodeRole) { - this.storage.merge({ metricNodeRole: value }); + this.dependencies.storage.merge({ metricNodeRole: value }); } - constructor() { + constructor(private dependencies: Dependencies ) { super(); makeObservable(this); autoBind(this); @@ -125,9 +123,6 @@ export class ClusterOverviewStore extends KubeObjectStore implements Cl reset() { super.reset(); this.resetMetrics(); - this.storage?.reset(); + this.dependencies.storage?.reset(); } } - -export const clusterOverviewStore = new ClusterOverviewStore(); -apiManager.registerStore(clusterOverviewStore); diff --git a/src/renderer/components/+cluster/cluster-overview.tsx b/src/renderer/components/+cluster/cluster-overview.tsx index 064b716e7e..a0f9b05b9f 100644 --- a/src/renderer/components/+cluster/cluster-overview.tsx +++ b/src/renderer/components/+cluster/cluster-overview.tsx @@ -26,28 +26,37 @@ import { reaction } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; import { nodesStore } from "../+nodes/nodes.store"; import { podsStore } from "../+workloads-pods/pods.store"; -import { getHostedClusterId, interval } from "../../utils"; +import { Disposer, getHostedClusterId, interval } from "../../utils"; import { TabLayout } from "../layout/tab-layout"; import { Spinner } from "../spinner"; import { ClusterIssues } from "./cluster-issues"; import { ClusterMetrics } from "./cluster-metrics"; -import { clusterOverviewStore } from "./cluster-overview.store"; +import type { ClusterOverviewStore } from "./cluster-overview-store/cluster-overview-store"; import { ClusterPieCharts } from "./cluster-pie-charts"; import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; import { ClusterMetricsResourceType } from "../../../common/cluster-types"; -import { ClusterStore } from "../../../common/cluster-store"; -import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api"; +import { ClusterStore } from "../../../common/cluster-store/cluster-store"; import { eventStore } from "../+events/event.store"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import kubeWatchApiInjectable from "../../kube-watch-api/kube-watch-api.injectable"; +import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; +import type { KubeObject } from "../../../common/k8s-api/kube-object"; +import clusterOverviewStoreInjectable from "./cluster-overview-store/cluster-overview-store.injectable"; + +interface Dependencies { + subscribeStores: (stores: KubeObjectStore[]) => Disposer, + clusterOverviewStore: ClusterOverviewStore +} @observer -export class ClusterOverview extends React.Component { +class NonInjectedClusterOverview extends React.Component { private metricPoller = interval(60, () => this.loadMetrics()); loadMetrics() { const cluster = ClusterStore.getInstance().getById(getHostedClusterId()); if (cluster.available) { - clusterOverviewStore.loadMetrics(); + this.props.clusterOverviewStore.loadMetrics(); } } @@ -55,13 +64,14 @@ export class ClusterOverview extends React.Component { this.metricPoller.start(true); disposeOnUnmount(this, [ - kubeWatchApi.subscribeStores([ + this.props.subscribeStores([ podsStore, eventStore, nodesStore, ]), + reaction( - () => clusterOverviewStore.metricNodeRole, // Toggle Master/Worker node switcher + () => this.props.clusterOverviewStore.metricNodeRole, // Toggle Master/Worker node switcher () => this.metricPoller.restart(true), ), ]); @@ -110,3 +120,14 @@ export class ClusterOverview extends React.Component { ); } } + +export const ClusterOverview = withInjectables( + NonInjectedClusterOverview, + + { + getProps: (di) => ({ + subscribeStores: di.inject(kubeWatchApiInjectable).subscribeStores, + clusterOverviewStore: di.inject(clusterOverviewStoreInjectable), + }), + }, +); diff --git a/src/renderer/components/+cluster/cluster-pie-charts.tsx b/src/renderer/components/+cluster/cluster-pie-charts.tsx index c88202c410..b844057994 100644 --- a/src/renderer/components/+cluster/cluster-pie-charts.tsx +++ b/src/renderer/components/+cluster/cluster-pie-charts.tsx @@ -23,7 +23,7 @@ import styles from "./cluster-pie-charts.module.scss"; import React from "react"; import { observer } from "mobx-react"; -import { clusterOverviewStore, MetricNodeRole } from "./cluster-overview.store"; +import { ClusterOverviewStore, MetricNodeRole } from "./cluster-overview-store/cluster-overview-store"; import { Spinner } from "../spinner"; import { Icon } from "../icon"; import { nodesStore } from "../+nodes/nodes.store"; @@ -32,12 +32,18 @@ import { ClusterNoMetrics } from "./cluster-no-metrics"; import { bytesToUnits, cssNames } from "../../utils"; import { ThemeStore } from "../../theme.store"; import { getMetricLastPoints } from "../../../common/k8s-api/endpoints/metrics.api"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import clusterOverviewStoreInjectable from "./cluster-overview-store/cluster-overview-store.injectable"; function createLabels(rawLabelData: [string, number | undefined][]): string[] { return rawLabelData.map(([key, value]) => `${key}: ${value?.toFixed(2) || "N/A"}`); } -export const ClusterPieCharts = observer(() => { +interface Dependencies { + clusterOverviewStore: ClusterOverviewStore +} + +const NonInjectedClusterPieCharts = observer(({ clusterOverviewStore }: Dependencies) => { const renderLimitWarning = () => { return (
@@ -213,9 +219,8 @@ export const ClusterPieCharts = observer(() => { ); }; - const renderContent = () => { + const renderContent = ({ metricNodeRole, metricsLoaded }: ClusterOverviewStore) => { const { masterNodes, workerNodes } = nodesStore; - const { metricNodeRole, metricsLoaded } = clusterOverviewStore; const nodes = metricNodeRole === MetricNodeRole.MASTER ? masterNodes : workerNodes; if (!nodes.length) { @@ -245,7 +250,17 @@ export const ClusterPieCharts = observer(() => { return (
- {renderContent()} + {renderContent(clusterOverviewStore)}
); }); + +export const ClusterPieCharts = withInjectables( + NonInjectedClusterPieCharts, + + { + getProps: (di) => ({ + clusterOverviewStore: di.inject(clusterOverviewStoreInjectable), + }), + }, +); diff --git a/src/renderer/components/+events/kube-event-details.tsx b/src/renderer/components/+events/kube-event-details.tsx index e573f97303..65e87ae5a2 100644 --- a/src/renderer/components/+events/kube-event-details.tsx +++ b/src/renderer/components/+events/kube-event-details.tsx @@ -25,21 +25,28 @@ import React from "react"; import { disposeOnUnmount, observer } from "mobx-react"; import { KubeObject } from "../../../common/k8s-api/kube-object"; import { DrawerItem, DrawerTitle } from "../drawer"; -import { cssNames } from "../../utils"; +import { cssNames, Disposer } from "../../utils"; import { LocaleDate } from "../locale-date"; import { eventStore } from "./event.store"; import logger from "../../../common/logger"; -import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api"; +import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import kubeWatchApiInjectable + from "../../kube-watch-api/kube-watch-api.injectable"; export interface KubeEventDetailsProps { object: KubeObject; } +interface Dependencies { + subscribeStores: (stores: KubeObjectStore[]) => Disposer +} + @observer -export class KubeEventDetails extends React.Component { +class NonInjectedKubeEventDetails extends React.Component { componentDidMount() { disposeOnUnmount(this, [ - kubeWatchApi.subscribeStores([ + this.props.subscribeStores([ eventStore, ]), ]); @@ -102,3 +109,17 @@ export class KubeEventDetails extends React.Component { ); } } + +export const KubeEventDetails = withInjectables( + NonInjectedKubeEventDetails, + + { + getProps: (di, props) => ({ + subscribeStores: di.inject(kubeWatchApiInjectable).subscribeStores, + ...props, + }), + }, +); + + + diff --git a/src/renderer/components/+extensions/__tests__/extensions.test.tsx b/src/renderer/components/+extensions/__tests__/extensions.test.tsx index 16c927a796..da5c6e72c2 100644 --- a/src/renderer/components/+extensions/__tests__/extensions.test.tsx +++ b/src/renderer/components/+extensions/__tests__/extensions.test.tsx @@ -30,13 +30,13 @@ import { ConfirmDialog } from "../../confirm-dialog"; import { Extensions } from "../extensions"; import mockFs from "mock-fs"; import { mockWindow } from "../../../../../__mocks__/windowMock"; -import { AppPaths } from "../../../../common/app-paths"; -import extensionLoaderInjectable - from "../../../../extensions/extension-loader/extension-loader.injectable"; +import extensionLoaderInjectable from "../../../../extensions/extension-loader/extension-loader.injectable"; import { getDiForUnitTesting } from "../../getDiForUnitTesting"; import { DiRender, renderFor } from "../../test-utils/renderFor"; -import extensionDiscoveryInjectable - from "../../../../extensions/extension-discovery/extension-discovery.injectable"; +import extensionDiscoveryInjectable from "../../../../extensions/extension-discovery/extension-discovery.injectable"; +import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; +import directoryForDownloadsInjectable + from "../../../../common/app-paths/directory-for-downloads/directory-for-downloads.injectable"; mockWindow(); @@ -59,42 +59,28 @@ jest.mock("../../../../common/utils/downloadFile", () => ({ jest.mock("../../../../common/utils/tar"); -jest.mock("electron", () => ({ - app: { - getVersion: () => "99.99.99", - getName: () => "lens", - setName: jest.fn(), - setPath: jest.fn(), - getPath: () => "tmp", - getLocale: () => "en", - setLoginItemSettings: jest.fn(), - }, - ipcMain: { - on: jest.fn(), - handle: jest.fn(), - }, -})); - -AppPaths.init(); - describe("Extensions", () => { let extensionLoader: ExtensionLoader; let extensionDiscovery: ExtensionDiscovery; let render: DiRender; beforeEach(async () => { - const di = getDiForUnitTesting(); + const di = getDiForUnitTesting({ doGeneralOverrides: true }); + + di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); + di.override(directoryForDownloadsInjectable, () => "some-directory-for-downloads"); + + mockFs({ + "some-directory-for-user-data": {}, + }); + + await di.runSetups(); render = renderFor(di); extensionLoader = di.inject(extensionLoaderInjectable); - extensionDiscovery = di.inject(extensionDiscoveryInjectable); - mockFs({ - "tmp": {}, - }); - extensionLoader.addExtension({ id: "extensionId", manifest: { diff --git a/src/renderer/components/+extensions/install-from-select-file-dialog/install-from-select-file-dialog.injectable.ts b/src/renderer/components/+extensions/install-from-select-file-dialog/install-from-select-file-dialog.injectable.ts index 34af624c46..84fe047576 100644 --- a/src/renderer/components/+extensions/install-from-select-file-dialog/install-from-select-file-dialog.injectable.ts +++ b/src/renderer/components/+extensions/install-from-select-file-dialog/install-from-select-file-dialog.injectable.ts @@ -21,11 +21,13 @@ import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { installFromSelectFileDialog } from "./install-from-select-file-dialog"; import attemptInstallsInjectable from "../attempt-installs/attempt-installs.injectable"; +import directoryForDownloadsInjectable from "../../../../common/app-paths/directory-for-downloads/directory-for-downloads.injectable"; const installFromSelectFileDialogInjectable = getInjectable({ instantiate: (di) => installFromSelectFileDialog({ attemptInstalls: di.inject(attemptInstallsInjectable), + directoryForDownloads: di.inject(directoryForDownloadsInjectable), }), lifecycle: lifecycleEnum.singleton, diff --git a/src/renderer/components/+extensions/install-from-select-file-dialog/install-from-select-file-dialog.ts b/src/renderer/components/+extensions/install-from-select-file-dialog/install-from-select-file-dialog.ts index bb8e12e17e..d19efed945 100644 --- a/src/renderer/components/+extensions/install-from-select-file-dialog/install-from-select-file-dialog.ts +++ b/src/renderer/components/+extensions/install-from-select-file-dialog/install-from-select-file-dialog.ts @@ -19,18 +19,18 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ import { dialog } from "../../../remote-helpers"; -import { AppPaths } from "../../../../common/app-paths"; import { supportedExtensionFormats } from "../supported-extension-formats"; interface Dependencies { attemptInstalls: (filePaths: string[]) => Promise + directoryForDownloads: string } export const installFromSelectFileDialog = - ({ attemptInstalls }: Dependencies) => + ({ attemptInstalls, directoryForDownloads }: Dependencies) => async () => { const { canceled, filePaths } = await dialog.showOpenDialog({ - defaultPath: AppPaths.get("downloads"), + defaultPath: directoryForDownloads, properties: ["openFile", "multiSelections"], message: `Select extensions to install (formats: ${supportedExtensionFormats.join( ", ", diff --git a/src/renderer/components/+namespaces/add-namespace-dialog-model/add-namespace-dialog-model.injectable.ts b/src/renderer/components/+namespaces/add-namespace-dialog-model/add-namespace-dialog-model.injectable.ts new file mode 100644 index 0000000000..d2667eca06 --- /dev/null +++ b/src/renderer/components/+namespaces/add-namespace-dialog-model/add-namespace-dialog-model.injectable.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { AddNamespaceDialogModel } from "./add-namespace-dialog-model"; + +const addNamespaceDialogModelInjectable = getInjectable({ + instantiate: () => new AddNamespaceDialogModel(), + lifecycle: lifecycleEnum.singleton, +}); + +export default addNamespaceDialogModelInjectable; diff --git a/src/renderer/hooks/useStorage.ts b/src/renderer/components/+namespaces/add-namespace-dialog-model/add-namespace-dialog-model.ts similarity index 73% rename from src/renderer/hooks/useStorage.ts rename to src/renderer/components/+namespaces/add-namespace-dialog-model/add-namespace-dialog-model.ts index 92ed73ac46..5f4ff9f531 100644 --- a/src/renderer/hooks/useStorage.ts +++ b/src/renderer/components/+namespaces/add-namespace-dialog-model/add-namespace-dialog-model.ts @@ -18,17 +18,24 @@ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +import { observable, makeObservable, action } from "mobx"; -import { useState } from "react"; -import { createStorage } from "../utils"; +export class AddNamespaceDialogModel { + isOpen = false; -export function useStorage(key: string, initialValue: T) { - const storage = createStorage(key, initialValue); - const [storageValue, setStorageValue] = useState(storage.get()); - const setValue = (value: T) => { - setStorageValue(value); - storage.set(value); + constructor() { + makeObservable(this, { + isOpen: observable, + open: action, + close: action, + }); + } + + open = () => { + this.isOpen = true; }; - return [storageValue, setValue] as [T, (value: T) => void]; + close = () => { + this.isOpen = false; + }; } diff --git a/src/renderer/components/+namespaces/add-namespace-dialog.tsx b/src/renderer/components/+namespaces/add-namespace-dialog.tsx index e01381efe9..0963c24565 100644 --- a/src/renderer/components/+namespaces/add-namespace-dialog.tsx +++ b/src/renderer/components/+namespaces/add-namespace-dialog.tsx @@ -26,38 +26,35 @@ import { observable, makeObservable } from "mobx"; import { observer } from "mobx-react"; import { Dialog, DialogProps } from "../dialog"; import { Wizard, WizardStep } from "../wizard"; -import { namespaceStore } from "./namespace.store"; import type { Namespace } from "../../../common/k8s-api/endpoints"; import { Input } from "../input"; import { systemName } from "../input/input_validators"; import { Notifications } from "../notifications"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import namespaceStoreInjectable from "./namespace-store/namespace-store.injectable"; +import type { AddNamespaceDialogModel } from "./add-namespace-dialog-model/add-namespace-dialog-model"; +import addNamespaceDialogModelInjectable + from "./add-namespace-dialog-model/add-namespace-dialog-model.injectable"; interface Props extends DialogProps { onSuccess?(ns: Namespace): void; onError?(error: any): void; } -const dialogState = observable.object({ - isOpen: false, -}); +interface Dependencies { + createNamespace: (params: { name: string }) => Promise, + model: AddNamespaceDialogModel +} @observer -export class AddNamespaceDialog extends React.Component { +class NonInjectedAddNamespaceDialog extends React.Component { @observable namespace = ""; - constructor(props: Props) { + constructor(props: Props & Dependencies) { super(props); makeObservable(this); } - static open() { - dialogState.isOpen = true; - } - - static close() { - dialogState.isOpen = false; - } - reset = () => { this.namespace = ""; }; @@ -67,10 +64,10 @@ export class AddNamespaceDialog extends React.Component { const { onSuccess, onError } = this.props; try { - const created = await namespaceStore.create({ name: namespace }); + const created = await this.props.createNamespace({ name: namespace }); onSuccess?.(created); - AddNamespaceDialog.close(); + this.props.close(); } catch (err) { Notifications.error(err); onError?.(err); @@ -78,7 +75,7 @@ export class AddNamespaceDialog extends React.Component { }; render() { - const { ...dialogProps } = this.props; + const { model, createNamespace, ...dialogProps } = this.props; const { namespace } = this; const header =
Create Namespace
; @@ -86,11 +83,11 @@ export class AddNamespaceDialog extends React.Component { - + { ); } } + +export const AddNamespaceDialog = withInjectables( + NonInjectedAddNamespaceDialog, + + { + getProps: (di, props) => ({ + createNamespace: di.inject(namespaceStoreInjectable).create, + model: di.inject(addNamespaceDialogModelInjectable), + + ...props, + }), + }, +); diff --git a/src/renderer/components/+namespaces/namespace-details.tsx b/src/renderer/components/+namespaces/namespace-details.tsx index 68136499ed..c2fb1d8c6a 100644 --- a/src/renderer/components/+namespaces/namespace-details.tsx +++ b/src/renderer/components/+namespaces/namespace-details.tsx @@ -25,7 +25,7 @@ import React from "react"; import { computed, makeObservable, observable, reaction } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; import { DrawerItem } from "../drawer"; -import { boundMethod, cssNames } from "../../utils"; +import { boundMethod, cssNames, Disposer } from "../../utils"; import { getMetricsForNamespace, IPodMetrics, Namespace } from "../../../common/k8s-api/endpoints"; import type { KubeObjectDetailsProps } from "../kube-object-details"; import { Link } from "react-router-dom"; @@ -39,16 +39,24 @@ import { ClusterMetricsResourceType } from "../../../common/cluster-types"; import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; import { getDetailsUrl } from "../kube-detail-params"; import logger from "../../../common/logger"; -import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api"; +import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; +import type { KubeObject } from "../../../common/k8s-api/kube-object"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import kubeWatchApiInjectable + from "../../kube-watch-api/kube-watch-api.injectable"; interface Props extends KubeObjectDetailsProps { } +interface Dependencies { + subscribeStores: (stores: KubeObjectStore[]) => Disposer +} + @observer -export class NamespaceDetails extends React.Component { +class NonInjectedNamespaceDetails extends React.Component { @observable metrics: IPodMetrics = null; - constructor(props: Props) { + constructor(props: Props & Dependencies) { super(props); makeObservable(this); } @@ -58,7 +66,8 @@ export class NamespaceDetails extends React.Component { reaction(() => this.props.object, () => { this.metrics = null; }), - kubeWatchApi.subscribeStores([ + + this.props.subscribeStores([ resourceQuotaStore, limitRangeStore, ]), @@ -138,3 +147,15 @@ export class NamespaceDetails extends React.Component { ); } } + +export const NamespaceDetails = withInjectables( + NonInjectedNamespaceDetails, + + { + getProps: (di, props) => ({ + subscribeStores: di.inject(kubeWatchApiInjectable).subscribeStores, + ...props, + }), + }, +); + diff --git a/src/renderer/components/+namespaces/namespace-select-filter-model/namespace-select-filter-model.injectable.ts b/src/renderer/components/+namespaces/namespace-select-filter-model/namespace-select-filter-model.injectable.ts new file mode 100644 index 0000000000..72411a4dc9 --- /dev/null +++ b/src/renderer/components/+namespaces/namespace-select-filter-model/namespace-select-filter-model.injectable.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { NamespaceSelectFilterModel } from "./namespace-select-filter-model"; +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; + +const NamespaceSelectFilterModelInjectable = getInjectable({ + instantiate: () => new NamespaceSelectFilterModel(), + + lifecycle: lifecycleEnum.singleton, +}); + +export default NamespaceSelectFilterModelInjectable; diff --git a/src/renderer/components/+namespaces/namespace-select-filter-model/namespace-select-filter-model.ts b/src/renderer/components/+namespaces/namespace-select-filter-model/namespace-select-filter-model.ts new file mode 100644 index 0000000000..59cb025bbc --- /dev/null +++ b/src/renderer/components/+namespaces/namespace-select-filter-model/namespace-select-filter-model.ts @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { observable, makeObservable, action } from "mobx"; + +export class NamespaceSelectFilterModel { + constructor() { + makeObservable(this, { + menuIsOpen: observable, + closeMenu: action, + openMenu: action, + toggleMenu: action, + isMultiSelection: observable, + setIsMultiSelection: action, + }); + } + + menuIsOpen = false; + + closeMenu = () => { + this.menuIsOpen = false; + }; + + openMenu = () => { + this.menuIsOpen = true; + }; + + toggleMenu = () => { + this.menuIsOpen = !this.menuIsOpen; + }; + + isMultiSelection = false; + + setIsMultiSelection = (isMultiSelection: boolean) => { + this.isMultiSelection = isMultiSelection; + }; +} diff --git a/src/renderer/components/+namespaces/namespace-select-filter.tsx b/src/renderer/components/+namespaces/namespace-select-filter.tsx index 929530aa97..7859ce4ad5 100644 --- a/src/renderer/components/+namespaces/namespace-select-filter.tsx +++ b/src/renderer/components/+namespaces/namespace-select-filter.tsx @@ -24,41 +24,26 @@ import "./namespace-select-filter.scss"; import React from "react"; import { disposeOnUnmount, observer } from "mobx-react"; import { components, PlaceholderProps } from "react-select"; -import { action, computed, makeObservable, observable, reaction } from "mobx"; +import { action, makeObservable, observable, reaction } from "mobx"; import { Icon } from "../icon"; import { NamespaceSelect } from "./namespace-select"; -import { namespaceStore } from "./namespace.store"; +import type { NamespaceStore } from "./namespace-store/namespace.store"; import type { SelectOption, SelectProps } from "../select"; import { isMac } from "../../../common/vars"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import namespaceStoreInjectable from "./namespace-store/namespace-store.injectable"; +import type { NamespaceSelectFilterModel } from "./namespace-select-filter-model/namespace-select-filter-model"; +import namespaceSelectFilterModelInjectable from "./namespace-select-filter-model/namespace-select-filter-model.injectable"; -const Placeholder = observer((props: PlaceholderProps) => { - const getPlaceholder = (): React.ReactNode => { - const namespaces = namespaceStore.contextNamespaces; - - if (namespaceStore.areAllSelectedImplicitly || !namespaces.length) { - return <>All namespaces; - } - - if (namespaces.length === 1) { - return <>Namespace: {namespaces[0]}; - } - - return <>Namespaces: {namespaces.join(", ")}; - }; - - return ( - - {getPlaceholder()} - - ); -}); +interface Dependencies { + model: NamespaceSelectFilterModel, + namespaceStore: NamespaceStore +} @observer -export class NamespaceSelectFilter extends React.Component { - static isMultiSelection = observable.box(false); - static isMenuOpen = observable.box(false); +class NonInjectedNamespaceSelectFilter extends React.Component { /** * Only updated on every open @@ -66,32 +51,20 @@ export class NamespaceSelectFilter extends React.Component { private selected = observable.set(); private didToggle = false; - constructor(props: SelectProps) { + constructor(props: SelectProps & Dependencies) { super(props); makeObservable(this); } - @computed get isMultiSelection() { - return NamespaceSelectFilter.isMultiSelection.get(); - } - - set isMultiSelection(val: boolean) { - NamespaceSelectFilter.isMultiSelection.set(val); - } - - @computed get isMenuOpen() { - return NamespaceSelectFilter.isMenuOpen.get(); - } - - set isMenuOpen(val: boolean) { - NamespaceSelectFilter.isMenuOpen.set(val); + get model() { + return this.props.model; } componentDidMount() { disposeOnUnmount(this, [ - reaction(() => this.isMenuOpen, newVal => { + reaction(() => this.model.menuIsOpen, newVal => { if (newVal) { // rising edge of selection - this.selected.replace(namespaceStore.selectedNames); + this.selected.replace(this.props.namespaceStore.selectedNames); this.didToggle = false; } }), @@ -100,7 +73,7 @@ export class NamespaceSelectFilter extends React.Component { formatOptionLabel({ value: namespace, label }: SelectOption) { if (namespace) { - const isSelected = namespaceStore.hasContext(namespace); + const isSelected = this.props.namespaceStore.hasContext(namespace); return (
@@ -117,14 +90,14 @@ export class NamespaceSelectFilter extends React.Component { @action onChange = ([{ value: namespace }]: SelectOption[]) => { if (namespace) { - if (this.isMultiSelection) { + if (this.model.isMultiSelection) { this.didToggle = true; - namespaceStore.toggleSingle(namespace); + this.props.namespaceStore.toggleSingle(namespace); } else { - namespaceStore.selectSingle(namespace); + this.props.namespaceStore.selectSingle(namespace); } } else { - namespaceStore.selectAll(); + this.props.namespaceStore.selectAll(); } }; @@ -139,33 +112,33 @@ export class NamespaceSelectFilter extends React.Component { @action onKeyDown = (e: React.KeyboardEvent) => { if (this.isSelectionKey(e)) { - this.isMultiSelection = true; + this.model.setIsMultiSelection(true); } }; @action onKeyUp = (e: React.KeyboardEvent) => { if (this.isSelectionKey(e)) { - this.isMultiSelection = false; + this.model.setIsMultiSelection(false); } - if (!this.isMultiSelection && this.didToggle) { - this.isMenuOpen = false; + if (!this.model.isMultiSelection && this.didToggle) { + this.model.closeMenu(); } }; @action onClick = () => { - if (!this.isMenuOpen) { - this.isMenuOpen = true; - } else if (!this.isMultiSelection) { - this.isMenuOpen = !this.isMenuOpen; + if (!this.model.menuIsOpen) { + this.model.openMenu(); + } else if (!this.model.isMultiSelection) { + this.model.toggleMenu(); } }; reset = () => { - this.isMultiSelection = false; - this.isMenuOpen = false; + this.model.setIsMultiSelection(true); + this.model.closeMenu(); }; render() { @@ -173,7 +146,7 @@ export class NamespaceSelectFilter extends React.Component {
{ ); } } + +export const NamespaceSelectFilter = withInjectables( + NonInjectedNamespaceSelectFilter, + + { + getProps: (di, props) => ({ + model: di.inject(namespaceSelectFilterModelInjectable), + namespaceStore: di.inject(namespaceStoreInjectable), + ...props, + }), + }, +); + +type CustomPlaceholderProps = PlaceholderProps; + +interface PlaceholderDependencies { + namespaceStore: NamespaceStore +} + +const NonInjectedPlaceholder = observer( + ({ namespaceStore, ...props }: CustomPlaceholderProps & PlaceholderDependencies) => { + const getPlaceholder = (): React.ReactNode => { + const namespaces = namespaceStore.contextNamespaces; + + if (namespaceStore.areAllSelectedImplicitly || !namespaces.length) { + return <>All namespaces; + } + + if (namespaces.length === 1) { + return <>Namespace: {namespaces[0]}; + } + + return <>Namespaces: {namespaces.join(", ")}; + }; + + return ( + + {getPlaceholder()} + + ); + }, +); + + +const Placeholder = withInjectables( + NonInjectedPlaceholder, + + { + getProps: (di, props) => ({ + namespaceStore: di.inject(namespaceStoreInjectable), + ...props, + }), + }, +); + diff --git a/src/renderer/components/+namespaces/namespace-select.tsx b/src/renderer/components/+namespaces/namespace-select.tsx index 7974b25f79..a9e0f7b9df 100644 --- a/src/renderer/components/+namespaces/namespace-select.tsx +++ b/src/renderer/components/+namespaces/namespace-select.tsx @@ -27,7 +27,9 @@ import { observer } from "mobx-react"; import { Select, SelectOption, SelectProps } from "../select"; import { cssNames } from "../../utils"; import { Icon } from "../icon"; -import { namespaceStore } from "./namespace.store"; +import type { NamespaceStore } from "./namespace-store/namespace.store"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import namespaceStoreInjectable from "./namespace-store/namespace-store.injectable"; interface Props extends SelectProps { showIcons?: boolean; @@ -40,11 +42,15 @@ const defaultProps: Partial = { showIcons: true, }; +interface Dependencies { + namespaceStore: NamespaceStore +} + @observer -export class NamespaceSelect extends React.Component { +class NonInjectedNamespaceSelect extends React.Component { static defaultProps = defaultProps as object; - constructor(props: Props) { + constructor(props: Props & Dependencies) { super(props); makeObservable(this); } @@ -53,7 +59,7 @@ export class NamespaceSelect extends React.Component { @computed.struct get options(): SelectOption[] { const { customizeOptions, showAllNamespacesOption, sort } = this.props; - let options: SelectOption[] = namespaceStore.items.map(ns => ({ value: ns.getName() })); + let options: SelectOption[] = this.props.namespaceStore.items.map(ns => ({ value: ns.getName() })); if (sort) { options.sort(sort); @@ -97,3 +103,14 @@ export class NamespaceSelect extends React.Component { ); } } + +export const NamespaceSelect = withInjectables( + NonInjectedNamespaceSelect, + + { + getProps: (di, props) => ({ + namespaceStore: di.inject(namespaceStoreInjectable), + ...props, + }), + }, +); diff --git a/src/renderer/components/+namespaces/namespace-store/namespace-store.injectable.ts b/src/renderer/components/+namespaces/namespace-store/namespace-store.injectable.ts new file mode 100644 index 0000000000..0739e195ee --- /dev/null +++ b/src/renderer/components/+namespaces/namespace-store/namespace-store.injectable.ts @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { NamespaceStore } from "./namespace.store"; +import apiManagerInjectable from "../../kube-object-menu/dependencies/api-manager.injectable"; +import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable"; + +const namespaceStoreInjectable = getInjectable({ + instantiate: (di) => { + const createStorage = di.inject(createStorageInjectable); + + const storage = createStorage( + "selected_namespaces", + undefined, + ); + + const namespaceStore = new NamespaceStore({ + storage, + }); + + const apiManager = di.inject(apiManagerInjectable); + + apiManager.registerStore(namespaceStore); + + return namespaceStore; + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default namespaceStoreInjectable; diff --git a/src/renderer/components/+namespaces/namespace.store.ts b/src/renderer/components/+namespaces/namespace-store/namespace.store.ts similarity index 87% rename from src/renderer/components/+namespaces/namespace.store.ts rename to src/renderer/components/+namespaces/namespace-store/namespace.store.ts index 34e4348488..920dfb25c2 100644 --- a/src/renderer/components/+namespaces/namespace.store.ts +++ b/src/renderer/components/+namespaces/namespace-store/namespace.store.ts @@ -20,16 +20,18 @@ */ import { action, comparer, computed, IReactionDisposer, makeObservable, reaction } from "mobx"; -import { autoBind, createStorage, noop, ToggleSet } from "../../utils"; -import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../../common/k8s-api/kube-object.store"; -import { Namespace, namespacesApi } from "../../../common/k8s-api/endpoints/namespaces.api"; -import { apiManager } from "../../../common/k8s-api/api-manager"; +import { autoBind, noop, StorageHelper, ToggleSet } from "../../../utils"; +import { KubeObjectStore, KubeObjectStoreLoadingParams } from "../../../../common/k8s-api/kube-object.store"; +import { Namespace, namespacesApi } from "../../../../common/k8s-api/endpoints/namespaces.api"; + +interface Dependencies { + storage: StorageHelper +} export class NamespaceStore extends KubeObjectStore { api = namespacesApi; - private storage = createStorage("selected_namespaces", undefined); - constructor() { + constructor(private dependencies: Dependencies) { super(); makeObservable(this); autoBind(this); @@ -39,7 +41,7 @@ export class NamespaceStore extends KubeObjectStore { private async init() { await this.contextReady; - await this.storage.whenReady; + await this.dependencies.storage.whenReady; this.selectNamespaces(this.initialNamespaces); this.autoLoadAllowedNamespaces(); @@ -61,7 +63,7 @@ export class NamespaceStore extends KubeObjectStore { private get initialNamespaces(): string[] { const { allowedNamespaces } = this; - const selectedNamespaces = this.storage.get(); // raw namespaces, undefined on first load + const selectedNamespaces = this.dependencies.storage.get(); // raw namespaces, undefined on first load // return previously saved namespaces from local-storage (if any) if (Array.isArray(selectedNamespaces)) { @@ -83,7 +85,7 @@ export class NamespaceStore extends KubeObjectStore { * The current value (list of namespaces names) in the storage layer */ @computed private get selectedNamespaces(): string[] { - return this.storage.get() ?? []; + return this.dependencies.storage.get() ?? []; } @computed get allowedNamespaces(): string[] { @@ -147,22 +149,21 @@ export class NamespaceStore extends KubeObjectStore { return namespaces; } - @action - selectNamespaces(namespace: string | string[]) { + @action selectNamespaces = (namespace: string | string[]) => { const namespaces = Array.from(new Set([namespace].flat())); - this.storage.set(namespaces); - } + this.dependencies.storage.set(namespaces); + }; @action clearSelected(namespaces?: string | string[]) { if (namespaces) { const resettingNamespaces = [namespaces].flat(); - const newNamespaces = this.storage.get().filter(ns => !resettingNamespaces.includes(ns)); + const newNamespaces = this.dependencies.storage.get().filter(ns => !resettingNamespaces.includes(ns)); - this.storage.set(newNamespaces); + this.dependencies.storage.set(newNamespaces); } else { - this.storage.reset(); + this.dependencies.storage.reset(); } } @@ -196,7 +197,7 @@ export class NamespaceStore extends KubeObjectStore { nextState.toggle(namespace); } - this.storage.set([...nextState]); + this.dependencies.storage.set([...nextState]); } /** @@ -209,14 +210,14 @@ export class NamespaceStore extends KubeObjectStore { const nextState = new ToggleSet(this.contextNamespaces); nextState.toggle(namespace); - this.storage.set([...nextState]); + this.dependencies.storage.set([...nextState]); } /** * Makes the given namespace the sole selected namespace */ selectSingle(namespace: string) { - this.storage.set([namespace]); + this.dependencies.storage.set([namespace]); } /** @@ -247,9 +248,6 @@ export class NamespaceStore extends KubeObjectStore { } } -export const namespaceStore = new NamespaceStore(); -apiManager.registerStore(namespaceStore); - export function getDummyNamespace(name: string) { return new Namespace({ kind: Namespace.kind, diff --git a/src/renderer/components/+namespaces/namespaces.tsx b/src/renderer/components/+namespaces/namespaces.tsx index dd2bf9e476..fc84e2bec4 100644 --- a/src/renderer/components/+namespaces/namespaces.tsx +++ b/src/renderer/components/+namespaces/namespaces.tsx @@ -28,9 +28,13 @@ import { TabLayout } from "../layout/tab-layout"; import { Badge } from "../badge"; import type { RouteComponentProps } from "react-router"; import { KubeObjectListLayout } from "../kube-object-list-layout"; -import { namespaceStore } from "./namespace.store"; +import type { NamespaceStore } from "./namespace-store/namespace.store"; import { KubeObjectStatusIcon } from "../kube-object-status-icon"; import type { NamespacesRouteParams } from "../../../common/routes"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import namespaceStoreInjectable from "./namespace-store/namespace-store.injectable"; +import addNamespaceDialogModelInjectable + from "./add-namespace-dialog-model/add-namespace-dialog-model.injectable"; enum columnId { name = "name", @@ -42,7 +46,12 @@ enum columnId { interface Props extends RouteComponentProps { } -export class Namespaces extends React.Component { +interface Dependencies { + namespaceStore: NamespaceStore + openAddNamespaceDialog: () => void +} + +class NonInjectedNamespaces extends React.Component { render() { return ( @@ -50,7 +59,7 @@ export class Namespaces extends React.Component { isConfigurable tableId="namespaces" className="Namespaces" - store={namespaceStore} + store={this.props.namespaceStore} sortingCallbacks={{ [columnId.name]: ns => ns.getName(), [columnId.labels]: ns => ns.getLabels(), @@ -78,7 +87,7 @@ export class Namespaces extends React.Component { ]} addRemoveButtons={{ addTooltip: "Add Namespace", - onAdd: () => AddNamespaceDialog.open(), + onAdd: () => this.props.openAddNamespaceDialog(), }} customizeTableRowProps={item => ({ disabled: item.getStatus() === NamespaceStatus.TERMINATING, @@ -89,3 +98,15 @@ export class Namespaces extends React.Component { ); } } + +export const Namespaces = withInjectables( + NonInjectedNamespaces, + + { + getProps: (di, props) => ({ + namespaceStore: di.inject(namespaceStoreInjectable), + openAddNamespaceDialog: di.inject(addNamespaceDialogModelInjectable).open, + ...props, + }), + }, +); diff --git a/src/renderer/components/+network-port-forwards/port-forward-menu.tsx b/src/renderer/components/+network-port-forwards/port-forward-menu.tsx index 5b49c3f85c..fc24dce416 100644 --- a/src/renderer/components/+network-port-forwards/port-forward-menu.tsx +++ b/src/renderer/components/+network-port-forwards/port-forward-menu.tsx @@ -21,25 +21,34 @@ import React from "react"; import { boundMethod, cssNames } from "../../utils"; -import { openPortForward, PortForwardItem, removePortForward } from "../../port-forward"; +import { openPortForward, PortForwardItem } from "../../port-forward"; import { MenuActions, MenuActionsProps } from "../menu/menu-actions"; import { MenuItem } from "../menu"; import { Icon } from "../icon"; -import { PortForwardDialog } from "../../port-forward"; import { Notifications } from "../notifications"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import removePortForwardInjectable + from "../../port-forward/port-forward-store/remove-port-forward/remove-port-forward.injectable"; +import portForwardDialogModelInjectable + from "../../port-forward/port-forward-dialog-model/port-forward-dialog-model.injectable"; interface Props extends MenuActionsProps { portForward: PortForwardItem; hideDetails?(): void; } -export class PortForwardMenu extends React.Component { +interface Dependencies { + removePortForward: (item: PortForwardItem) => Promise, + openPortForwardDialog: (item: PortForwardItem) => void +} + +class NonInjectedPortForwardMenu extends React.Component { @boundMethod remove() { const { portForward } = this.props; try { - removePortForward(portForward); + this.props.removePortForward(portForward); } catch (error) { Notifications.error(`Error occurred stopping the port-forward from port ${portForward.forwardPort}. The port-forward may still be active.`); } @@ -56,7 +65,7 @@ export class PortForwardMenu extends React.Component { Open - PortForwardDialog.open(portForward)}> + this.props.openPortForwardDialog(portForward)}> Edit @@ -78,3 +87,15 @@ export class PortForwardMenu extends React.Component { ); } } + +export const PortForwardMenu = withInjectables( + NonInjectedPortForwardMenu, + + { + getProps: (di, props) => ({ + removePortForward: di.inject(removePortForwardInjectable), + openPortForwardDialog: di.inject(portForwardDialogModelInjectable).open, + ...props, + }), + }, +); diff --git a/src/renderer/components/+network-port-forwards/port-forwards.tsx b/src/renderer/components/+network-port-forwards/port-forwards.tsx index a76588619d..8adb5af1b3 100644 --- a/src/renderer/components/+network-port-forwards/port-forwards.tsx +++ b/src/renderer/components/+network-port-forwards/port-forwards.tsx @@ -25,11 +25,13 @@ import React from "react"; import { disposeOnUnmount, observer } from "mobx-react"; import type { RouteComponentProps } from "react-router-dom"; import { ItemListLayout } from "../item-object-list/item-list-layout"; -import { PortForwardItem, portForwardStore } from "../../port-forward"; +import type { PortForwardItem, PortForwardStore } from "../../port-forward"; import { PortForwardMenu } from "./port-forward-menu"; import { PortForwardsRouteParams, portForwardsURL } from "../../../common/routes"; import { PortForwardDetails } from "./port-forward-details"; import { navigation } from "../../navigation"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import portForwardStoreInjectable from "../../port-forward/port-forward-store/port-forward-store.injectable"; enum columnId { name = "name", @@ -44,19 +46,23 @@ enum columnId { interface Props extends RouteComponentProps { } +interface Dependencies { + portForwardStore: PortForwardStore +} + @observer -export class PortForwards extends React.Component { +class NonInjectedPortForwards extends React.Component { componentDidMount() { disposeOnUnmount(this, [ - portForwardStore.watch(), + this.props.portForwardStore.watch(), ]); } get selectedPortForward() { const { match: { params: { forwardport }}} = this.props; - return portForwardStore.getById(forwardport); + return this.props.portForwardStore.getById(forwardport); } onDetails = (item: PortForwardItem) => { @@ -96,7 +102,7 @@ export class PortForwards extends React.Component { item.getName(), [columnId.namespace]: item => item.getNs(), @@ -150,3 +156,15 @@ export class PortForwards extends React.Component { ); } } + +export const PortForwards = withInjectables( + NonInjectedPortForwards, + + { + getProps: (di, props) => ({ + portForwardStore: di.inject(portForwardStoreInjectable), + ...props, + }), + }, +); + diff --git a/src/renderer/components/+network-services/service-details.tsx b/src/renderer/components/+network-services/service-details.tsx index 36dc9dee8a..2328d9d2b8 100644 --- a/src/renderer/components/+network-services/service-details.tsx +++ b/src/renderer/components/+network-services/service-details.tsx @@ -31,25 +31,37 @@ import { KubeObjectMeta } from "../kube-object-meta"; import { ServicePortComponent } from "./service-port-component"; import { endpointStore } from "../+network-endpoints/endpoints.store"; import { ServiceDetailsEndpoint } from "./service-details-endpoint"; -import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api"; -import { portForwardStore } from "../../port-forward"; +import type { PortForwardStore } from "../../port-forward"; import logger from "../../../common/logger"; +import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; +import type { KubeObject } from "../../../common/k8s-api/kube-object"; +import type { Disposer } from "../../../common/utils"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import kubeWatchApiInjectable + from "../../kube-watch-api/kube-watch-api.injectable"; +import portForwardStoreInjectable from "../../port-forward/port-forward-store/port-forward-store.injectable"; +import type { KubeWatchSubscribeStoreOptions } from "../../kube-watch-api/kube-watch-api"; interface Props extends KubeObjectDetailsProps { } +interface Dependencies { + subscribeStores: (stores: KubeObjectStore[], options: KubeWatchSubscribeStoreOptions) => Disposer + portForwardStore: PortForwardStore +} + @observer -export class ServiceDetails extends React.Component { +class NonInjectedServiceDetails extends React.Component { componentDidMount() { const { object: service } = this.props; disposeOnUnmount(this, [ - kubeWatchApi.subscribeStores([ + this.props.subscribeStores([ endpointStore, ], { namespaces: [service.getNs()], }), - portForwardStore.watch(), + this.props.portForwardStore.watch(), ]); } @@ -140,3 +152,15 @@ export class ServiceDetails extends React.Component { ); } } + +export const ServiceDetails = withInjectables( + NonInjectedServiceDetails, + + { + getProps: (di, props) => ({ + subscribeStores: di.inject(kubeWatchApiInjectable).subscribeStores, + portForwardStore: di.inject(portForwardStoreInjectable), + ...props, + }), + }, +); diff --git a/src/renderer/components/+network-services/service-port-component.tsx b/src/renderer/components/+network-services/service-port-component.tsx index 95355d0f0d..e3caf33dea 100644 --- a/src/renderer/components/+network-services/service-port-component.tsx +++ b/src/renderer/components/+network-services/service-port-component.tsx @@ -28,22 +28,36 @@ import { observable, makeObservable, reaction } from "mobx"; import { cssNames } from "../../utils"; import { Notifications } from "../notifications"; import { Button } from "../button"; -import { aboutPortForwarding, addPortForward, getPortForward, getPortForwards, openPortForward, PortForwardDialog, portForwardStore, predictProtocol, removePortForward } from "../../port-forward"; +import { aboutPortForwarding, getPortForward, getPortForwards, openPortForward, PortForwardStore, predictProtocol } from "../../port-forward"; import type { ForwardedPort } from "../../port-forward"; import { Spinner } from "../spinner"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import portForwardStoreInjectable from "../../port-forward/port-forward-store/port-forward-store.injectable"; +import removePortForwardInjectable from "../../port-forward/port-forward-store/remove-port-forward/remove-port-forward.injectable"; +import portForwardDialogModelInjectable + from "../../port-forward/port-forward-dialog-model/port-forward-dialog-model.injectable"; +import addPortForwardInjectable + from "../../port-forward/port-forward-store/add-port-forward/add-port-forward.injectable"; interface Props { service: Service; port: ServicePort; } +interface Dependencies { + portForwardStore: PortForwardStore + removePortForward: (item: ForwardedPort) => Promise + addPortForward: (item: ForwardedPort) => Promise + openPortForwardDialog: (item: ForwardedPort, options: { openInBrowser: boolean }) => void +} + @observer -export class ServicePortComponent extends React.Component { +class NonInjectedServicePortComponent extends React.Component { @observable waiting = false; @observable forwardPort = 0; @observable isPortForwarded = false; - constructor(props: Props) { + constructor(props: Props & Dependencies) { super(props); makeObservable(this); this.checkExistingPortForwarding(); @@ -51,7 +65,7 @@ export class ServicePortComponent extends React.Component { componentDidMount() { disposeOnUnmount(this, [ - reaction(() => [portForwardStore.portForwards, this.props.service], () => this.checkExistingPortForwarding()), + reaction(() => [this.props.portForwardStore.portForwards, this.props.service], () => this.checkExistingPortForwarding()), ]); } @@ -96,7 +110,7 @@ export class ServicePortComponent extends React.Component { // determine how many port-forwards are already active const { length } = await getPortForwards(); - this.forwardPort = await addPortForward(portForward); + this.forwardPort = await this.props.addPortForward(portForward); if (this.forwardPort) { portForward.forwardPort = this.forwardPort; @@ -129,7 +143,7 @@ export class ServicePortComponent extends React.Component { this.waiting = true; try { - await removePortForward(portForward); + await this.props.removePortForward(portForward); this.isPortForwarded = false; } catch (error) { Notifications.error(`Error occurred stopping the port-forward from port ${portForward.forwardPort}.`); @@ -155,7 +169,7 @@ export class ServicePortComponent extends React.Component { protocol: predictProtocol(port.name), }; - PortForwardDialog.open(portForward, { openInBrowser: true }); + this.props.openPortForwardDialog(portForward, { openInBrowser: true }); } }; @@ -172,3 +186,18 @@ export class ServicePortComponent extends React.Component { ); } } + +export const ServicePortComponent = withInjectables( + NonInjectedServicePortComponent, + + { + getProps: (di, props) => ({ + portForwardStore: di.inject(portForwardStoreInjectable), + removePortForward: di.inject(removePortForwardInjectable), + addPortForward: di.inject(addPortForwardInjectable), + openPortForwardDialog: di.inject(portForwardDialogModelInjectable).open, + ...props, + }), + }, +); + diff --git a/src/renderer/components/+nodes/node-details.tsx b/src/renderer/components/+nodes/node-details.tsx index c576397903..dac1e252c4 100644 --- a/src/renderer/components/+nodes/node-details.tsx +++ b/src/renderer/components/+nodes/node-details.tsx @@ -39,18 +39,26 @@ import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; import { ClusterMetricsResourceType } from "../../../common/cluster-types"; import { NodeDetailsResources } from "./node-details-resources"; import { DrawerTitle } from "../drawer/drawer-title"; -import { boundMethod } from "../../utils"; +import { boundMethod, Disposer } from "../../utils"; import logger from "../../../common/logger"; -import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api"; +import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; +import type { KubeObject } from "../../../common/k8s-api/kube-object"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import kubeWatchApiInjectable + from "../../kube-watch-api/kube-watch-api.injectable"; interface Props extends KubeObjectDetailsProps { } +interface Dependencies { + subscribeStores: (stores: KubeObjectStore[]) => Disposer +} + @observer -export class NodeDetails extends React.Component { +class NonInjectedNodeDetails extends React.Component { @observable metrics: Partial; - constructor(props: Props) { + constructor(props: Props & Dependencies) { super(props); makeObservable(this); } @@ -60,7 +68,8 @@ export class NodeDetails extends React.Component { reaction(() => this.props.object.getName(), () => { this.metrics = null; }), - kubeWatchApi.subscribeStores([ + + this.props.subscribeStores([ podsStore, ]), ]); @@ -190,3 +199,15 @@ export class NodeDetails extends React.Component { ); } } + +export const NodeDetails = withInjectables( + NonInjectedNodeDetails, + + { + getProps: (di, props) => ({ + subscribeStores: di.inject(kubeWatchApiInjectable).subscribeStores, + ...props, + }), + }, +); + diff --git a/src/renderer/components/+preferences/kubectl-binaries.tsx b/src/renderer/components/+preferences/kubectl-binaries.tsx index bffc69e3c0..d4480b599b 100644 --- a/src/renderer/components/+preferences/kubectl-binaries.tsx +++ b/src/renderer/components/+preferences/kubectl-binaries.tsx @@ -22,14 +22,20 @@ import React, { useState } from "react"; import { Input, InputValidators } from "../input"; import { SubTitle } from "../layout/sub-title"; -import { getDefaultKubectlDownloadPath, UserStore } from "../../../common/user-store"; -import { observer } from "mobx-react"; -import { bundledKubectlPath } from "../../../main/kubectl"; +import { UserStore } from "../../../common/user-store"; +import { bundledKubectlPath } from "../../../main/kubectl/kubectl"; import { SelectOption, Select } from "../select"; import { FormSwitch, Switcher } from "../switch"; import { packageMirrors } from "../../../common/user-store/preferences-helpers"; +import directoryForBinariesInjectable + from "../../../common/app-paths/directory-for-binaries/directory-for-binaries.injectable"; +import { withInjectables } from "@ogre-tools/injectable-react"; -export const KubectlBinaries = observer(() => { +interface Dependencies { + defaultPathForKubectlBinaries: string +} + +const NonInjectedKubectlBinaries: React.FC = (({ defaultPathForKubectlBinaries }) => { const userStore = UserStore.getInstance(); const [downloadPath, setDownloadPath] = useState(userStore.downloadBinariesPath || ""); const [binariesPath, setBinariesPath] = useState(userStore.kubectlBinariesPath || ""); @@ -78,7 +84,7 @@ export const KubectlBinaries = observer(() => { { ); }); + +export const KubectlBinaries = withInjectables(NonInjectedKubectlBinaries, { + getProps: (di) => ({ + defaultPathForKubectlBinaries: di.inject(directoryForBinariesInjectable), + }), +}); diff --git a/src/renderer/components/+storage-classes/storage-class-details.tsx b/src/renderer/components/+storage-classes/storage-class-details.tsx index 30e0df6a6b..acd50f1e74 100644 --- a/src/renderer/components/+storage-classes/storage-class-details.tsx +++ b/src/renderer/components/+storage-classes/storage-class-details.tsx @@ -33,16 +33,25 @@ import { storageClassStore } from "./storage-class.store"; import { VolumeDetailsList } from "../+storage-volumes/volume-details-list"; import { volumesStore } from "../+storage-volumes/volumes.store"; import logger from "../../../common/logger"; -import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api"; +import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; +import type { KubeObject } from "../../../common/k8s-api/kube-object"; +import type { Disposer } from "../../../common/utils"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import kubeWatchApiInjectable + from "../../kube-watch-api/kube-watch-api.injectable"; interface Props extends KubeObjectDetailsProps { } +interface Dependencies { + subscribeStores: (stores: KubeObjectStore[]) => Disposer +} + @observer -export class StorageClassDetails extends React.Component { +class NonInjectedStorageClassDetails extends React.Component { componentDidMount() { disposeOnUnmount(this, [ - kubeWatchApi.subscribeStores([ + this.props.subscribeStores([ volumesStore, ]), ]); @@ -102,3 +111,15 @@ export class StorageClassDetails extends React.Component { ); } } + +export const StorageClassDetails = withInjectables( + NonInjectedStorageClassDetails, + + { + getProps: (di, props) => ({ + subscribeStores: di.inject(kubeWatchApiInjectable).subscribeStores, + ...props, + }), + }, +); + diff --git a/src/renderer/components/+user-management/+cluster-role-bindings/__tests__/dialog.test.tsx b/src/renderer/components/+user-management/+cluster-role-bindings/__tests__/dialog.test.tsx index 132f338685..f128fd21ba 100644 --- a/src/renderer/components/+user-management/+cluster-role-bindings/__tests__/dialog.test.tsx +++ b/src/renderer/components/+user-management/+cluster-role-bindings/__tests__/dialog.test.tsx @@ -19,17 +19,26 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { render } from "@testing-library/react"; import React from "react"; import { ClusterRoleBindingDialog } from "../dialog"; import { clusterRolesStore } from "../../+cluster-roles/store"; import { ClusterRole } from "../../../../../common/k8s-api/endpoints"; import userEvent from "@testing-library/user-event"; +import { getDiForUnitTesting } from "../../../getDiForUnitTesting"; +import { DiRender, renderFor } from "../../../test-utils/renderFor"; jest.mock("../../+cluster-roles/store"); describe("ClusterRoleBindingDialog tests", () => { - beforeEach(() => { + let render: DiRender; + + beforeEach(async () => { + const di = getDiForUnitTesting({ doGeneralOverrides: true }); + + await di.runSetups(); + + render = renderFor(di); + (clusterRolesStore as any).items = [new ClusterRole({ apiVersion: "rbac.authorization.k8s.io/v1", kind: "ClusterRole", diff --git a/src/renderer/components/+user-management/+role-bindings/__tests__/dialog.test.tsx b/src/renderer/components/+user-management/+role-bindings/__tests__/dialog.test.tsx index 5b66d7a8c7..766aa1104f 100644 --- a/src/renderer/components/+user-management/+role-bindings/__tests__/dialog.test.tsx +++ b/src/renderer/components/+user-management/+role-bindings/__tests__/dialog.test.tsx @@ -19,17 +19,31 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { render } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import React from "react"; import { clusterRolesStore } from "../../+cluster-roles/store"; import { ClusterRole } from "../../../../../common/k8s-api/endpoints"; import { RoleBindingDialog } from "../dialog"; +import { getDiForUnitTesting } from "../../../getDiForUnitTesting"; +import type { DiRender } from "../../../test-utils/renderFor"; +import { renderFor } from "../../../test-utils/renderFor"; +import directoryForUserDataInjectable + from "../../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; jest.mock("../../+cluster-roles/store"); describe("RoleBindingDialog tests", () => { - beforeEach(() => { + let render: DiRender; + + beforeEach(async () => { + const di = getDiForUnitTesting({ doGeneralOverrides: true }); + + di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); + + await di.runSetups(); + + render = renderFor(di); + (clusterRolesStore as any).items = [new ClusterRole({ apiVersion: "rbac.authorization.k8s.io/v1", kind: "ClusterRole", diff --git a/src/renderer/components/+workloads-cronjobs/cronjob-details.tsx b/src/renderer/components/+workloads-cronjobs/cronjob-details.tsx index a4ba0cbeea..58a91032df 100644 --- a/src/renderer/components/+workloads-cronjobs/cronjob-details.tsx +++ b/src/renderer/components/+workloads-cronjobs/cronjob-details.tsx @@ -34,16 +34,25 @@ import { getDetailsUrl } from "../kube-detail-params"; import { CronJob, Job } from "../../../common/k8s-api/endpoints"; import { KubeObjectMeta } from "../kube-object-meta"; import logger from "../../../common/logger"; -import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api"; +import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; +import type { KubeObject } from "../../../common/k8s-api/kube-object"; +import type { Disposer } from "../../../common/utils"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import kubeWatchApiInjectable + from "../../kube-watch-api/kube-watch-api.injectable"; interface Props extends KubeObjectDetailsProps { } +interface Dependencies { + subscribeStores: (stores: KubeObjectStore[]) => Disposer +} + @observer -export class CronJobDetails extends React.Component { +class NonInjectedCronJobDetails extends React.Component { componentDidMount() { disposeOnUnmount(this, [ - kubeWatchApi.subscribeStores([ + this.props.subscribeStores([ jobStore, ]), ]); @@ -119,3 +128,14 @@ export class CronJobDetails extends React.Component { ); } } + +export const CronJobDetails = withInjectables( + NonInjectedCronJobDetails, + + { + getProps: (di, props) => ({ + subscribeStores: di.inject(kubeWatchApiInjectable).subscribeStores, + ...props, + }), + }, +); diff --git a/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx b/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx index 3e431986c3..c7db2ebacb 100644 --- a/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx +++ b/src/renderer/components/+workloads-daemonsets/daemonset-details.tsx @@ -39,18 +39,26 @@ import { PodDetailsList } from "../+workloads-pods/pod-details-list"; import { KubeObjectMeta } from "../kube-object-meta"; import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; import { ClusterMetricsResourceType } from "../../../common/cluster-types"; -import { boundMethod } from "../../utils"; +import { boundMethod, Disposer } from "../../utils"; import logger from "../../../common/logger"; -import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api"; +import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; +import type { KubeObject } from "../../../common/k8s-api/kube-object"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import kubeWatchApiInjectable + from "../../kube-watch-api/kube-watch-api.injectable"; interface Props extends KubeObjectDetailsProps { } +interface Dependencies { + subscribeStores: (stores: KubeObjectStore[]) => Disposer +} + @observer -export class DaemonSetDetails extends React.Component { +class NonInjectedDaemonSetDetails extends React.Component { @observable metrics: IPodMetrics = null; - constructor(props: Props) { + constructor(props: Props & Dependencies) { super(props); makeObservable(this); } @@ -60,7 +68,7 @@ export class DaemonSetDetails extends React.Component { reaction(() => this.props.object, () => { this.metrics = null; }), - kubeWatchApi.subscribeStores([ + this.props.subscribeStores([ podsStore, ]), ]); @@ -139,3 +147,14 @@ export class DaemonSetDetails extends React.Component { ); } } + +export const DaemonSetDetails = withInjectables( + NonInjectedDaemonSetDetails, + + { + getProps: (di, props) => ({ + subscribeStores: di.inject(kubeWatchApiInjectable).subscribeStores, + ...props, + }), + }, +); diff --git a/src/renderer/components/+workloads-deployments/deployment-details.tsx b/src/renderer/components/+workloads-deployments/deployment-details.tsx index f0ae0c1d6e..d725a95232 100644 --- a/src/renderer/components/+workloads-deployments/deployment-details.tsx +++ b/src/renderer/components/+workloads-deployments/deployment-details.tsx @@ -41,18 +41,26 @@ import { replicaSetStore } from "../+workloads-replicasets/replicasets.store"; import { DeploymentReplicaSets } from "./deployment-replicasets"; import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; import { ClusterMetricsResourceType } from "../../../common/cluster-types"; -import { boundMethod } from "../../utils"; +import { boundMethod, Disposer } from "../../utils"; import logger from "../../../common/logger"; -import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api"; +import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; +import type { KubeObject } from "../../../common/k8s-api/kube-object"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import kubeWatchApiInjectable + from "../../kube-watch-api/kube-watch-api.injectable"; interface Props extends KubeObjectDetailsProps { } +interface Dependencies { + subscribeStores: (stores: KubeObjectStore[]) => Disposer +} + @observer -export class DeploymentDetails extends React.Component { +class NonInjectedDeploymentDetails extends React.Component { @observable metrics: IPodMetrics = null; - constructor(props: Props) { + constructor(props: Props & Dependencies) { super(props); makeObservable(this); } @@ -62,7 +70,8 @@ export class DeploymentDetails extends React.Component { reaction(() => this.props.object, () => { this.metrics = null; }), - kubeWatchApi.subscribeStores([ + + this.props.subscribeStores([ podsStore, replicaSetStore, ]), @@ -162,3 +171,15 @@ export class DeploymentDetails extends React.Component { ); } } + +export const DeploymentDetails = withInjectables( + NonInjectedDeploymentDetails, + + { + getProps: (di, props) => ({ + subscribeStores: di.inject(kubeWatchApiInjectable).subscribeStores, + ...props, + }), + }, +); + diff --git a/src/renderer/components/+workloads-jobs/job-details.tsx b/src/renderer/components/+workloads-jobs/job-details.tsx index a0445874d8..103a21ab44 100644 --- a/src/renderer/components/+workloads-jobs/job-details.tsx +++ b/src/renderer/components/+workloads-jobs/job-details.tsx @@ -45,16 +45,25 @@ import { boundMethod } from "autobind-decorator"; import { getDetailsUrl } from "../kube-detail-params"; import { apiManager } from "../../../common/k8s-api/api-manager"; import logger from "../../../common/logger"; -import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api"; +import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; +import type { KubeObject } from "../../../common/k8s-api/kube-object"; +import type { Disposer } from "../../../common/utils"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import kubeWatchApiInjectable + from "../../kube-watch-api/kube-watch-api.injectable"; interface Props extends KubeObjectDetailsProps { } +interface Dependencies { + subscribeStores: (stores: KubeObjectStore[]) => Disposer +} + @observer -export class JobDetails extends React.Component { +class NonInjectedJobDetails extends React.Component { @observable metrics: IPodMetrics = null; - constructor(props: Props) { + constructor(props: Props & Dependencies) { super(props); makeObservable(this); } @@ -64,7 +73,7 @@ export class JobDetails extends React.Component { reaction(() => this.props.object, () => { this.metrics = null; }), - kubeWatchApi.subscribeStores([ + this.props.subscribeStores([ podsStore, ]), ]); @@ -171,3 +180,15 @@ export class JobDetails extends React.Component { ); } } + +export const JobDetails = withInjectables( + NonInjectedJobDetails, + + { + getProps: (di, props) => ({ + subscribeStores: di.inject(kubeWatchApiInjectable).subscribeStores, + ...props, + }), + }, +); + diff --git a/src/renderer/components/+workloads-overview/overview-statuses.tsx b/src/renderer/components/+workloads-overview/overview-statuses.tsx index e48c4ff8b8..5ba0449173 100644 --- a/src/renderer/components/+workloads-overview/overview-statuses.tsx +++ b/src/renderer/components/+workloads-overview/overview-statuses.tsx @@ -26,12 +26,14 @@ import { observer } from "mobx-react"; import { OverviewWorkloadStatus } from "./overview-workload-status"; import { Link } from "react-router-dom"; import { workloadStores } from "../+workloads"; -import { namespaceStore } from "../+namespaces/namespace.store"; +import type { NamespaceStore } from "../+namespaces/namespace-store/namespace.store"; import type { KubeResource } from "../../../common/rbac"; import { ResourceNames } from "../../utils/rbac"; import { boundMethod } from "../../utils"; import { workloadURL } from "../../../common/routes"; import { isAllowedResource } from "../../../common/utils/allowed-resource"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import namespaceStoreInjectable from "../+namespaces/namespace-store/namespace-store.injectable"; const resources: KubeResource[] = [ "pods", @@ -43,8 +45,12 @@ const resources: KubeResource[] = [ "cronjobs", ]; +interface Dependencies { + namespaceStore: NamespaceStore +} + @observer -export class OverviewStatuses extends React.Component { +class NonInjectedOverviewStatuses extends React.Component { @boundMethod renderWorkload(resource: KubeResource): React.ReactElement { const store = workloadStores.get(resource); @@ -53,7 +59,7 @@ export class OverviewStatuses extends React.Component { return null; } - const items = store.getAllByNs(namespaceStore.contextNamespaces); + const items = store.getAllByNs(this.props.namespaceStore.contextNamespaces); return (
@@ -79,3 +85,13 @@ export class OverviewStatuses extends React.Component { ); } } + +export const OverviewStatuses = withInjectables( + NonInjectedOverviewStatuses, + + { + getProps: (di) => ({ + namespaceStore: di.inject(namespaceStoreInjectable), + }), + }, +); diff --git a/src/renderer/components/+workloads-overview/overview.tsx b/src/renderer/components/+workloads-overview/overview.tsx index fa7ad590b0..635444254e 100644 --- a/src/renderer/components/+workloads-overview/overview.tsx +++ b/src/renderer/components/+workloads-overview/overview.tsx @@ -32,32 +32,41 @@ import { statefulSetStore } from "../+workloads-statefulsets/statefulset.store"; import { replicaSetStore } from "../+workloads-replicasets/replicasets.store"; import { jobStore } from "../+workloads-jobs/job.store"; import { cronJobStore } from "../+workloads-cronjobs/cronjob.store"; -import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api"; import { WorkloadsOverviewDetailRegistry } from "../../../extensions/registries"; import type { WorkloadsOverviewRouteParams } from "../../../common/routes"; import { makeObservable, observable, reaction } from "mobx"; import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter"; import { Icon } from "../icon"; import { TooltipPosition } from "../tooltip"; -import type { ClusterContext } from "../../../common/k8s-api/cluster-context"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import clusterFrameContextInjectable from "../../cluster-frame-context/cluster-frame-context.injectable"; +import type { ClusterFrameContext } from "../../cluster-frame-context/cluster-frame-context"; +import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; +import type { KubeObject } from "../../../common/k8s-api/kube-object"; +import type { Disposer } from "../../../common/utils"; +import kubeWatchApiInjectable from "../../kube-watch-api/kube-watch-api.injectable"; +import type { KubeWatchSubscribeStoreOptions } from "../../kube-watch-api/kube-watch-api"; interface Props extends RouteComponentProps { } -@observer -export class WorkloadsOverview extends React.Component { - static clusterContext: ClusterContext; +interface Dependencies { + clusterFrameContext: ClusterFrameContext + subscribeStores: (stores: KubeObjectStore[], options: KubeWatchSubscribeStoreOptions) => Disposer +} +@observer +class NonInjectedWorkloadsOverview extends React.Component { @observable loadErrors: string[] = []; - constructor(props: Props) { + constructor(props: Props & Dependencies) { super(props); makeObservable(this); } componentDidMount() { disposeOnUnmount(this, [ - kubeWatchApi.subscribeStores([ + this.props.subscribeStores([ cronJobStore, daemonSetStore, deploymentStore, @@ -69,7 +78,7 @@ export class WorkloadsOverview extends React.Component { ], { onLoadFailure: error => this.loadErrors.push(String(error)), }), - reaction(() => WorkloadsOverview.clusterContext.contextNamespaces.slice(), () => { + reaction(() => this.props.clusterFrameContext.contextNamespaces.slice(), () => { // clear load errors this.loadErrors.length = 0; }), @@ -117,3 +126,15 @@ export class WorkloadsOverview extends React.Component { ); } } + +export const WorkloadsOverview = withInjectables( + NonInjectedWorkloadsOverview, + + { + getProps: (di, props) => ({ + clusterFrameContext: di.inject(clusterFrameContextInjectable), + subscribeStores: di.inject(kubeWatchApiInjectable).subscribeStores, + ...props, + }), + }, +); diff --git a/src/renderer/components/+workloads-pods/__tests__/pod-tolerations.test.tsx b/src/renderer/components/+workloads-pods/__tests__/pod-tolerations.test.tsx index f7c77f14ad..785655626a 100644 --- a/src/renderer/components/+workloads-pods/__tests__/pod-tolerations.test.tsx +++ b/src/renderer/components/+workloads-pods/__tests__/pod-tolerations.test.tsx @@ -21,9 +21,13 @@ import React from "react"; import "@testing-library/jest-dom/extend-expect"; -import { fireEvent, render } from "@testing-library/react"; +import { fireEvent } from "@testing-library/react"; import type { IToleration } from "../../../../common/k8s-api/workload-kube-object"; import { PodTolerations } from "../pod-tolerations"; +import { getDiForUnitTesting } from "../../getDiForUnitTesting"; +import { DiRender, renderFor } from "../../test-utils/renderFor"; +import directoryForLensLocalStorageInjectable + from "../../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable"; jest.mock("electron", () => ({ app: { @@ -47,6 +51,21 @@ const tolerations: IToleration[] =[ ]; describe("", () => { + let render: DiRender; + + beforeEach(async () => { + const di = getDiForUnitTesting({ doGeneralOverrides: true }); + + di.override( + directoryForLensLocalStorageInjectable, + () => "some-directory-for-lens-local-storage", + ); + + await di.runSetups(); + + render = renderFor(di); + }); + it("renders w/o errors", () => { const { container } = render(); diff --git a/src/renderer/components/+workloads-pods/pod-container-port.tsx b/src/renderer/components/+workloads-pods/pod-container-port.tsx index 50417ce9dd..c290a94c67 100644 --- a/src/renderer/components/+workloads-pods/pod-container-port.tsx +++ b/src/renderer/components/+workloads-pods/pod-container-port.tsx @@ -28,9 +28,16 @@ import { observable, makeObservable, reaction } from "mobx"; import { cssNames } from "../../utils"; import { Notifications } from "../notifications"; import { Button } from "../button"; -import { aboutPortForwarding, addPortForward, getPortForward, getPortForwards, openPortForward, PortForwardDialog, portForwardStore, predictProtocol, removePortForward } from "../../port-forward"; +import { aboutPortForwarding, getPortForward, getPortForwards, openPortForward, PortForwardStore, predictProtocol } from "../../port-forward"; import type { ForwardedPort } from "../../port-forward"; import { Spinner } from "../spinner"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import portForwardStoreInjectable from "../../port-forward/port-forward-store/port-forward-store.injectable"; +import removePortForwardInjectable from "../../port-forward/port-forward-store/remove-port-forward/remove-port-forward.injectable"; +import portForwardDialogModelInjectable + from "../../port-forward/port-forward-dialog-model/port-forward-dialog-model.injectable"; +import addPortForwardInjectable + from "../../port-forward/port-forward-store/add-port-forward/add-port-forward.injectable"; interface Props { pod: Pod; @@ -41,13 +48,20 @@ interface Props { } } +interface Dependencies { + portForwardStore: PortForwardStore; + removePortForward: (item: ForwardedPort) => Promise; + addPortForward: (item: ForwardedPort) => Promise; + openPortForwardDialog: (item: ForwardedPort, options: { openInBrowser: boolean }) => void; +} + @observer -export class PodContainerPort extends React.Component { +class NonInjectedPodContainerPort extends React.Component { @observable waiting = false; @observable forwardPort = 0; @observable isPortForwarded = false; - constructor(props: Props) { + constructor(props: Props & Dependencies) { super(props); makeObservable(this); this.checkExistingPortForwarding(); @@ -55,7 +69,7 @@ export class PodContainerPort extends React.Component { componentDidMount() { disposeOnUnmount(this, [ - reaction(() => [portForwardStore.portForwards, this.props.pod], () => this.checkExistingPortForwarding()), + reaction(() => [this.props.portForwardStore.portForwards, this.props.pod], () => this.checkExistingPortForwarding()), ]); } @@ -100,7 +114,7 @@ export class PodContainerPort extends React.Component { // determine how many port-forwards are already active const { length } = await getPortForwards(); - this.forwardPort = await addPortForward(portForward); + this.forwardPort = await this.props.addPortForward(portForward); if (this.forwardPort) { portForward.forwardPort = this.forwardPort; @@ -133,7 +147,7 @@ export class PodContainerPort extends React.Component { this.waiting = true; try { - await removePortForward(portForward); + await this.props.removePortForward(portForward); this.isPortForwarded = false; } catch (error) { Notifications.error(`Error occurred stopping the port-forward from port ${portForward.forwardPort}.`); @@ -161,7 +175,7 @@ export class PodContainerPort extends React.Component { protocol: predictProtocol(port.name), }; - PortForwardDialog.open(portForward, { openInBrowser: true }); + this.props.openPortForwardDialog(portForward, { openInBrowser: true }); } }; @@ -178,3 +192,17 @@ export class PodContainerPort extends React.Component { ); } } + +export const PodContainerPort = withInjectables( + NonInjectedPodContainerPort, + + { + getProps: (di, props) => ({ + portForwardStore: di.inject(portForwardStoreInjectable), + removePortForward: di.inject(removePortForwardInjectable), + addPortForward: di.inject(addPortForwardInjectable), + openPortForwardDialog: di.inject(portForwardDialogModelInjectable).open, + ...props, + }), + }, +); diff --git a/src/renderer/components/+workloads-pods/pod-details-container.tsx b/src/renderer/components/+workloads-pods/pod-details-container.tsx index b4a4c5eaf2..da78ff71b2 100644 --- a/src/renderer/components/+workloads-pods/pod-details-container.tsx +++ b/src/renderer/components/+workloads-pods/pod-details-container.tsx @@ -35,8 +35,10 @@ import { ContainerCharts } from "./container-charts"; import { LocaleDate } from "../locale-date"; import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; import { ClusterMetricsResourceType } from "../../../common/cluster-types"; -import { portForwardStore } from "../../port-forward/port-forward.store"; +import type { PortForwardStore } from "../../port-forward"; import { disposeOnUnmount, observer } from "mobx-react"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import portForwardStoreInjectable from "../../port-forward/port-forward-store/port-forward-store.injectable"; interface Props { pod: Pod; @@ -44,12 +46,16 @@ interface Props { metrics?: { [key: string]: IMetrics }; } +interface Dependencies { + portForwardStore: PortForwardStore +} + @observer -export class PodDetailsContainer extends React.Component { +class NonInjectedPodDetailsContainer extends React.Component { componentDidMount() { disposeOnUnmount(this, [ - portForwardStore.watch(), + this.props.portForwardStore.watch(), ]); } @@ -200,3 +206,14 @@ export class PodDetailsContainer extends React.Component { ); } } + +export const PodDetailsContainer = withInjectables( + NonInjectedPodDetailsContainer, + + { + getProps: (di, props) => ({ + portForwardStore: di.inject(portForwardStoreInjectable), + ...props, + }), + }, +); diff --git a/src/renderer/components/+workloads-replicasets/replicaset-details.tsx b/src/renderer/components/+workloads-replicasets/replicaset-details.tsx index 4e38ae9e1e..d1ad5c1455 100644 --- a/src/renderer/components/+workloads-replicasets/replicaset-details.tsx +++ b/src/renderer/components/+workloads-replicasets/replicaset-details.tsx @@ -38,18 +38,26 @@ import { PodDetailsList } from "../+workloads-pods/pod-details-list"; import { KubeObjectMeta } from "../kube-object-meta"; import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; import { ClusterMetricsResourceType } from "../../../common/cluster-types"; -import { boundMethod } from "../../utils"; +import { boundMethod, Disposer } from "../../utils"; import logger from "../../../common/logger"; -import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api"; +import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; +import type { KubeObject } from "../../../common/k8s-api/kube-object"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import kubeWatchApiInjectable + from "../../kube-watch-api/kube-watch-api.injectable"; interface Props extends KubeObjectDetailsProps { } +interface Dependencies { + subscribeStores: (stores: KubeObjectStore[]) => Disposer +} + @observer -export class ReplicaSetDetails extends React.Component { +class NonInjectedReplicaSetDetails extends React.Component { @observable metrics: IPodMetrics = null; - constructor(props: Props) { + constructor(props: Props & Dependencies) { super(props); makeObservable(this); } @@ -59,7 +67,8 @@ export class ReplicaSetDetails extends React.Component { reaction(() => this.props.object, () => { this.metrics = null; }), - kubeWatchApi.subscribeStores([ + + this.props.subscribeStores([ podsStore, ]), ]); @@ -140,3 +149,14 @@ export class ReplicaSetDetails extends React.Component { ); } } + +export const ReplicaSetDetails = withInjectables( + NonInjectedReplicaSetDetails, + + { + getProps: (di, props) => ({ + subscribeStores: di.inject(kubeWatchApiInjectable).subscribeStores, + ...props, + }), + }, +); diff --git a/src/renderer/components/+workloads-statefulsets/statefulset-details.tsx b/src/renderer/components/+workloads-statefulsets/statefulset-details.tsx index 7d2d66568c..fc95f41703 100644 --- a/src/renderer/components/+workloads-statefulsets/statefulset-details.tsx +++ b/src/renderer/components/+workloads-statefulsets/statefulset-details.tsx @@ -39,18 +39,26 @@ import { PodDetailsList } from "../+workloads-pods/pod-details-list"; import { KubeObjectMeta } from "../kube-object-meta"; import { getActiveClusterEntity } from "../../api/catalog-entity-registry"; import { ClusterMetricsResourceType } from "../../../common/cluster-types"; -import { boundMethod } from "../../utils"; +import { boundMethod, Disposer } from "../../utils"; import logger from "../../../common/logger"; -import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api"; +import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; +import type { KubeObject } from "../../../common/k8s-api/kube-object"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import kubeWatchApiInjectable + from "../../kube-watch-api/kube-watch-api.injectable"; interface Props extends KubeObjectDetailsProps { } +interface Dependencies { + subscribeStores: (stores: KubeObjectStore[]) => Disposer +} + @observer -export class StatefulSetDetails extends React.Component { +class NonInjectedStatefulSetDetails extends React.Component { @observable metrics: IPodMetrics = null; - constructor(props: Props) { + constructor(props: Props & Dependencies) { super(props); makeObservable(this); } @@ -60,7 +68,8 @@ export class StatefulSetDetails extends React.Component { reaction(() => this.props.object, () => { this.metrics = null; }), - kubeWatchApi.subscribeStores([ + + this.props.subscribeStores([ podsStore, ]), ]); @@ -137,3 +146,15 @@ export class StatefulSetDetails extends React.Component { ); } } + +export const StatefulSetDetails = withInjectables( + NonInjectedStatefulSetDetails, + + { + getProps: (di, props) => ({ + subscribeStores: di.inject(kubeWatchApiInjectable).subscribeStores, + ...props, + }), + }, +); + diff --git a/src/renderer/components/app-paths/app-paths.injectable.ts b/src/renderer/components/app-paths/app-paths.injectable.ts new file mode 100644 index 0000000000..3c9e71e6c8 --- /dev/null +++ b/src/renderer/components/app-paths/app-paths.injectable.ts @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { lifecycleEnum } from "@ogre-tools/injectable"; +import { + AppPaths, + appPathsInjectionToken, + appPathsIpcChannel, +} from "../../../common/app-paths/app-path-injection-token"; +import getValueFromRegisteredChannelInjectable from "./get-value-from-registered-channel/get-value-from-registered-channel.injectable"; + +let syncAppPaths: AppPaths; + +const appPathsInjectable = getInjectable({ + setup: async (di) => { + const getValueFromRegisteredChannel = di.inject( + getValueFromRegisteredChannelInjectable, + ); + + syncAppPaths = await getValueFromRegisteredChannel(appPathsIpcChannel); + }, + + instantiate: () => syncAppPaths, + + injectionToken: appPathsInjectionToken, + + lifecycle: lifecycleEnum.singleton, +}); + +export default appPathsInjectable; diff --git a/src/renderer/components/app-paths/get-value-from-registered-channel/get-value-from-registered-channel.injectable.ts b/src/renderer/components/app-paths/get-value-from-registered-channel/get-value-from-registered-channel.injectable.ts new file mode 100644 index 0000000000..2e35452934 --- /dev/null +++ b/src/renderer/components/app-paths/get-value-from-registered-channel/get-value-from-registered-channel.injectable.ts @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import ipcRendererInjectable from "./ipc-renderer/ipc-renderer.injectable"; +import { getValueFromRegisteredChannel } from "./get-value-from-registered-channel"; + +const getValueFromRegisteredChannelInjectable = getInjectable({ + instantiate: (di) => + getValueFromRegisteredChannel({ ipcRenderer: di.inject(ipcRendererInjectable) }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default getValueFromRegisteredChannelInjectable; diff --git a/src/renderer/components/app-paths/get-value-from-registered-channel/get-value-from-registered-channel.ts b/src/renderer/components/app-paths/get-value-from-registered-channel/get-value-from-registered-channel.ts new file mode 100644 index 0000000000..aabfe3f5ba --- /dev/null +++ b/src/renderer/components/app-paths/get-value-from-registered-channel/get-value-from-registered-channel.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import type { IpcRenderer } from "electron"; +import type { Channel } from "../../../../common/ipc-channel/channel"; + +interface Dependencies { + ipcRenderer: IpcRenderer; +} + +export const getValueFromRegisteredChannel = + ({ ipcRenderer }: Dependencies) => + , TInstance>( + channel: TChannel, + ): Promise => + ipcRenderer.invoke(channel.name); diff --git a/src/renderer/components/app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable.ts b/src/renderer/components/app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable.ts new file mode 100644 index 0000000000..7fd26e8063 --- /dev/null +++ b/src/renderer/components/app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable.ts @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { lifecycleEnum } from "@ogre-tools/injectable"; +import { ipcRenderer } from "electron"; + +const ipcRendererInjectable = getInjectable({ + instantiate: () => ipcRenderer, + lifecycle: lifecycleEnum.singleton, + causesSideEffects: true, +}); + +export default ipcRendererInjectable; diff --git a/src/renderer/components/cluster-manager/bottom-bar.test.tsx b/src/renderer/components/cluster-manager/bottom-bar.test.tsx index 4390175889..0a2e79d061 100644 --- a/src/renderer/components/cluster-manager/bottom-bar.test.tsx +++ b/src/renderer/components/cluster-manager/bottom-bar.test.tsx @@ -26,10 +26,10 @@ import "@testing-library/jest-dom/extend-expect"; import { BottomBar } from "./bottom-bar"; import { StatusBarRegistry } from "../../../extensions/registries"; import { HotbarStore } from "../../../common/hotbar-store"; -import { AppPaths } from "../../../common/app-paths"; import { CommandOverlay } from "../command-palette"; import { HotbarSwitchCommand } from "../hotbar/hotbar-switch-command"; import { ActiveHotbarName } from "./active-hotbar-name"; +import { getDisForUnitTesting } from "../../../test-utils/get-dis-for-unit-testing"; jest.mock("../command-palette", () => ({ CommandOverlay: { @@ -37,8 +37,6 @@ jest.mock("../command-palette", () => ({ }, })); -AppPaths.init(); - jest.mock("electron", () => ({ app: { getName: () => "lens", @@ -56,7 +54,11 @@ jest.mock("electron", () => ({ })); describe("", () => { - beforeEach(() => { + beforeEach(async () => { + const dis = getDisForUnitTesting({ doGeneralOverrides: true }); + + await dis.runSetups(); + const mockOpts = { "tmp": { "test-store.json": JSON.stringify({}), diff --git a/src/renderer/components/cluster-manager/cluster-manager.tsx b/src/renderer/components/cluster-manager/cluster-manager.tsx index 04af444c30..c062289118 100644 --- a/src/renderer/components/cluster-manager/cluster-manager.tsx +++ b/src/renderer/components/cluster-manager/cluster-manager.tsx @@ -25,7 +25,7 @@ import React from "react"; import { Redirect, Route, Switch } from "react-router"; import { disposeOnUnmount, observer } from "mobx-react"; import { BottomBar } from "./bottom-bar"; -import { Catalog, previousActiveTab } from "../+catalog"; +import { Catalog } from "../+catalog"; import { Preferences } from "../+preferences"; import { AddCluster } from "../+add-cluster"; import { ClusterView } from "./cluster-view"; @@ -41,9 +41,16 @@ import { navigation } from "../../navigation"; import { setEntityOnRouteMatch } from "../../../main/catalog-sources/helpers/general-active-sync"; import { TopBar } from "../layout/topbar"; import { catalogURL, getPreviousTabUrl } from "../../../common/routes"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import catalogPreviousActiveTabStorageInjectable + from "../+catalog/catalog-previous-active-tab-storage/catalog-previous-active-tab-storage.injectable"; + +interface Dependencies { + catalogPreviousActiveTabStorage: { get: () => string } +} @observer -export class ClusterManager extends React.Component { +class NonInjectedClusterManager extends React.Component { componentDidMount() { disposeOnUnmount(this, [ reaction(() => navigation.location, () => setEntityOnRouteMatch(), { fireImmediately: true }), @@ -53,11 +60,18 @@ export class ClusterManager extends React.Component { render() { return (
- +
-
+
- + + @@ -65,19 +79,24 @@ export class ClusterManager extends React.Component { - { - GlobalPageRegistry.getInstance().getItems() - .map(({ url, components: { Page }}) => ( - - )) - } - + {GlobalPageRegistry.getInstance() + .getItems() + .map(({ url, components: { Page }}) => ( + + ))} +
- - - + + +
); } } + +export const ClusterManager = withInjectables(NonInjectedClusterManager, { + getProps: di => ({ + catalogPreviousActiveTabStorage: di.inject(catalogPreviousActiveTabStorageInjectable), + }), +}); diff --git a/src/renderer/components/cluster-manager/cluster-status.tsx b/src/renderer/components/cluster-manager/cluster-status.tsx index 5b7e73510c..954e413c78 100644 --- a/src/renderer/components/cluster-manager/cluster-status.tsx +++ b/src/renderer/components/cluster-manager/cluster-status.tsx @@ -26,7 +26,7 @@ import { disposeOnUnmount, observer } from "mobx-react"; import React from "react"; import { clusterActivateHandler } from "../../../common/cluster-ipc"; import { ipcRendererOn, requestMain } from "../../../common/ipc"; -import type { Cluster } from "../../../main/cluster"; +import type { Cluster } from "../../../common/cluster/cluster"; import { cssNames, IClassName } from "../../utils"; import { Button } from "../button"; import { Icon } from "../icon"; diff --git a/src/renderer/components/cluster-manager/cluster-view.tsx b/src/renderer/components/cluster-manager/cluster-view.tsx index b22e3b8adf..573df577d4 100644 --- a/src/renderer/components/cluster-manager/cluster-view.tsx +++ b/src/renderer/components/cluster-manager/cluster-view.tsx @@ -26,8 +26,8 @@ import { disposeOnUnmount, observer } from "mobx-react"; import type { RouteComponentProps } from "react-router"; import { ClusterStatus } from "./cluster-status"; import { ClusterFrameHandler } from "./lens-views"; -import type { Cluster } from "../../../main/cluster"; -import { ClusterStore } from "../../../common/cluster-store"; +import type { Cluster } from "../../../common/cluster/cluster"; +import { ClusterStore } from "../../../common/cluster-store/cluster-store"; import { requestMain } from "../../../common/ipc"; import { clusterActivateHandler } from "../../../common/cluster-ipc"; import { catalogEntityRegistry } from "../../api/catalog-entity-registry"; diff --git a/src/renderer/components/cluster-manager/lens-views.ts b/src/renderer/components/cluster-manager/lens-views.ts index 784e645e59..2179a85e4c 100644 --- a/src/renderer/components/cluster-manager/lens-views.ts +++ b/src/renderer/components/cluster-manager/lens-views.ts @@ -22,7 +22,7 @@ import { action, IReactionDisposer, makeObservable, observable, when } from "mobx"; import logger from "../../../main/logger"; import { clusterVisibilityHandler } from "../../../common/cluster-ipc"; -import { ClusterStore } from "../../../common/cluster-store"; +import { ClusterStore } from "../../../common/cluster-store/cluster-store"; import type { ClusterId } from "../../../common/cluster-types"; import { getClusterFrameUrl, Singleton } from "../../utils"; import { ipcRenderer } from "electron"; diff --git a/src/renderer/components/cluster-settings/cluster-settings.tsx b/src/renderer/components/cluster-settings/cluster-settings.tsx index 762d0789fc..d7081b098b 100644 --- a/src/renderer/components/cluster-settings/cluster-settings.tsx +++ b/src/renderer/components/cluster-settings/cluster-settings.tsx @@ -21,7 +21,7 @@ import React from "react"; import type { KubernetesCluster } from "../../../common/catalog-entities"; -import { ClusterStore } from "../../../common/cluster-store"; +import { ClusterStore } from "../../../common/cluster-store/cluster-store"; import type { EntitySettingViewProps } from "../../../extensions/registries"; import type { CatalogEntity } from "../../api/catalog-entity"; import * as components from "./components"; diff --git a/src/renderer/components/cluster-settings/components/cluster-accessible-namespaces.tsx b/src/renderer/components/cluster-settings/components/cluster-accessible-namespaces.tsx index 62f426efe1..4691f73538 100644 --- a/src/renderer/components/cluster-settings/components/cluster-accessible-namespaces.tsx +++ b/src/renderer/components/cluster-settings/components/cluster-accessible-namespaces.tsx @@ -21,7 +21,7 @@ import React from "react"; import { observer } from "mobx-react"; -import type { Cluster } from "../../../../main/cluster"; +import type { Cluster } from "../../../../common/cluster/cluster"; import { SubTitle } from "../../layout/sub-title"; import { EditableList } from "../../editable-list"; import { observable, makeObservable } from "mobx"; diff --git a/src/renderer/components/cluster-settings/components/cluster-icon-settings.tsx b/src/renderer/components/cluster-settings/components/cluster-icon-settings.tsx index a27860863e..bc17e6a24b 100644 --- a/src/renderer/components/cluster-settings/components/cluster-icon-settings.tsx +++ b/src/renderer/components/cluster-settings/components/cluster-icon-settings.tsx @@ -20,7 +20,7 @@ */ import React from "react"; -import type { Cluster } from "../../../../main/cluster"; +import type { Cluster } from "../../../../common/cluster/cluster"; import { boundMethod } from "../../../utils"; import { observable } from "mobx"; import { observer } from "mobx-react"; diff --git a/src/renderer/components/cluster-settings/components/cluster-kubeconfig.tsx b/src/renderer/components/cluster-settings/components/cluster-kubeconfig.tsx index 583b44237a..3e63988fb7 100644 --- a/src/renderer/components/cluster-settings/components/cluster-kubeconfig.tsx +++ b/src/renderer/components/cluster-settings/components/cluster-kubeconfig.tsx @@ -20,7 +20,7 @@ */ import React from "react"; -import type { Cluster } from "../../../../main/cluster"; +import type { Cluster } from "../../../../common/cluster/cluster"; import { observer } from "mobx-react"; import { SubTitle } from "../../layout/sub-title"; import { boundMethod } from "../../../../common/utils"; diff --git a/src/renderer/components/cluster-settings/components/cluster-local-terminal-settings.tsx b/src/renderer/components/cluster-settings/components/cluster-local-terminal-settings.tsx index 22774a5af8..da545b542f 100644 --- a/src/renderer/components/cluster-settings/components/cluster-local-terminal-settings.tsx +++ b/src/renderer/components/cluster-settings/components/cluster-local-terminal-settings.tsx @@ -21,7 +21,7 @@ import React, { useEffect, useState } from "react"; import { observer } from "mobx-react"; -import type { Cluster } from "../../../../main/cluster"; +import type { Cluster } from "../../../../common/cluster/cluster"; import { Input } from "../../input"; import { SubTitle } from "../../layout/sub-title"; import { stat } from "fs/promises"; diff --git a/src/renderer/components/cluster-settings/components/cluster-metrics-setting.tsx b/src/renderer/components/cluster-settings/components/cluster-metrics-setting.tsx index 847d38ec77..4a4b2899b2 100644 --- a/src/renderer/components/cluster-settings/components/cluster-metrics-setting.tsx +++ b/src/renderer/components/cluster-settings/components/cluster-metrics-setting.tsx @@ -25,7 +25,7 @@ import { Select, SelectOption } from "../../select/select"; import { Icon } from "../../icon/icon"; import { Button } from "../../button/button"; import { SubTitle } from "../../layout/sub-title"; -import type { Cluster } from "../../../../main/cluster"; +import type { Cluster } from "../../../../common/cluster/cluster"; import { observable, reaction, makeObservable } from "mobx"; import { ClusterMetricsResourceType } from "../../../../common/cluster-types"; diff --git a/src/renderer/components/cluster-settings/components/cluster-name-setting.tsx b/src/renderer/components/cluster-settings/components/cluster-name-setting.tsx index af556c29b4..95e4084caf 100644 --- a/src/renderer/components/cluster-settings/components/cluster-name-setting.tsx +++ b/src/renderer/components/cluster-settings/components/cluster-name-setting.tsx @@ -20,7 +20,7 @@ */ import React from "react"; -import type { Cluster } from "../../../../main/cluster"; +import type { Cluster } from "../../../../common/cluster/cluster"; import { Input } from "../../input"; import { observable, autorun, makeObservable } from "mobx"; import { observer, disposeOnUnmount } from "mobx-react"; diff --git a/src/renderer/components/cluster-settings/components/cluster-node-shell-setting.tsx b/src/renderer/components/cluster-settings/components/cluster-node-shell-setting.tsx index d431412719..96bbaf599f 100644 --- a/src/renderer/components/cluster-settings/components/cluster-node-shell-setting.tsx +++ b/src/renderer/components/cluster-settings/components/cluster-node-shell-setting.tsx @@ -19,7 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import type { Cluster } from "../../../../main/cluster"; +import type { Cluster } from "../../../../common/cluster/cluster"; import { makeObservable, observable } from "mobx"; import { SubTitle } from "../../layout/sub-title"; import React from "react"; diff --git a/src/renderer/components/cluster-settings/components/cluster-prometheus-setting.tsx b/src/renderer/components/cluster-settings/components/cluster-prometheus-setting.tsx index c9a1acccc9..946a1455e8 100644 --- a/src/renderer/components/cluster-settings/components/cluster-prometheus-setting.tsx +++ b/src/renderer/components/cluster-settings/components/cluster-prometheus-setting.tsx @@ -21,7 +21,7 @@ import React from "react"; import { observer, disposeOnUnmount } from "mobx-react"; -import type { Cluster } from "../../../../main/cluster"; +import type { Cluster } from "../../../../common/cluster/cluster"; import { SubTitle } from "../../layout/sub-title"; import { Select, SelectOption } from "../../select"; import { Input } from "../../input"; diff --git a/src/renderer/components/cluster-settings/components/cluster-proxy-setting.tsx b/src/renderer/components/cluster-settings/components/cluster-proxy-setting.tsx index 046c57e47c..a7e5773d43 100644 --- a/src/renderer/components/cluster-settings/components/cluster-proxy-setting.tsx +++ b/src/renderer/components/cluster-settings/components/cluster-proxy-setting.tsx @@ -22,7 +22,7 @@ import React from "react"; import { observable, autorun, makeObservable } from "mobx"; import { observer, disposeOnUnmount } from "mobx-react"; -import type { Cluster } from "../../../../main/cluster"; +import type { Cluster } from "../../../../common/cluster/cluster"; import { Input, InputValidators } from "../../input"; import { SubTitle } from "../../layout/sub-title"; diff --git a/src/renderer/components/cluster-settings/components/cluster-show-metrics.tsx b/src/renderer/components/cluster-settings/components/cluster-show-metrics.tsx index effdc16160..0d20f4012d 100644 --- a/src/renderer/components/cluster-settings/components/cluster-show-metrics.tsx +++ b/src/renderer/components/cluster-settings/components/cluster-show-metrics.tsx @@ -21,7 +21,7 @@ import React from "react"; import { disposeOnUnmount, observer } from "mobx-react"; -import type { Cluster } from "../../../../main/cluster"; +import type { Cluster } from "../../../../common/cluster/cluster"; import { observable, reaction, makeObservable } from "mobx"; import { Badge } from "../../badge/badge"; import { Icon } from "../../icon/icon"; diff --git a/src/renderer/components/delete-cluster-dialog/__tests__/delete-cluster-dialog.test.tsx b/src/renderer/components/delete-cluster-dialog/__tests__/delete-cluster-dialog.test.tsx index 8270e6e31a..499af7c336 100644 --- a/src/renderer/components/delete-cluster-dialog/__tests__/delete-cluster-dialog.test.tsx +++ b/src/renderer/components/delete-cluster-dialog/__tests__/delete-cluster-dialog.test.tsx @@ -25,9 +25,12 @@ import mockFs from "mock-fs"; import React from "react"; import selectEvent from "react-select-event"; -import { Cluster } from "../../../../main/cluster"; +import type { Cluster } from "../../../../common/cluster/cluster"; import { DeleteClusterDialog } from "../delete-cluster-dialog"; -import { AppPaths } from "../../../../common/app-paths"; + +import type { ClusterModel } from "../../../../common/cluster-types"; +import { getDisForUnitTesting } from "../../../../test-utils/get-dis-for-unit-testing"; +import { createClusterInjectionToken } from "../../../../common/cluster/create-cluster-injection-token"; jest.mock("electron", () => ({ app: { @@ -45,8 +48,6 @@ jest.mock("electron", () => ({ }, })); -AppPaths.init(); - const kubeconfig = ` apiVersion: v1 clusters: @@ -101,6 +102,22 @@ users: let config: KubeConfig; describe("", () => { + let createCluster: (model: ClusterModel) => Cluster; + + beforeEach(async () => { + const { mainDi, runSetups } = getDisForUnitTesting({ doGeneralOverrides: true }); + + mockFs(); + + await runSetups(); + + createCluster = mainDi.inject(createClusterInjectionToken); + }); + + afterEach(() => { + mockFs.restore(); + }); + describe("Kubeconfig with different clusters", () => { beforeEach(async () => { const mockOpts = { @@ -124,7 +141,7 @@ describe("", () => { }); it("shows warning when deleting non-current-context cluster", () => { - const cluster = new Cluster({ + const cluster = createCluster({ id: "test", contextName: "test", preferences: { @@ -142,7 +159,7 @@ describe("", () => { }); it("shows warning when deleting current-context cluster", () => { - const cluster = new Cluster({ + const cluster = createCluster({ id: "other-cluster", contextName: "other-context", preferences: { @@ -159,7 +176,7 @@ describe("", () => { }); it("shows context switcher when deleting current cluster", async () => { - const cluster = new Cluster({ + const cluster = createCluster({ id: "other-cluster", contextName: "other-context", preferences: { @@ -180,7 +197,7 @@ describe("", () => { }); it("shows context switcher after checkbox click", async () => { - const cluster = new Cluster({ + const cluster = createCluster({ id: "some-cluster", contextName: "test", preferences: { @@ -205,7 +222,7 @@ describe("", () => { }); it("shows warning for internal kubeconfig cluster", () => { - const cluster = new Cluster({ + const cluster = createCluster({ id: "some-cluster", contextName: "test", preferences: { @@ -243,7 +260,7 @@ describe("", () => { }); it("shows warning if no other contexts left", () => { - const cluster = new Cluster({ + const cluster = createCluster({ id: "other-cluster", contextName: "other-context", preferences: { diff --git a/src/renderer/components/delete-cluster-dialog/delete-cluster-dialog.tsx b/src/renderer/components/delete-cluster-dialog/delete-cluster-dialog.tsx index e6a03444b6..9f32dd59e9 100644 --- a/src/renderer/components/delete-cluster-dialog/delete-cluster-dialog.tsx +++ b/src/renderer/components/delete-cluster-dialog/delete-cluster-dialog.tsx @@ -26,7 +26,7 @@ import React from "react"; import { Button } from "../button"; import type { KubeConfig } from "@kubernetes/client-node"; -import type { Cluster } from "../../../main/cluster"; +import type { Cluster } from "../../../common/cluster/cluster"; import { saveKubeconfig } from "./save-config"; import { requestMain } from "../../../common/ipc"; import { clusterClearDeletingHandler, clusterDeleteHandler, clusterSetDeletingHandler } from "../../../common/cluster-ipc"; diff --git a/src/renderer/components/dock/__test__/dock-tabs.test.tsx b/src/renderer/components/dock/__test__/dock-tabs.test.tsx index 9d4ae2b345..cf654a913e 100644 --- a/src/renderer/components/dock/__test__/dock-tabs.test.tsx +++ b/src/renderer/components/dock/__test__/dock-tabs.test.tsx @@ -20,16 +20,20 @@ */ import React from "react"; -import { fireEvent, render } from "@testing-library/react"; +import { fireEvent } from "@testing-library/react"; import "@testing-library/jest-dom/extend-expect"; import fse from "fs-extra"; import { DockTabs } from "../dock-tabs"; -import { dockStore, DockTab, TabKind } from "../dock.store"; +import { DockStore, DockTab, TabKind } from "../dock-store/dock.store"; import { noop } from "../../../utils"; import { ThemeStore } from "../../../theme.store"; -import { TerminalStore } from "../terminal.store"; import { UserStore } from "../../../../common/user-store"; -import { AppPaths } from "../../../../common/app-paths"; +import { getDiForUnitTesting } from "../../getDiForUnitTesting"; +import dockStoreInjectable from "../dock-store/dock-store.injectable"; +import type { DiRender } from "../../test-utils/renderFor"; +import { renderFor } from "../../test-utils/renderFor"; +import directoryForUserDataInjectable + from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; jest.mock("electron", () => ({ app: { @@ -46,7 +50,6 @@ jest.mock("electron", () => ({ handle: jest.fn(), }, })); -AppPaths.init(); const initialTabs: DockTab[] = [ { id: "terminal", kind: TabKind.TERMINAL, title: "Terminal", pinned: false }, @@ -56,7 +59,7 @@ const initialTabs: DockTab[] = [ { id: "logs", kind: TabKind.POD_LOGS, title: "Logs", pinned: false }, ]; -const getComponent = () => ( +const getComponent = (dockStore: DockStore) => ( ( /> ); -const renderTabs = () => render(getComponent()); -const getTabKinds = () => dockStore.tabs.map(tab => tab.kind); +const getTabKinds = (dockStore: DockStore) => dockStore.tabs.map((tab) => tab.kind); describe("", () => { + let dockStore: DockStore; + let render: DiRender; + beforeEach(async () => { + const di = getDiForUnitTesting({ doGeneralOverrides: true }); + + + render = renderFor(di); + + di.override( + directoryForUserDataInjectable, + () => "some-test-suite-specific-directory-for-user-data", + ); + + await di.runSetups(); + + dockStore = di.inject(dockStoreInjectable); + UserStore.createInstance(); ThemeStore.createInstance(); - TerminalStore.createInstance(); await dockStore.whenReady; dockStore.tabs = initialTabs; }); afterEach(() => { ThemeStore.resetInstance(); - TerminalStore.resetInstance(); UserStore.resetInstance(); - fse.remove("tmp"); + + // TODO: A unit test may not cause side effects. Here accessing file system is a side effect. + fse.remove("some-test-suite-specific-directory-for-user-data"); }); it("renders w/o errors", () => { - const { container } = renderTabs(); + const { container } = render(getComponent(dockStore)); expect(container).toBeInstanceOf(HTMLElement); }); it("has 6 tabs (1 tab is initial terminal)", () => { - const { container } = renderTabs(); + const { container } = render(getComponent(dockStore)); const tabs = container.querySelectorAll(".Tab"); expect(tabs.length).toBe(initialTabs.length); }); it("opens a context menu", () => { - const { container, getByText } = renderTabs(); + const { container, getByText } = render(getComponent(dockStore)); const tab = container.querySelector(".Tab"); fireEvent.contextMenu(tab); @@ -106,17 +125,22 @@ describe("", () => { }); it("closes selected tab", () => { - const { container, getByText, rerender } = renderTabs(); + const { container, getByText, rerender } = render( + getComponent(dockStore), + ); + const tab = container.querySelector(".Tab"); fireEvent.contextMenu(tab); fireEvent.click(getByText("Close")); - rerender(getComponent()); + + rerender(getComponent(dockStore)); const tabs = container.querySelectorAll(".Tab"); expect(tabs.length).toBe(initialTabs.length - 1); - expect(getTabKinds()).toEqual([ + + expect(getTabKinds(dockStore)).toEqual([ TabKind.CREATE_RESOURCE, TabKind.EDIT_RESOURCE, TabKind.INSTALL_CHART, @@ -125,42 +149,42 @@ describe("", () => { }); it("closes other tabs", () => { - const { container, getByText, rerender } = renderTabs(); + const { container, getByText, rerender } = render(getComponent(dockStore)); const tab = container.querySelectorAll(".Tab")[3]; fireEvent.contextMenu(tab); fireEvent.click(getByText("Close other tabs")); - rerender(getComponent()); + rerender(getComponent(dockStore)); const tabs = container.querySelectorAll(".Tab"); expect(tabs.length).toBe(1); - expect(getTabKinds()).toEqual([initialTabs[3].kind]); + expect(getTabKinds(dockStore)).toEqual([initialTabs[3].kind]); }); it("closes all tabs", () => { - const { container, getByText, rerender } = renderTabs(); + const { container, getByText, rerender } = render(getComponent(dockStore)); const tab = container.querySelector(".Tab"); fireEvent.contextMenu(tab); const command = getByText("Close all tabs"); fireEvent.click(command); - rerender(getComponent()); + rerender(getComponent(dockStore)); const tabs = container.querySelectorAll(".Tab"); expect(tabs.length).toBe(0); }); it("closes tabs to the right", () => { - const { container, getByText, rerender } = renderTabs(); + const { container, getByText, rerender } = render(getComponent(dockStore)); const tab = container.querySelectorAll(".Tab")[3]; // 4th of 5 fireEvent.contextMenu(tab); fireEvent.click(getByText("Close tabs to the right")); - rerender(getComponent()); + rerender(getComponent(dockStore)); - expect(getTabKinds()).toEqual( + expect(getTabKinds(dockStore)).toEqual( initialTabs.slice(0, 4).map(tab => tab.kind), ); }); @@ -169,7 +193,7 @@ describe("", () => { dockStore.tabs = [{ id: "terminal", kind: TabKind.TERMINAL, title: "Terminal", pinned: false, }]; - const { container, getByText } = renderTabs(); + const { container, getByText } = render(getComponent(dockStore)); const tab = container.querySelector(".Tab"); fireEvent.contextMenu(tab); @@ -185,7 +209,7 @@ describe("", () => { { id: "terminal", kind: TabKind.TERMINAL, title: "Terminal", pinned: false }, { id: "logs", kind: TabKind.POD_LOGS, title: "Pod Logs", pinned: false }, ]; - const { container, getByText } = renderTabs(); + const { container, getByText } = render(getComponent(dockStore)); const tab = container.querySelectorAll(".Tab")[1]; fireEvent.contextMenu(tab); diff --git a/src/renderer/components/dock/__test__/log-resource-selector.test.tsx b/src/renderer/components/dock/__test__/log-resource-selector.test.tsx index b87adba0a7..d856768a2b 100644 --- a/src/renderer/components/dock/__test__/log-resource-selector.test.tsx +++ b/src/renderer/components/dock/__test__/log-resource-selector.test.tsx @@ -21,17 +21,20 @@ import React from "react"; import "@testing-library/jest-dom/extend-expect"; -import { render } from "@testing-library/react"; import selectEvent from "react-select-event"; import { Pod } from "../../../../common/k8s-api/endpoints"; import { LogResourceSelector } from "../log-resource-selector"; -import type { LogTabData } from "../log-tab.store"; +import type { LogTabData } from "../log-tab-store/log-tab.store"; import { dockerPod, deploymentPod1 } from "./pod.mock"; import { ThemeStore } from "../../../theme.store"; import { UserStore } from "../../../../common/user-store"; import mockFs from "mock-fs"; -import { AppPaths } from "../../../../common/app-paths"; +import { getDiForUnitTesting } from "../../getDiForUnitTesting"; +import type { DiRender } from "../../test-utils/renderFor"; +import { renderFor } from "../../test-utils/renderFor"; +import directoryForUserDataInjectable + from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; jest.mock("electron", () => ({ app: { @@ -49,8 +52,6 @@ jest.mock("electron", () => ({ }, })); -AppPaths.init(); - const getComponent = (tabData: LogTabData) => { return ( { }; describe("", () => { - beforeEach(() => { + let render: DiRender; + + beforeEach(async () => { + const di = getDiForUnitTesting({ doGeneralOverrides: true }); + + di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); + + render = renderFor(di); + + await di.runSetups(); + mockFs({ "tmp": {}, }); + UserStore.createInstance(); ThemeStore.createInstance(); }); diff --git a/src/renderer/components/dock/__test__/log-tab.store.test.ts b/src/renderer/components/dock/__test__/log-tab.store.test.ts index a342ddce79..17dff0eff2 100644 --- a/src/renderer/components/dock/__test__/log-tab.store.test.ts +++ b/src/renderer/components/dock/__test__/log-tab.store.test.ts @@ -23,49 +23,47 @@ import { podsStore } from "../../+workloads-pods/pods.store"; import { UserStore } from "../../../../common/user-store"; import { Pod } from "../../../../common/k8s-api/endpoints"; import { ThemeStore } from "../../../theme.store"; -import { dockStore } from "../dock.store"; -import { logTabStore } from "../log-tab.store"; import { deploymentPod1, deploymentPod2, deploymentPod3, dockerPod } from "./pod.mock"; -import fse from "fs-extra"; import { mockWindow } from "../../../../../__mocks__/windowMock"; -import { AppPaths } from "../../../../common/app-paths"; +import { getDiForUnitTesting } from "../../getDiForUnitTesting"; +import logTabStoreInjectable from "../log-tab-store/log-tab-store.injectable"; +import type { LogTabStore } from "../log-tab-store/log-tab.store"; +import dockStoreInjectable from "../dock-store/dock-store.injectable"; +import type { DockStore } from "../dock-store/dock.store"; +import directoryForUserDataInjectable + from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; +import mockFs from "mock-fs"; mockWindow(); -jest.mock("electron", () => ({ - app: { - getVersion: () => "99.99.99", - getName: () => "lens", - setName: jest.fn(), - setPath: jest.fn(), - getPath: () => "tmp", - getLocale: () => "en", - setLoginItemSettings: jest.fn(), - }, - ipcMain: { - on: jest.fn(), - handle: jest.fn(), - }, -})); - -AppPaths.init(); - podsStore.items.push(new Pod(dockerPod)); podsStore.items.push(new Pod(deploymentPod1)); podsStore.items.push(new Pod(deploymentPod2)); describe("log tab store", () => { - beforeEach(() => { + let logTabStore: LogTabStore; + let dockStore: DockStore; + + beforeEach(async () => { + const di = getDiForUnitTesting({ doGeneralOverrides: true }); + + mockFs(); + + di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); + + await di.runSetups(); + + dockStore = di.inject(dockStoreInjectable); + logTabStore = di.inject(logTabStoreInjectable); + UserStore.createInstance(); ThemeStore.createInstance(); }); afterEach(() => { - logTabStore.reset(); - dockStore.reset(); UserStore.resetInstance(); ThemeStore.resetInstance(); - fse.remove("tmp"); + mockFs.restore(); }); it("creates log tab without sibling pods", () => { diff --git a/src/renderer/components/dock/create-install-chart-tab/create-install-chart-tab.injectable.ts b/src/renderer/components/dock/create-install-chart-tab/create-install-chart-tab.injectable.ts new file mode 100644 index 0000000000..8eef858912 --- /dev/null +++ b/src/renderer/components/dock/create-install-chart-tab/create-install-chart-tab.injectable.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { createInstallChartTab } from "./create-install-chart-tab"; +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import installChartStoreInjectable from "../install-chart-store/install-chart-store.injectable"; +import dockStoreInjectable from "../dock-store/dock-store.injectable"; + +const createInstallChartTabInjectable = getInjectable({ + instantiate: (di) => createInstallChartTab({ + installChartStore: di.inject(installChartStoreInjectable), + createDockTab: di.inject(dockStoreInjectable).createTab, + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default createInstallChartTabInjectable; diff --git a/src/renderer/components/dock/create-install-chart-tab/create-install-chart-tab.ts b/src/renderer/components/dock/create-install-chart-tab/create-install-chart-tab.ts new file mode 100644 index 0000000000..953ed58173 --- /dev/null +++ b/src/renderer/components/dock/create-install-chart-tab/create-install-chart-tab.ts @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import type { HelmChart } from "../../../../common/k8s-api/endpoints/helm-charts.api"; +import { + DockTab, + DockTabCreate, + DockTabCreateSpecific, + TabKind, +} from "../dock-store/dock.store"; + +import type { InstallChartStore } from "../install-chart-store/install-chart.store"; + +interface Dependencies { + createDockTab: (rawTab: DockTabCreate, addNumber: boolean) => DockTab; + installChartStore: InstallChartStore; +} + +export const createInstallChartTab = + ({ createDockTab, installChartStore }: Dependencies) => + (chart: HelmChart, tabParams: DockTabCreateSpecific = {}) => { + const { name, repo, version } = chart; + + const tab = createDockTab( + { + title: `Helm Install: ${repo}/${name}`, + ...tabParams, + kind: TabKind.INSTALL_CHART, + }, + false, + ); + + installChartStore.setData(tab.id, { + name, + repo, + version, + namespace: "default", + releaseName: "", + description: "", + }); + + return tab; + }; diff --git a/src/renderer/components/dock/create-resource-store/create-resource-store.injectable.ts b/src/renderer/components/dock/create-resource-store/create-resource-store.injectable.ts new file mode 100644 index 0000000000..741543bc84 --- /dev/null +++ b/src/renderer/components/dock/create-resource-store/create-resource-store.injectable.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import dockStoreInjectable from "../dock-store/dock-store.injectable"; +import { CreateResourceStore } from "./create-resource.store"; +import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable"; + +const createResourceStoreInjectable = getInjectable({ + instantiate: (di) => new CreateResourceStore({ + dockStore: di.inject(dockStoreInjectable), + createStorage: di.inject(createStorageInjectable), + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default createResourceStoreInjectable; diff --git a/src/renderer/components/dock/create-resource.store.ts b/src/renderer/components/dock/create-resource-store/create-resource.store.ts similarity index 86% rename from src/renderer/components/dock/create-resource.store.ts rename to src/renderer/components/dock/create-resource-store/create-resource.store.ts index 0ba7ced44d..a535dfa1e4 100644 --- a/src/renderer/components/dock/create-resource.store.ts +++ b/src/renderer/components/dock/create-resource-store/create-resource.store.ts @@ -25,15 +25,21 @@ import os from "os"; import groupBy from "lodash/groupBy"; import filehound from "filehound"; import { watch } from "chokidar"; -import { autoBind } from "../../utils"; -import { DockTabStore } from "./dock-tab.store"; -import { dockStore, DockTabCreateSpecific, TabKind } from "./dock.store"; +import { autoBind, StorageHelper } from "../../../utils"; +import { DockTabStorageState, DockTabStore } from "../dock-tab-store/dock-tab.store"; +import type { DockStore } from "../dock-store/dock.store"; + +interface Dependencies { + dockStore: DockStore, + createStorage: (storageKey: string, options: DockTabStorageState) => StorageHelper> +} export class CreateResourceStore extends DockTabStore { - constructor() { - super({ + constructor(protected dependencies: Dependencies) { + super(dependencies, { storageKey: "create_resource", }); + autoBind(this); fs.ensureDirSync(this.userTemplatesFolder); } @@ -78,13 +84,3 @@ export class CreateResourceStore extends DockTabStore { }); } } - -export const createResourceStore = new CreateResourceStore(); - -export function createResourceTab(tabParams: DockTabCreateSpecific = {}) { - return dockStore.createTab({ - title: "Create resource", - ...tabParams, - kind: TabKind.CREATE_RESOURCE, - }); -} diff --git a/src/renderer/components/dock/create-resource-tab/create-resource-tab.injectable.ts b/src/renderer/components/dock/create-resource-tab/create-resource-tab.injectable.ts new file mode 100644 index 0000000000..5eb4c62551 --- /dev/null +++ b/src/renderer/components/dock/create-resource-tab/create-resource-tab.injectable.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { createResourceTab } from "./create-resource-tab"; +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import dockStoreInjectable from "../dock-store/dock-store.injectable"; + +const createResourceTabInjectable = getInjectable({ + instantiate: (di) => createResourceTab({ + dockStore: di.inject(dockStoreInjectable), + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default createResourceTabInjectable; diff --git a/src/renderer/components/dock/create-resource-tab/create-resource-tab.ts b/src/renderer/components/dock/create-resource-tab/create-resource-tab.ts new file mode 100644 index 0000000000..0b9efabe4c --- /dev/null +++ b/src/renderer/components/dock/create-resource-tab/create-resource-tab.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { DockStore, DockTabCreateSpecific, TabKind } from "../dock-store/dock.store"; + +interface Dependencies { + dockStore: DockStore +} + +export const createResourceTab = + ({ dockStore }: Dependencies) => + (tabParams: DockTabCreateSpecific = {}) => + dockStore.createTab({ + title: "Create resource", + ...tabParams, + kind: TabKind.CREATE_RESOURCE, + }); diff --git a/src/renderer/components/dock/create-resource.tsx b/src/renderer/components/dock/create-resource.tsx index 513b7134bd..c6689d786a 100644 --- a/src/renderer/components/dock/create-resource.tsx +++ b/src/renderer/components/dock/create-resource.tsx @@ -28,8 +28,8 @@ import { GroupSelectOption, Select, SelectOption } from "../select"; import yaml from "js-yaml"; import { makeObservable, observable } from "mobx"; import { observer } from "mobx-react"; -import { createResourceStore } from "./create-resource.store"; -import type { DockTab } from "./dock.store"; +import type { CreateResourceStore } from "./create-resource-store/create-resource.store"; +import type { DockTab } from "./dock-store/dock.store"; import { EditorPanel } from "./editor-panel"; import { InfoPanel } from "./info-panel"; import * as resourceApplierApi from "../../../common/k8s-api/endpoints/resource-applier.api"; @@ -40,25 +40,32 @@ import { getDetailsUrl } from "../kube-detail-params"; import { apiManager } from "../../../common/k8s-api/api-manager"; import { prevDefault } from "../../utils"; import { navigate } from "../../navigation"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import createResourceStoreInjectable + from "./create-resource-store/create-resource-store.injectable"; interface Props { tab: DockTab; } +interface Dependencies { + createResourceStore: CreateResourceStore +} + @observer -export class CreateResource extends React.Component { +class NonInjectedCreateResource extends React.Component { @observable currentTemplates: Map = new Map(); @observable error = ""; @observable templates: GroupSelectOption[] = []; - constructor(props: Props) { + constructor(props: Props & Dependencies) { super(props); makeObservable(this); } componentDidMount() { - createResourceStore.getMergedTemplates().then(v => this.updateGroupSelectOptions(v)); - createResourceStore.watchUserTemplates(() => createResourceStore.getMergedTemplates().then(v => this.updateGroupSelectOptions(v))); + this.props.createResourceStore.getMergedTemplates().then(v => this.updateGroupSelectOptions(v)); + this.props.createResourceStore.watchUserTemplates(() => this.props.createResourceStore.getMergedTemplates().then(v => this.updateGroupSelectOptions(v))); } updateGroupSelectOptions(templates: Record) { @@ -77,7 +84,7 @@ export class CreateResource extends React.Component { } get data() { - return createResourceStore.getData(this.tabId); + return this.props.createResourceStore.getData(this.tabId); } get currentTemplate() { @@ -86,7 +93,7 @@ export class CreateResource extends React.Component { onChange = (value: string) => { this.error = ""; // reset first, validation goes later - createResourceStore.setData(this.tabId, value); + this.props.createResourceStore.setData(this.tabId, value); }; onError = (error: Error | string) => { @@ -96,7 +103,7 @@ export class CreateResource extends React.Component { onSelectTemplate = (item: SelectOption) => { this.currentTemplates.set(this.tabId, item); fs.readFile(item.value, "utf8").then(v => { - createResourceStore.setData(this.tabId, v); + this.props.createResourceStore.setData(this.tabId, v); }); }; @@ -180,3 +187,14 @@ export class CreateResource extends React.Component { ); } } + +export const CreateResource = withInjectables( + NonInjectedCreateResource, + + { + getProps: (di, props) => ({ + createResourceStore: di.inject(createResourceStoreInjectable), + ...props, + }), + }, +); diff --git a/src/renderer/components/dock/create-terminal-tab/create-terminal-tab.injectable.ts b/src/renderer/components/dock/create-terminal-tab/create-terminal-tab.injectable.ts new file mode 100644 index 0000000000..1b50bfc319 --- /dev/null +++ b/src/renderer/components/dock/create-terminal-tab/create-terminal-tab.injectable.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { createTerminalTab } from "./create-terminal-tab"; +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import dockStoreInjectable from "../dock-store/dock-store.injectable"; + +const createTerminalTabInjectable = getInjectable({ + instantiate: (di) => createTerminalTab({ + dockStore: di.inject(dockStoreInjectable), + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default createTerminalTabInjectable; diff --git a/src/renderer/components/dock/create-terminal-tab/create-terminal-tab.ts b/src/renderer/components/dock/create-terminal-tab/create-terminal-tab.ts new file mode 100644 index 0000000000..f6835f8247 --- /dev/null +++ b/src/renderer/components/dock/create-terminal-tab/create-terminal-tab.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { + DockStore, + DockTabCreateSpecific, + TabKind, +} from "../dock-store/dock.store"; + +interface Dependencies { + dockStore: DockStore; +} + +export const createTerminalTab = + ({ dockStore }: Dependencies) => + (tabParams: DockTabCreateSpecific = {}) => + dockStore.createTab({ + title: `Terminal`, + ...tabParams, + kind: TabKind.TERMINAL, + }); diff --git a/src/renderer/components/dock/create-upgrade-chart-tab/create-upgrade-chart-tab.injectable.ts b/src/renderer/components/dock/create-upgrade-chart-tab/create-upgrade-chart-tab.injectable.ts new file mode 100644 index 0000000000..e4057eb80b --- /dev/null +++ b/src/renderer/components/dock/create-upgrade-chart-tab/create-upgrade-chart-tab.injectable.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { createUpgradeChartTab } from "./create-upgrade-chart-tab"; +import upgradeChartStoreInjectable from "../upgrade-chart-store/upgrade-chart-store.injectable"; +import dockStoreInjectable from "../dock-store/dock-store.injectable"; + +const createUpgradeChartTabInjectable = getInjectable({ + instantiate: (di) => createUpgradeChartTab({ + upgradeChartStore: di.inject(upgradeChartStoreInjectable), + dockStore: di.inject(dockStoreInjectable), + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default createUpgradeChartTabInjectable; diff --git a/src/renderer/components/dock/create-upgrade-chart-tab/create-upgrade-chart-tab.ts b/src/renderer/components/dock/create-upgrade-chart-tab/create-upgrade-chart-tab.ts new file mode 100644 index 0000000000..721ed5b209 --- /dev/null +++ b/src/renderer/components/dock/create-upgrade-chart-tab/create-upgrade-chart-tab.ts @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import type { HelmRelease } from "../../../../common/k8s-api/endpoints/helm-releases.api"; +import { DockStore, DockTabCreateSpecific, TabKind } from "../dock-store/dock.store"; +import type { UpgradeChartStore } from "../upgrade-chart-store/upgrade-chart.store"; + +interface Dependencies { + upgradeChartStore: UpgradeChartStore; + dockStore: DockStore +} + +export const createUpgradeChartTab = + ({ upgradeChartStore, dockStore }: Dependencies) => + (release: HelmRelease, tabParams: DockTabCreateSpecific = {}) => { + let tab = upgradeChartStore.getTabByRelease(release.getName()); + + if (tab) { + dockStore.open(); + dockStore.selectTab(tab.id); + } + + if (!tab) { + tab = dockStore.createTab( + { + title: `Helm Upgrade: ${release.getName()}`, + ...tabParams, + kind: TabKind.UPGRADE_CHART, + }, + false, + ); + + upgradeChartStore.setData(tab.id, { + releaseName: release.getName(), + releaseNamespace: release.getNs(), + }); + } + + return tab; + }; diff --git a/src/renderer/components/dock/dock-store/dock-store.injectable.ts b/src/renderer/components/dock/dock-store/dock-store.injectable.ts new file mode 100644 index 0000000000..58bf854485 --- /dev/null +++ b/src/renderer/components/dock/dock-store/dock-store.injectable.ts @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { DockStorageState, DockStore, TabKind } from "./dock.store"; +import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable"; + +const dockStoreInjectable = getInjectable({ + instantiate: (di) => { + const createStorage = di.inject(createStorageInjectable); + + const storage = createStorage("dock", { + height: 300, + tabs: [ + { + id: "terminal", + kind: TabKind.TERMINAL, + title: "Terminal", + pinned: false, + }, + ], + }); + + return new DockStore({ + storage, + }); + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default dockStoreInjectable; diff --git a/src/renderer/components/dock/dock.store.ts b/src/renderer/components/dock/dock-store/dock.store.ts similarity index 90% rename from src/renderer/components/dock/dock.store.ts rename to src/renderer/components/dock/dock-store/dock.store.ts index 372ecbf871..e3cc16acb1 100644 --- a/src/renderer/components/dock/dock.store.ts +++ b/src/renderer/components/dock/dock-store/dock.store.ts @@ -21,7 +21,7 @@ import * as uuid from "uuid"; import { action, comparer, computed, makeObservable, observable, reaction, runInAction } from "mobx"; -import { autoBind, createStorage } from "../../utils"; +import { autoBind, StorageHelper } from "../../../utils"; import throttle from "lodash/throttle"; export type TabId = string; @@ -113,8 +113,12 @@ export interface DockTabCloseEvent { tabId: TabId; // closed tab id } +interface Dependencies { + storage: StorageHelper +} + export class DockStore implements DockStorageState { - constructor() { + constructor(private dependencies: Dependencies) { makeObservable(this); autoBind(this); this.init(); @@ -123,45 +127,38 @@ export class DockStore implements DockStorageState { readonly minHeight = 100; @observable fullSize = false; - private storage = createStorage("dock", { - height: 300, - tabs: [ - { id: "terminal", kind: TabKind.TERMINAL, title: "Terminal", pinned: false }, - ], - }); - get whenReady() { - return this.storage.whenReady; + return this.dependencies.storage.whenReady; } get isOpen(): boolean { - return this.storage.get().isOpen; + return this.dependencies.storage.get().isOpen; } set isOpen(isOpen: boolean) { - this.storage.merge({ isOpen }); + this.dependencies.storage.merge({ isOpen }); } get height(): number { - return this.storage.get().height; + return this.dependencies.storage.get().height; } set height(height: number) { - this.storage.merge({ + this.dependencies.storage.merge({ height: Math.max(this.minHeight, Math.min(height || this.minHeight, this.maxHeight)), }); } get tabs(): DockTab[] { - return this.storage.get().tabs; + return this.dependencies.storage.get().tabs; } set tabs(tabs: DockTab[]) { - this.storage.merge({ tabs }); + this.dependencies.storage.merge({ tabs }); } get selectedTabId(): TabId | undefined { - return this.storage.get().selectedTabId + return this.dependencies.storage.get().selectedTabId || ( this.tabs.length > 0 ? this.tabs[0]?.id @@ -172,7 +169,7 @@ export class DockStore implements DockStorageState { set selectedTabId(tabId: TabId) { if (tabId && !this.getTabById(tabId)) return; // skip invalid ids - this.storage.merge({ selectedTabId: tabId }); + this.dependencies.storage.merge({ selectedTabId: tabId }); } @computed get selectedTab() { @@ -206,7 +203,7 @@ export class DockStore implements DockStorageState { } onTabClose(callback: (evt: DockTabCloseEvent) => void, opts: { fireImmediately?: boolean } = {}) { - return reaction(() => dockStore.tabs.map(tab => tab.id), (tabs: TabId[], prevTabs?: TabId[]) => { + return reaction(() => this.tabs.map(tab => tab.id), (tabs: TabId[], prevTabs?: TabId[]) => { if (!Array.isArray(prevTabs)) { return; // tabs not yet modified } @@ -292,7 +289,7 @@ export class DockStore implements DockStorageState { } @action - createTab(rawTabDesc: DockTabCreate, addNumber = true): DockTab { + createTab = (rawTabDesc: DockTabCreate, addNumber = true): DockTab => { const { id = uuid.v4(), kind, @@ -322,7 +319,7 @@ export class DockStore implements DockStorageState { this.open(); return tab; - } + }; @action closeTab(tabId: TabId) { @@ -381,8 +378,6 @@ export class DockStore implements DockStorageState { @action reset() { - this.storage?.reset(); + this.dependencies.storage?.reset(); } } - -export const dockStore = new DockStore(); diff --git a/src/renderer/components/dock/dock-tab-store/create-dock-tab-store.injectable.ts b/src/renderer/components/dock/dock-tab-store/create-dock-tab-store.injectable.ts new file mode 100644 index 0000000000..a3a99876bb --- /dev/null +++ b/src/renderer/components/dock/dock-tab-store/create-dock-tab-store.injectable.ts @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { DockTabStore, DockTabStoreOptions } from "./dock-tab.store"; +import dockStoreInjectable from "../dock-store/dock-store.injectable"; +import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable"; + +const createDockTabStoreInjectable = getInjectable({ + instantiate: (di) => { + const dependencies = { + dockStore: di.inject(dockStoreInjectable), + createStorage: di.inject(createStorageInjectable), + }; + + return (options: DockTabStoreOptions = {}) => new DockTabStore(dependencies, options); + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default createDockTabStoreInjectable; diff --git a/src/renderer/components/dock/dock-tab.store.ts b/src/renderer/components/dock/dock-tab-store/dock-tab.store.ts similarity index 84% rename from src/renderer/components/dock/dock-tab.store.ts rename to src/renderer/components/dock/dock-tab-store/dock-tab.store.ts index a554e3fbd2..61a037f3fd 100644 --- a/src/renderer/components/dock/dock-tab.store.ts +++ b/src/renderer/components/dock/dock-tab-store/dock-tab.store.ts @@ -20,8 +20,8 @@ */ import { autorun, observable, reaction } from "mobx"; -import { autoBind, createStorage, StorageHelper, toJS } from "../../utils"; -import { dockStore, TabId } from "./dock.store"; +import { autoBind, StorageHelper, toJS } from "../../../utils"; +import type { DockStore, TabId } from "../dock-store/dock.store"; export interface DockTabStoreOptions { autoInit?: boolean; // load data from storage when `storageKey` is provided and bind events, default: true @@ -30,11 +30,16 @@ export interface DockTabStoreOptions { export type DockTabStorageState = Record; +interface Dependencies { + dockStore: DockStore + createStorage: (storageKey: string, options: DockTabStorageState) => StorageHelper> +} + export class DockTabStore { protected storage?: StorageHelper>; protected data = observable.map(); - constructor(protected options: DockTabStoreOptions = {}) { + constructor(protected dependencies: Dependencies, protected options: DockTabStoreOptions) { autoBind(this); this.options = { @@ -52,7 +57,7 @@ export class DockTabStore { // auto-save to local-storage if (storageKey) { - this.storage = createStorage(storageKey, {}); + this.storage = this.dependencies.createStorage(storageKey, {}); this.storage.whenReady.then(() => { this.data.replace(this.storage.get()); reaction(() => this.toJSON(), data => this.storage.set(data)); @@ -61,7 +66,7 @@ export class DockTabStore { // clear data for closed tabs autorun(() => { - const currentTabs = dockStore.tabs.map(tab => tab.id); + const currentTabs = this.dependencies.dockStore.tabs.map(tab => tab.id); Array.from(this.data.keys()).forEach(tabId => { if (!currentTabs.includes(tabId)) { diff --git a/src/renderer/components/dock/dock-tab.tsx b/src/renderer/components/dock/dock-tab.tsx index 6bd186579a..90208b0e53 100644 --- a/src/renderer/components/dock/dock-tab.tsx +++ b/src/renderer/components/dock/dock-tab.tsx @@ -24,22 +24,28 @@ import "./dock-tab.scss"; import React from "react"; import { observer } from "mobx-react"; import { boundMethod, cssNames, prevDefault, isMiddleClick } from "../../utils"; -import { dockStore, DockTab as DockTabModel } from "./dock.store"; +import type { DockStore, DockTab as DockTabModel } from "./dock-store/dock.store"; import { Tab, TabProps } from "../tabs"; import { Icon } from "../icon"; import { Menu, MenuItem } from "../menu"; import { observable, makeObservable } from "mobx"; import { isMac } from "../../../common/vars"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import dockStoreInjectable from "./dock-store/dock-store.injectable"; export interface DockTabProps extends TabProps { moreActions?: React.ReactNode; } +interface Dependencies { + dockStore: DockStore +} + @observer -export class DockTab extends React.Component { +class NonInjectedDockTab extends React.Component { @observable menuVisible = false; - constructor(props: DockTabProps) { + constructor(props: DockTabProps & Dependencies) { super(props); makeObservable(this); } @@ -50,11 +56,11 @@ export class DockTab extends React.Component { @boundMethod close() { - dockStore.closeTab(this.tabId); + this.props.dockStore.closeTab(this.tabId); } renderMenu() { - const { closeTab, closeAllTabs, closeOtherTabs, closeTabsToTheRight, tabs, getTabIndex } = dockStore; + const { closeTab, closeAllTabs, closeOtherTabs, closeTabsToTheRight, tabs, getTabIndex } = this.props.dockStore; const closeAllDisabled = tabs.length === 1; const closeOtherDisabled = tabs.length === 1; const closeRightDisabled = getTabIndex(this.tabId) === tabs.length - 1; @@ -86,7 +92,7 @@ export class DockTab extends React.Component { } render() { - const { className, moreActions, ...tabProps } = this.props; + const { className, moreActions, dockStore, ...tabProps } = this.props; const { title, pinned } = tabProps.value; const label = (
@@ -116,3 +122,14 @@ export class DockTab extends React.Component { ); } } + +export const DockTab = withInjectables( + NonInjectedDockTab, + + { + getProps: (di, props) => ({ + dockStore: di.inject(dockStoreInjectable), + ...props, + }), + }, +); diff --git a/src/renderer/components/dock/dock-tabs.tsx b/src/renderer/components/dock/dock-tabs.tsx index 372ef100d9..7a218320e6 100644 --- a/src/renderer/components/dock/dock-tabs.tsx +++ b/src/renderer/components/dock/dock-tabs.tsx @@ -24,8 +24,8 @@ import React, { Fragment } from "react"; import { Icon } from "../icon"; import { Tabs } from "../tabs/tabs"; import { DockTab } from "./dock-tab"; -import type { DockTab as DockTabModel } from "./dock.store"; -import { TabKind } from "./dock.store"; +import type { DockTab as DockTabModel } from "./dock-store/dock.store"; +import { TabKind } from "./dock-store/dock.store"; import { TerminalTab } from "./terminal-tab"; interface Props { diff --git a/src/renderer/components/dock/dock.tsx b/src/renderer/components/dock/dock.tsx index 747fd37a5c..c636352b30 100644 --- a/src/renderer/components/dock/dock.tsx +++ b/src/renderer/components/dock/dock.tsx @@ -30,22 +30,30 @@ import { MenuItem } from "../menu"; import { MenuActions } from "../menu/menu-actions"; import { ResizeDirection, ResizingAnchor } from "../resizing-anchor"; import { CreateResource } from "./create-resource"; -import { createResourceTab } from "./create-resource.store"; import { DockTabs } from "./dock-tabs"; -import { dockStore, DockTab, TabKind } from "./dock.store"; +import { DockStore, DockTab, TabKind } from "./dock-store/dock.store"; import { EditResource } from "./edit-resource"; import { InstallChart } from "./install-chart"; import { Logs } from "./logs"; import { TerminalWindow } from "./terminal-window"; -import { createTerminalTab } from "./terminal.store"; import { UpgradeChart } from "./upgrade-chart"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import createResourceTabInjectable from "./create-resource-tab/create-resource-tab.injectable"; +import dockStoreInjectable from "./dock-store/dock-store.injectable"; +import createTerminalTabInjectable from "./create-terminal-tab/create-terminal-tab.injectable"; interface Props { className?: string; } +interface Dependencies { + createResourceTab: () => void + createTerminalTab: () => void + dockStore: DockStore +} + @observer -export class Dock extends React.Component { +class NonInjectedDock extends React.Component { private element = React.createRef(); componentDidMount() { @@ -57,7 +65,7 @@ export class Dock extends React.Component { } onKeyDown = (evt: KeyboardEvent) => { - const { close, selectedTab, closeTab } = dockStore; + const { close, selectedTab, closeTab } = this.props.dockStore; const { code, ctrlKey, metaKey, shiftKey } = evt; // Determine if user working inside or using any other areas in app const dockIsFocused = this.element?.current.contains(document.activeElement); @@ -75,7 +83,7 @@ export class Dock extends React.Component { }; onChangeTab = (tab: DockTab) => { - const { open, selectTab } = dockStore; + const { open, selectTab } = this.props.dockStore; open(); selectTab(tab.id); @@ -100,7 +108,7 @@ export class Dock extends React.Component { } renderTabContent() { - const { isOpen, height, selectedTab } = dockStore; + const { isOpen, height, selectedTab } = this.props.dockStore; if (!isOpen || !selectedTab) return null; @@ -112,8 +120,8 @@ export class Dock extends React.Component { } render() { - const { className } = this.props; - const { isOpen, toggle, tabs, toggleFillSize, selectedTab, hasTabs, fullSize } = dockStore; + const { className, dockStore } = this.props; + const { isOpen, toggle, tabs, toggleFillSize, selectedTab, hasTabs, fullSize } = this.props.dockStore; return (
{
- createTerminalTab()}> + this.props.createTerminalTab()}> Terminal session - createResourceTab()}> + this.props.createResourceTab()}> Create resource @@ -173,3 +181,17 @@ export class Dock extends React.Component { ); } } + +export const Dock = withInjectables( + NonInjectedDock, + + { + getProps: (di, props) => ({ + createResourceTab: di.inject(createResourceTabInjectable), + dockStore: di.inject(dockStoreInjectable), + createTerminalTab: di.inject(createTerminalTabInjectable), + ...props, + }), + }, +); + diff --git a/src/renderer/components/dock/edit-resource-store/edit-resource-store.injectable.ts b/src/renderer/components/dock/edit-resource-store/edit-resource-store.injectable.ts new file mode 100644 index 0000000000..dc1c33090d --- /dev/null +++ b/src/renderer/components/dock/edit-resource-store/edit-resource-store.injectable.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import dockStoreInjectable from "../dock-store/dock-store.injectable"; +import { EditResourceStore } from "./edit-resource.store"; +import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable"; + +const editResourceStoreInjectable = getInjectable({ + instantiate: (di) => + new EditResourceStore({ + dockStore: di.inject(dockStoreInjectable), + createStorage: di.inject(createStorageInjectable), + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default editResourceStoreInjectable; diff --git a/src/renderer/components/dock/edit-resource.store.ts b/src/renderer/components/dock/edit-resource-store/edit-resource.store.ts similarity index 74% rename from src/renderer/components/dock/edit-resource.store.ts rename to src/renderer/components/dock/edit-resource-store/edit-resource.store.ts index 1b9c5fc1f8..e793537efe 100644 --- a/src/renderer/components/dock/edit-resource.store.ts +++ b/src/renderer/components/dock/edit-resource-store/edit-resource.store.ts @@ -19,13 +19,13 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { autoBind, noop } from "../../utils"; -import { DockTabStore } from "./dock-tab.store"; +import { autoBind, noop, StorageHelper } from "../../../utils"; +import { DockTabStorageState, DockTabStore } from "../dock-tab-store/dock-tab.store"; import { autorun, IReactionDisposer } from "mobx"; -import { dockStore, DockTab, DockTabCreateSpecific, TabId, TabKind } from "./dock.store"; -import type { KubeObject } from "../../../common/k8s-api/kube-object"; -import { apiManager } from "../../../common/k8s-api/api-manager"; -import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; +import type { DockStore, DockTab, TabId } from "../dock-store/dock.store"; +import type { KubeObject } from "../../../../common/k8s-api/kube-object"; +import { apiManager } from "../../../../common/k8s-api/api-manager"; +import type { KubeObjectStore } from "../../../../common/k8s-api/kube-object.store"; export interface EditingResource { resource: string; // resource path, e.g. /api/v1/namespaces/default @@ -33,13 +33,19 @@ export interface EditingResource { firstDraft?: string; } +interface Dependencies { + dockStore: DockStore + createStorage: (storageKey: string, options: DockTabStorageState) => StorageHelper> +} + export class EditResourceStore extends DockTabStore { private watchers = new Map(); - constructor() { - super({ + constructor(protected dependencies: Dependencies) { + super(dependencies, { storageKey: "edit_resource_store", }); + autoBind(this); } @@ -56,7 +62,7 @@ export class EditResourceStore extends DockTabStore { const store = apiManager.getStore(resource); if (store) { - const isActiveTab = dockStore.isOpen && dockStore.selectedTabId === tabId; + const isActiveTab = this.dependencies.dockStore.isOpen && this.dependencies.dockStore.selectedTabId === tabId; const obj = store.getByPath(resource); // preload resource for editing @@ -65,7 +71,7 @@ export class EditResourceStore extends DockTabStore { } // auto-close tab when resource removed from store else if (!obj && store.isLoaded) { - dockStore.closeTab(tabId); + this.dependencies.dockStore.closeTab(tabId); } } }, { @@ -102,7 +108,7 @@ export class EditResourceStore extends DockTabStore { return object.selfLink === resource; }) || []; - return dockStore.getTabById(tabId); + return this.dependencies.dockStore.getTabById(tabId); } clearInitialDraft(tabId: TabId): void { @@ -117,29 +123,3 @@ export class EditResourceStore extends DockTabStore { }); } } - -export const editResourceStore = new EditResourceStore(); - -export function editResourceTab(object: KubeObject, tabParams: DockTabCreateSpecific = {}) { - // use existing tab if already opened - let tab = editResourceStore.getTabByResource(object); - - if (tab) { - dockStore.open(); - dockStore.selectTab(tab.id); - } - - // or create new tab - if (!tab) { - tab = dockStore.createTab({ - title: `${object.kind}: ${object.getName()}`, - ...tabParams, - kind: TabKind.EDIT_RESOURCE, - }, false); - editResourceStore.setData(tab.id, { - resource: object.selfLink, - }); - } - - return tab; -} diff --git a/src/renderer/components/kube-object-menu/dependencies/edit-resource-tab.injectable.ts b/src/renderer/components/dock/edit-resource-tab/edit-resource-tab.injectable.ts similarity index 77% rename from src/renderer/components/kube-object-menu/dependencies/edit-resource-tab.injectable.ts rename to src/renderer/components/dock/edit-resource-tab/edit-resource-tab.injectable.ts index 69b2c0a8e1..087ecc461c 100644 --- a/src/renderer/components/kube-object-menu/dependencies/edit-resource-tab.injectable.ts +++ b/src/renderer/components/dock/edit-resource-tab/edit-resource-tab.injectable.ts @@ -18,11 +18,17 @@ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { editResourceTab } from "../../dock/edit-resource.store"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { editResourceTab } from "./edit-resource-tab"; +import editResourceStoreInjectable from "../edit-resource-store/edit-resource-store.injectable"; +import dockStoreInjectable from "../dock-store/dock-store.injectable"; const editResourceTabInjectable = getInjectable({ - instantiate: () => editResourceTab, + instantiate: (di) => editResourceTab({ + dockStore: di.inject(dockStoreInjectable), + editResourceStore: di.inject(editResourceStoreInjectable), + }), + lifecycle: lifecycleEnum.singleton, }); diff --git a/src/renderer/components/dock/edit-resource-tab/edit-resource-tab.ts b/src/renderer/components/dock/edit-resource-tab/edit-resource-tab.ts new file mode 100644 index 0000000000..30878fe2c7 --- /dev/null +++ b/src/renderer/components/dock/edit-resource-tab/edit-resource-tab.ts @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import type { KubeObject } from "../../../../common/k8s-api/kube-object"; +import { DockStore, DockTabCreateSpecific, TabKind } from "../dock-store/dock.store"; +import type { EditResourceStore } from "../edit-resource-store/edit-resource.store"; + +interface Dependencies { + dockStore: DockStore; + editResourceStore: EditResourceStore; +} + +export const editResourceTab = + ({ dockStore, editResourceStore }: Dependencies) => + (object: KubeObject, tabParams: DockTabCreateSpecific = {}) => { + // use existing tab if already opened + let tab = editResourceStore.getTabByResource(object); + + if (tab) { + dockStore.open(); + dockStore.selectTab(tab.id); + } + + // or create new tab + if (!tab) { + tab = dockStore.createTab( + { + title: `${object.kind}: ${object.getName()}`, + ...tabParams, + kind: TabKind.EDIT_RESOURCE, + }, + false, + ); + editResourceStore.setData(tab.id, { + resource: object.selfLink, + }); + } + + return tab; + }; diff --git a/src/renderer/components/dock/edit-resource.tsx b/src/renderer/components/dock/edit-resource.tsx index 78171db965..506364b61f 100644 --- a/src/renderer/components/dock/edit-resource.tsx +++ b/src/renderer/components/dock/edit-resource.tsx @@ -25,24 +25,30 @@ import React from "react"; import { computed, makeObservable, observable } from "mobx"; import { observer } from "mobx-react"; import yaml from "js-yaml"; -import type { DockTab } from "./dock.store"; -import { editResourceStore } from "./edit-resource.store"; +import type { DockTab } from "./dock-store/dock.store"; +import type { EditResourceStore } from "./edit-resource-store/edit-resource.store"; import { InfoPanel } from "./info-panel"; import { Badge } from "../badge"; import { EditorPanel } from "./editor-panel"; import { Spinner } from "../spinner"; import type { KubeObject } from "../../../common/k8s-api/kube-object"; import { createPatch } from "rfc6902"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import editResourceStoreInjectable from "./edit-resource-store/edit-resource-store.injectable"; interface Props { tab: DockTab; } +interface Dependencies { + editResourceStore: EditResourceStore +} + @observer -export class EditResource extends React.Component { +class NonInjectedEditResource extends React.Component { @observable error = ""; - constructor(props: Props) { + constructor(props: Props & Dependencies) { super(props); makeObservable(this); } @@ -52,11 +58,11 @@ export class EditResource extends React.Component { } get isReadyForEditing() { - return editResourceStore.isReady(this.tabId); + return this.props.editResourceStore.isReady(this.tabId); } get resource(): KubeObject | undefined { - return editResourceStore.getResource(this.tabId); + return this.props.editResourceStore.getResource(this.tabId); } @computed get draft(): string { @@ -64,7 +70,7 @@ export class EditResource extends React.Component { return ""; // wait until tab's data and kube-object resource are loaded } - const editData = editResourceStore.getData(this.tabId); + const editData = this.props.editResourceStore.getData(this.tabId); if (typeof editData.draft === "string") { return editData.draft; @@ -76,7 +82,7 @@ export class EditResource extends React.Component { } saveDraft(draft: string) { - editResourceStore.getData(this.tabId).draft = draft; + this.props.editResourceStore.getData(this.tabId).draft = draft; } onChange = (draft: string) => { @@ -93,13 +99,13 @@ export class EditResource extends React.Component { return null; } - const store = editResourceStore.getStore(this.tabId); + const store = this.props.editResourceStore.getStore(this.tabId); const currentVersion = yaml.load(this.draft); - const firstVersion = yaml.load(editResourceStore.getData(this.tabId).firstDraft ?? this.draft); + const firstVersion = yaml.load(this.props.editResourceStore.getData(this.tabId).firstDraft ?? this.draft); const patches = createPatch(firstVersion, currentVersion); const updatedResource = await store.patch(this.resource, patches); - editResourceStore.clearInitialDraft(this.tabId); + this.props.editResourceStore.clearInitialDraft(this.tabId); return (

@@ -141,3 +147,14 @@ export class EditResource extends React.Component { ); } } + +export const EditResource = withInjectables( + NonInjectedEditResource, + + { + getProps: (di, props) => ({ + editResourceStore: di.inject(editResourceStoreInjectable), + ...props, + }), + }, +); diff --git a/src/renderer/components/dock/editor-panel.tsx b/src/renderer/components/dock/editor-panel.tsx index df9cc15e10..80547bf3a6 100644 --- a/src/renderer/components/dock/editor-panel.tsx +++ b/src/renderer/components/dock/editor-panel.tsx @@ -24,9 +24,11 @@ import throttle from "lodash/throttle"; import React from "react"; import { makeObservable, observable, reaction } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; -import { dockStore, TabId } from "./dock.store"; +import type { DockStore, TabId } from "./dock-store/dock.store"; import { cssNames } from "../../utils"; import { MonacoEditor, MonacoEditorProps } from "../monaco-editor"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import dockStoreInjectable from "./dock-store/dock-store.injectable"; export interface EditorPanelProps { tabId: TabId; @@ -37,17 +39,21 @@ export interface EditorPanelProps { onError?: MonacoEditorProps["onError"]; } +interface Dependencies { + dockStore: DockStore +} + const defaultProps: Partial = { autoFocus: true, }; @observer -export class EditorPanel extends React.Component { +class NonInjectedEditorPanel extends React.Component { static defaultProps = defaultProps as object; @observable.ref editor?: MonacoEditor; - constructor(props: EditorPanelProps) { + constructor(props: EditorPanelProps & Dependencies) { super(props); makeObservable(this); } @@ -55,12 +61,12 @@ export class EditorPanel extends React.Component { componentDidMount() { disposeOnUnmount(this, [ // keep focus on editor's area when just opened - reaction(() => dockStore.isOpen, isOpen => isOpen && this.editor?.focus(), { + reaction(() => this.props.dockStore.isOpen, isOpen => isOpen && this.editor?.focus(), { fireImmediately: true, }), // focus to editor on dock's resize or turning into fullscreen mode - dockStore.onResize(throttle(() => this.editor?.focus(), 250)), + this.props.dockStore.onResize(throttle(() => this.editor?.focus(), 250)), ]); } @@ -82,3 +88,14 @@ export class EditorPanel extends React.Component { ); } } + +export const EditorPanel = withInjectables( + NonInjectedEditorPanel, + + { + getProps: (di, props) => ({ + dockStore: di.inject(dockStoreInjectable), + ...props, + }), + }, +); diff --git a/src/renderer/components/dock/info-panel.tsx b/src/renderer/components/dock/info-panel.tsx index f14e7b86f1..18ba23e546 100644 --- a/src/renderer/components/dock/info-panel.tsx +++ b/src/renderer/components/dock/info-panel.tsx @@ -28,8 +28,10 @@ import { cssNames } from "../../utils"; import { Button } from "../button"; import { Icon } from "../icon"; import { Spinner } from "../spinner"; -import { dockStore, TabId } from "./dock.store"; +import type { DockStore, TabId } from "./dock-store/dock.store"; import { Notifications } from "../notifications"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import dockStoreInjectable from "./dock-store/dock-store.injectable"; interface Props extends OptionalProps { tabId: TabId; @@ -50,8 +52,12 @@ interface OptionalProps { showStatusPanel?: boolean; } +interface Dependencies { + dockStore: DockStore +} + @observer -export class InfoPanel extends Component { +class NonInjectedInfoPanel extends Component { static defaultProps: OptionalProps = { submitLabel: "Submit", submittingMessage: "Submitting..", @@ -65,7 +71,7 @@ export class InfoPanel extends Component { @observable error = ""; @observable waiting = false; - constructor(props: Props) { + constructor(props: Props & Dependencies) { super(props); makeObservable(this); } @@ -104,7 +110,7 @@ export class InfoPanel extends Component { }; close = () => { - dockStore.closeTab(this.props.tabId); + this.props.dockStore.closeTab(this.props.tabId); }; renderErrorIcon() { @@ -159,3 +165,14 @@ export class InfoPanel extends Component { ); } } + +export const InfoPanel = withInjectables( + NonInjectedInfoPanel, + + { + getProps: (di, props) => ({ + dockStore: di.inject(dockStoreInjectable), + ...props, + }), + }, +); diff --git a/src/renderer/components/dock/install-chart-store/install-chart-store.injectable.ts b/src/renderer/components/dock/install-chart-store/install-chart-store.injectable.ts new file mode 100644 index 0000000000..7dafc1f357 --- /dev/null +++ b/src/renderer/components/dock/install-chart-store/install-chart-store.injectable.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { InstallChartStore } from "./install-chart.store"; +import dockStoreInjectable from "../dock-store/dock-store.injectable"; +import createDockTabStoreInjectable from "../dock-tab-store/create-dock-tab-store.injectable"; +import type { IReleaseUpdateDetails } from "../../../../common/k8s-api/endpoints/helm-releases.api"; +import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable"; + +const installChartStoreInjectable = getInjectable({ + instantiate: (di) => { + const createDockTabStore = di.inject(createDockTabStoreInjectable); + + return new InstallChartStore({ + dockStore: di.inject(dockStoreInjectable), + createStorage: di.inject(createStorageInjectable), + versionsStore: createDockTabStore(), + detailsStore: createDockTabStore(), + }); + }, + lifecycle: lifecycleEnum.singleton, +}); + +export default installChartStoreInjectable; diff --git a/src/renderer/components/dock/install-chart.store.ts b/src/renderer/components/dock/install-chart-store/install-chart.store.ts similarity index 71% rename from src/renderer/components/dock/install-chart.store.ts rename to src/renderer/components/dock/install-chart-store/install-chart.store.ts index 5f624bc0c1..b9aa2f694f 100644 --- a/src/renderer/components/dock/install-chart.store.ts +++ b/src/renderer/components/dock/install-chart-store/install-chart.store.ts @@ -20,11 +20,12 @@ */ import { action, autorun, makeObservable } from "mobx"; -import { dockStore, DockTabCreateSpecific, TabId, TabKind } from "./dock.store"; -import { DockTabStore } from "./dock-tab.store"; -import { getChartDetails, getChartValues, HelmChart } from "../../../common/k8s-api/endpoints/helm-charts.api"; -import type { IReleaseUpdateDetails } from "../../../common/k8s-api/endpoints/helm-releases.api"; -import { Notifications } from "../notifications"; +import { DockStore, TabId, TabKind } from "../dock-store/dock.store"; +import { DockTabStorageState, DockTabStore } from "../dock-tab-store/dock-tab.store"; +import { getChartDetails, getChartValues } from "../../../../common/k8s-api/endpoints/helm-charts.api"; +import type { IReleaseUpdateDetails } from "../../../../common/k8s-api/endpoints/helm-releases.api"; +import { Notifications } from "../../notifications"; +import type { StorageHelper } from "../../../utils"; export interface IChartInstallData { name: string; @@ -37,17 +38,24 @@ export interface IChartInstallData { lastVersion?: boolean; } -export class InstallChartStore extends DockTabStore { - public versions = new DockTabStore(); - public details = new DockTabStore(); +interface Dependencies { + dockStore: DockStore, + createStorage: (storageKey: string, options: DockTabStorageState) => StorageHelper> - constructor() { - super({ - storageKey: "install_charts", - }); + versionsStore: DockTabStore, + detailsStore: DockTabStore +} + +export class InstallChartStore extends DockTabStore { + constructor(protected dependencies: Dependencies) { + super( + dependencies, + { storageKey: "install_charts" }, + ); + makeObservable(this); autorun(() => { - const { selectedTab, isOpen } = dockStore; + const { selectedTab, isOpen } = dependencies.dockStore; if (selectedTab?.kind === TabKind.INSTALL_CHART && isOpen) { this.loadData(selectedTab.id) @@ -56,6 +64,14 @@ export class InstallChartStore extends DockTabStore { }, { delay: 250 }); } + get versions() { + return this.dependencies.versionsStore; + } + + get details() { + return this.dependencies.detailsStore; + } + @action async loadData(tabId: string) { const promises = []; @@ -99,25 +115,3 @@ export class InstallChartStore extends DockTabStore { super.setData(tabId, data); } } - -export const installChartStore = new InstallChartStore(); - -export function createInstallChartTab(chart: HelmChart, tabParams: DockTabCreateSpecific = {}) { - const { name, repo, version } = chart; - const tab = dockStore.createTab({ - title: `Helm Install: ${repo}/${name}`, - ...tabParams, - kind: TabKind.INSTALL_CHART, - }, false); - - installChartStore.setData(tab.id, { - name, - repo, - version, - namespace: "default", - releaseName: "", - description: "", - }); - - return tab; -} diff --git a/src/renderer/components/dock/install-chart.tsx b/src/renderer/components/dock/install-chart.tsx index aa6fceff5d..f8b1bad581 100644 --- a/src/renderer/components/dock/install-chart.tsx +++ b/src/renderer/components/dock/install-chart.tsx @@ -24,39 +24,52 @@ import "./install-chart.scss"; import React, { Component } from "react"; import { action, makeObservable, observable } from "mobx"; import { observer } from "mobx-react"; -import { dockStore, DockTab } from "./dock.store"; +import type { DockStore, DockTab } from "./dock-store/dock.store"; import { InfoPanel } from "./info-panel"; import { Badge } from "../badge"; import { NamespaceSelect } from "../+namespaces/namespace-select"; import { prevDefault } from "../../utils"; -import { IChartInstallData, installChartStore } from "./install-chart.store"; +import type { IChartInstallData, InstallChartStore } from "./install-chart-store/install-chart.store"; import { Spinner } from "../spinner"; import { Icon } from "../icon"; import { Button } from "../button"; -import { releaseStore } from "../+apps-releases/release.store"; import { LogsDialog } from "../dialog/logs-dialog"; import { Select, SelectOption } from "../select"; import { Input } from "../input"; import { EditorPanel } from "./editor-panel"; import { navigate } from "../../navigation"; import { releaseURL } from "../../../common/routes"; +import type { + IReleaseCreatePayload, + IReleaseUpdateDetails, +} from "../../../common/k8s-api/endpoints/helm-releases.api"; +import releaseStoreInjectable from "../+apps-releases/release-store.injectable"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import installChartStoreInjectable from "./install-chart-store/install-chart-store.injectable"; +import dockStoreInjectable from "./dock-store/dock-store.injectable"; interface Props { tab: DockTab; } +interface Dependencies { + createRelease: (payload: IReleaseCreatePayload) => Promise + installChartStore: InstallChartStore + dockStore: DockStore +} + @observer -export class InstallChart extends Component { +class NonInjectedInstallChart extends Component { @observable error = ""; @observable showNotes = false; - constructor(props: Props) { + constructor(props: Props & Dependencies) { super(props); makeObservable(this); } get chartData() { - return installChartStore.getData(this.tabId); + return this.props.installChartStore.getData(this.tabId); } get tabId() { @@ -64,11 +77,11 @@ export class InstallChart extends Component { } get versions() { - return installChartStore.versions.getData(this.tabId); + return this.props.installChartStore.versions.getData(this.tabId); } get releaseDetails() { - return installChartStore.details.getData(this.tabId); + return this.props.installChartStore.details.getData(this.tabId); } viewRelease = () => { @@ -80,20 +93,20 @@ export class InstallChart extends Component { namespace: release.namespace, }, })); - dockStore.closeTab(this.tabId); + this.props.dockStore.closeTab(this.tabId); }; save(data: Partial) { const chart = { ...this.chartData, ...data }; - installChartStore.setData(this.tabId, chart); + this.props.installChartStore.setData(this.tabId, chart); } onVersionChange = (option: SelectOption) => { const version = option.value; this.save({ version, values: "" }); - installChartStore.loadValues(this.tabId); + this.props.installChartStore.loadValues(this.tabId); }; @action @@ -117,13 +130,13 @@ export class InstallChart extends Component { install = async () => { const { repo, name, version, namespace, values, releaseName } = this.chartData; - const details = await releaseStore.create({ + const details = await this.props.createRelease({ name: releaseName || undefined, chart: name, repo, namespace, version, values, }); - installChartStore.details.setData(this.tabId, details); + this.props.installChartStore.details.setData(this.tabId, details); return (

Chart Release {details.release.name} successfully created.

@@ -219,3 +232,16 @@ export class InstallChart extends Component { ); } } + +export const InstallChart = withInjectables( + NonInjectedInstallChart, + + { + getProps: (di, props) => ({ + createRelease: di.inject(releaseStoreInjectable).create, + installChartStore: di.inject(installChartStoreInjectable), + dockStore: di.inject(dockStoreInjectable), + ...props, + }), + }, +); diff --git a/src/renderer/components/dock/log-controls.tsx b/src/renderer/components/dock/log-controls.tsx index b7ac1c5fb4..c8904be314 100644 --- a/src/renderer/components/dock/log-controls.tsx +++ b/src/renderer/components/dock/log-controls.tsx @@ -26,10 +26,12 @@ import { observer } from "mobx-react"; import { Pod } from "../../../common/k8s-api/endpoints"; import { cssNames, saveFileDialog } from "../../utils"; -import { logStore } from "./log.store"; import { Checkbox } from "../checkbox"; import { Icon } from "../icon"; -import type { LogTabData } from "./log-tab.store"; +import type { LogTabData } from "./log-tab-store/log-tab.store"; +import type { LogStore } from "./log-store/log.store"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import logStoreInjectable from "./log-store/log-store.injectable"; interface Props { tabData?: LogTabData @@ -38,8 +40,12 @@ interface Props { reload: () => void } -export const LogControls = observer((props: Props) => { - const { tabData, save, reload, logs } = props; +interface Dependencies { + logStore: LogStore +} + +const NonInjectedLogControls = observer((props: Props & Dependencies) => { + const { tabData, save, reload, logs, logStore } = props; if (!tabData) { return null; @@ -98,3 +104,15 @@ export const LogControls = observer((props: Props) => {
); }); + +export const LogControls = withInjectables( + NonInjectedLogControls, + + { + getProps: (di, props) => ({ + logStore: di.inject(logStoreInjectable), + ...props, + }), + }, +); + diff --git a/src/renderer/components/dock/log-list.tsx b/src/renderer/components/dock/log-list.tsx index 1da9fc0dfe..85b04e3531 100644 --- a/src/renderer/components/dock/log-list.tsx +++ b/src/renderer/components/dock/log-list.tsx @@ -29,15 +29,18 @@ import { action, computed, observable, makeObservable, reaction } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; import moment from "moment-timezone"; import type { Align, ListOnScrollProps } from "react-window"; - -import { SearchStore, searchStore } from "../../../common/search-store"; +import { SearchStore } from "../../search-store/search-store"; import { UserStore } from "../../../common/user-store"; import { array, boundMethod, cssNames } from "../../utils"; import { Spinner } from "../spinner"; import { VirtualList } from "../virtual-list"; -import { logStore } from "./log.store"; -import { logTabStore } from "./log-tab.store"; +import type { LogStore } from "./log-store/log.store"; +import type { LogTabStore } from "./log-tab-store/log-tab.store"; import { ToBottom } from "./to-bottom"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import logTabStoreInjectable from "./log-tab-store/log-tab-store.injectable"; +import logStoreInjectable from "./log-store/log-store.injectable"; +import searchStoreInjectable from "../../search-store/search-store.injectable"; interface Props { logs: string[] @@ -48,8 +51,14 @@ interface Props { const colorConverter = new AnsiUp(); +interface Dependencies { + logTabStore: LogTabStore + logStore: LogStore + searchStore: SearchStore +} + @observer -export class LogList extends React.Component { +class NonInjectedLogList extends React.Component { @observable isJumpButtonVisible = false; @observable isLastLineVisible = true; @@ -57,7 +66,7 @@ export class LogList extends React.Component { private virtualListRef = React.createRef(); // A reference for VirtualList component private lineHeight = 18; // Height of a log line. Should correlate with styles in pod-log-list.scss - constructor(props: Props) { + constructor(props: Props & Dependencies) { super(props); makeObservable(this); } @@ -108,14 +117,14 @@ export class LogList extends React.Component { */ @computed get logs() { - const showTimestamps = logTabStore.getData(this.props.id)?.showTimestamps; + const showTimestamps = this.props.logTabStore.getData(this.props.id)?.showTimestamps; if (!showTimestamps) { - return logStore.logsWithoutTimestamps; + return this.props.logStore.logsWithoutTimestamps; } return this.props.logs - .map(log => logStore.splitOutTimestamp(log)) + .map(log => this.props.logStore.splitOutTimestamp(log)) .map(([logTimestamp, log]) => (`${logTimestamp && moment.tz(logTimestamp, UserStore.getInstance().localeTimezone).format()}${log}`)); } @@ -187,7 +196,7 @@ export class LogList extends React.Component { * @returns A react element with a row itself */ getLogRow = (rowIndex: number) => { - const { searchQuery, isActiveOverlay } = searchStore; + const { searchQuery, isActiveOverlay } = this.props.searchStore; const item = this.logs[rowIndex]; const contents: React.ReactElement[] = []; const ansiToHtml = (ansi: string) => DOMPurify.sanitize(colorConverter.ansi_to_html(ansi)); @@ -270,3 +279,17 @@ export class LogList extends React.Component { ); } } + +export const LogList = withInjectables( + NonInjectedLogList, + + { + getProps: (di, props) => ({ + logTabStore: di.inject(logTabStoreInjectable), + logStore: di.inject(logStoreInjectable), + searchStore: di.inject(searchStoreInjectable), + ...props, + }), + }, +); + diff --git a/src/renderer/components/dock/log-resource-selector.tsx b/src/renderer/components/dock/log-resource-selector.tsx index 8bdfa475c4..c623d555fd 100644 --- a/src/renderer/components/dock/log-resource-selector.tsx +++ b/src/renderer/components/dock/log-resource-selector.tsx @@ -27,9 +27,11 @@ import { observer } from "mobx-react"; import { Pod } from "../../../common/k8s-api/endpoints"; import { Badge } from "../badge"; import { Select, SelectOption } from "../select"; -import { LogTabData, logTabStore } from "./log-tab.store"; +import type { LogTabData, LogTabStore } from "./log-tab-store/log-tab.store"; import { podsStore } from "../+workloads-pods/pods.store"; -import type { TabId } from "./dock.store"; +import type { TabId } from "./dock-store/dock.store"; +import logTabStoreInjectable from "./log-tab-store/log-tab-store.injectable"; +import { withInjectables } from "@ogre-tools/injectable-react"; interface Props { tabId: TabId @@ -38,8 +40,12 @@ interface Props { reload: () => void } -export const LogResourceSelector = observer((props: Props) => { - const { tabData, save, reload, tabId } = props; +interface Dependencies { + logTabStore: LogTabStore +} + +const NonInjectedLogResourceSelector = observer((props: Props & Dependencies) => { + const { tabData, save, reload, tabId, logTabStore } = props; const { selectedPod, selectedContainer, pods } = tabData; const pod = new Pod(selectedPod); const containers = pod.getContainers(); @@ -58,6 +64,7 @@ export const LogResourceSelector = observer((props: Props) => { const selectedPod = podsStore.getByName(option.value, pod.getNs()); save({ selectedPod }); + logTabStore.renameTab(tabId); }; @@ -114,3 +121,15 @@ export const LogResourceSelector = observer((props: Props) => {
); }); + +export const LogResourceSelector = withInjectables( + NonInjectedLogResourceSelector, + + { + getProps: (di, props) => ({ + logTabStore: di.inject(logTabStoreInjectable), + ...props, + }), + }, +); + diff --git a/src/renderer/components/dock/log-search.tsx b/src/renderer/components/dock/log-search.tsx index 14b4b3d30c..d0b26b3a15 100644 --- a/src/renderer/components/dock/log-search.tsx +++ b/src/renderer/components/dock/log-search.tsx @@ -24,8 +24,10 @@ import "./log-search.scss"; import React, { useEffect } from "react"; import { observer } from "mobx-react"; import { SearchInput } from "../input"; -import { searchStore } from "../../../common/search-store"; +import type { SearchStore } from "../../search-store/search-store"; import { Icon } from "../icon"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import searchStoreInjectable from "../../search-store/search-store.injectable"; export interface PodLogSearchProps { onSearch: (query: string) => void @@ -37,8 +39,12 @@ interface Props extends PodLogSearchProps { logs: string[] } -export const LogSearch = observer((props: Props) => { - const { logs, onSearch, toPrevOverlay, toNextOverlay } = props; +interface Dependencies { + searchStore: SearchStore +} + +const NonInjectedLogSearch = observer((props: Props & Dependencies) => { + const { logs, onSearch, toPrevOverlay, toNextOverlay, searchStore } = props; const { setNextOverlayActive, setPrevOverlayActive, searchQuery, occurrences, activeFind, totalFinds } = searchStore; const jumpDisabled = !searchQuery || !occurrences.length; const findCounts = ( @@ -102,3 +108,14 @@ export const LogSearch = observer((props: Props) => {
); }); + +export const LogSearch = withInjectables( + NonInjectedLogSearch, + + { + getProps: (di, props) => ({ + searchStore: di.inject(searchStoreInjectable), + ...props, + }), + }, +); diff --git a/src/renderer/components/dock/log-store/log-store.injectable.ts b/src/renderer/components/dock/log-store/log-store.injectable.ts new file mode 100644 index 0000000000..a0c17acb57 --- /dev/null +++ b/src/renderer/components/dock/log-store/log-store.injectable.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { LogStore } from "./log.store"; +import logTabStoreInjectable from "../log-tab-store/log-tab-store.injectable"; +import dockStoreInjectable from "../dock-store/dock-store.injectable"; + +const logStoreInjectable = getInjectable({ + instantiate: (di) => new LogStore({ + logTabStore: di.inject(logTabStoreInjectable), + dockStore: di.inject(dockStoreInjectable), + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default logStoreInjectable; diff --git a/src/renderer/components/dock/log.store.ts b/src/renderer/components/dock/log-store/log.store.ts similarity index 89% rename from src/renderer/components/dock/log.store.ts rename to src/renderer/components/dock/log-store/log.store.ts index 3517db01ae..9f2a26d2af 100644 --- a/src/renderer/components/dock/log.store.ts +++ b/src/renderer/components/dock/log-store/log.store.ts @@ -21,18 +21,23 @@ import { autorun, computed, observable, makeObservable } from "mobx"; -import { IPodLogsQuery, Pod, podsApi } from "../../../common/k8s-api/endpoints"; -import { autoBind, interval } from "../../utils"; -import { dockStore, TabId, TabKind } from "./dock.store"; -import { logTabStore } from "./log-tab.store"; +import { IPodLogsQuery, Pod, podsApi } from "../../../../common/k8s-api/endpoints"; +import { autoBind, interval } from "../../../utils"; +import { DockStore, TabId, TabKind } from "../dock-store/dock.store"; +import type { LogTabStore } from "../log-tab-store/log-tab.store"; type PodLogLine = string; const logLinesToLoad = 500; +interface Dependencies { + logTabStore: LogTabStore + dockStore: DockStore +} + export class LogStore { private refresher = interval(10, () => { - const id = dockStore.selectedTabId; + const id = this.dependencies.dockStore.selectedTabId; if (!this.podLogs.get(id)) return; this.loadMore(id); @@ -40,12 +45,12 @@ export class LogStore { @observable podLogs = observable.map(); - constructor() { + constructor(private dependencies: Dependencies) { makeObservable(this); autoBind(this); autorun(() => { - const { selectedTab, isOpen } = dockStore; + const { selectedTab, isOpen } = this.dependencies.dockStore; if (selectedTab?.kind === TabKind.POD_LOGS && isOpen) { this.refresher.start(); @@ -121,7 +126,7 @@ export class LogStore { * @returns A fetch request promise */ async loadLogs(tabId: TabId, params: Partial): Promise { - const data = logTabStore.getData(tabId); + const data = this.dependencies.logTabStore.getData(tabId); const { selectedContainer, previous } = data; const pod = new Pod(data.selectedPod); const namespace = pod.getNs(); @@ -152,7 +157,7 @@ export class LogStore { */ @computed get logs() { - return this.podLogs.get(dockStore.selectedTabId) ?? []; + return this.podLogs.get(this.dependencies.dockStore.selectedTabId) ?? []; } /** @@ -201,5 +206,3 @@ export class LogStore { this.podLogs.delete(tabId); } } - -export const logStore = new LogStore(); diff --git a/src/renderer/components/dock/log-tab-store/log-tab-store.injectable.ts b/src/renderer/components/dock/log-tab-store/log-tab-store.injectable.ts new file mode 100644 index 0000000000..1b4fba2d32 --- /dev/null +++ b/src/renderer/components/dock/log-tab-store/log-tab-store.injectable.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { LogTabStore } from "./log-tab.store"; +import dockStoreInjectable from "../dock-store/dock-store.injectable"; +import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable"; + +const logTabStoreInjectable = getInjectable({ + instantiate: (di) => new LogTabStore({ + dockStore: di.inject(dockStoreInjectable), + createStorage: di.inject(createStorageInjectable), + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default logTabStoreInjectable; diff --git a/src/renderer/components/dock/log-tab.store.ts b/src/renderer/components/dock/log-tab-store/log-tab.store.ts similarity index 81% rename from src/renderer/components/dock/log-tab.store.ts rename to src/renderer/components/dock/log-tab-store/log-tab.store.ts index 8b767ba91f..7357bf6166 100644 --- a/src/renderer/components/dock/log-tab.store.ts +++ b/src/renderer/components/dock/log-tab-store/log-tab.store.ts @@ -21,13 +21,14 @@ import uniqueId from "lodash/uniqueId"; import { reaction } from "mobx"; -import { podsStore } from "../+workloads-pods/pods.store"; +import { podsStore } from "../../+workloads-pods/pods.store"; -import { IPodContainer, Pod } from "../../../common/k8s-api/endpoints"; -import type { WorkloadKubeObject } from "../../../common/k8s-api/workload-kube-object"; -import logger from "../../../common/logger"; -import { DockTabStore } from "./dock-tab.store"; -import { dockStore, DockTabCreateSpecific, TabKind } from "./dock.store"; +import { IPodContainer, Pod } from "../../../../common/k8s-api/endpoints"; +import type { WorkloadKubeObject } from "../../../../common/k8s-api/workload-kube-object"; +import logger from "../../../../common/logger"; +import { DockTabStorageState, DockTabStore } from "../dock-tab-store/dock-tab.store"; +import { DockStore, DockTabCreateSpecific, TabKind } from "../dock-store/dock.store"; +import type { StorageHelper } from "../../../utils"; export interface LogTabData { pods: Pod[]; @@ -46,9 +47,14 @@ interface WorkloadLogsTabData { workload: WorkloadKubeObject } +interface Dependencies { + dockStore: DockStore + createStorage: (storageKey: string, options: DockTabStorageState) => StorageHelper> +} + export class LogTabStore extends DockTabStore { - constructor() { - super({ + constructor(protected dependencies: Dependencies) { + super(dependencies, { storageKey: "pod_logs", }); @@ -86,11 +92,11 @@ export class LogTabStore extends DockTabStore { renameTab(tabId: string) { const { selectedPod } = this.getData(tabId); - dockStore.renameTab(tabId, `Pod ${selectedPod.metadata.name}`); + this.dependencies.dockStore.renameTab(tabId, `Pod ${selectedPod.metadata.name}`); } private createDockTab(tabParams: DockTabCreateSpecific) { - dockStore.createTab({ + this.dependencies.dockStore.createTab({ ...tabParams, kind: TabKind.POD_LOGS, }, false); @@ -143,8 +149,7 @@ export class LogTabStore extends DockTabStore { private closeTab(tabId: string) { this.clearData(tabId); - dockStore.closeTab(tabId); + this.dependencies.dockStore.closeTab(tabId); } } -export const logTabStore = new LogTabStore(); diff --git a/src/renderer/components/dock/logs.tsx b/src/renderer/components/dock/logs.tsx index 6c5c8d115f..ea982aa527 100644 --- a/src/renderer/components/dock/logs.tsx +++ b/src/renderer/components/dock/logs.tsx @@ -23,29 +23,39 @@ import React from "react"; import { observable, reaction, makeObservable } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; -import { searchStore } from "../../../common/search-store"; import { boundMethod } from "../../utils"; -import type { DockTab } from "./dock.store"; +import type { DockTab } from "./dock-store/dock.store"; import { InfoPanel } from "./info-panel"; import { LogResourceSelector } from "./log-resource-selector"; import { LogList } from "./log-list"; -import { logStore } from "./log.store"; +import type { LogStore } from "./log-store/log.store"; import { LogSearch } from "./log-search"; import { LogControls } from "./log-controls"; -import { LogTabData, logTabStore } from "./log-tab.store"; +import type { LogTabData, LogTabStore } from "./log-tab-store/log-tab.store"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import logTabStoreInjectable from "./log-tab-store/log-tab-store.injectable"; +import logStoreInjectable from "./log-store/log-store.injectable"; +import type { SearchStore } from "../../search-store/search-store"; +import searchStoreInjectable from "../../search-store/search-store.injectable"; interface Props { className?: string tab: DockTab } +interface Dependencies { + logTabStore: LogTabStore + logStore: LogStore + searchStore: SearchStore +} + @observer -export class Logs extends React.Component { +class NonInjectedLogs extends React.Component { @observable isLoading = true; - private logListElement = React.createRef(); // A reference for VirtualList component + private logListElement = React.createRef(); // A reference for VirtualList component - constructor(props: Props) { + constructor(props: Props & Dependencies) { super(props); makeObservable(this); } @@ -62,12 +72,12 @@ export class Logs extends React.Component { load = async () => { this.isLoading = true; - await logStore.load(this.tabId); + await this.props.logStore.load(this.tabId); this.isLoading = false; }; reload = async () => { - logStore.clearLogs(this.tabId); + this.props.logStore.clearLogs(this.tabId); await this.load(); }; @@ -85,10 +95,12 @@ export class Logs extends React.Component { */ @boundMethod toOverlay() { - const { activeOverlayLine } = searchStore; + const { activeOverlayLine } = this.props.searchStore; if (!this.logListElement.current || activeOverlayLine === undefined) return; // Scroll vertically + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore this.logListElement.current.scrollToItem(activeOverlayLine, "center"); // Scroll horizontally in timeout since virtual list need some time to prepare its contents setTimeout(() => { @@ -104,14 +116,14 @@ export class Logs extends React.Component { return null; } - const logs = logStore.logs; - const searchLogs = data.showTimestamps ? logs : logStore.logsWithoutTimestamps; + const logs = this.props.logStore.logs; + const searchLogs = data.showTimestamps ? logs : this.props.logStore.logsWithoutTimestamps; const controls = (
logTabStore.setData(this.tabId, { ...data, ...newData })} + save={newData => this.props.logTabStore.setData(this.tabId, { ...data, ...newData })} reload={this.reload} /> { } render() { - const logs = logStore.logs; - const data = logTabStore.getData(this.tabId); + const logs = this.props.logStore.logs; + const data = this.props.logTabStore.getData(this.tabId); if (!data) { this.reload(); @@ -150,15 +162,30 @@ export class Logs extends React.Component { id={this.tabId} isLoading={this.isLoading} load={this.load} + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore ref={this.logListElement} /> logTabStore.setData(this.tabId, { ...data, ...newData })} + save={newData => this.props.logTabStore.setData(this.tabId, { ...data, ...newData })} reload={this.reload} />
); } } + +export const Logs = withInjectables( + NonInjectedLogs, + + { + getProps: (di, props) => ({ + logTabStore: di.inject(logTabStoreInjectable), + logStore: di.inject(logStoreInjectable), + searchStore: di.inject(searchStoreInjectable), + ...props, + }), + }, +); diff --git a/src/renderer/components/dock/terminal-store/terminal-store.injectable.ts b/src/renderer/components/dock/terminal-store/terminal-store.injectable.ts new file mode 100644 index 0000000000..67d9a1a36f --- /dev/null +++ b/src/renderer/components/dock/terminal-store/terminal-store.injectable.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { TerminalStore } from "./terminal.store"; +import createTerminalTabInjectable from "../create-terminal-tab/create-terminal-tab.injectable"; +import dockStoreInjectable from "../dock-store/dock-store.injectable"; +import createTerminalInjectable from "../terminal/create-terminal.injectable"; + +const terminalStoreInjectable = getInjectable({ + instantiate: (di) => new TerminalStore({ + createTerminalTab: di.inject(createTerminalTabInjectable), + dockStore: di.inject(dockStoreInjectable), + createTerminal: di.inject(createTerminalInjectable), + }), + + lifecycle: lifecycleEnum.singleton, +}); + +export default terminalStoreInjectable; diff --git a/src/renderer/components/dock/terminal.store.ts b/src/renderer/components/dock/terminal-store/terminal.store.ts similarity index 70% rename from src/renderer/components/dock/terminal.store.ts rename to src/renderer/components/dock/terminal-store/terminal.store.ts index bc6bda61e8..f3b33f1b38 100644 --- a/src/renderer/components/dock/terminal.store.ts +++ b/src/renderer/components/dock/terminal-store/terminal.store.ts @@ -20,36 +20,39 @@ */ import { autorun, observable, when } from "mobx"; -import { autoBind, noop, Singleton } from "../../utils"; -import { Terminal } from "./terminal"; -import { TerminalApi, TerminalChannels } from "../../api/terminal-api"; -import { dockStore, DockTab, DockTabCreateSpecific, TabId, TabKind } from "./dock.store"; -import { WebSocketApiState } from "../../api/websocket-api"; -import { Notifications } from "../notifications"; +import { autoBind, noop } from "../../../utils"; +import type { Terminal } from "../terminal/terminal"; +import { TerminalApi, TerminalChannels } from "../../../api/terminal-api"; +import { + DockStore, + DockTab, + DockTabCreate, + TabId, + TabKind, +} from "../dock-store/dock.store"; +import { WebSocketApiState } from "../../../api/websocket-api"; +import { Notifications } from "../../notifications"; export interface ITerminalTab extends DockTab { node?: string; // activate node shell mode } -export function createTerminalTab(tabParams: DockTabCreateSpecific = {}) { - return dockStore.createTab({ - title: `Terminal`, - ...tabParams, - kind: TabKind.TERMINAL, - }); +interface Dependencies { + createTerminalTab: () => DockTabCreate + dockStore: DockStore + createTerminal: (tabId: TabId, api: TerminalApi) => Terminal } -export class TerminalStore extends Singleton { +export class TerminalStore { protected terminals = new Map(); protected connections = observable.map(); - constructor() { - super(); + constructor(private dependencies: Dependencies) { autoBind(this); // connect active tab autorun(() => { - const { selectedTab, isOpen } = dockStore; + const { selectedTab, isOpen } = dependencies.dockStore; if (selectedTab?.kind === TabKind.TERMINAL && isOpen) { this.connect(selectedTab.id); @@ -57,7 +60,7 @@ export class TerminalStore extends Singleton { }); // disconnect closed tabs autorun(() => { - const currentTabs = dockStore.tabs.map(tab => tab.id); + const currentTabs = dependencies.dockStore.tabs.map(tab => tab.id); for (const [tabId] of this.connections) { if (!currentTabs.includes(tabId)) this.disconnect(tabId); @@ -69,12 +72,12 @@ export class TerminalStore extends Singleton { if (this.isConnected(tabId)) { return; } - const tab: ITerminalTab = dockStore.getTabById(tabId); + const tab: ITerminalTab = this.dependencies.dockStore.getTabById(tabId); const api = new TerminalApi({ id: tabId, node: tab.node, }); - const terminal = new Terminal(tabId, api); + const terminal = this.dependencies.createTerminal(tabId, api); this.connections.set(tabId, api); this.terminals.set(tabId, terminal); @@ -111,11 +114,11 @@ export class TerminalStore extends Singleton { const { enter, newTab, tabId } = options; if (tabId) { - dockStore.selectTab(tabId); + this.dependencies.dockStore.selectTab(tabId); } if (newTab) { - const tab = createTerminalTab(); + const tab = this.dependencies.createTerminalTab(); await when(() => this.connections.has(tab.id)); @@ -134,7 +137,7 @@ export class TerminalStore extends Singleton { clearTimeout(notifyVeryLong); } - const terminalApi = this.connections.get(dockStore.selectedTabId); + const terminalApi = this.connections.get(this.dependencies.dockStore.selectedTabId); if (terminalApi) { if (enter) { @@ -146,7 +149,10 @@ export class TerminalStore extends Singleton { data: command, }); } else { - console.warn("The selected tab is does not have a connection. Cannot send command.", { tabId: dockStore.selectedTabId, command }); + console.warn( + "The selected tab is does not have a connection. Cannot send command.", + { tabId: this.dependencies.dockStore.selectedTabId, command }, + ); } } @@ -160,25 +166,3 @@ export class TerminalStore extends Singleton { }); } } - -/** - * @deprecated use `TerminalStore.getInstance()` instead - */ -export const terminalStore = new Proxy({}, { - get(target, p) { - if (p === "$$typeof") { - return "TerminalStore"; - } - - const ts = TerminalStore.getInstance(); - const res = (ts as any)?.[p]; - - if (typeof res === "function") { - return function (...args: any[]) { - return res.apply(ts, args); - }; - } - - return res; - }, -}) as TerminalStore; diff --git a/src/renderer/components/dock/terminal-tab.tsx b/src/renderer/components/dock/terminal-tab.tsx index 64832a5c3e..0c327669b7 100644 --- a/src/renderer/components/dock/terminal-tab.tsx +++ b/src/renderer/components/dock/terminal-tab.tsx @@ -26,18 +26,26 @@ import { observer } from "mobx-react"; import { boundMethod, cssNames } from "../../utils"; import { DockTab, DockTabProps } from "./dock-tab"; import { Icon } from "../icon"; -import { terminalStore } from "./terminal.store"; -import { dockStore } from "./dock.store"; +import type { TerminalStore } from "./terminal-store/terminal.store"; +import type { DockStore } from "./dock-store/dock.store"; import { reaction } from "mobx"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import dockStoreInjectable from "./dock-store/dock-store.injectable"; +import terminalStoreInjectable from "./terminal-store/terminal-store.injectable"; interface Props extends DockTabProps { } +interface Dependencies { + dockStore: DockStore + terminalStore: TerminalStore +} + @observer -export class TerminalTab extends React.Component { +class NonInjectedTerminalTab extends React.Component { componentDidMount() { reaction(() => this.isDisconnected === true, () => { - dockStore.closeTab(this.tabId); + this.props.dockStore.closeTab(this.tabId); }); } @@ -46,12 +54,12 @@ export class TerminalTab extends React.Component { } get isDisconnected() { - return terminalStore.isDisconnected(this.tabId); + return this.props.terminalStore.isDisconnected(this.tabId); } @boundMethod reconnect() { - terminalStore.reconnect(this.tabId); + this.props.terminalStore.reconnect(this.tabId); } render() { @@ -60,9 +68,11 @@ export class TerminalTab extends React.Component { disconnected: this.isDisconnected, }); + const { dockStore, terminalStore, ...tabProps } = this.props; + return ( { ); } } + +export const TerminalTab = withInjectables( + NonInjectedTerminalTab, + + { + getProps: (di, props) => ({ + dockStore: di.inject(dockStoreInjectable), + terminalStore: di.inject(terminalStoreInjectable), + ...props, + }), + }, +); + diff --git a/src/renderer/components/dock/terminal-window.tsx b/src/renderer/components/dock/terminal-window.tsx index 58508a11a5..1e03cb1256 100644 --- a/src/renderer/components/dock/terminal-window.tsx +++ b/src/renderer/components/dock/terminal-window.tsx @@ -24,29 +24,37 @@ import "./terminal-window.scss"; import React from "react"; import { disposeOnUnmount, observer } from "mobx-react"; import { cssNames } from "../../utils"; -import type { Terminal } from "./terminal"; -import { TerminalStore } from "./terminal.store"; +import type { Terminal } from "./terminal/terminal"; +import type { TerminalStore } from "./terminal-store/terminal.store"; import { ThemeStore } from "../../theme.store"; -import { dockStore, DockTab, TabKind, TabId } from "./dock.store"; +import { DockTab, TabKind, TabId, DockStore } from "./dock-store/dock.store"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import dockStoreInjectable from "./dock-store/dock-store.injectable"; +import terminalStoreInjectable from "./terminal-store/terminal-store.injectable"; interface Props { tab: DockTab; } +interface Dependencies { + dockStore: DockStore + terminalStore: TerminalStore +} + @observer -export class TerminalWindow extends React.Component { +class NonInjectedTerminalWindow extends React.Component { public elem: HTMLElement; public terminal: Terminal; componentDidMount() { disposeOnUnmount(this, [ - dockStore.onTabChange(({ tabId }) => this.activate(tabId), { + this.props.dockStore.onTabChange(({ tabId }) => this.activate(tabId), { tabKind: TabKind.TERMINAL, fireImmediately: true, }), // refresh terminal available space (cols/rows) when resized - dockStore.onResize(() => this.terminal?.fitLazy(), { + this.props.dockStore.onResize(() => this.terminal?.fitLazy(), { fireImmediately: true, }), ]); @@ -54,7 +62,7 @@ export class TerminalWindow extends React.Component { activate(tabId: TabId) { this.terminal?.detach(); // detach previous - this.terminal = TerminalStore.getInstance().getTerminal(tabId); + this.terminal = this.props.terminalStore.getTerminal(tabId); this.terminal.attachTo(this.elem); } @@ -67,3 +75,16 @@ export class TerminalWindow extends React.Component { ); } } + +export const TerminalWindow = withInjectables( + NonInjectedTerminalWindow, + + { + getProps: (di, props) => ({ + dockStore: di.inject(dockStoreInjectable), + terminalStore: di.inject(terminalStoreInjectable), + ...props, + }), + }, +); + diff --git a/src/renderer/components/dock/terminal/create-terminal.injectable.ts b/src/renderer/components/dock/terminal/create-terminal.injectable.ts new file mode 100644 index 0000000000..23f308455e --- /dev/null +++ b/src/renderer/components/dock/terminal/create-terminal.injectable.ts @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { Terminal } from "./terminal"; +import type { TabId } from "../dock-store/dock.store"; +import type { TerminalApi } from "../../../api/terminal-api"; +import dockStoreInjectable from "../dock-store/dock-store.injectable"; + +const createTerminalInjectable = getInjectable({ + instantiate: (di) => { + const dependencies = { + dockStore: di.inject(dockStoreInjectable), + }; + + return (tabId: TabId, api: TerminalApi) => + new Terminal(dependencies, tabId, api); + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default createTerminalInjectable; diff --git a/src/renderer/components/dock/terminal.ts b/src/renderer/components/dock/terminal/terminal.ts similarity index 89% rename from src/renderer/components/dock/terminal.ts rename to src/renderer/components/dock/terminal/terminal.ts index e88bfc067e..5eaaad1764 100644 --- a/src/renderer/components/dock/terminal.ts +++ b/src/renderer/components/dock/terminal/terminal.ts @@ -23,15 +23,19 @@ import debounce from "lodash/debounce"; import { reaction } from "mobx"; import { Terminal as XTerm } from "xterm"; import { FitAddon } from "xterm-addon-fit"; -import { dockStore, TabId } from "./dock.store"; -import { TerminalApi, TerminalChannels } from "../../api/terminal-api"; -import { ThemeStore } from "../../theme.store"; -import { boundMethod, disposer } from "../../utils"; -import { isMac } from "../../../common/vars"; +import type { DockStore, TabId } from "../dock-store/dock.store"; +import { TerminalApi, TerminalChannels } from "../../../api/terminal-api"; +import { ThemeStore } from "../../../theme.store"; +import { boundMethod, disposer } from "../../../utils"; +import { isMac } from "../../../../common/vars"; import { camelCase, once } from "lodash"; -import { UserStore } from "../../../common/user-store"; +import { UserStore } from "../../../../common/user-store"; import { clipboard } from "electron"; -import logger from "../../../common/logger"; +import logger from "../../../../common/logger"; + +interface Dependencies { + dockStore: DockStore +} export class Terminal { public static readonly spawningPool = (() => { @@ -47,7 +51,7 @@ export class Terminal { })(); static async preloadFonts() { - const fontPath = require("../fonts/roboto-mono-nerd.ttf").default; // eslint-disable-line @typescript-eslint/no-var-requires + const fontPath = require("../../fonts/roboto-mono-nerd.ttf").default; // eslint-disable-line @typescript-eslint/no-var-requires const fontFace = new FontFace("RobotoMono", `url(${fontPath})`); await fontFace.load(); @@ -90,7 +94,7 @@ export class Terminal { } get isActive() { - const { isOpen, selectedTabId } = dockStore; + const { isOpen, selectedTabId } = this.dependencies.dockStore; return isOpen && selectedTabId === this.tabId; } @@ -108,7 +112,7 @@ export class Terminal { } } - constructor(public tabId: TabId, protected api: TerminalApi) { + constructor(private dependencies: Dependencies, public tabId: TabId, protected api: TerminalApi) { // enable terminal addons this.xterm.loadAddon(this.fitAddon); @@ -132,7 +136,7 @@ export class Terminal { reaction(() => ThemeStore.getInstance().activeTheme.colors, this.setTheme, { fireImmediately: true, }), - dockStore.onResize(this.onResize), + dependencies.dockStore.onResize(this.onResize), () => onDataHandler.dispose(), () => this.fitAddon.dispose(), () => this.api.removeAllListeners(), diff --git a/src/extensions/getDiForUnitTesting.ts b/src/renderer/components/dock/upgrade-chart-store/upgrade-chart-store.injectable.ts similarity index 53% rename from src/extensions/getDiForUnitTesting.ts rename to src/renderer/components/dock/upgrade-chart-store/upgrade-chart-store.injectable.ts index ee821aac3b..8dde8e3655 100644 --- a/src/extensions/getDiForUnitTesting.ts +++ b/src/renderer/components/dock/upgrade-chart-store/upgrade-chart-store.injectable.ts @@ -18,40 +18,28 @@ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import { UpgradeChartStore } from "./upgrade-chart.store"; +import releaseStoreInjectable from "../../+apps-releases/release-store.injectable"; +import dockStoreInjectable from "../dock-store/dock-store.injectable"; +import createDockTabStoreInjectable from "../dock-tab-store/create-dock-tab-store.injectable"; +import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable"; -import glob from "glob"; -import { memoize } from "lodash/fp"; +const upgradeChartStoreInjectable = getInjectable({ + instantiate: (di) => { + const createDockTabStore = di.inject(createDockTabStoreInjectable); -import { - createContainer, - ConfigurableDependencyInjectionContainer, -} from "@ogre-tools/injectable"; -import { setLegacyGlobalDiForExtensionApi } from "./as-legacy-global-function-for-extension-api/legacy-global-di-for-extension-api"; + const valuesStore = createDockTabStore(); -export const getDiForUnitTesting = () => { - const di: ConfigurableDependencyInjectionContainer = createContainer(); + return new UpgradeChartStore({ + releaseStore: di.inject(releaseStoreInjectable), + dockStore: di.inject(dockStoreInjectable), + createStorage: di.inject(createStorageInjectable), + valuesStore, + }); + }, - setLegacyGlobalDiForExtensionApi(di); - - getInjectableFilePaths() - .map(key => { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const injectable = require(key).default; + lifecycle: lifecycleEnum.singleton, +}); - return { - id: key, - ...injectable, - aliases: [injectable, ...(injectable.aliases || [])], - }; - }) - - .forEach(injectable => di.register(injectable)); - - di.preventSideEffects(); - - return di; -}; - -const getInjectableFilePaths = memoize(() => [ - ...glob.sync("./**/*.injectable.{ts,tsx}", { cwd: __dirname }), -]); +export default upgradeChartStoreInjectable; diff --git a/src/renderer/components/dock/upgrade-chart.store.ts b/src/renderer/components/dock/upgrade-chart-store/upgrade-chart.store.ts similarity index 67% rename from src/renderer/components/dock/upgrade-chart.store.ts rename to src/renderer/components/dock/upgrade-chart-store/upgrade-chart.store.ts index 81230466b9..d94f67ffca 100644 --- a/src/renderer/components/dock/upgrade-chart.store.ts +++ b/src/renderer/components/dock/upgrade-chart-store/upgrade-chart.store.ts @@ -20,35 +20,44 @@ */ import { action, autorun, computed, IReactionDisposer, reaction, makeObservable } from "mobx"; -import { dockStore, DockTab, DockTabCreateSpecific, TabId, TabKind } from "./dock.store"; -import { DockTabStore } from "./dock-tab.store"; -import { getReleaseValues, HelmRelease } from "../../../common/k8s-api/endpoints/helm-releases.api"; -import { releaseStore } from "../+apps-releases/release.store"; -import { iter } from "../../utils"; +import { DockStore, DockTab, TabId, TabKind } from "../dock-store/dock.store"; +import { DockTabStorageState, DockTabStore } from "../dock-tab-store/dock-tab.store"; +import { getReleaseValues } from "../../../../common/k8s-api/endpoints/helm-releases.api"; +import type { ReleaseStore } from "../../+apps-releases/release.store"; +import { iter, StorageHelper } from "../../../utils"; export interface IChartUpgradeData { releaseName: string; releaseNamespace: string; } +interface Dependencies { + releaseStore: ReleaseStore + valuesStore: DockTabStore + dockStore: DockStore + createStorage: (storageKey: string, options: DockTabStorageState) => StorageHelper> +} + export class UpgradeChartStore extends DockTabStore { private watchers = new Map(); - values = new DockTabStore(); - @computed private get releaseNameReverseLookup(): Map { return new Map(iter.map(this.data, ([id, { releaseName }]) => [releaseName, id])); } - constructor() { - super({ + get values() { + return this.dependencies.valuesStore; + } + + constructor(protected dependencies : Dependencies) { + super(dependencies, { storageKey: "chart_releases", }); makeObservable(this); autorun(() => { - const { selectedTab, isOpen } = dockStore; + const { selectedTab, isOpen } = dependencies.dockStore; if (selectedTab?.kind === TabKind.UPGRADE_CHART && isOpen) { this.loadData(selectedTab.id); @@ -67,20 +76,20 @@ export class UpgradeChartStore extends DockTabStore { return; } const dispose = reaction(() => { - const release = releaseStore.getByName(releaseName); + const release = this.dependencies.releaseStore.getByName(releaseName); return release?.getRevision(); // watch changes only by revision }, release => { const releaseTab = this.getTabByRelease(releaseName); - if (!releaseStore.isLoaded || !releaseTab) { + if (!this.dependencies.releaseStore.isLoaded || !releaseTab) { return; } // auto-reload values if was loaded before if (release) { - if (dockStore.selectedTab === releaseTab && this.values.getData(releaseTab.id)) { + if (this.dependencies.dockStore.selectedTab === releaseTab && this.values.getData(releaseTab.id)) { this.loadValues(releaseTab.id); } } @@ -88,17 +97,17 @@ export class UpgradeChartStore extends DockTabStore { else { dispose(); this.watchers.delete(releaseName); - dockStore.closeTab(releaseTab.id); + this.dependencies.dockStore.closeTab(releaseTab.id); } }); this.watchers.set(releaseName, dispose); } - isLoading(tabId = dockStore.selectedTabId) { + isLoading(tabId = this.dependencies.dockStore.selectedTabId) { const values = this.values.getData(tabId); - return !releaseStore.isLoaded || values === undefined; + return !this.dependencies.releaseStore.isLoaded || values === undefined; } @action @@ -106,7 +115,7 @@ export class UpgradeChartStore extends DockTabStore { const values = this.values.getData(tabId); await Promise.all([ - !releaseStore.isLoaded && releaseStore.loadFromContextNamespaces(), + !this.dependencies.releaseStore.isLoaded && this.dependencies.releaseStore.loadFromContextNamespaces(), !values && this.loadValues(tabId), ]); } @@ -121,32 +130,6 @@ export class UpgradeChartStore extends DockTabStore { } getTabByRelease(releaseName: string): DockTab { - return dockStore.getTabById(this.releaseNameReverseLookup.get(releaseName)); + return this.dependencies.dockStore.getTabById(this.releaseNameReverseLookup.get(releaseName)); } } - -export const upgradeChartStore = new UpgradeChartStore(); - -export function createUpgradeChartTab(release: HelmRelease, tabParams: DockTabCreateSpecific = {}) { - let tab = upgradeChartStore.getTabByRelease(release.getName()); - - if (tab) { - dockStore.open(); - dockStore.selectTab(tab.id); - } - - if (!tab) { - tab = dockStore.createTab({ - title: `Helm Upgrade: ${release.getName()}`, - ...tabParams, - kind: TabKind.UPGRADE_CHART, - }, false); - - upgradeChartStore.setData(tab.id, { - releaseName: release.getName(), - releaseNamespace: release.getNs(), - }); - } - - return tab; -} diff --git a/src/renderer/components/dock/upgrade-chart.tsx b/src/renderer/components/dock/upgrade-chart.tsx index 4eaaaa4d21..27acbf295e 100644 --- a/src/renderer/components/dock/upgrade-chart.tsx +++ b/src/renderer/components/dock/upgrade-chart.tsx @@ -25,29 +25,37 @@ import React from "react"; import { action, makeObservable, observable, reaction } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; import { cssNames } from "../../utils"; -import type { DockTab } from "./dock.store"; +import type { DockTab } from "./dock-store/dock.store"; import { InfoPanel } from "./info-panel"; -import { upgradeChartStore } from "./upgrade-chart.store"; +import type { UpgradeChartStore } from "./upgrade-chart-store/upgrade-chart.store"; import { Spinner } from "../spinner"; -import { releaseStore } from "../+apps-releases/release.store"; +import type { ReleaseStore } from "../+apps-releases/release.store"; import { Badge } from "../badge"; import { EditorPanel } from "./editor-panel"; import { helmChartStore, IChartVersion } from "../+apps-helm-charts/helm-chart.store"; import type { HelmRelease } from "../../../common/k8s-api/endpoints/helm-releases.api"; import { Select, SelectOption } from "../select"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import releaseStoreInjectable from "../+apps-releases/release-store.injectable"; +import upgradeChartStoreInjectable from "./upgrade-chart-store/upgrade-chart-store.injectable"; interface Props { className?: string; tab: DockTab; } +interface Dependencies { + releaseStore: ReleaseStore + upgradeChartStore: UpgradeChartStore +} + @observer -export class UpgradeChart extends React.Component { +export class NonInjectedUpgradeChart extends React.Component { @observable error: string; @observable versions = observable.array(); @observable version: IChartVersion; - constructor(props: Props) { + constructor(props: Props & Dependencies) { super(props); makeObservable(this); } @@ -65,15 +73,15 @@ export class UpgradeChart extends React.Component { } get release(): HelmRelease { - const tabData = upgradeChartStore.getData(this.tabId); + const tabData = this.props.upgradeChartStore.getData(this.tabId); if (!tabData) return null; - return releaseStore.getByName(tabData.releaseName); + return this.props.releaseStore.getByName(tabData.releaseName); } get value() { - return upgradeChartStore.values.getData(this.tabId); + return this.props.upgradeChartStore.values.getData(this.tabId); } async loadVersions() { @@ -89,7 +97,7 @@ export class UpgradeChart extends React.Component { @action onChange = (value: string) => { this.error = ""; - upgradeChartStore.values.setData(this.tabId, value); + this.props.upgradeChartStore.values.setData(this.tabId, value); }; @action @@ -103,7 +111,7 @@ export class UpgradeChart extends React.Component { const releaseName = this.release.getName(); const releaseNs = this.release.getNs(); - await releaseStore.update(releaseName, releaseNs, { + await this.props.releaseStore.update(releaseName, releaseNs, { chart: this.release.getChart(), values: this.value, repo, version, @@ -127,7 +135,7 @@ export class UpgradeChart extends React.Component { const { tabId, release, value, error, onChange, onError, upgrade, versions, version } = this; const { className } = this.props; - if (!release || upgradeChartStore.isLoading() || !version) { + if (!release || this.props.upgradeChartStore.isLoading() || !version) { return ; } const currentVersion = release.getVersion(); @@ -169,3 +177,15 @@ export class UpgradeChart extends React.Component { ); } } + +export const UpgradeChart = withInjectables( + NonInjectedUpgradeChart, + + { + getProps: (di, props) => ({ + releaseStore: di.inject(releaseStoreInjectable), + upgradeChartStore: di.inject(upgradeChartStoreInjectable), + ...props, + }), + }, +); diff --git a/src/renderer/components/drawer/drawer-storage/drawer-storage.injectable.ts b/src/renderer/components/drawer/drawer-storage/drawer-storage.injectable.ts new file mode 100644 index 0000000000..e230c265e9 --- /dev/null +++ b/src/renderer/components/drawer/drawer-storage/drawer-storage.injectable.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable"; + +export const defaultDrawerWidth = 725; + +const drawerStorageInjectable = getInjectable({ + instantiate: (di) => { + const createStorage = di.inject(createStorageInjectable); + + return createStorage("drawer", { + width: defaultDrawerWidth, + }); + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default drawerStorageInjectable; diff --git a/src/renderer/components/drawer/drawer.tsx b/src/renderer/components/drawer/drawer.tsx index 4c93b8cada..a534786021 100644 --- a/src/renderer/components/drawer/drawer.tsx +++ b/src/renderer/components/drawer/drawer.tsx @@ -24,11 +24,15 @@ import "./drawer.scss"; import React from "react"; import { clipboard } from "electron"; import { createPortal } from "react-dom"; -import { createStorage, cssNames, noop } from "../../utils"; +import { cssNames, noop, StorageHelper } from "../../utils"; import { Icon } from "../icon"; import { Animate, AnimateName } from "../animate"; import { history } from "../../navigation"; import { ResizeDirection, ResizeGrowthDirection, ResizeSide, ResizingAnchor } from "../resizing-anchor"; +import drawerStorageInjectable, { + defaultDrawerWidth, +} from "./drawer-storage/drawer-storage.injectable"; +import { withInjectables } from "@ogre-tools/injectable-react"; export type DrawerPosition = "top" | "left" | "right" | "bottom"; @@ -70,12 +74,11 @@ resizingAnchorProps.set("left", [ResizeDirection.HORIZONTAL, ResizeSide.TRAILING resizingAnchorProps.set("top", [ResizeDirection.VERTICAL, ResizeSide.TRAILING, ResizeGrowthDirection.TOP_TO_BOTTOM]); resizingAnchorProps.set("bottom", [ResizeDirection.VERTICAL, ResizeSide.LEADING, ResizeGrowthDirection.BOTTOM_TO_TOP]); -const defaultDrawerWidth = 725; -const drawerStorage = createStorage("drawer", { - width: defaultDrawerWidth, -}); +interface Dependencies { + drawerStorage: StorageHelper<{ width: number }>; +} -export class Drawer extends React.Component { +class NonInjectedDrawer extends React.Component { static defaultProps = defaultProps as object; private mouseDownTarget: HTMLElement; @@ -89,7 +92,7 @@ export class Drawer extends React.Component { public state = { isCopied: false, - width: drawerStorage.get().width, + width: this.props.drawerStorage.get().width, }; componentDidMount() { @@ -110,7 +113,7 @@ export class Drawer extends React.Component { resizeWidth = (width: number) => { this.setState({ width }); - drawerStorage.merge({ width }); + this.props.drawerStorage.merge({ width }); }; fixUpTripleClick = (ev: MouseEvent) => { @@ -239,3 +242,15 @@ export class Drawer extends React.Component { return usePortal ? createPortal(drawer, document.body) : drawer; } } + +export const Drawer = withInjectables( + NonInjectedDrawer, + + { + getProps: (di, props) => ({ + drawerStorage: di.inject(drawerStorageInjectable), + ...props, + }), + }, +); + diff --git a/src/renderer/components/getDi.tsx b/src/renderer/components/getDi.tsx index ba997137f3..2a42878d89 100644 --- a/src/renderer/components/getDi.tsx +++ b/src/renderer/components/getDi.tsx @@ -26,6 +26,7 @@ export const getDi = () => { const di = createContainer( getRequireContextForRendererCode, getRequireContextForCommonExtensionCode, + getRequireContextForCommonCode, ); setLegacyGlobalDiForExtensionApi(di); @@ -38,3 +39,6 @@ const getRequireContextForRendererCode = () => const getRequireContextForCommonExtensionCode = () => require.context("../../extensions", true, /\.injectable\.(ts|tsx)$/); + +const getRequireContextForCommonCode = () => + require.context("../../common", true, /\.injectable\.(ts|tsx)$/); diff --git a/src/renderer/components/getDiForUnitTesting.tsx b/src/renderer/components/getDiForUnitTesting.tsx index 4ab8a47df5..8b1cf381b3 100644 --- a/src/renderer/components/getDiForUnitTesting.tsx +++ b/src/renderer/components/getDiForUnitTesting.tsx @@ -27,8 +27,11 @@ import { ConfigurableDependencyInjectionContainer, } from "@ogre-tools/injectable"; import { setLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-global-function-for-extension-api/legacy-global-di-for-extension-api"; +import getValueFromRegisteredChannelInjectable from "./app-paths/get-value-from-registered-channel/get-value-from-registered-channel.injectable"; +import writeJsonFileInjectable from "../../common/fs/write-json-file/write-json-file.injectable"; +import readJsonFileInjectable from "../../common/fs/read-json-file/read-json-file.injectable"; -export const getDiForUnitTesting = () => { +export const getDiForUnitTesting = ({ doGeneralOverrides } = { doGeneralOverrides: false }) => { const di: ConfigurableDependencyInjectionContainer = createContainer(); setLegacyGlobalDiForExtensionApi(di); @@ -47,11 +50,24 @@ export const getDiForUnitTesting = () => { .forEach(injectable => di.register(injectable)); di.preventSideEffects(); + + if (doGeneralOverrides) { + di.override(getValueFromRegisteredChannelInjectable, () => () => undefined); + + di.override(writeJsonFileInjectable, () => () => { + throw new Error("Tried to write JSON file to file system without specifying explicit override."); + }); + + di.override(readJsonFileInjectable, () => () => { + throw new Error("Tried to read JSON file from file system without specifying explicit override."); + }); + } return di; }; const getInjectableFilePaths = memoize(() => [ - ...glob.sync("./**/*.injectable.{ts,tsx}", { cwd: __dirname }), + ...glob.sync("../**/*.injectable.{ts,tsx}", { cwd: __dirname }), ...glob.sync("../../extensions/**/*.injectable.{ts,tsx}", { cwd: __dirname }), + ...glob.sync("../../common/**/*.injectable.{ts,tsx}", { cwd: __dirname }), ]); diff --git a/src/renderer/components/hotbar/__tests__/hotbar-remove-command.test.tsx b/src/renderer/components/hotbar/__tests__/hotbar-remove-command.test.tsx index 7a292e7e5f..92fd94ec3e 100644 --- a/src/renderer/components/hotbar/__tests__/hotbar-remove-command.test.tsx +++ b/src/renderer/components/hotbar/__tests__/hotbar-remove-command.test.tsx @@ -27,25 +27,8 @@ import { ThemeStore } from "../../../theme.store"; import { UserStore } from "../../../../common/user-store"; import { Notifications } from "../../notifications"; import mockFs from "mock-fs"; -import { AppPaths } from "../../../../common/app-paths"; - -jest.mock("electron", () => ({ - app: { - getVersion: () => "99.99.99", - getName: () => "lens", - setName: jest.fn(), - setPath: jest.fn(), - getPath: () => "tmp", - getLocale: () => "en", - setLoginItemSettings: jest.fn(), - }, - ipcMain: { - on: jest.fn(), - handle: jest.fn(), - }, -})); - -AppPaths.init(); +import { getDiForUnitTesting } from "../../getDiForUnitTesting"; +import directoryForUserDataInjectable from "../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; const mockHotbars: { [id: string]: any } = { "1": { @@ -67,10 +50,15 @@ jest.mock("../../../../common/hotbar-store", () => ({ })); describe("", () => { - beforeEach(() => { - mockFs({ - "tmp": {}, - }); + beforeEach(async () => { + const di = getDiForUnitTesting({ doGeneralOverrides: true }); + + mockFs(); + + di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); + + await di.runSetups(); + UserStore.createInstance(); ThemeStore.createInstance(); }); diff --git a/src/renderer/components/item-object-list/item-list-layout-storage/item-list-layout-storage.injectable.ts b/src/renderer/components/item-object-list/item-list-layout-storage/item-list-layout-storage.injectable.ts new file mode 100644 index 0000000000..674a33a795 --- /dev/null +++ b/src/renderer/components/item-object-list/item-list-layout-storage/item-list-layout-storage.injectable.ts @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import createStorageInjectable from "../../../utils/create-storage/create-storage.injectable"; + +const itemListLayoutStorageInjectable = getInjectable({ + instantiate: (di) => { + const createStorage = di.inject(createStorageInjectable); + + return createStorage("item_list_layout", { + showFilters: false, // setup defaults + }); + }, + + lifecycle: lifecycleEnum.singleton, +}); + +export default itemListLayoutStorageInjectable; diff --git a/src/renderer/components/item-object-list/item-list-layout.tsx b/src/renderer/components/item-object-list/item-list-layout.tsx index 7e5123c48f..814aecd652 100644 --- a/src/renderer/components/item-object-list/item-list-layout.tsx +++ b/src/renderer/components/item-object-list/item-list-layout.tsx @@ -27,7 +27,17 @@ import { computed, makeObservable } from "mobx"; import { observer } from "mobx-react"; import { ConfirmDialog, ConfirmDialogParams } from "../confirm-dialog"; import { Table, TableCell, TableCellProps, TableHead, TableProps, TableRow, TableRowProps, TableSortCallbacks } from "../table"; -import { boundMethod, createStorage, cssNames, IClassName, isReactNode, noop, ObservableToggleSet, prevDefault, stopPropagation } from "../../utils"; +import { + boundMethod, + cssNames, + IClassName, + isReactNode, + noop, + ObservableToggleSet, + prevDefault, + stopPropagation, + StorageHelper, +} from "../../utils"; import { AddRemoveButtons, AddRemoveButtonsProps } from "../add-remove-buttons"; import { NoItems } from "../no-items"; import { Spinner } from "../spinner"; @@ -40,8 +50,11 @@ import { MenuActions } from "../menu/menu-actions"; import { MenuItem } from "../menu"; import { Checkbox } from "../checkbox"; import { UserStore } from "../../../common/user-store"; -import { namespaceStore } from "../+namespaces/namespace.store"; - +import type { NamespaceStore } from "../+namespaces/namespace-store/namespace.store"; +import namespaceStoreInjectable from "../+namespaces/namespace-store/namespace-store.injectable"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import itemListLayoutStorageInjectable + from "./item-list-layout-storage/item-list-layout-storage.injectable"; export type SearchFilter = (item: I) => string | number | (string | number)[]; @@ -72,7 +85,7 @@ export interface ItemListLayoutProps { // header (title, filtering, searching, etc.) showHeader?: boolean; headerClassName?: IClassName; - renderHeaderTitle?: ReactNode | ((parent: ItemListLayout) => ReactNode); + renderHeaderTitle?: ReactNode | ((parent: NonInjectedItemListLayout) => ReactNode); customizeHeader?: HeaderCustomizer | HeaderCustomizer[]; // items list configuration @@ -96,7 +109,7 @@ export interface ItemListLayoutProps { // other customizeRemoveDialog?: (selectedItems: I[]) => Partial; - renderFooter?: (parent: ItemListLayout) => React.ReactNode; + renderFooter?: (parent: NonInjectedItemListLayout) => React.ReactNode; /** * Message to display when a store failed to load @@ -125,25 +138,26 @@ const defaultProps: Partial> = { failedToLoadMessage: "Failed to load items", }; +interface Dependencies { + namespaceStore: NamespaceStore; + itemListLayoutStorage: StorageHelper<{ showFilters: boolean }>; +} + @observer -export class ItemListLayout extends React.Component> { +class NonInjectedItemListLayout extends React.Component & Dependencies> { static defaultProps = defaultProps as object; - private storage = createStorage("item_list_layout", { - showFilters: false, // setup defaults - }); - - constructor(props: ItemListLayoutProps) { + constructor(props: ItemListLayoutProps & Dependencies) { super(props); makeObservable(this); } get showFilters(): boolean { - return this.storage.get().showFilters; + return this.props.itemListLayoutStorage.get().showFilters; } set showFilters(showFilters: boolean) { - this.storage.merge({ showFilters }); + this.props.itemListLayoutStorage.merge({ showFilters }); } async componentDidMount() { @@ -166,7 +180,7 @@ export class ItemListLayout extends React.Component store.loadAll(namespaceStore.contextNamespaces)); + stores.forEach(store => store.loadAll(this.props.namespaceStore.contextNamespaces)); } private filterCallbacks: ItemsFilters = { @@ -528,3 +542,19 @@ export class ItemListLayout extends React.Component( + props: ItemListLayoutProps, +) { + return withInjectables>( + NonInjectedItemListLayout, + + { + getProps: (di, props) => ({ + namespaceStore: di.inject(namespaceStoreInjectable), + itemListLayoutStorage: di.inject(itemListLayoutStorageInjectable), + ...props, + }), + }, + )(props); +} diff --git a/src/renderer/components/kube-object-list-layout/kube-object-list-layout.tsx b/src/renderer/components/kube-object-list-layout/kube-object-list-layout.tsx index f29a6da6e7..129fc8cee2 100644 --- a/src/renderer/components/kube-object-list-layout/kube-object-list-layout.tsx +++ b/src/renderer/components/kube-object-list-layout/kube-object-list-layout.tsx @@ -29,13 +29,17 @@ import type { KubeObject } from "../../../common/k8s-api/kube-object"; import { ItemListLayout, ItemListLayoutProps } from "../item-object-list/item-list-layout"; import type { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; import { KubeObjectMenu } from "../kube-object-menu"; -import { kubeWatchApi } from "../../../common/k8s-api/kube-watch-api"; import { NamespaceSelectFilter } from "../+namespaces/namespace-select-filter"; import { ResourceKindMap, ResourceNames } from "../../utils/rbac"; import { kubeSelectedUrlParam, toggleDetails } from "../kube-detail-params"; import { Icon } from "../icon"; import { TooltipPosition } from "../tooltip"; -import type { ClusterContext } from "../../../common/k8s-api/cluster-context"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import type { ClusterFrameContext } from "../../cluster-frame-context/cluster-frame-context"; +import clusterFrameContextInjectable from "../../cluster-frame-context/cluster-frame-context.injectable"; +import kubeWatchApiInjectable + from "../../kube-watch-api/kube-watch-api.injectable"; +import type { KubeWatchSubscribeStoreOptions } from "../../kube-watch-api/kube-watch-api"; export interface KubeObjectListLayoutProps extends ItemListLayoutProps { store: KubeObjectStore; @@ -48,12 +52,16 @@ const defaultProps: Partial> = { subscribeStores: true, }; -@observer -export class KubeObjectListLayout extends React.Component> { - static defaultProps = defaultProps as object; - static clusterContext: ClusterContext; +interface Dependencies { + clusterFrameContext: ClusterFrameContext + subscribeToStores: (stores: KubeObjectStore[], options: KubeWatchSubscribeStoreOptions) => Disposer +} - constructor(props: KubeObjectListLayoutProps) { +@observer +class NonInjectedKubeObjectListLayout extends React.Component & Dependencies> { + static defaultProps = defaultProps as object; + + constructor(props: KubeObjectListLayoutProps & Dependencies) { super(props); makeObservable(this); } @@ -68,7 +76,7 @@ export class KubeObjectListLayout extends React.Component< const { store, dependentStores = [], subscribeStores } = this.props; const stores = Array.from(new Set([store, ...dependentStores])); const reactions: Disposer[] = [ - reaction(() => KubeObjectListLayout.clusterContext.contextNamespaces.slice(), () => { + reaction(() => this.props.clusterFrameContext.contextNamespaces.slice(), () => { // clear load errors this.loadErrors.length = 0; }), @@ -76,7 +84,7 @@ export class KubeObjectListLayout extends React.Component< if (subscribeStores) { reactions.push( - kubeWatchApi.subscribeStores(stores, { + this.props.subscribeToStores(stores, { onLoadFailure: error => this.loadErrors.push(String(error)), }), ); @@ -145,3 +153,19 @@ export class KubeObjectListLayout extends React.Component< ); } } + +export function KubeObjectListLayout( + props: KubeObjectListLayoutProps, +) { + return withInjectables>( + NonInjectedKubeObjectListLayout, + + { + getProps: (di, props) => ({ + clusterFrameContext: di.inject(clusterFrameContextInjectable), + subscribeToStores: di.inject(kubeWatchApiInjectable).subscribeStores, + ...props, + }), + }, + )(props); +} diff --git a/src/renderer/components/kube-object-menu/__snapshots__/kube-object-menu.test.tsx.snap b/src/renderer/components/kube-object-menu/__snapshots__/kube-object-menu.test.tsx.snap index c7b3a53142..35f29db302 100644 --- a/src/renderer/components/kube-object-menu/__snapshots__/kube-object-menu.test.tsx.snap +++ b/src/renderer/components/kube-object-menu/__snapshots__/kube-object-menu.test.tsx.snap @@ -2,6 +2,10 @@ exports[`kube-object-menu given kube object renders 1`] = ` +