diff --git a/src/common/__tests__/cluster-store.test.ts b/src/common/__tests__/cluster-store.test.ts index 724a83f0d72c..8c911298cc35 100644 --- a/src/common/__tests__/cluster-store.test.ts +++ b/src/common/__tests__/cluster-store.test.ts @@ -64,13 +64,13 @@ describe("empty config", () => { it("sets active cluster", () => { clusterStore.setActive("foo"); - expect(clusterStore.activeCluster.id).toBe("foo"); + expect(clusterStore.active.id).toBe("foo"); }) }) describe("with prod and dev clusters added", () => { beforeEach(() => { - clusterStore.addCluster( + clusterStore.addClusters( new Cluster({ id: "prod", contextName: "prod", diff --git a/src/common/__tests__/workspace-store.test.ts b/src/common/__tests__/workspace-store.test.ts index 8ac3ac599d98..97edfa77cfe9 100644 --- a/src/common/__tests__/workspace-store.test.ts +++ b/src/common/__tests__/workspace-store.test.ts @@ -10,7 +10,7 @@ jest.mock("electron", () => { } }) -import { WorkspaceStore } from "../workspace-store" +import { Workspace, WorkspaceStore } from "../workspace-store" describe("workspace store tests", () => { describe("for an empty config", () => { @@ -35,16 +35,16 @@ describe("workspace store tests", () => { it("cannot remove the default workspace", () => { const ws = WorkspaceStore.getInstance(); - expect(() => ws.removeWorkspace(WorkspaceStore.defaultId)).toThrowError("Cannot remove"); + expect(() => ws.removeWorkspaceById(WorkspaceStore.defaultId)).toThrowError("Cannot remove"); }) it("can update default workspace name", () => { const ws = WorkspaceStore.getInstance(); - ws.saveWorkspace({ + ws.addWorkspace(new Workspace({ id: WorkspaceStore.defaultId, name: "foobar", - }); + })); expect(ws.currentWorkspace.name).toBe("foobar"); }) @@ -52,10 +52,10 @@ describe("workspace store tests", () => { it("can add workspaces", () => { const ws = WorkspaceStore.getInstance(); - ws.saveWorkspace({ + ws.addWorkspace(new Workspace({ id: "123", name: "foobar", - }); + })); expect(ws.getById("123").name).toBe("foobar"); }) @@ -69,10 +69,10 @@ describe("workspace store tests", () => { it("can set a existent workspace to be active", () => { const ws = WorkspaceStore.getInstance(); - ws.saveWorkspace({ + ws.addWorkspace(new Workspace({ id: "abc", name: "foobar", - }); + })); expect(() => ws.setActive("abc")).not.toThrowError(); }) @@ -80,15 +80,15 @@ describe("workspace store tests", () => { it("can remove a workspace", () => { const ws = WorkspaceStore.getInstance(); - ws.saveWorkspace({ + ws.addWorkspace(new Workspace({ id: "123", name: "foobar", - }); - ws.saveWorkspace({ + })); + ws.addWorkspace(new Workspace({ id: "1234", name: "foobar 1", - }); - ws.removeWorkspace("123"); + })); + ws.removeWorkspaceById("123"); expect(ws.workspaces.size).toBe(2); }) @@ -96,10 +96,10 @@ describe("workspace store tests", () => { it("cannot create workspace with existent name", () => { const ws = WorkspaceStore.getInstance(); - ws.saveWorkspace({ + ws.addWorkspace(new Workspace({ id: "someid", name: "default", - }); + })); expect(ws.workspacesList.length).toBe(1); // default workspace only }) @@ -107,10 +107,10 @@ describe("workspace store tests", () => { it("cannot create workspace with empty name", () => { const ws = WorkspaceStore.getInstance(); - ws.saveWorkspace({ + ws.addWorkspace(new Workspace({ id: "random", name: "", - }); + })); expect(ws.workspacesList.length).toBe(1); // default workspace only }) @@ -118,10 +118,10 @@ describe("workspace store tests", () => { it("cannot create workspace with ' ' name", () => { const ws = WorkspaceStore.getInstance(); - ws.saveWorkspace({ + ws.addWorkspace(new Workspace({ id: "random", name: " ", - }); + })); expect(ws.workspacesList.length).toBe(1); // default workspace only }) @@ -129,10 +129,10 @@ describe("workspace store tests", () => { it("trim workspace name", () => { const ws = WorkspaceStore.getInstance(); - ws.saveWorkspace({ + ws.addWorkspace(new Workspace({ id: "random", name: "default ", - }); + })); expect(ws.workspacesList.length).toBe(1); // default workspace only }) @@ -169,4 +169,4 @@ describe("workspace store tests", () => { expect(ws.currentWorkspaceId).toBe("abc"); }) }) -}) \ No newline at end of file +}) diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index fddd0f3be6c7..838e6cc1193f 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -33,11 +33,12 @@ export type ClusterId = string; export interface ClusterModel { id: ClusterId; + kubeConfigPath: string; workspace?: WorkspaceId; contextName?: string; preferences?: ClusterPreferences; metadata?: ClusterMetadata; - kubeConfigPath: string; + ownerRef?: string; /** @deprecated */ kubeConfig?: string; // yaml @@ -72,25 +73,34 @@ export class ClusterStore extends BaseStore { return filePath; } + @observable activeCluster: ClusterId; + @observable removedClusters = observable.map(); + @observable clusters = observable.map(); + private constructor() { super({ configName: "lens-cluster-store", accessPropertiesByDotNotation: false, // To make dots safe in cluster context names migrations: migrations, }); + + this.pushStateToViewsPeriodically() } - @observable activeClusterId: ClusterId; - @observable removedClusters = observable.map(); - @observable clusters = observable.map(); + protected pushStateToViewsPeriodically() { + if (!ipcRenderer) { + // This is a bit of a hack, we need to do this because we might loose messages that are sent before a view is ready + setInterval(() => { + this.pushState() + }, 5000) + } + } registerIpcListener() { logger.info(`[CLUSTER-STORE] start to listen (${webFrame.routingId})`) - ipcRenderer.on("cluster:state", (event, model: ClusterState) => { - this.applyWithoutSync(() => { - logger.silly(`[CLUSTER-STORE]: received push-state at ${location.host} (${webFrame.routingId})`, model); - this.getById(model.id)?.updateModel(model); - }) + ipcRenderer.on("cluster:state", (event, clusterId: string, state: ClusterState) => { + logger.silly(`[CLUSTER-STORE]: received push-state at ${location.host} (${webFrame.routingId})`, clusterId, state); + this.getById(clusterId)?.setState(state) }) } @@ -99,21 +109,35 @@ export class ClusterStore extends BaseStore { ipcRenderer.removeAllListeners("cluster:state") } - @computed get activeCluster(): Cluster | null { - return this.getById(this.activeClusterId); + pushState() { + this.clusters.forEach((c) => { + c.pushState() + }) + } + + get activeClusterId() { + return this.activeCluster } @computed get clustersList(): Cluster[] { return Array.from(this.clusters.values()); } + @computed get enabledClustersList(): Cluster[] { + return this.clustersList.filter((c) => c.enabled) + } + + @computed get active(): Cluster | null { + return this.getById(this.activeCluster); + } + isActive(id: ClusterId) { - return this.activeClusterId === id; + return this.activeCluster === id; } @action setActive(id: ClusterId) { - this.activeClusterId = this.clusters.has(id) ? id : null; + this.activeCluster = this.clusters.has(id) ? id : null; } @action @@ -145,12 +169,28 @@ export class ClusterStore extends BaseStore { } @action - addCluster(...models: ClusterModel[]) { + addClusters(...models: ClusterModel[]): Cluster[] { + const clusters: Cluster[] = [] models.forEach(model => { - appEventBus.emit({name: "cluster", action: "add"}) - const cluster = new Cluster(model); - this.clusters.set(model.id, cluster); + clusters.push(this.addCluster(model)) }) + + return clusters + } + + @action + addCluster(model: ClusterModel | Cluster ): Cluster { + appEventBus.emit({name: "cluster", action: "add"}) + let cluster = model as Cluster; + if (!(model instanceof Cluster)) { + cluster = new Cluster(model) + } + this.clusters.set(model.id, cluster); + return cluster + } + + async removeCluster(model: ClusterModel) { + await this.removeById(model.id) } @action @@ -159,7 +199,7 @@ export class ClusterStore extends BaseStore { const cluster = this.getById(clusterId); if (cluster) { this.clusters.delete(clusterId); - if (this.activeClusterId === clusterId) { + if (this.activeCluster === clusterId) { this.setActive(null); } // remove only custom kubeconfigs (pasted as text) @@ -189,6 +229,9 @@ export class ClusterStore extends BaseStore { cluster.updateModel(clusterModel); } else { cluster = new Cluster(clusterModel); + if (!cluster.isManaged) { + cluster.enabled = true + } } newClusters.set(clusterModel.id, cluster); } @@ -200,14 +243,14 @@ export class ClusterStore extends BaseStore { } }); - this.activeClusterId = newClusters.has(activeCluster) ? activeCluster : null; + this.activeCluster = newClusters.has(activeCluster) ? activeCluster : null; this.clusters.replace(newClusters); this.removedClusters.replace(removedClusters); } toJSON(): ClusterStoreModel { return toJS({ - activeCluster: this.activeClusterId, + activeCluster: this.activeCluster, clusters: this.clustersList.map(cluster => cluster.toJSON()), }, { recurseEverything: true diff --git a/src/common/workspace-store.ts b/src/common/workspace-store.ts index b7f246701383..97611a01d3ef 100644 --- a/src/common/workspace-store.ts +++ b/src/common/workspace-store.ts @@ -1,19 +1,77 @@ -import { action, computed, observable, toJS } from "mobx"; +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 { broadcastIpc } from "../common/ipc"; +import logger from "../main/logger"; export type WorkspaceId = string; export interface WorkspaceStoreModel { currentWorkspace?: WorkspaceId; - workspaces: Workspace[] + workspaces: WorkspaceModel[] } -export interface Workspace { +export interface WorkspaceModel { id: WorkspaceId; name: string; description?: string; + ownerRef?: string; +} + +export interface WorkspaceState { + enabled: boolean; +} + +export class Workspace implements WorkspaceModel, WorkspaceState { + @observable id: WorkspaceId + @observable name: string + @observable description?: string + @observable ownerRef?: string + @observable enabled: boolean + + constructor(data: WorkspaceModel) { + Object.assign(this, data) + + if (!ipcRenderer) { + reaction(() => this.getState(), () => { + this.pushState() + }) + } + } + + get isManaged(): boolean { + return !!this.ownerRef + } + + getState(): WorkspaceState { + return { + enabled: this.enabled + } + } + + pushState(state = this.getState()) { + logger.silly("[WORKSPACE] pushing state", {...state, id: this.id}) + broadcastIpc({ + channel: "workspace:state", + args: [this.id, toJS(state)], + }); + } + + @action + setState(state: WorkspaceState) { + Object.assign(this, state) + } + + toJSON(): WorkspaceModel { + return toJS({ + id: this.id, + name: this.name, + description: this.description, + ownerRef: this.ownerRef + }) + } } export class WorkspaceStore extends BaseStore { @@ -23,15 +81,33 @@ export class WorkspaceStore extends BaseStore { super({ configName: "lens-workspace-store", }); + + if (!ipcRenderer) { + setInterval(() => { + this.pushState() + }, 5000) + } + } + + 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") } @observable currentWorkspaceId = WorkspaceStore.defaultId; @observable workspaces = observable.map({ - [WorkspaceStore.defaultId]: { + [WorkspaceStore.defaultId]: new Workspace({ id: WorkspaceStore.defaultId, name: "default" - } + }) }); @computed get currentWorkspace(): Workspace { @@ -42,6 +118,16 @@ export class WorkspaceStore extends BaseStore { 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; } @@ -61,11 +147,11 @@ export class WorkspaceStore extends BaseStore { throw new Error(`workspace ${id} doesn't exist`); } this.currentWorkspaceId = id; - clusterStore.activeClusterId = null; // fixme: handle previously selected cluster from current workspace + clusterStore.activeCluster = null; // fixme: handle previously selected cluster from current workspace } @action - saveWorkspace(workspace: Workspace) { + addWorkspace(workspace: Workspace) { const { id, name } = workspace; const existingWorkspace = this.getById(id); if (!name.trim() || this.getByName(name.trim())) { @@ -82,7 +168,12 @@ export class WorkspaceStore extends BaseStore { } @action - removeWorkspace(id: WorkspaceId) { + removeWorkspace(workspace: Workspace) { + this.removeWorkspaceById(workspace.id) + } + + @action + removeWorkspaceById(id: WorkspaceId) { const workspace = this.getById(id); if (!workspace) return; if (this.isDefault(id)) { @@ -103,7 +194,11 @@ export class WorkspaceStore extends BaseStore { } if (workspaces.length) { this.workspaces.clear(); - workspaces.forEach(workspace => { + workspaces.forEach(ws => { + const workspace = new Workspace(ws) + if (!workspace.isManaged) { + workspace.enabled = true + } this.workspaces.set(workspace.id, workspace) }) } @@ -112,7 +207,7 @@ export class WorkspaceStore extends BaseStore { toJSON(): WorkspaceStoreModel { return toJS({ currentWorkspace: this.currentWorkspaceId, - workspaces: this.workspacesList, + workspaces: this.workspacesList.map((w) => w.toJSON()), }, { recurseEverything: true }) diff --git a/src/extensions/core-api/stores.ts b/src/extensions/core-api/stores.ts index dfaae325afa6..d39314f7623b 100644 --- a/src/extensions/core-api/stores.ts +++ b/src/extensions/core-api/stores.ts @@ -1,4 +1,4 @@ export { ExtensionStore } from "../extension-store" export { clusterStore, ClusterModel } from "../../common/cluster-store" -export { workspaceStore} from "../../common/workspace-store" -export type { Cluster } from "../../main/cluster" +export { Cluster } from "../../main/cluster" +export { workspaceStore, Workspace, WorkspaceModel } from "../../common/workspace-store" diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts index 21556180b434..0620356e9aef 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster-manager.ts @@ -10,7 +10,7 @@ export class ClusterManager { constructor(public readonly port: number) { // auto-init clusters autorun(() => { - clusterStore.clusters.forEach(cluster => { + clusterStore.enabledClustersList.forEach(cluster => { if (!cluster.initialized) { logger.info(`[CLUSTER-MANAGER]: init cluster`, cluster.getMeta()); cluster.init(port); diff --git a/src/main/cluster.ts b/src/main/cluster.ts index 11964ba319eb..57b9e219c39d 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -1,3 +1,4 @@ +import { ipcMain } from "electron" import type { ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences } from "../common/cluster-store" import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api"; import type { WorkspaceId } from "../common/workspace-store"; @@ -33,7 +34,7 @@ export type ClusterRefreshOptions = { refreshMetadata?: boolean } -export interface ClusterState extends ClusterModel { +export interface ClusterState { initialized: boolean; apiUrl: string; online: boolean; @@ -47,11 +48,12 @@ export interface ClusterState extends ClusterModel { allowedResources: string[] } -export class Cluster implements ClusterModel { +export class Cluster implements ClusterModel, ClusterState { public id: ClusterId; public frameId: number; public kubeCtl: Kubectl public contextHandler: ContextHandler; + public ownerRef: string; protected kubeconfigManager: KubeconfigManager; protected eventDisposers: Function[] = []; protected activated = false; @@ -65,6 +67,7 @@ export class Cluster implements ClusterModel { @observable kubeConfigPath: string; @observable apiUrl: string; // cluster server url @observable kubeProxyUrl: string; // lens-proxy to kube-api url + @observable enabled = false; @observable online = false; @observable accessible = false; @observable ready = false; @@ -81,6 +84,7 @@ export class Cluster implements ClusterModel { @computed get available() { return this.accessible && !this.disconnected; } + get version(): string { return String(this.metadata?.version) || "" } @@ -93,6 +97,10 @@ export class Cluster implements ClusterModel { } } + get isManaged(): boolean { + return !!this.ownerRef + } + @action updateModel(model: ClusterModel) { Object.assign(this, model); @@ -123,13 +131,15 @@ export class Cluster implements ClusterModel { const refreshTimer = setInterval(() => !this.disconnected && this.refresh(), 30000); // every 30s const refreshMetadataTimer = setInterval(() => !this.disconnected && this.refreshMetadata(), 900000); // every 15 minutes - this.eventDisposers.push( - reaction(this.getState, this.pushState), - () => { - clearInterval(refreshTimer); - clearInterval(refreshMetadataTimer); - }, - ); + if (ipcMain) { + this.eventDisposers.push( + reaction(() => this.getState(), () => this.pushState()), + () => { + clearInterval(refreshTimer); + clearInterval(refreshMetadataTimer); + }, + ); + } } protected unbindEvents() { @@ -361,6 +371,7 @@ export class Cluster implements ClusterModel { workspace: this.workspace, preferences: this.preferences, metadata: this.metadata, + ownerRef: this.ownerRef }; return toJS(model, { recurseEverything: true @@ -368,9 +379,8 @@ export class Cluster implements ClusterModel { } // serializable cluster-state used for sync btw main <-> renderer - getState = (): ClusterState => { + getState(): ClusterState { const state: ClusterState = { - ...this.toJSON(), initialized: this.initialized, apiUrl: this.apiUrl, online: this.online, @@ -388,14 +398,18 @@ export class Cluster implements ClusterModel { }) } - pushState = (state = this.getState()): ClusterState => { + @action + setState(state: ClusterState) { + Object.assign(this, state) + } + + pushState(state = this.getState()) { logger.silly(`[CLUSTER]: push-state`, state); broadcastIpc({ channel: "cluster:state", frameId: this.frameId, - args: [state], - }); - return state; + args: [this.id, state], + }) } // get cluster system meta, e.g. use in "logger" diff --git a/src/main/tray.ts b/src/main/tray.ts index 31cc99b3145f..428fd5cb3d1b 100644 --- a/src/main/tray.ts +++ b/src/main/tray.ts @@ -80,7 +80,7 @@ export function createTrayMenu(windowManager: WindowManager): Menu { }, { label: "Clusters", - submenu: workspaceStore.workspacesList + submenu: workspaceStore.enabledWorkspacesList .filter(workspace => clusterStore.getByWorkspaceId(workspace.id).length > 0) // hide empty workspaces .map(workspace => { const clusters = clusterStore.getByWorkspaceId(workspace.id); diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index 5d92dd624d4d..7311e0e0cf09 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -40,6 +40,7 @@ export async function bootstrap(App: AppComponent) { // Register additional store listeners clusterStore.registerIpcListener(); + workspaceStore.registerIpcListener(); // init app's dependencies if any if (App.init) { diff --git a/src/renderer/components/+add-cluster/add-cluster.tsx b/src/renderer/components/+add-cluster/add-cluster.tsx index 0751dd2d8c3b..8acd3a51ea26 100644 --- a/src/renderer/components/+add-cluster/add-cluster.tsx +++ b/src/renderer/components/+add-cluster/add-cluster.tsx @@ -163,7 +163,7 @@ export class AddCluster extends React.Component { }) runInAction(() => { - clusterStore.addCluster(...newClusters); + clusterStore.addClusters(...newClusters); if (newClusters.length === 1) { const clusterId = newClusters[0].id; clusterStore.setActive(clusterId); diff --git a/src/renderer/components/+cluster-settings/components/cluster-workspace-setting.tsx b/src/renderer/components/+cluster-settings/components/cluster-workspace-setting.tsx index 6cd933ca1111..ea4ee5a5715b 100644 --- a/src/renderer/components/+cluster-settings/components/cluster-workspace-setting.tsx +++ b/src/renderer/components/+cluster-settings/components/cluster-workspace-setting.tsx @@ -26,11 +26,11 @@ export class ClusterWorkspaceSetting extends React.Component {