From 8a0cf91ab4652caef78926c8ee8e8532953b5e6b Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Tue, 30 Mar 2021 18:50:23 +0300 Subject: [PATCH] wip: catalog Signed-off-by: Jari Kolehmainen --- package.json | 2 +- src/common/__tests__/cluster-store.test.ts | 63 +--- src/common/__tests__/workspace-store.test.ts | 188 ---------- src/common/catalog-category-registry.ts | 42 +++ src/common/catalog-entities/index.ts | 1 + .../catalog-entities/kubernetes-cluster.ts | 73 ++++ src/common/catalog-entity-registry.ts | 32 ++ src/common/catalog-entity.ts | 63 ++++ src/common/cluster-ipc.ts | 8 + src/common/cluster-store.ts | 26 +- src/common/workspace-store.ts | 345 ------------------ src/extensions/core-api/catalog.ts | 0 src/extensions/core-api/stores.ts | 7 - src/extensions/lens-main-extension.ts | 10 + src/extensions/registries/command-registry.ts | 2 - src/extensions/stores/cluster-store.ts | 128 ------- src/extensions/stores/workspace-store.ts | 118 ------ src/main/__test__/cluster.test.ts | 7 +- src/main/__test__/kubeconfig-manager.test.ts | 7 +- src/main/catalog-pusher.ts | 26 ++ src/main/cluster-manager.ts | 64 +++- src/main/cluster.ts | 8 - src/main/index.ts | 5 +- src/main/menu.ts | 8 + src/main/tray.ts | 28 -- .../__tests__/catalog-entity-registry.test.ts | 135 +++++++ src/renderer/api/catalog-entity-registry.ts | 56 +++ src/renderer/bootstrap.tsx | 4 - .../components/+add-cluster/add-cluster.tsx | 2 - .../+cluster-settings/cluster-settings.tsx | 2 - .../components/cluster-icon-setting.tsx | 79 ---- .../components/cluster-workspace-setting.tsx | 31 -- .../components/+cluster-settings/general.tsx | 4 - .../+landing-page/catalog-entity.store.ts | 48 +++ .../+landing-page/landing-page.scss | 16 +- .../components/+landing-page/landing-page.tsx | 66 ++-- .../+landing-page/workspace-cluster-menu.tsx | 74 ---- .../+landing-page/workspace-cluster.store.ts | 72 ---- .../+landing-page/workspace-overview.scss | 32 -- .../+landing-page/workspace-overview.tsx | 85 ----- .../components/+workspaces/add-workspace.tsx | 64 ---- .../components/+workspaces/edit-workspace.tsx | 82 ----- src/renderer/components/+workspaces/index.ts | 1 - .../+workspaces/remove-workspace.tsx | 68 ---- .../components/+workspaces/workspaces.tsx | 99 ----- src/renderer/components/app.tsx | 14 +- .../components/cluster-icon/cluster-icon.scss | 38 -- .../components/cluster-icon/cluster-icon.tsx | 81 ---- src/renderer/components/cluster-icon/index.ts | 1 - .../components/cluster-manager/bottom-bar.tsx | 10 - .../cluster-manager/cluster-manager.tsx | 16 +- .../cluster-manager/clusters-menu.scss | 67 ---- .../cluster-manager/clusters-menu.tsx | 198 ---------- .../command-palette/command-container.tsx | 4 +- .../command-palette/command-dialog.tsx | 7 +- .../components/hotbar/hotbar-icon.scss | 44 +++ .../components/hotbar/hotbar-icon.tsx | 58 +++ .../components/hotbar/hotbar-menu.scss | 29 ++ .../components/hotbar/hotbar-menu.tsx | 43 +++ .../__test__/main-layout-header.test.tsx | 2 - .../components/layout/page-layout.tsx | 8 +- src/renderer/lens-app.tsx | 2 + src/renderer/protocol-handler/app-handlers.ts | 11 - 63 files changed, 823 insertions(+), 2091 deletions(-) delete mode 100644 src/common/__tests__/workspace-store.test.ts create mode 100644 src/common/catalog-category-registry.ts create mode 100644 src/common/catalog-entities/index.ts create mode 100644 src/common/catalog-entities/kubernetes-cluster.ts create mode 100644 src/common/catalog-entity-registry.ts create mode 100644 src/common/catalog-entity.ts delete mode 100644 src/common/workspace-store.ts create mode 100644 src/extensions/core-api/catalog.ts delete mode 100644 src/extensions/stores/cluster-store.ts delete mode 100644 src/extensions/stores/workspace-store.ts create mode 100644 src/main/catalog-pusher.ts create mode 100644 src/renderer/api/__tests__/catalog-entity-registry.test.ts create mode 100644 src/renderer/api/catalog-entity-registry.ts delete mode 100644 src/renderer/components/+cluster-settings/components/cluster-icon-setting.tsx delete mode 100644 src/renderer/components/+cluster-settings/components/cluster-workspace-setting.tsx create mode 100644 src/renderer/components/+landing-page/catalog-entity.store.ts delete mode 100644 src/renderer/components/+landing-page/workspace-cluster-menu.tsx delete mode 100644 src/renderer/components/+landing-page/workspace-cluster.store.ts delete mode 100644 src/renderer/components/+landing-page/workspace-overview.scss delete mode 100644 src/renderer/components/+landing-page/workspace-overview.tsx delete mode 100644 src/renderer/components/+workspaces/add-workspace.tsx delete mode 100644 src/renderer/components/+workspaces/edit-workspace.tsx delete mode 100644 src/renderer/components/+workspaces/index.ts delete mode 100644 src/renderer/components/+workspaces/remove-workspace.tsx delete mode 100644 src/renderer/components/+workspaces/workspaces.tsx delete mode 100644 src/renderer/components/cluster-icon/cluster-icon.scss delete mode 100644 src/renderer/components/cluster-icon/cluster-icon.tsx delete mode 100644 src/renderer/components/cluster-icon/index.ts delete mode 100644 src/renderer/components/cluster-manager/clusters-menu.scss delete mode 100644 src/renderer/components/cluster-manager/clusters-menu.tsx create mode 100644 src/renderer/components/hotbar/hotbar-icon.scss create mode 100644 src/renderer/components/hotbar/hotbar-icon.tsx create mode 100644 src/renderer/components/hotbar/hotbar-menu.scss create mode 100644 src/renderer/components/hotbar/hotbar-menu.tsx diff --git a/package.json b/package.json index e346f8c11f..1399309ede 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "dev-run": "nodemon --watch static/build/main.js --exec \"electron --remote-debugging-port=9223 --inspect .\"", "dev:main": "yarn run compile:main --watch", "dev:renderer": "yarn run webpack-dev-server --config webpack.renderer.ts", - "dev:extension-types": "yarn run compile:extension-types --watch --progress", + "dev:extension-types": "yarn run compile:extension-types --watch", "compile": "env NODE_ENV=production concurrently yarn:compile:*", "compile:main": "yarn run webpack --config webpack.main.ts", "compile:renderer": "yarn run webpack --config webpack.renderer.ts", diff --git a/src/common/__tests__/cluster-store.test.ts b/src/common/__tests__/cluster-store.test.ts index d1d2f76603..85b148d74b 100644 --- a/src/common/__tests__/cluster-store.test.ts +++ b/src/common/__tests__/cluster-store.test.ts @@ -3,7 +3,6 @@ import mockFs from "mock-fs"; import yaml from "js-yaml"; import { Cluster } from "../../main/cluster"; import { ClusterStore, getClusterIdFromHost } from "../cluster-store"; -import { workspaceStore } from "../workspace-store"; const testDataIcon = fs.readFileSync("test-data/cluster-store-migration-icon.png"); const kubeconfig = ` @@ -76,8 +75,7 @@ describe("empty config", () => { icon: "data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5", clusterName: "minikube" }, - kubeConfigPath: ClusterStore.embedCustomKubeConfig("foo", kubeconfig), - workspace: workspaceStore.currentWorkspaceId + kubeConfigPath: ClusterStore.embedCustomKubeConfig("foo", kubeconfig) }) ); }); @@ -91,12 +89,6 @@ describe("empty config", () => { expect(storedCluster.enabled).toBe(true); }); - it("adds cluster to default workspace", () => { - const storedCluster = clusterStore.getById("foo"); - - expect(storedCluster.workspace).toBe("default"); - }); - it("removes cluster from store", async () => { await clusterStore.removeById("foo"); expect(clusterStore.getById("foo")).toBeUndefined(); @@ -105,7 +97,6 @@ describe("empty config", () => { it("sets active cluster", () => { clusterStore.setActive("foo"); expect(clusterStore.active.id).toBe("foo"); - expect(workspaceStore.currentWorkspace.lastActiveClusterId).toBe("foo"); }); }); @@ -118,8 +109,7 @@ describe("empty config", () => { preferences: { clusterName: "prod" }, - kubeConfigPath: ClusterStore.embedCustomKubeConfig("prod", kubeconfig), - workspace: "workstation" + kubeConfigPath: ClusterStore.embedCustomKubeConfig("prod", kubeconfig) }), new Cluster({ id: "dev", @@ -127,8 +117,7 @@ describe("empty config", () => { preferences: { clusterName: "dev" }, - kubeConfigPath: ClusterStore.embedCustomKubeConfig("dev", kubeconfig), - workspace: "workstation" + kubeConfigPath: ClusterStore.embedCustomKubeConfig("dev", kubeconfig) }) ); }); @@ -138,51 +127,11 @@ describe("empty config", () => { expect(clusterStore.clusters.size).toBe(2); }); - it("gets clusters by workspaces", () => { - const wsClusters = clusterStore.getByWorkspaceId("workstation"); - const defaultClusters = clusterStore.getByWorkspaceId("default"); - - expect(defaultClusters.length).toBe(0); - expect(wsClusters.length).toBe(2); - expect(wsClusters[0].id).toBe("prod"); - expect(wsClusters[1].id).toBe("dev"); - }); - it("check if cluster's kubeconfig file saved", () => { const file = ClusterStore.embedCustomKubeConfig("boo", "kubeconfig"); expect(fs.readFileSync(file, "utf8")).toBe("kubeconfig"); }); - - it("check if reorderring works for same from and to", () => { - clusterStore.swapIconOrders("workstation", 1, 1); - - const clusters = clusterStore.getByWorkspaceId("workstation"); - - expect(clusters[0].id).toBe("prod"); - expect(clusters[0].preferences.iconOrder).toBe(0); - expect(clusters[1].id).toBe("dev"); - expect(clusters[1].preferences.iconOrder).toBe(1); - }); - - it("check if reorderring works for different from and to", () => { - clusterStore.swapIconOrders("workstation", 0, 1); - - const clusters = clusterStore.getByWorkspaceId("workstation"); - - expect(clusters[0].id).toBe("dev"); - expect(clusters[0].preferences.iconOrder).toBe(0); - expect(clusters[1].id).toBe("prod"); - expect(clusters[1].preferences.iconOrder).toBe(1); - }); - - it("check if after icon reordering, changing workspaces still works", () => { - clusterStore.swapIconOrders("workstation", 1, 1); - clusterStore.getById("prod").workspace = "default"; - - expect(clusterStore.getByWorkspaceId("workstation").length).toBe(1); - expect(clusterStore.getByWorkspaceId("default").length).toBe(1); - }); }); }); @@ -485,12 +434,6 @@ describe("for a pre 2.7.0-beta.0 config without a workspace", () => { afterEach(() => { mockFs.restore(); }); - - it("adds cluster to default workspace", async () => { - const storedClusterData = clusterStore.clustersList[0]; - - expect(storedClusterData.workspace).toBe("default"); - }); }); describe("pre 3.6.0-beta.1 config with an existing cluster", () => { diff --git a/src/common/__tests__/workspace-store.test.ts b/src/common/__tests__/workspace-store.test.ts deleted file mode 100644 index 355eb8b2ce..0000000000 --- a/src/common/__tests__/workspace-store.test.ts +++ /dev/null @@ -1,188 +0,0 @@ -import mockFs from "mock-fs"; - -jest.mock("electron", () => { - return { - app: { - getVersion: () => "99.99.99", - getPath: () => "tmp", - getLocale: () => "en" - }, - ipcMain: { - handle: jest.fn(), - on: jest.fn() - } - }; -}); - -import { Workspace, WorkspaceStore } from "../workspace-store"; - -describe("workspace store tests", () => { - describe("for an empty config", () => { - beforeEach(async () => { - WorkspaceStore.resetInstance(); - mockFs({ tmp: { "lens-workspace-store.json": "{}" } }); - - await WorkspaceStore.getInstance().load(); - }); - - afterEach(() => { - mockFs.restore(); - }); - - it("default workspace should always exist", () => { - const ws = WorkspaceStore.getInstance(); - - expect(ws.workspaces.size).toBe(1); - expect(ws.getById(WorkspaceStore.defaultId)).not.toBe(null); - }); - - it("default workspace should be enabled", () => { - const ws = WorkspaceStore.getInstance(); - - expect(ws.workspaces.size).toBe(1); - expect(ws.getById(WorkspaceStore.defaultId).enabled).toBe(true); - }); - - it("cannot remove the default workspace", () => { - const ws = WorkspaceStore.getInstance(); - - expect(() => ws.removeWorkspaceById(WorkspaceStore.defaultId)).toThrowError("Cannot remove"); - }); - - it("can update workspace description", () => { - const ws = WorkspaceStore.getInstance(); - const workspace = ws.addWorkspace(new Workspace({ - id: "foobar", - name: "foobar", - })); - - workspace.description = "Foobar description"; - ws.updateWorkspace(workspace); - - expect(ws.getById("foobar").description).toBe("Foobar description"); - }); - - it("can add workspaces", () => { - const ws = WorkspaceStore.getInstance(); - - ws.addWorkspace(new Workspace({ - id: "123", - name: "foobar", - })); - - const workspace = ws.getById("123"); - - expect(workspace.name).toBe("foobar"); - expect(workspace.enabled).toBe(true); - }); - - it("cannot set a non-existent workspace to be active", () => { - const ws = WorkspaceStore.getInstance(); - - expect(() => ws.setActive("abc")).toThrow("doesn't exist"); - }); - - it("can set a existent workspace to be active", () => { - const ws = WorkspaceStore.getInstance(); - - ws.addWorkspace(new Workspace({ - id: "abc", - name: "foobar", - })); - - expect(() => ws.setActive("abc")).not.toThrowError(); - }); - - it("can remove a workspace", () => { - const ws = WorkspaceStore.getInstance(); - - ws.addWorkspace(new Workspace({ - id: "123", - name: "foobar", - })); - ws.addWorkspace(new Workspace({ - id: "1234", - name: "foobar 1", - })); - ws.removeWorkspaceById("123"); - - expect(ws.workspaces.size).toBe(2); - }); - - it("cannot create workspace with existent name", () => { - const ws = WorkspaceStore.getInstance(); - - ws.addWorkspace(new Workspace({ - id: "someid", - name: "default", - })); - - expect(ws.workspacesList.length).toBe(1); // default workspace only - }); - - it("cannot create workspace with empty name", () => { - const ws = WorkspaceStore.getInstance(); - - ws.addWorkspace(new Workspace({ - id: "random", - name: "", - })); - - expect(ws.workspacesList.length).toBe(1); // default workspace only - }); - - it("cannot create workspace with ' ' name", () => { - const ws = WorkspaceStore.getInstance(); - - ws.addWorkspace(new Workspace({ - id: "random", - name: " ", - })); - - expect(ws.workspacesList.length).toBe(1); // default workspace only - }); - - it("trim workspace name", () => { - const ws = WorkspaceStore.getInstance(); - - ws.addWorkspace(new Workspace({ - id: "random", - name: "default ", - })); - - expect(ws.workspacesList.length).toBe(1); // default workspace only - }); - }); - - describe("for a non-empty config", () => { - beforeEach(async () => { - WorkspaceStore.resetInstance(); - mockFs({ - tmp: { - "lens-workspace-store.json": JSON.stringify({ - currentWorkspace: "abc", - workspaces: [{ - id: "abc", - name: "test" - }, { - id: "default", - name: "default" - }] - }) - } - }); - - await WorkspaceStore.getInstance().load(); - }); - - afterEach(() => { - mockFs.restore(); - }); - - it("doesn't revert to default workspace", async () => { - const ws = WorkspaceStore.getInstance(); - - expect(ws.currentWorkspaceId).toBe("abc"); - }); - }); -}); diff --git a/src/common/catalog-category-registry.ts b/src/common/catalog-category-registry.ts new file mode 100644 index 0000000000..ec20aa73c0 --- /dev/null +++ b/src/common/catalog-category-registry.ts @@ -0,0 +1,42 @@ +import { action, computed, observable, toJS } from "mobx"; +import { CatalogCategory, CatalogEntityData } from "./catalog-entity"; + +export class CatalogCategoryRegistry { + @observable protected categories: CatalogCategory[] = []; + + @action add(category: CatalogCategory) { + this.categories.push(category); + } + + @action remove(category: CatalogCategory) { + this.categories = this.categories.filter((cat) => cat.apiVersion !== category.apiVersion && cat.kind !== category.kind); + } + + @computed get items() { + return toJS(this.categories); + } + + getEntityForData(data: CatalogEntityData) { + const splitApiVersion = data.apiVersion.split("/"); + const group = splitApiVersion[0]; + const version = splitApiVersion[1]; + + const category = this.categories.find((category) => { + return category.spec.group === group && category.spec.names.kind === data.kind; + }); + + if (!category) { + return null; + } + + const specVersion = category.spec.versions.find((v) => v.name === version); + + if (!specVersion) { + return null; + } + + return new specVersion.entityClass(data); + } +} + +export const catalogCategoryRegistry = new CatalogCategoryRegistry(); diff --git a/src/common/catalog-entities/index.ts b/src/common/catalog-entities/index.ts new file mode 100644 index 0000000000..1e6254a472 --- /dev/null +++ b/src/common/catalog-entities/index.ts @@ -0,0 +1 @@ +export * from "./kubernetes-cluster"; diff --git a/src/common/catalog-entities/kubernetes-cluster.ts b/src/common/catalog-entities/kubernetes-cluster.ts new file mode 100644 index 0000000000..a2ccfcf600 --- /dev/null +++ b/src/common/catalog-entities/kubernetes-cluster.ts @@ -0,0 +1,73 @@ +import { observable } from "mobx"; +import { catalogCategoryRegistry } from "../catalog-category-registry"; +import { CatalogCategory, CatalogEntity, CatalogEntityActionContext, CatalogEntityData, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog-entity"; +import { clusterStore } from "../cluster-store"; + +export type KubernetesClusterSpec = { + kubeconfigPath: string; + kubeconfigContext: string; +}; + +export interface KubernetesClusterStatus extends CatalogEntityStatus { + phase: "connected" | "disconnected"; +} + +export class KubernetesCluster implements CatalogEntity { + public readonly apiVersion = "entity.k8slens.dev/v1alpha1"; + public readonly kind = "KubernetesCluster"; + @observable public metadata: CatalogEntityMetadata; + @observable public status: KubernetesClusterStatus; + @observable public spec: KubernetesClusterSpec; + + constructor(data: CatalogEntityData) { + this.metadata = data.metadata; + this.status = data.status as KubernetesClusterStatus; + this.spec = data.spec as KubernetesClusterSpec; + } + + getId() { + return this.metadata.uid; + } + + getName() { + return this.metadata.name; + } + + get cluster() { + return clusterStore.getById(this.metadata.uid); + } + + async onRun(context: CatalogEntityActionContext) { + context.navigate(`/cluster/${this.metadata.uid}`); + } + + async onDetailsOpen() { + // + } + + async onContextMenuOpen() { + // + } +} + +export class KubernetesClusterCategory implements CatalogCategory { + public readonly apiVersion = "catalog.k8slens.dev/v1alpha1"; + public readonly kind = "CatalogCategory"; + public metadata = { + name: "Kubernetes Clusters" + }; + public spec = { + group: "entity.k8slens.dev", + versions: [ + { + name: "v1alpha1", + entityClass: KubernetesCluster + } + ], + names: { + kind: "KubernetesCluster" + } + }; +} + +catalogCategoryRegistry.add(new KubernetesClusterCategory()); diff --git a/src/common/catalog-entity-registry.ts b/src/common/catalog-entity-registry.ts new file mode 100644 index 0000000000..bf859423fc --- /dev/null +++ b/src/common/catalog-entity-registry.ts @@ -0,0 +1,32 @@ +import { action, computed, observable } from "mobx"; +import { CatalogEntity } from "./catalog-entity"; + +export class CatalogEntityRegistry { + @observable.deep protected sources: Map = observable.map(new Map(), { deep: true }); + + @action addSource(id: string, source: CatalogEntity[]) { + this.sources.set(id, source); + } + + @action removeSource(id: string) { + this.sources.delete(id); + } + + @computed get items() { + const catalogItems: CatalogEntity[] = []; + + for (const [id, items] of this.sources) { + items.forEach((item) => catalogItems.push(item)); + } + + return catalogItems; + } + + getItemsForApiKind(apiVersion: string, kind: string): T[] { + const items = this.items.filter((item) => item.apiVersion === apiVersion && item.kind === kind); + + return items as T[]; + } +} + +export const catalogEntityRegistry = new CatalogEntityRegistry(); diff --git a/src/common/catalog-entity.ts b/src/common/catalog-entity.ts new file mode 100644 index 0000000000..e69eeb9f58 --- /dev/null +++ b/src/common/catalog-entity.ts @@ -0,0 +1,63 @@ +export interface CatalogCategoryVersion { + name: string; + entityClass: { new(data: CatalogEntityData): CatalogEntity }; +} + +export interface CatalogCategory { + apiVersion: string; + kind: string; + metadata: { + name: string; + } + spec: { + group: string; + versions: CatalogCategoryVersion[]; + names: { + kind: string; + } + } +} + +export type CatalogEntityMetadata = { + uid: string; + name: string; + description?: string; + source?: string; + labels: { + [key: string]: string; + } + [key: string]: string | object; +}; + +export type CatalogEntityStatus = { + phase: string; + reason?: string; + message?: string; + active?: boolean; +}; + +export interface CatalogEntityActionContext { + navigate: (url: string) => void; +} + +export interface CatalogEntityContextMenuContext extends CatalogEntityActionContext { + menu: any +} + +export type CatalogEntityData = { + apiVersion: string; + kind: string; + metadata: CatalogEntityMetadata; + status: CatalogEntityStatus; + spec: { + [key: string]: any; + } +}; + +export interface CatalogEntity extends CatalogEntityData { + getId: () => string; + getName: () => string; + onRun: (context: CatalogEntityActionContext) => Promise; + onDetailsOpen: (context: CatalogEntityActionContext) => Promise; + onContextMenuOpen: (context: CatalogEntityContextMenuContext) => Promise; +} diff --git a/src/common/cluster-ipc.ts b/src/common/cluster-ipc.ts index d44634407e..30e485bece 100644 --- a/src/common/cluster-ipc.ts +++ b/src/common/cluster-ipc.ts @@ -4,6 +4,7 @@ import { appEventBus } from "./event-bus"; import { ResourceApplier } from "../main/resource-applier"; import { ipcMain, IpcMainInvokeEvent } from "electron"; import { clusterFrameMap } from "./cluster-frames"; +import { catalogEntityRegistry } from "./catalog-entity-registry"; export const clusterActivateHandler = "cluster:activate"; export const clusterSetFrameIdHandler = "cluster:set-frame-id"; @@ -17,6 +18,10 @@ if (ipcMain) { const cluster = clusterStore.getById(clusterId); if (cluster) { + catalogEntityRegistry.getItemsForApiKind("entity.k8slens.dev/v1alpha1", "KubernetesCluster").forEach((item) => { + if (item.metadata.uid === cluster.id) item.status.active = true; + }); + return cluster.activate(force); } }); @@ -43,6 +48,9 @@ if (ipcMain) { if (cluster) { cluster.disconnect(); + catalogEntityRegistry.getItemsForApiKind("entity.k8slens.dev/v1alpha1", "KubernetesCluster").forEach((item) => { + if (item.metadata.uid === cluster.id) item.status.active = false; + }); clusterFrameMap.delete(cluster.id); } }); diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index d1006f0a3d..a161f95189 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -1,4 +1,3 @@ -import { workspaceStore } from "./workspace-store"; import path from "path"; import { app, ipcRenderer, remote, webFrame } from "electron"; import { unlink } from "fs-extra"; @@ -12,9 +11,7 @@ import { dumpConfigYaml } from "./kube-helpers"; import { saveToAppFiles } from "./utils/saveToAppFiles"; import { KubeConfig } from "@kubernetes/client-node"; import { handleRequest, requestMain, subscribeToBroadcast, unsubscribeAllFromBroadcast } from "./ipc"; -import _ from "lodash"; import move from "array-move"; -import type { WorkspaceId } from "./workspace-store"; import { ResourceType } from "../renderer/components/+cluster-settings/components/cluster-metrics-setting"; export interface ClusterIconUpload { @@ -47,9 +44,6 @@ export interface ClusterModel { /** Path to cluster kubeconfig */ kubeConfigPath: string; - /** Workspace id */ - workspace?: WorkspaceId; - /** User context in kubeconfig */ contextName?: string; @@ -110,6 +104,7 @@ export class ClusterStore extends BaseStore { @observable removedClusters = observable.map(); @observable clusters = observable.map(); + private static stateRequestChannel = "cluster:states"; private constructor() { @@ -222,7 +217,6 @@ export class ClusterStore extends BaseStore { const clusterId = this.clusters.has(id) ? id : null; this.activeCluster = clusterId; - workspaceStore.setLastActiveClusterId(clusterId); } deactivate(id: ClusterId) { @@ -232,8 +226,8 @@ export class ClusterStore extends BaseStore { } @action - swapIconOrders(workspace: WorkspaceId, from: number, to: number) { - const clusters = this.getByWorkspaceId(workspace); + swapIconOrders(from: number, to: number) { + const clusters = this.enabledClustersList; if (from < 0 || to < 0 || from >= clusters.length || to >= clusters.length || isNaN(from) || isNaN(to)) { throw new Error(`invalid from<->to arguments`); @@ -255,13 +249,6 @@ export class ClusterStore extends BaseStore { return this.clusters.get(id); } - getByWorkspaceId(workspaceId: string): Cluster[] { - const clusters = Array.from(this.clusters.values()) - .filter(cluster => cluster.workspace === workspaceId); - - return _.sortBy(clusters, cluster => cluster.preferences.iconOrder); - } - @action addClusters(...models: ClusterModel[]): Cluster[] { const clusters: Cluster[] = []; @@ -313,13 +300,6 @@ export class ClusterStore extends BaseStore { } } - @action - removeByWorkspaceId(workspaceId: string) { - this.getByWorkspaceId(workspaceId).forEach(cluster => { - this.removeById(cluster.id); - }); - } - @action protected fromStore({ activeCluster, clusters = [] }: ClusterStoreModel = {}) { const currentClusters = this.clusters.toJS(); diff --git a/src/common/workspace-store.ts b/src/common/workspace-store.ts deleted file mode 100644 index b037124721..0000000000 --- a/src/common/workspace-store.ts +++ /dev/null @@ -1,345 +0,0 @@ -import { ipcRenderer } from "electron"; -import { action, computed, observable, toJS, reaction } from "mobx"; -import { BaseStore } from "./base-store"; -import { clusterStore } from "./cluster-store"; -import { appEventBus } from "./event-bus"; -import { broadcastMessage, handleRequest, requestMain } from "../common/ipc"; -import logger from "../main/logger"; -import type { ClusterId } from "./cluster-store"; - -export type WorkspaceId = string; - -export interface WorkspaceStoreModel { - workspaces: WorkspaceModel[]; - currentWorkspace?: WorkspaceId; -} - -export interface WorkspaceModel { - id: WorkspaceId; - name: string; - description?: string; - ownerRef?: string; - lastActiveClusterId?: ClusterId; -} - -export interface WorkspaceState { - enabled: boolean; -} - -const updateFromModel = Symbol("updateFromModel"); - -/** - * Workspace - * - * @beta - */ -export class Workspace implements WorkspaceModel, WorkspaceState { - /** - * Unique id for workspace - * - * @observable - */ - @observable id: WorkspaceId; - /** - * Workspace name - * - * @observable - */ - @observable name: string; - /** - * Workspace description - * - * @observable - */ - @observable description?: string; - /** - * Workspace owner reference - * - * If extension sets ownerRef then it needs to explicitly mark workspace as enabled onActivate (or when workspace is saved) - * - * @observable - */ - @observable ownerRef?: string; - - /** - * Last active cluster id - * - * @observable - */ - @observable lastActiveClusterId?: ClusterId; - - - @observable private _enabled: boolean; - - constructor(data: WorkspaceModel) { - Object.assign(this, data); - - if (!ipcRenderer) { - reaction(() => this.getState(), () => { - this.pushState(); - }); - } - } - - /** - * Is workspace enabled - * - * Workspaces that don't have ownerRef will be enabled by default. Workspaces with ownerRef need to explicitly enable a workspace. - * - * @observable - */ - get enabled(): boolean { - return !this.isManaged || this._enabled; - } - - set enabled(enabled: boolean) { - this._enabled = enabled; - } - - /** - * Is workspace managed by an extension - */ - get isManaged(): boolean { - return !!this.ownerRef; - } - - /** - * Get workspace state - * - */ - getState(): WorkspaceState { - return toJS({ - enabled: this.enabled - }); - } - - /** - * Push state - * - * @internal - * @param state workspace state - */ - pushState(state = this.getState()) { - logger.silly("[WORKSPACE] pushing state", {...state, id: this.id}); - broadcastMessage("workspace:state", this.id, toJS(state)); - } - - /** - * - * @param state workspace state - */ - @action setState(state: WorkspaceState) { - Object.assign(this, state); - } - - [updateFromModel] = action((model: WorkspaceModel) => { - Object.assign(this, model); - }); - - toJSON(): WorkspaceModel { - return toJS({ - id: this.id, - name: this.name, - description: this.description, - ownerRef: this.ownerRef, - lastActiveClusterId: this.lastActiveClusterId - }); - } -} - -export class WorkspaceStore extends BaseStore { - static readonly defaultId: WorkspaceId = "default"; - private static stateRequestChannel = "workspace:states"; - - @observable currentWorkspaceId = WorkspaceStore.defaultId; - @observable workspaces = observable.map(); - - private constructor() { - super({ - configName: "lens-workspace-store", - }); - - this.workspaces.set(WorkspaceStore.defaultId, new Workspace({ - id: WorkspaceStore.defaultId, - name: "default" - })); - } - - async load() { - await super.load(); - type workspaceStateSync = { - id: string; - state: WorkspaceState; - }; - - if (ipcRenderer) { - logger.info("[WORKSPACE-STORE] requesting initial state sync"); - const workspaceStates: workspaceStateSync[] = await requestMain(WorkspaceStore.stateRequestChannel); - - workspaceStates.forEach((workspaceState) => { - const workspace = this.getById(workspaceState.id); - - if (workspace) { - workspace.setState(workspaceState.state); - } - }); - } else { - handleRequest(WorkspaceStore.stateRequestChannel, (): workspaceStateSync[] => { - const states: workspaceStateSync[] = []; - - this.workspacesList.forEach((workspace) => { - states.push({ - state: workspace.getState(), - id: workspace.id - }); - }); - - return states; - }); - } - } - - registerIpcListener() { - logger.info("[WORKSPACE-STORE] starting to listen state events"); - ipcRenderer.on("workspace:state", (event, workspaceId: string, state: WorkspaceState) => { - this.getById(workspaceId)?.setState(state); - }); - } - - unregisterIpcListener() { - super.unregisterIpcListener(); - ipcRenderer.removeAllListeners("workspace:state"); - } - - @computed get currentWorkspace(): Workspace { - return this.getById(this.currentWorkspaceId); - } - - @computed get workspacesList() { - return Array.from(this.workspaces.values()); - } - - @computed get enabledWorkspacesList() { - return this.workspacesList.filter((w) => w.enabled); - } - - pushState() { - this.workspaces.forEach((w) => { - w.pushState(); - }); - } - - isDefault(id: WorkspaceId) { - return id === WorkspaceStore.defaultId; - } - - getById(id: WorkspaceId): Workspace { - return this.workspaces.get(id); - } - - getByName(name: string): Workspace { - return this.workspacesList.find(workspace => workspace.name === name); - } - - @action - setActive(id = WorkspaceStore.defaultId) { - if (id === this.currentWorkspaceId) return; - - if (!this.getById(id)) { - throw new Error(`workspace ${id} doesn't exist`); - } - this.currentWorkspaceId = id; - } - - @action - addWorkspace(workspace: Workspace) { - const { id, name } = workspace; - - if (!name.trim() || this.getByName(name.trim())) { - return; - } - this.workspaces.set(id, workspace); - - if (!workspace.isManaged) { - workspace.enabled = true; - } - - appEventBus.emit({name: "workspace", action: "add"}); - - return workspace; - } - - @action - updateWorkspace(workspace: Workspace) { - this.workspaces.set(workspace.id, workspace); - appEventBus.emit({name: "workspace", action: "update"}); - } - - @action - removeWorkspace(workspace: Workspace) { - this.removeWorkspaceById(workspace.id); - } - - @action - removeWorkspaceById(id: WorkspaceId) { - const workspace = this.getById(id); - - if (!workspace) return; - - if (this.isDefault(id)) { - throw new Error("Cannot remove default workspace"); - } - - if (this.currentWorkspaceId === id) { - this.currentWorkspaceId = WorkspaceStore.defaultId; // reset to default - } - this.workspaces.delete(id); - appEventBus.emit({name: "workspace", action: "remove"}); - clusterStore.removeByWorkspaceId(id); - } - - @action - setLastActiveClusterId(clusterId?: ClusterId, workspaceId = this.currentWorkspaceId) { - this.getById(workspaceId).lastActiveClusterId = clusterId; - } - - @action - protected fromStore({ currentWorkspace, workspaces = [] }: WorkspaceStoreModel) { - if (currentWorkspace) { - this.currentWorkspaceId = currentWorkspace; - } - - const currentWorkspaces = this.workspaces.toJS(); - const newWorkspaceIds = new Set([WorkspaceStore.defaultId]); // never delete default - - for (const workspaceModel of workspaces) { - const oldWorkspace = this.workspaces.get(workspaceModel.id); - - if (oldWorkspace) { - oldWorkspace[updateFromModel](workspaceModel); - } else { - this.workspaces.set(workspaceModel.id, new Workspace(workspaceModel)); - } - - newWorkspaceIds.add(workspaceModel.id); - } - - // remove deleted workspaces - for (const workspaceId of currentWorkspaces.keys()) { - if (!newWorkspaceIds.has(workspaceId)) { - this.workspaces.delete(workspaceId); - } - } - } - - toJSON(): WorkspaceStoreModel { - return toJS({ - currentWorkspace: this.currentWorkspaceId, - workspaces: this.workspacesList.map((w) => w.toJSON()), - }, { - recurseEverything: true - }); - } -} - -export const workspaceStore = WorkspaceStore.getInstance(); diff --git a/src/extensions/core-api/catalog.ts b/src/extensions/core-api/catalog.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/extensions/core-api/stores.ts b/src/extensions/core-api/stores.ts index 706c336fdf..b79c11248a 100644 --- a/src/extensions/core-api/stores.ts +++ b/src/extensions/core-api/stores.ts @@ -1,8 +1 @@ export { ExtensionStore } from "../extension-store"; - -export { clusterStore, Cluster, ClusterStore } from "../stores/cluster-store"; -export type { ClusterModel, ClusterId } from "../stores/cluster-store"; - -export { workspaceStore, Workspace, WorkspaceStore } from "../stores/workspace-store"; -export type { WorkspaceId, WorkspaceModel } from "../stores/workspace-store"; - diff --git a/src/extensions/lens-main-extension.ts b/src/extensions/lens-main-extension.ts index f0e943540d..12519ed656 100644 --- a/src/extensions/lens-main-extension.ts +++ b/src/extensions/lens-main-extension.ts @@ -2,6 +2,8 @@ import type { MenuRegistration } from "./registries/menu-registry"; import { LensExtension } from "./lens-extension"; import { WindowManager } from "../main/window-manager"; import { getExtensionPageUrl } from "./registries/page-registry"; +import { catalogEntityRegistry } from "../common/catalog-entity-registry"; +import { CatalogEntity } from "../common/catalog-entity"; export class LensMainExtension extends LensExtension { appMenus: MenuRegistration[] = []; @@ -16,4 +18,12 @@ export class LensMainExtension extends LensExtension { await windowManager.navigate(pageUrl, frameId); } + + addCatalogSource(id: string, source: CatalogEntity[]) { + catalogEntityRegistry.addSource(`${this.name}:${id}`, source); + } + + removeCatalogSorce(id: string) { + catalogEntityRegistry.removeSource(`${this.name}:${id}`); + } } diff --git a/src/extensions/registries/command-registry.ts b/src/extensions/registries/command-registry.ts index 0b1fc0252c..802593dbb4 100644 --- a/src/extensions/registries/command-registry.ts +++ b/src/extensions/registries/command-registry.ts @@ -1,14 +1,12 @@ // Extensions API -> Commands import type { Cluster } from "../../main/cluster"; -import type { Workspace } from "../../common/workspace-store"; import { BaseRegistry } from "./base-registry"; import { action } from "mobx"; import { LensExtension } from "../lens-extension"; export type CommandContext = { cluster?: Cluster; - workspace?: Workspace; }; export interface CommandRegistration { diff --git a/src/extensions/stores/cluster-store.ts b/src/extensions/stores/cluster-store.ts deleted file mode 100644 index b2aba41c06..0000000000 --- a/src/extensions/stores/cluster-store.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { clusterStore as internalClusterStore, ClusterId } from "../../common/cluster-store"; -import type { ClusterModel } from "../../common/cluster-store"; -import { Cluster } from "../../main/cluster"; -import { Singleton } from "../core-api/utils"; -import { ObservableMap } from "mobx"; - -export { Cluster } from "../../main/cluster"; -export type { ClusterModel, ClusterId } from "../../common/cluster-store"; - -/** - * Store for all added clusters - * - * @beta - */ -export class ClusterStore extends Singleton { - - /** - * Active cluster id - */ - get activeClusterId(): string { - return internalClusterStore.activeCluster; - } - - /** - * Set active cluster id - */ - set activeClusterId(id : ClusterId) { - internalClusterStore.activeCluster = id; - } - - /** - * Map of all clusters - */ - get clusters(): ObservableMap { - return internalClusterStore.clusters; - } - - /** - * Get active cluster (a cluster which is currently visible) - */ - get activeCluster(): Cluster { - if (!this.activeClusterId) { - return null; - } - - return this.getById(this.activeClusterId); - } - - /** - * Array of all clusters - */ - get clustersList(): Cluster[] { - return internalClusterStore.clustersList; - } - - /** - * Array of all enabled clusters - */ - get enabledClustersList(): Cluster[] { - return internalClusterStore.enabledClustersList; - } - - /** - * Array of all clusters that have active connection to a Kubernetes cluster - */ - get connectedClustersList(): Cluster[] { - return internalClusterStore.connectedClustersList; - } - - /** - * Get cluster object by cluster id - * @param id cluster id - */ - getById(id: ClusterId): Cluster { - return internalClusterStore.getById(id); - } - - /** - * Get all clusters belonging to a workspace - * @param workspaceId workspace id - */ - getByWorkspaceId(workspaceId: string): Cluster[] { - return internalClusterStore.getByWorkspaceId(workspaceId); - } - - /** - * Add clusters to store - * @param models list of cluster models - */ - addClusters(...models: ClusterModel[]): Cluster[] { - return internalClusterStore.addClusters(...models); - } - - /** - * Add a cluster to store - * @param model cluster - */ - addCluster(model: ClusterModel | Cluster): Cluster { - return internalClusterStore.addCluster(model); - } - - /** - * Remove a cluster from store - * @param model cluster - */ - async removeCluster(model: ClusterModel) { - return this.removeById(model.id); - } - - /** - * Remove a cluster from store by id - * @param clusterId cluster id - */ - async removeById(clusterId: ClusterId) { - return internalClusterStore.removeById(clusterId); - } - - /** - * Remove all clusters belonging to a workspaces - * @param workspaceId workspace id - */ - removeByWorkspaceId(workspaceId: string) { - return internalClusterStore.removeByWorkspaceId(workspaceId); - } -} - - -export const clusterStore = ClusterStore.getInstance(); diff --git a/src/extensions/stores/workspace-store.ts b/src/extensions/stores/workspace-store.ts deleted file mode 100644 index 2ff4a830fd..0000000000 --- a/src/extensions/stores/workspace-store.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { Singleton } from "../core-api/utils"; -import { workspaceStore as internalWorkspaceStore, WorkspaceStore as InternalWorkspaceStore, Workspace, WorkspaceId } from "../../common/workspace-store"; -import { ObservableMap } from "mobx"; - -export { Workspace } from "../../common/workspace-store"; -export type { WorkspaceId, WorkspaceModel } from "../../common/workspace-store"; - -/** - * Stores all workspaces - * - * @beta - */ -export class WorkspaceStore extends Singleton { - /** - * Default workspace id, this workspace is always present - */ - static readonly defaultId: WorkspaceId = InternalWorkspaceStore.defaultId; - - /** - * Currently active workspace id - */ - get currentWorkspaceId(): string { - return internalWorkspaceStore.currentWorkspaceId; - } - - /** - * Set active workspace id - */ - set currentWorkspaceId(id: string) { - internalWorkspaceStore.currentWorkspaceId = id; - } - - /** - * Map of all workspaces - */ - get workspaces(): ObservableMap { - return internalWorkspaceStore.workspaces; - } - - /** - * Currently active workspace - */ - get currentWorkspace(): Workspace { - return internalWorkspaceStore.currentWorkspace; - } - - /** - * Array of all workspaces - */ - get workspacesList(): Workspace[] { - return internalWorkspaceStore.workspacesList; - } - - /** - * Array of all enabled (visible) workspaces - */ - get enabledWorkspacesList(): Workspace[] { - return internalWorkspaceStore.enabledWorkspacesList; - } - - /** - * Get workspace by id - * @param id workspace id - */ - getById(id: WorkspaceId): Workspace { - return internalWorkspaceStore.getById(id); - } - - /** - * Get workspace by name - * @param name workspace name - */ - getByName(name: string): Workspace { - return internalWorkspaceStore.getByName(name); - } - - /** - * Set active workspace - * @param id workspace id - */ - setActive(id = WorkspaceStore.defaultId) { - return internalWorkspaceStore.setActive(id); - } - - /** - * Add a workspace to store - * @param workspace workspace - */ - addWorkspace(workspace: Workspace) { - return internalWorkspaceStore.addWorkspace(workspace); - } - - /** - * Update a workspace in store - * @param workspace workspace - */ - updateWorkspace(workspace: Workspace) { - return internalWorkspaceStore.updateWorkspace(workspace); - } - - /** - * Remove workspace from store - * @param workspace workspace - */ - removeWorkspace(workspace: Workspace) { - return internalWorkspaceStore.removeWorkspace(workspace); - } - - /** - * Remove workspace by id - * @param id workspace - */ - removeWorkspaceById(id: WorkspaceId) { - return internalWorkspaceStore.removeWorkspaceById(id); - } -} - -export const workspaceStore = WorkspaceStore.getInstance(); diff --git a/src/main/__test__/cluster.test.ts b/src/main/__test__/cluster.test.ts index 4b11a19879..113bb49a0c 100644 --- a/src/main/__test__/cluster.test.ts +++ b/src/main/__test__/cluster.test.ts @@ -31,7 +31,6 @@ jest.mock("request-promise-native"); import { Console } from "console"; import mockFs from "mock-fs"; -import { workspaceStore } from "../../common/workspace-store"; import { Cluster } from "../cluster"; import { ContextHandler } from "../context-handler"; import { getFreePort } from "../port"; @@ -81,8 +80,7 @@ describe("create clusters", () => { c = new Cluster({ id: "foo", contextName: "minikube", - kubeConfigPath: "minikube-config.yml", - workspace: workspaceStore.currentWorkspaceId + kubeConfigPath: "minikube-config.yml" }); }); @@ -162,8 +160,7 @@ describe("create clusters", () => { }({ id: "foo", contextName: "minikube", - kubeConfigPath: "minikube-config.yml", - workspace: workspaceStore.currentWorkspaceId + kubeConfigPath: "minikube-config.yml" }); await c.init(port); diff --git a/src/main/__test__/kubeconfig-manager.test.ts b/src/main/__test__/kubeconfig-manager.test.ts index d04f7492f2..a272d1e597 100644 --- a/src/main/__test__/kubeconfig-manager.test.ts +++ b/src/main/__test__/kubeconfig-manager.test.ts @@ -26,7 +26,6 @@ jest.mock("winston", () => ({ import { KubeconfigManager } from "../kubeconfig-manager"; import mockFs from "mock-fs"; import { Cluster } from "../cluster"; -import { workspaceStore } from "../../common/workspace-store"; import { ContextHandler } from "../context-handler"; import { getFreePort } from "../port"; import fse from "fs-extra"; @@ -77,8 +76,7 @@ describe("kubeconfig manager tests", () => { const cluster = new Cluster({ id: "foo", contextName: "minikube", - kubeConfigPath: "minikube-config.yml", - workspace: workspaceStore.currentWorkspaceId + kubeConfigPath: "minikube-config.yml" }); const contextHandler = new ContextHandler(cluster); const port = await getFreePort(); @@ -98,8 +96,7 @@ describe("kubeconfig manager tests", () => { const cluster = new Cluster({ id: "foo", contextName: "minikube", - kubeConfigPath: "minikube-config.yml", - workspace: workspaceStore.currentWorkspaceId + kubeConfigPath: "minikube-config.yml" }); const contextHandler = new ContextHandler(cluster); const port = await getFreePort(); diff --git a/src/main/catalog-pusher.ts b/src/main/catalog-pusher.ts new file mode 100644 index 0000000000..268566b81e --- /dev/null +++ b/src/main/catalog-pusher.ts @@ -0,0 +1,26 @@ +import { autorun, toJS } from "mobx"; +import { broadcastMessage, subscribeToBroadcast } from "../common/ipc"; +import { CatalogEntityRegistry} from "../common/catalog-entity-registry"; +import "../common/catalog-entities/kubernetes-cluster"; + +export class CatalogPusher { + static init(catalog: CatalogEntityRegistry) { + new CatalogPusher(catalog).init(); + } + + constructor(private catalog: CatalogEntityRegistry) {} + + init() { + autorun(() => { + this.broadcast(); + }); + + subscribeToBroadcast("catalog:broadcast", () => { + this.broadcast(); + }); + } + + broadcast() { + broadcastMessage("catalog:items", toJS(this.catalog.items, { recurseEverything: true })); + } +} diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts index dfcda98203..f12f6e77c0 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster-manager.ts @@ -1,16 +1,23 @@ import "../common/cluster-ipc"; import type http from "http"; import { ipcMain } from "electron"; -import { autorun, reaction } from "mobx"; +import { action, autorun, observable, reaction, toJS } from "mobx"; import { clusterStore, getClusterIdFromHost } from "../common/cluster-store"; import { Cluster } from "./cluster"; import logger from "./logger"; import { apiKubePrefix } from "../common/vars"; import { Singleton } from "../common/utils"; +import { CatalogEntity } from "../common/catalog-entity"; +import { KubernetesCluster } from "../common/catalog-entities/kubernetes-cluster"; +import { catalogEntityRegistry } from "../common/catalog-entity-registry"; export class ClusterManager extends Singleton { + @observable.deep catalogSource: CatalogEntity[] = []; + constructor(public readonly port: number) { super(); + + catalogEntityRegistry.addSource("lens:kubernetes-clusters", this.catalogSource); // auto-init clusters reaction(() => clusterStore.enabledClustersList, (clusters) => { clusters.forEach((cluster) => { @@ -19,8 +26,14 @@ export class ClusterManager extends Singleton { cluster.init(port); } }); + }, { fireImmediately: true }); + reaction(() => toJS(clusterStore.enabledClustersList, { recurseEverything: true }), () => { + this.updateCatalogSource(clusterStore.enabledClustersList); + }, { fireImmediately: true }); + + // auto-stop removed clusters autorun(() => { const removedClusters = Array.from(clusterStore.removedClusters.values()); @@ -40,6 +53,55 @@ export class ClusterManager extends Singleton { ipcMain.on("network:online", () => { this.onNetworkOnline(); }); } + @action protected updateCatalogSource(clusters: Cluster[]) { + this.catalogSource.forEach((entity, index) => { + const clusterIndex = clusters.findIndex((cluster) => entity.metadata.uid === cluster.id); + + if (clusterIndex === -1) { + this.catalogSource.splice(index, 1); + } + }); + + clusters.forEach((cluster) => { + const entityIndex = this.catalogSource.findIndex((entity) => entity.metadata.uid === cluster.id); + const newEntity = this.catalogEntityFromCluster(cluster); + + if (entityIndex === -1) { + this.catalogSource.push(newEntity); + } else { + const entity = this.catalogSource[entityIndex] as KubernetesCluster; + + newEntity.status.active = entity.status.active; + this.catalogSource.splice(entityIndex, 1, newEntity); + } + }); + } + + protected catalogEntityFromCluster(cluster: Cluster) { + return new KubernetesCluster(toJS({ + apiVersion: "entity.k8slens.dev/v1alpha1", + kind: "KubernetesCluster", + metadata: { + uid: cluster.id, + name: cluster.name, + source: "local", + labels: { + "distro": cluster.metadata["distro"]?.toString() + } + }, + spec: { + kubeconfigPath: cluster.kubeConfigPath, + kubeconfigContext: cluster.contextName + }, + status: { + phase: cluster.disconnected ? "disconnected" : "connected", + reason: "", + message: "", + active: !cluster.disconnected + } + })); + } + protected onNetworkOffline() { logger.info("[CLUSTER-MANAGER]: network is offline"); clusterStore.enabledClustersList.forEach((cluster) => { diff --git a/src/main/cluster.ts b/src/main/cluster.ts index 19f0945c3e..dd0dc618db 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -1,7 +1,6 @@ import { ipcMain } from "electron"; import type { ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences } from "../common/cluster-store"; import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api"; -import type { WorkspaceId } from "../common/workspace-store"; import { action, comparer, computed, observable, reaction, toJS, when } from "mobx"; import { apiKubePrefix } from "../common/vars"; import { broadcastMessage, InvalidKubeconfigChannel, ClusterListNamespaceForbiddenChannel } from "../common/ipc"; @@ -104,12 +103,6 @@ export class Cluster implements ClusterModel, ClusterState { * @observable */ @observable contextName: string; - /** - * Workspace id - * - * @observable - */ - @observable workspace: WorkspaceId; /** * Path to kubeconfig * @@ -611,7 +604,6 @@ export class Cluster implements ClusterModel, ClusterState { id: this.id, contextName: this.contextName, kubeConfigPath: this.kubeConfigPath, - workspace: this.workspace, preferences: this.preferences, metadata: this.metadata, ownerRef: this.ownerRef, diff --git a/src/main/index.ts b/src/main/index.ts index 0a946b9c36..b40f9bf52e 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -17,7 +17,6 @@ import { registerFileProtocol } from "../common/register-protocol"; import logger from "./logger"; import { clusterStore } from "../common/cluster-store"; import { userStore } from "../common/user-store"; -import { workspaceStore } from "../common/workspace-store"; import { appEventBus } from "../common/event-bus"; import { extensionLoader } from "../extensions/extension-loader"; import { extensionsStore } from "../extensions/extensions-store"; @@ -30,6 +29,8 @@ import { getAppVersion, getAppVersionFromProxyServer } from "../common/utils"; import { bindBroadcastHandlers } from "../common/ipc"; import { startUpdateChecking } from "./app-updater"; import { IpcRendererNavigationEvents } from "../renderer/navigation/events"; +import { CatalogPusher } from "./catalog-pusher"; +import { catalogEntityRegistry } from "../common/catalog-entity-registry"; const workingDir = path.join(app.getPath("appData"), appName); let proxyPort: number; @@ -107,7 +108,6 @@ app.on("ready", async () => { await Promise.all([ userStore.load(), clusterStore.load(), - workspaceStore.load(), extensionsStore.load(), filesystemProvisionerStore.load(), ]); @@ -164,6 +164,7 @@ app.on("ready", async () => { } ipcMain.on(IpcRendererNavigationEvents.LOADED, () => { + CatalogPusher.init(catalogEntityRegistry); startUpdateChecking(); LensProtocolRouterMain .getInstance() diff --git a/src/main/menu.ts b/src/main/menu.ts index 142c776878..1225c38a1e 100644 --- a/src/main/menu.ts +++ b/src/main/menu.ts @@ -7,6 +7,7 @@ import { preferencesURL } from "../renderer/components/+preferences/preferences. import { whatsNewURL } from "../renderer/components/+whats-new/whats-new.route"; import { clusterSettingsURL } from "../renderer/components/+cluster-settings/cluster-settings.route"; import { extensionsURL } from "../renderer/components/+extensions/extensions.route"; +import { landingURL } from "../renderer/components/+landing-page/landing-page.route"; import { menuRegistry } from "../extensions/registries/menu-registry"; import logger from "./logger"; import { exitApp } from "./exit-app"; @@ -175,6 +176,13 @@ export function buildMenu(windowManager: WindowManager) { const viewMenu: MenuItemConstructorOptions = { label: "View", submenu: [ + { + label: "Catalog", + accelerator: "Shift+CmdOrCtrl+C", + click() { + navigate(landingURL()); + } + }, { label: "Command Palette...", accelerator: "Shift+CmdOrCtrl+P", diff --git a/src/main/tray.ts b/src/main/tray.ts index 3d6d2dd624..4f4186f255 100644 --- a/src/main/tray.ts +++ b/src/main/tray.ts @@ -5,10 +5,7 @@ import { autorun } from "mobx"; import { showAbout } from "./menu"; import { checkForUpdates } from "./app-updater"; import { WindowManager } from "./window-manager"; -import { clusterStore } from "../common/cluster-store"; -import { workspaceStore } from "../common/workspace-store"; import { preferencesURL } from "../renderer/components/+preferences/preferences.route"; -import { clusterViewURL } from "../renderer/components/cluster-manager/cluster-view.route"; import logger from "./logger"; import { isDevelopment, isWindows } from "../common/vars"; import { exitApp } from "./exit-app"; @@ -74,31 +71,6 @@ function createTrayMenu(windowManager: WindowManager): Menu { windowManager.navigate(preferencesURL()); }, }, - { - label: "Clusters", - submenu: workspaceStore.enabledWorkspacesList - .filter(workspace => clusterStore.getByWorkspaceId(workspace.id).length > 0) // hide empty workspaces - .map(workspace => { - const clusters = clusterStore.getByWorkspaceId(workspace.id); - - return { - label: workspace.name, - toolTip: workspace.description, - submenu: clusters.map(cluster => { - const { id: clusterId, name: label, online, workspace } = cluster; - - return { - label: `${online ? "✓" : "\x20".repeat(3)/*offset*/}${label}`, - toolTip: clusterId, - async click() { - workspaceStore.setActive(workspace); - windowManager.navigate(clusterViewURL({ params: { clusterId } })); - } - }; - }) - }; - }), - }, { label: "Check for updates", async click() { diff --git a/src/renderer/api/__tests__/catalog-entity-registry.test.ts b/src/renderer/api/__tests__/catalog-entity-registry.test.ts new file mode 100644 index 0000000000..2b36086856 --- /dev/null +++ b/src/renderer/api/__tests__/catalog-entity-registry.test.ts @@ -0,0 +1,135 @@ +import { CatalogEntityRegistry } from "../catalog-entity-registry"; +import "../../../common/catalog-entities"; +import { catalogCategoryRegistry } from "../../../common/catalog-category-registry"; + +describe("CatalogEntityRegistry", () => { + describe("updateItems", () => { + it("adds new catalog item", () => { + const catalog = new CatalogEntityRegistry(catalogCategoryRegistry); + const items = [{ + apiVersion: "entity.k8slens.dev/v1alpha1", + kind: "KubernetesCluster", + metadata: { + uid: "123", + name: "foobar", + source: "test", + labels: {} + }, + status: { + phase: "disconnected" + }, + spec: {} + }]; + + catalog.updateItems(items); + expect(catalog.items.length).toEqual(1); + + items.push({ + apiVersion: "entity.k8slens.dev/v1alpha1", + kind: "KubernetesCluster", + metadata: { + uid: "456", + name: "barbaz", + source: "test", + labels: {} + }, + status: { + phase: "disconnected" + }, + spec: {} + }); + + catalog.updateItems(items); + expect(catalog.items.length).toEqual(2); + }); + + it("ignores unknown items", () => { + const catalog = new CatalogEntityRegistry(catalogCategoryRegistry); + const items = [{ + apiVersion: "entity.k8slens.dev/v1alpha1", + kind: "FooBar", + metadata: { + uid: "123", + name: "foobar", + source: "test", + labels: {} + }, + status: { + phase: "disconnected" + }, + spec: {} + }]; + + catalog.updateItems(items); + expect(catalog.items.length).toEqual(0); + }); + + it("updates existing items", () => { + const catalog = new CatalogEntityRegistry(catalogCategoryRegistry); + const items = [{ + apiVersion: "entity.k8slens.dev/v1alpha1", + kind: "KubernetesCluster", + metadata: { + uid: "123", + name: "foobar", + source: "test", + labels: {} + }, + status: { + phase: "disconnected" + }, + spec: {} + }]; + + catalog.updateItems(items); + expect(catalog.items.length).toEqual(1); + expect(catalog.items[0].status.phase).toEqual("disconnected"); + + items[0].status.phase = "connected"; + + catalog.updateItems(items); + expect(catalog.items.length).toEqual(1); + expect(catalog.items[0].status.phase).toEqual("connected"); + }); + + it("removes deleted items", () => { + const catalog = new CatalogEntityRegistry(catalogCategoryRegistry); + const items = [ + { + apiVersion: "entity.k8slens.dev/v1alpha1", + kind: "KubernetesCluster", + metadata: { + uid: "123", + name: "foobar", + source: "test", + labels: {} + }, + status: { + phase: "disconnected" + }, + spec: {} + }, + { + apiVersion: "entity.k8slens.dev/v1alpha1", + kind: "KubernetesCluster", + metadata: { + uid: "456", + name: "barbaz", + source: "test", + labels: {} + }, + status: { + phase: "disconnected" + }, + spec: {} + } + ]; + + catalog.updateItems(items); + items.splice(0, 1); + catalog.updateItems(items); + expect(catalog.items.length).toEqual(1); + expect(catalog.items[0].metadata.uid).toEqual("456"); + }); + }); +}); diff --git a/src/renderer/api/catalog-entity-registry.ts b/src/renderer/api/catalog-entity-registry.ts new file mode 100644 index 0000000000..1d02e0308b --- /dev/null +++ b/src/renderer/api/catalog-entity-registry.ts @@ -0,0 +1,56 @@ +import { action, observable } from "mobx"; +import { broadcastMessage, subscribeToBroadcast } from "../../common/ipc"; +import { CatalogEntity, CatalogEntityData } from "../../common/catalog-entity"; +import { catalogCategoryRegistry, CatalogCategoryRegistry } from "../../common/catalog-category-registry"; +import "../../common/catalog-entities"; + +export { CatalogEntity, CatalogEntityData } from "../../common/catalog-entity"; + +export class CatalogEntityRegistry { + @observable protected _items: CatalogEntity[] = observable.array([], { deep: true }); + + constructor(private categoryRegistry: CatalogCategoryRegistry) {} + + init() { + subscribeToBroadcast("catalog:items", (ev, items: CatalogEntityData[]) => { + this.updateItems(items); + }); + broadcastMessage("catalog:broadcast"); + } + + @action updateItems(items: CatalogEntityData[]) { + this._items.forEach((item, index) => { + const foundIndex = items.findIndex((i) => i.apiVersion === item.apiVersion && i.kind === item.kind && i.metadata.uid === item.metadata.uid); + + if (foundIndex === -1) { + this._items.splice(index, 1); + } + }); + + items.forEach((data) => { + const item = this.categoryRegistry.getEntityForData(data); + + if (!item) return; // invalid data + + const index = this._items.findIndex((i) => i.apiVersion === item.apiVersion && i.kind === item.kind && i.metadata.uid === item.metadata.uid); + + if (index === -1) { + this._items.push(item); + } else { + this._items.splice(index, 1, item); + } + }); + } + + get items() { + return this._items; + } + + getItemsForApiKind(apiVersion: string, kind: string): T[] { + const items = this._items.filter((item) => item.apiVersion === apiVersion && item.kind === kind); + + return items as T[]; + } +} + +export const catalogEntityRegistry = new CatalogEntityRegistry(catalogCategoryRegistry); diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index 0d412e257c..a45188b6b5 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -10,7 +10,6 @@ import { clusterStore } from "../common/cluster-store"; import { userStore } from "../common/user-store"; import { delay } from "../common/utils"; import { isMac, isDevelopment } from "../common/vars"; -import { workspaceStore } from "../common/workspace-store"; import * as LensExtensions from "../extensions/extension-api"; import { extensionDiscovery } from "../extensions/extension-discovery"; import { extensionLoader } from "../extensions/extension-loader"; @@ -56,7 +55,6 @@ export async function bootstrap(App: AppComponent) { // preload common stores await Promise.all([ userStore.load(), - workspaceStore.load(), clusterStore.load(), extensionsStore.load(), filesystemProvisionerStore.load(), @@ -65,7 +63,6 @@ export async function bootstrap(App: AppComponent) { // Register additional store listeners clusterStore.registerIpcListener(); - workspaceStore.registerIpcListener(); // init app's dependencies if any if (App.init) { @@ -74,7 +71,6 @@ export async function bootstrap(App: AppComponent) { window.addEventListener("message", (ev: MessageEvent) => { if (ev.data === "teardown") { userStore.unregisterIpcListener(); - workspaceStore.unregisterIpcListener(); clusterStore.unregisterIpcListener(); unmountComponentAtNode(rootElem); window.location.href = "about:blank"; diff --git a/src/renderer/components/+add-cluster/add-cluster.tsx b/src/renderer/components/+add-cluster/add-cluster.tsx index dc5cdf4d4c..16914cd3a3 100644 --- a/src/renderer/components/+add-cluster/add-cluster.tsx +++ b/src/renderer/components/+add-cluster/add-cluster.tsx @@ -12,7 +12,6 @@ import { Button } from "../button"; import { Icon } from "../icon"; import { kubeConfigDefaultPath, loadConfig, splitConfig, validateConfig, validateKubeConfig } from "../../../common/kube-helpers"; import { ClusterModel, ClusterStore, clusterStore } from "../../../common/cluster-store"; -import { workspaceStore } from "../../../common/workspace-store"; import { v4 as uuid } from "uuid"; import { navigate } from "../../navigation"; import { userStore } from "../../../common/user-store"; @@ -171,7 +170,6 @@ export class AddCluster extends React.Component { return { id: clusterId, kubeConfigPath, - workspace: workspaceStore.currentWorkspaceId, contextName: kubeConfig.currentContext, preferences: { clusterName: kubeConfig.currentContext, diff --git a/src/renderer/components/+cluster-settings/cluster-settings.tsx b/src/renderer/components/+cluster-settings/cluster-settings.tsx index 40472be1ec..02d1a74ff8 100644 --- a/src/renderer/components/+cluster-settings/cluster-settings.tsx +++ b/src/renderer/components/+cluster-settings/cluster-settings.tsx @@ -9,7 +9,6 @@ import { Removal } from "./removal"; import { Status } from "./status"; import { General } from "./general"; import { Cluster } from "../../../main/cluster"; -import { ClusterIcon } from "../cluster-icon"; import { IClusterSettingsRouteParams } from "./cluster-settings.route"; import { clusterStore } from "../../../common/cluster-store"; import { PageLayout } from "../layout/page-layout"; @@ -58,7 +57,6 @@ export class ClusterSettings extends React.Component { if (!cluster) return null; const header = ( <> -

{cluster.preferences.clusterName}

); diff --git a/src/renderer/components/+cluster-settings/components/cluster-icon-setting.tsx b/src/renderer/components/+cluster-settings/components/cluster-icon-setting.tsx deleted file mode 100644 index 466afc14da..0000000000 --- a/src/renderer/components/+cluster-settings/components/cluster-icon-setting.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import React from "react"; -import { Cluster } from "../../../../main/cluster"; -import { FilePicker, OverSizeLimitStyle } from "../../file-picker"; -import { autobind } from "../../../utils"; -import { Button } from "../../button"; -import { observable } from "mobx"; -import { observer } from "mobx-react"; -import { SubTitle } from "../../layout/sub-title"; -import { ClusterIcon } from "../../cluster-icon"; - -enum GeneralInputStatus { - CLEAN = "clean", - ERROR = "error", -} - -interface Props { - cluster: Cluster; -} - -@observer -export class ClusterIconSetting extends React.Component { - @observable status = GeneralInputStatus.CLEAN; - @observable errorText?: string; - - @autobind() - async onIconPick([file]: File[]) { - const { cluster } = this.props; - - try { - if (file) { - const buf = Buffer.from(await file.arrayBuffer()); - - cluster.preferences.icon = `data:${file.type};base64,${buf.toString("base64")}`; - } else { - // this has to be done as a seperate branch (and not always) because `cluster` - // is observable and triggers an update loop. - cluster.preferences.icon = undefined; - } - } catch (e) { - this.errorText = e.toString(); - this.status = GeneralInputStatus.ERROR; - } - } - - getClearButton() { - if (this.props.cluster.preferences.icon) { - return ; - } - } - - render() { - const label = ( - <> - - {"Browse for new icon..."} - - ); - - return ( - <> - -

Define cluster icon. By default automatically generated.

-
- - {this.getClearButton()} -
- - ); - } -} diff --git a/src/renderer/components/+cluster-settings/components/cluster-workspace-setting.tsx b/src/renderer/components/+cluster-settings/components/cluster-workspace-setting.tsx deleted file mode 100644 index fa76dde806..0000000000 --- a/src/renderer/components/+cluster-settings/components/cluster-workspace-setting.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from "react"; -import { observer } from "mobx-react"; -import { workspaceStore } from "../../../../common/workspace-store"; -import { Cluster } from "../../../../main/cluster"; -import { Select } from "../../../components/select"; -import { SubTitle } from "../../layout/sub-title"; - -interface Props { - cluster: Cluster; -} - -@observer -export class ClusterWorkspaceSetting extends React.Component { - render() { - return ( - <> - -

- Define cluster workspace. -

- this.onSubmit(v)} - dirty={true} - showValidationLine={true} /> - - Please provide a new workspace name (Press "Enter" to confirm or "Escape" to cancel) - - - ); - } -} - -commandRegistry.add({ - id: "workspace.addWorkspace", - title: "Workspace: Add workspace ...", - scope: "global", - action: () => CommandOverlay.open() -}); diff --git a/src/renderer/components/+workspaces/edit-workspace.tsx b/src/renderer/components/+workspaces/edit-workspace.tsx deleted file mode 100644 index 3ab4b44d5a..0000000000 --- a/src/renderer/components/+workspaces/edit-workspace.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import React from "react"; -import { observer } from "mobx-react"; -import { WorkspaceStore, workspaceStore } from "../../../common/workspace-store"; -import { commandRegistry } from "../../../extensions/registries/command-registry"; -import { Input, InputValidator } from "../input"; -import { CommandOverlay } from "../command-palette/command-container"; - -const validateWorkspaceName: InputValidator = { - condition: ({ required }) => required, - message: () => `Workspace with this name already exists`, - validate: (value) => { - const current = workspaceStore.currentWorkspace; - - if (current.name === value.trim()) { - return true; - } - - return !workspaceStore.enabledWorkspacesList.find((workspace) => workspace.name === value); - } -}; - -interface EditWorkspaceState { - name: string; -} - -@observer -export class EditWorkspace extends React.Component<{}, EditWorkspaceState> { - - state: EditWorkspaceState = { - name: "" - }; - - componentDidMount() { - this.setState({name: workspaceStore.currentWorkspace.name}); - } - - onSubmit(name: string) { - if (name.trim() === "") { - return; - } - - workspaceStore.currentWorkspace.name = name; - CommandOverlay.close(); - } - - onChange(name: string) { - this.setState({name}); - } - - get name() { - return this.state.name; - } - - render() { - return ( - <> - this.onChange(v)} - onSubmit={(v) => this.onSubmit(v)} - dirty={true} - value={this.name} - showValidationLine={true} /> - - Please provide a new workspace name (Press "Enter" to confirm or "Escape" to cancel) - - - ); - } -} - -commandRegistry.add({ - id: "workspace.editCurrentWorkspace", - title: "Workspace: Edit current workspace ...", - scope: "global", - action: () => CommandOverlay.open(), - isActive: (context) => context.workspace?.id !== WorkspaceStore.defaultId -}); diff --git a/src/renderer/components/+workspaces/index.ts b/src/renderer/components/+workspaces/index.ts deleted file mode 100644 index 5b84fc9b00..0000000000 --- a/src/renderer/components/+workspaces/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./workspaces"; diff --git a/src/renderer/components/+workspaces/remove-workspace.tsx b/src/renderer/components/+workspaces/remove-workspace.tsx deleted file mode 100644 index 9f66292447..0000000000 --- a/src/renderer/components/+workspaces/remove-workspace.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import React from "react"; -import { observer } from "mobx-react"; -import { computed} from "mobx"; -import { WorkspaceStore, workspaceStore } from "../../../common/workspace-store"; -import { ConfirmDialog } from "../confirm-dialog"; -import { commandRegistry } from "../../../extensions/registries/command-registry"; -import { Select } from "../select"; -import { CommandOverlay } from "../command-palette/command-container"; - -@observer -export class RemoveWorkspace extends React.Component { - @computed get options() { - return workspaceStore.enabledWorkspacesList.filter((workspace) => workspace.id !== WorkspaceStore.defaultId).map((workspace) => { - return { value: workspace.id, label: workspace.name }; - }); - } - - onChange(id: string) { - const workspace = workspaceStore.enabledWorkspacesList.find((workspace) => workspace.id === id); - - if (!workspace ) { - return; - } - - CommandOverlay.close(); - ConfirmDialog.open({ - okButtonProps: { - label: `Remove Workspace`, - primary: false, - accent: true, - }, - ok: () => { - workspaceStore.removeWorkspace(workspace); - }, - message: ( -
-

- Are you sure you want remove workspace {workspace.name}? -

-

- All clusters within workspace will be cleared as well -

-
- ), - }); - } - - render() { - return ( - this.onChange(v.value)} - components={{ DropdownIndicator: null, IndicatorSeparator: null }} - menuIsOpen={true} - options={this.options} - autoFocus={true} - escapeClearsValue={false} - placeholder="Switch to workspace" /> - ); - } -} - -commandRegistry.add({ - id: "workspace.chooseWorkspace", - title: "Workspace: Switch to workspace ...", - scope: "global", - action: () => CommandOverlay.open() -}); diff --git a/src/renderer/components/app.tsx b/src/renderer/components/app.tsx index f5ad684595..2110898885 100755 --- a/src/renderer/components/app.tsx +++ b/src/renderer/components/app.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { computed, observable, reaction } from "mobx"; +import { observable } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; import { Redirect, Route, Router, Switch } from "react-router"; import { history } from "../navigation"; @@ -36,7 +36,7 @@ import { webFrame } from "electron"; import { clusterPageRegistry, getExtensionPageUrl } from "../../extensions/registries/page-registry"; import { extensionLoader } from "../../extensions/extension-loader"; import { appEventBus } from "../../common/event-bus"; -import { broadcastMessage, requestMain } from "../../common/ipc"; +import { requestMain } from "../../common/ipc"; import whatInput from "what-input"; import { clusterSetFrameIdHandler } from "../../common/cluster-ipc"; import { ClusterPageMenuRegistration, clusterPageMenuRegistry } from "../../extensions/registries"; @@ -86,20 +86,12 @@ export class App extends React.Component { disposeOnUnmount(this, [ kubeWatchApi.subscribeStores([podsStore, nodesStore, eventStore], { preload: true, - }), - - reaction(() => this.warningsTotal, (count: number) => { - broadcastMessage(`cluster-warning-event-count:${getHostedCluster().id}`, count); - }), + }) ]); } @observable startUrl = isAllowedResource(["events", "nodes", "pods"]) ? clusterURL() : workloadsURL(); - @computed get warningsTotal(): number { - return nodesStore.getWarningsCount() + eventStore.getWarningsCount(); - } - getTabLayoutRoutes(menuItem: ClusterPageMenuRegistration) { const routes: TabLayoutRoute[] = []; diff --git a/src/renderer/components/cluster-icon/cluster-icon.scss b/src/renderer/components/cluster-icon/cluster-icon.scss deleted file mode 100644 index 540cecf9eb..0000000000 --- a/src/renderer/components/cluster-icon/cluster-icon.scss +++ /dev/null @@ -1,38 +0,0 @@ -.ClusterIcon { - --size: 37px; - - position: relative; - border-radius: $radius; - padding: $radius; - user-select: none; - cursor: pointer; - - &.interactive { - img { - opacity: .55; - } - } - - &.active, &.interactive:hover { - background-color: #fff; - - img { - opacity: 1; - } - } - - img { - width: var(--size); - height: var(--size); - } - - .Badge { - position: absolute; - right: 0; - bottom: 0; - margin: -$padding; - font-size: $font-size-small; - background: $colorError; - color: white; - } -} \ No newline at end of file diff --git a/src/renderer/components/cluster-icon/cluster-icon.tsx b/src/renderer/components/cluster-icon/cluster-icon.tsx deleted file mode 100644 index 48d23eb91f..0000000000 --- a/src/renderer/components/cluster-icon/cluster-icon.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import "./cluster-icon.scss"; - -import React, { DOMAttributes } from "react"; -import { disposeOnUnmount, observer } from "mobx-react"; -import { Params as HashiconParams } from "@emeraldpay/hashicon"; -import { Hashicon } from "@emeraldpay/hashicon-react"; -import { Cluster } from "../../../main/cluster"; -import { cssNames, IClassName } from "../../utils"; -import { Badge } from "../badge"; -import { Tooltip } from "../tooltip"; -import { subscribeToBroadcast } from "../../../common/ipc"; -import { observable } from "mobx"; - -interface Props extends DOMAttributes { - cluster: Cluster; - className?: IClassName; - errorClass?: IClassName; - showErrors?: boolean; - showTooltip?: boolean; - interactive?: boolean; - isActive?: boolean; - options?: HashiconParams; -} - -const defaultProps: Partial = { - showErrors: true, - showTooltip: true, -}; - -@observer -export class ClusterIcon extends React.Component { - static defaultProps = defaultProps as object; - - @observable eventCount = 0; - - get eventCountBroadcast() { - return `cluster-warning-event-count:${this.props.cluster.id}`; - } - - componentDidMount() { - const subscriber = subscribeToBroadcast(this.eventCountBroadcast, (ev, eventCount) => { - this.eventCount = eventCount; - }); - - disposeOnUnmount(this, [ - subscriber - ]); - } - - render() { - const { - cluster, showErrors, showTooltip, errorClass, options, interactive, isActive, - children, ...elemProps - } = this.props; - const { name, preferences, id: clusterId, online } = cluster; - const eventCount = this.eventCount; - const { icon } = preferences; - const clusterIconId = `cluster-icon-${clusterId}`; - const className = cssNames("ClusterIcon flex inline", this.props.className, { - interactive: interactive !== undefined ? interactive : !!this.props.onClick, - active: isActive, - }); - - return ( -
- {showTooltip && ( - {name} - )} - {icon && {name}/} - {!icon && } - {showErrors && eventCount > 0 && !isActive && online && ( - = 1000 ? `${Math.ceil(eventCount / 1000)}k+` : eventCount} - /> - )} - {children} -
- ); - } -} diff --git a/src/renderer/components/cluster-icon/index.ts b/src/renderer/components/cluster-icon/index.ts deleted file mode 100644 index 7879490b85..0000000000 --- a/src/renderer/components/cluster-icon/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./cluster-icon"; diff --git a/src/renderer/components/cluster-manager/bottom-bar.tsx b/src/renderer/components/cluster-manager/bottom-bar.tsx index e192c67dc6..70b52685ea 100644 --- a/src/renderer/components/cluster-manager/bottom-bar.tsx +++ b/src/renderer/components/cluster-manager/bottom-bar.tsx @@ -2,11 +2,7 @@ import "./bottom-bar.scss"; import React from "react"; import { observer } from "mobx-react"; -import { Icon } from "../icon"; -import { workspaceStore } from "../../../common/workspace-store"; import { StatusBarRegistration, statusBarRegistry } from "../../../extensions/registries"; -import { CommandOverlay } from "../command-palette/command-container"; -import { ChooseWorkspace } from "../+workspaces"; @observer export class BottomBar extends React.Component { @@ -45,14 +41,8 @@ export class BottomBar extends React.Component { } render() { - const { currentWorkspace } = workspaceStore; - return (
-
CommandOverlay.open()}> - - {currentWorkspace.name} -
{this.renderRegisteredItems()}
); diff --git a/src/renderer/components/cluster-manager/cluster-manager.tsx b/src/renderer/components/cluster-manager/cluster-manager.tsx index 1c3306b65f..a30e141956 100644 --- a/src/renderer/components/cluster-manager/cluster-manager.tsx +++ b/src/renderer/components/cluster-manager/cluster-manager.tsx @@ -4,19 +4,19 @@ import React from "react"; import { Redirect, Route, Switch } from "react-router"; import { comparer, reaction } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; -import { ClustersMenu } from "./clusters-menu"; import { BottomBar } from "./bottom-bar"; import { LandingPage, landingRoute, landingURL } from "../+landing-page"; import { Preferences, preferencesRoute } from "../+preferences"; import { AddCluster, addClusterRoute } from "../+add-cluster"; import { ClusterView } from "./cluster-view"; import { ClusterSettings, clusterSettingsRoute } from "../+cluster-settings"; -import { clusterViewRoute, clusterViewURL } from "./cluster-view.route"; +import { clusterViewRoute } from "./cluster-view.route"; import { clusterStore } from "../../../common/cluster-store"; import { hasLoadedView, initView, lensViews, refreshViews } from "./lens-views"; import { globalPageRegistry } from "../../../extensions/registries/page-registry"; import { Extensions, extensionsRoute } from "../+extensions"; import { getMatchedClusterId } from "../../navigation"; +import { HotbarMenu } from "../hotbar/hotbar-menu"; @observer export class ClusterManager extends React.Component { @@ -44,16 +44,6 @@ export class ClusterManager extends React.Component { } get startUrl() { - const { activeClusterId } = clusterStore; - - if (activeClusterId) { - return clusterViewURL({ - params: { - clusterId: activeClusterId - } - }); - } - return landingURL(); } @@ -75,7 +65,7 @@ export class ClusterManager extends React.Component { - + ); diff --git a/src/renderer/components/cluster-manager/clusters-menu.scss b/src/renderer/components/cluster-manager/clusters-menu.scss deleted file mode 100644 index 5256209b0a..0000000000 --- a/src/renderer/components/cluster-manager/clusters-menu.scss +++ /dev/null @@ -1,67 +0,0 @@ -.ClustersMenu { - $spacing: $padding * 2; - - position: relative; - text-align: center; - background: $clusterMenuBackground; - border-right: 1px solid $clusterMenuBorderColor; - padding: $spacing 0; - min-width: 75px; - - .is-mac &:before { - content: ""; - height: 20px; // extra spacing for mac-os "traffic-light" buttons - } - - .clusters { - @include hidden-scrollbar; - padding: 0 $spacing; // extra spacing for cluster-icon's badge - margin-bottom: $margin; - - .ClusterIcon { - margin-bottom: $margin * 1.5; - } - - &:empty { - display: none; - } - } - - > .WorkspaceMenu { - position: relative; - margin-bottom: $margin; - - .Icon { - margin-bottom: $margin * 1.5; - border-radius: $radius; - padding: $padding / 3; - color: var(--textColorPrimary); - background: unset; - cursor: pointer; - - &.active { - opacity: 1; - } - - &:hover { - box-shadow: none; - color: var(--textColorAccent); - background-color: unset; - } - } - } - - > .extensions { - &:not(:empty) { - padding-top: $spacing; - } - - .Icon { - --size: 40px; - } - } -} - -.Menu.WorkspaceMenu { - z-index: 2; // Place behind Preferences, Extension pages etc... -} \ No newline at end of file diff --git a/src/renderer/components/cluster-manager/clusters-menu.tsx b/src/renderer/components/cluster-manager/clusters-menu.tsx deleted file mode 100644 index 03711e41fc..0000000000 --- a/src/renderer/components/cluster-manager/clusters-menu.tsx +++ /dev/null @@ -1,198 +0,0 @@ -import "./clusters-menu.scss"; - -import React from "react"; -import { remote } from "electron"; -import type { Cluster } from "../../../main/cluster"; -import { DragDropContext, Draggable, DraggableProvided, Droppable, DroppableProvided, DropResult } from "react-beautiful-dnd"; -import { observer } from "mobx-react"; -import { ClusterId, clusterStore } from "../../../common/cluster-store"; -import { workspaceStore } from "../../../common/workspace-store"; -import { ClusterIcon } from "../cluster-icon"; -import { Icon } from "../icon"; -import { autobind, cssNames, IClassName } from "../../utils"; -import { isActiveRoute, navigate } from "../../navigation"; -import { addClusterURL } from "../+add-cluster"; -import { landingURL } from "../+landing-page"; -import { clusterViewURL } from "./cluster-view.route"; -import { ClusterActions } from "./cluster-actions"; -import { getExtensionPageUrl, globalPageMenuRegistry, globalPageRegistry } from "../../../extensions/registries"; -import { commandRegistry } from "../../../extensions/registries/command-registry"; -import { CommandOverlay } from "../command-palette/command-container"; -import { computed, observable } from "mobx"; -import { Select } from "../select"; -import { Menu, MenuItem } from "../menu"; - -interface Props { - className?: IClassName; -} - -@observer -export class ClustersMenu extends React.Component { - @observable workspaceMenuVisible = false; - - showCluster = (clusterId: ClusterId) => { - navigate(clusterViewURL({ params: { clusterId } })); - }; - - showContextMenu = (cluster: Cluster) => { - const { Menu, MenuItem } = remote; - const menu = new Menu(); - const actions = ClusterActions(cluster); - - menu.append(new MenuItem({ - label: `Settings`, - click: actions.showSettings - })); - - if (cluster.online) { - menu.append(new MenuItem({ - label: `Disconnect`, - click: actions.disconnect - })); - } - - if (!cluster.isManaged) { - menu.append(new MenuItem({ - label: `Remove`, - click: actions.remove - })); - } - menu.popup({ - window: remote.getCurrentWindow() - }); - }; - - @autobind() - swapClusterIconOrder(result: DropResult) { - if (result.reason === "DROP") { - const { currentWorkspaceId } = workspaceStore; - const { - source: { index: from }, - destination: { index: to }, - } = result; - - clusterStore.swapIconOrders(currentWorkspaceId, from, to); - } - } - - render() { - const { className } = this.props; - const workspace = workspaceStore.getById(workspaceStore.currentWorkspaceId); - const clusters = clusterStore.getByWorkspaceId(workspace.id).filter(cluster => cluster.enabled); - const activeClusterId = clusterStore.activeCluster; - - return ( -
-
- - - {({ innerRef, droppableProps, placeholder }: DroppableProvided) => ( -
- {clusters.map((cluster, index) => { - const isActive = cluster.id === activeClusterId; - - return ( - - {({ draggableProps, dragHandleProps, innerRef }: DraggableProvided) => ( -
- this.showCluster(cluster.id)} - onContextMenu={() => this.showContextMenu(cluster)} - /> -
- )} -
- ); - })} - {placeholder} -
- )} -
-
-
- -
- - this.workspaceMenuVisible = true} - close={() => this.workspaceMenuVisible = false} - toggleEvent="click" - > - navigate(addClusterURL())} data-test-id="add-cluster-menu-item"> - Add Cluster - - navigate(landingURL())} data-test-id="workspace-overview-menu-item"> - Workspace Overview - - -
-
- {globalPageMenuRegistry.getItems().map(({ title, target, components: { Icon } }) => { - const registeredPage = globalPageRegistry.getByPageTarget(target); - - if (!registeredPage){ - return; - } - const pageUrl = getExtensionPageUrl(target); - const isActive = isActiveRoute(registeredPage.url); - - return ( - navigate(pageUrl)} - /> - ); - })} -
-
- ); - } -} - -@observer -export class ChooseCluster extends React.Component { - @computed get options() { - const clusters = clusterStore.getByWorkspaceId(workspaceStore.currentWorkspaceId).filter(cluster => cluster.enabled); - const options = clusters.map((cluster) => { - return { value: cluster.id, label: cluster.name }; - }); - - return options; - } - - onChange(clusterId: string) { - navigate(clusterViewURL({ params: { clusterId } })); - CommandOverlay.close(); - } - - render() { - return ( -