diff --git a/src/common/__tests__/cluster-store.test.ts b/src/common/__tests__/cluster-store.test.ts index 603a7c1b9e..d2d31302d1 100644 --- a/src/common/__tests__/cluster-store.test.ts +++ b/src/common/__tests__/cluster-store.test.ts @@ -7,6 +7,20 @@ import { workspaceStore } from "../workspace-store"; const testDataIcon = fs.readFileSync("test-data/cluster-store-migration-icon.png"); +jest.mock("electron", () => { + return { + app: { + getVersion: () => "99.99.99", + getPath: () => "tmp", + getLocale: () => "en" + }, + ipcMain: { + handle: jest.fn(), + on: jest.fn() + } + }; +}); + let clusterStore: ClusterStore; describe("empty config", () => { @@ -48,6 +62,7 @@ describe("empty config", () => { expect(storedCluster.id).toBe("foo"); expect(storedCluster.preferences.terminalCWD).toBe("/tmp"); expect(storedCluster.preferences.icon).toBe("data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5"); + expect(storedCluster.enabled).toBe(true); }); it("adds cluster to default workspace", () => { @@ -170,7 +185,8 @@ describe("config with existing clusters", () => { kubeConfig: "foo", contextName: "foo", preferences: { terminalCWD: "/foo" }, - workspace: "foo" + workspace: "foo", + ownerRef: "foo" }, ] }) @@ -208,6 +224,12 @@ describe("config with existing clusters", () => { expect(storedClusters[1].preferences.terminalCWD).toBe("/foo2"); expect(storedClusters[2].id).toBe("cluster3"); }); + + it("marks owned cluster disabled by default", () => { + const storedClusters = clusterStore.clustersList; + expect(storedClusters[0].enabled).toBe(true); + expect(storedClusters[2].enabled).toBe(false); + }); }); describe("pre 2.0 config with an existing cluster", () => { diff --git a/src/common/__tests__/workspace-store.test.ts b/src/common/__tests__/workspace-store.test.ts index e50bb23c99..a15128f4e3 100644 --- a/src/common/__tests__/workspace-store.test.ts +++ b/src/common/__tests__/workspace-store.test.ts @@ -6,6 +6,10 @@ jest.mock("electron", () => { getVersion: () => "99.99.99", getPath: () => "tmp", getLocale: () => "en" + }, + ipcMain: { + handle: jest.fn(), + on: jest.fn() } }; }); @@ -60,7 +64,9 @@ describe("workspace store tests", () => { name: "foobar", })); - expect(ws.getById("123").name).toBe("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", () => { diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index 35ec663a55..5a9827eee5 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -11,7 +11,7 @@ import { appEventBus } from "./event-bus"; import { dumpConfigYaml } from "./kube-helpers"; import { saveToAppFiles } from "./utils/saveToAppFiles"; import { KubeConfig } from "@kubernetes/client-node"; -import { subscribeToBroadcast, unsubscribeAllFromBroadcast } from "./ipc"; +import { handleRequest, requestMain, subscribeToBroadcast, unsubscribeAllFromBroadcast } from "./ipc"; import _ from "lodash"; import move from "array-move"; import type { WorkspaceId } from "./workspace-store"; @@ -40,13 +40,30 @@ export interface ClusterStoreModel { export type ClusterId = string; export interface ClusterModel { + /** Unique id for a cluster */ id: ClusterId; + + /** Path to cluster kubeconfig */ kubeConfigPath: string; + + /** Workspace id */ workspace?: WorkspaceId; + + /** User context in kubeconfig */ contextName?: string; + + /** Preferences */ preferences?: ClusterPreferences; + + /** Metadata */ metadata?: ClusterMetadata; + + /** + * If extension sets ownerRef it has to explicitly mark a cluster as enabled during onActive (or when cluster is saved) + */ ownerRef?: string; + + /** List of accessible namespaces */ accessibleNamespaces?: string[]; /** @deprecated */ @@ -89,6 +106,8 @@ export class ClusterStore extends BaseStore { @observable removedClusters = observable.map(); @observable clusters = observable.map(); + private static stateRequestChannel = "cluster:states"; + private constructor() { super({ configName: "lens-cluster-store", @@ -102,8 +121,40 @@ export class ClusterStore extends BaseStore { this.pushStateToViewsAutomatically(); } + async load() { + await super.load(); + type clusterStateSync = { + id: string; + state: ClusterState; + }; + if (ipcRenderer) { + logger.info("[CLUSTER-STORE] requesting initial state sync"); + const clusterStates: clusterStateSync[] = await requestMain(ClusterStore.stateRequestChannel); + clusterStates.forEach((clusterState) => { + const cluster = this.getById(clusterState.id); + if (cluster) { + cluster.setState(clusterState.state); + } + }); + } else { + handleRequest(ClusterStore.stateRequestChannel, (): clusterStateSync[] => { + const states: clusterStateSync[] = []; + this.clustersList.forEach((cluster) => { + states.push({ + state: cluster.getState(), + id: cluster.id + }); + }); + return states; + }); + } + } + protected pushStateToViewsAutomatically() { if (!ipcRenderer) { + reaction(() => this.enabledClustersList, () => { + this.pushState(); + }); reaction(() => this.connectedClustersList, () => { this.pushState(); }); @@ -205,6 +256,9 @@ export class ClusterStore extends BaseStore { if (!(model instanceof Cluster)) { cluster = new Cluster(model); } + if (!cluster.isManaged) { + cluster.enabled = true; + } this.clusters.set(model.id, cluster); return cluster; } diff --git a/src/common/workspace-store.ts b/src/common/workspace-store.ts index 75ab36f19e..4d5af6da98 100644 --- a/src/common/workspace-store.ts +++ b/src/common/workspace-store.ts @@ -3,7 +3,7 @@ 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 } from "../common/ipc"; +import { broadcastMessage, handleRequest, requestMain } from "../common/ipc"; import logger from "../main/logger"; import type { ClusterId } from "./cluster-store"; @@ -27,11 +27,45 @@ export interface WorkspaceState { } 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; + /** + * Is workspace enabled + * + * Workspaces that don't have ownerRef will be enabled by default. Workspaces with ownerRef need to explicitly enable a workspace. + * + * @observable + */ @observable enabled: boolean; + /** + * Last active cluster id + * + * @observable + */ @observable lastActiveClusterId?: ClusterId; constructor(data: WorkspaceModel) { @@ -49,9 +83,9 @@ export class Workspace implements WorkspaceModel, WorkspaceState { } getState(): WorkspaceState { - return { + return toJS({ enabled: this.enabled - }; + }); } pushState(state = this.getState()) { @@ -77,16 +111,40 @@ export class Workspace implements WorkspaceModel, WorkspaceState { export class WorkspaceStore extends BaseStore { static readonly defaultId: WorkspaceId = "default"; + private static stateRequestChannel = "workspace:states"; private constructor() { super({ configName: "lens-workspace-store", }); + } - if (!ipcRenderer) { - setInterval(() => { - this.pushState(); - }, 5000); + 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; + }); } } @@ -157,6 +215,10 @@ export class WorkspaceStore extends BaseStore { return; } this.workspaces.set(id, workspace); + if (!workspace.isManaged) { + workspace.enabled = true; + } + appEventBus.emit({name: "workspace", action: "add"}); return workspace; } diff --git a/src/main/cluster.ts b/src/main/cluster.ts index 5ee7457529..3f03f90716 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -37,6 +37,7 @@ export type ClusterRefreshOptions = { export interface ClusterState { initialized: boolean; + enabled: boolean; apiUrl: string; online: boolean; disconnected: boolean; @@ -351,6 +352,7 @@ export class Cluster implements ClusterModel, ClusterState { getState(): ClusterState { const state: ClusterState = { initialized: this.initialized, + enabled: this.enabled, apiUrl: this.apiUrl, online: this.online, ready: this.ready, diff --git a/src/renderer/components/+workspaces/workspaces.tsx b/src/renderer/components/+workspaces/workspaces.tsx index ccffcca3b7..4dd3cd14b5 100644 --- a/src/renderer/components/+workspaces/workspaces.tsx +++ b/src/renderer/components/+workspaces/workspaces.tsx @@ -21,7 +21,7 @@ export class Workspaces extends React.Component { @computed get workspaces(): Workspace[] { const currentWorkspaces: Map = new Map(); - workspaceStore.workspacesList.forEach((w) => { + workspaceStore.enabledWorkspacesList.forEach((w) => { currentWorkspaces.set(w.id, w); }); const allWorkspaces = new Map([ diff --git a/src/renderer/components/cluster-manager/clusters-menu.tsx b/src/renderer/components/cluster-manager/clusters-menu.tsx index ab608815aa..c3c710f8d9 100644 --- a/src/renderer/components/cluster-manager/clusters-menu.tsx +++ b/src/renderer/components/cluster-manager/clusters-menu.tsx @@ -65,26 +65,28 @@ export class ClustersMenu extends React.Component { } })); } - menu.append(new MenuItem({ - label: _i18n._(t`Remove`), - click: () => { - ConfirmDialog.open({ - okButtonProps: { - primary: false, - accent: true, - label: _i18n._(t`Remove`), - }, - ok: () => { - if (clusterStore.activeClusterId === cluster.id) { - navigate(landingURL()); - clusterStore.setActive(null); - } - clusterStore.removeById(cluster.id); - }, - message:

Are you sure want to remove cluster {cluster.contextName}?

, - }); - } - })); + if (!cluster.isManaged) { + menu.append(new MenuItem({ + label: _i18n._(t`Remove`), + click: () => { + ConfirmDialog.open({ + okButtonProps: { + primary: false, + accent: true, + label: _i18n._(t`Remove`), + }, + ok: () => { + if (clusterStore.activeClusterId === cluster.id) { + navigate(landingURL()); + clusterStore.setActive(null); + } + clusterStore.removeById(cluster.id); + }, + message:

Are you sure want to remove cluster {cluster.contextName}?

, + }); + } + })); + } menu.popup({ window: remote.getCurrentWindow() }); @@ -106,7 +108,7 @@ export class ClustersMenu extends React.Component { const { className } = this.props; const { newContexts } = userStore; const workspace = workspaceStore.getById(workspaceStore.currentWorkspaceId); - const clusters = clusterStore.getByWorkspaceId(workspace.id); + const clusters = clusterStore.getByWorkspaceId(workspace.id).filter(cluster => cluster.enabled); const activeClusterId = clusterStore.activeCluster; return (