From 4dd6d587ab84ee2de0c75617b4bb847cd198e5e0 Mon Sep 17 00:00:00 2001 From: Jari Kolehmainen Date: Thu, 29 Oct 2020 17:15:30 +0200 Subject: [PATCH 1/5] improve how extensions can manage cluster/workspace stores Signed-off-by: Jari Kolehmainen --- src/common/__tests__/cluster-store.test.ts | 2 +- src/common/cluster-store.ts | 55 +++++++-- src/common/ipc.ts | 3 + src/common/workspace-store.ts | 114 ++++++++++++++++-- src/main/cluster-manager.ts | 2 +- src/main/cluster.ts | 44 ++++--- src/main/tray.ts | 2 +- src/renderer/bootstrap.tsx | 1 + .../components/+add-cluster/add-cluster.tsx | 2 +- .../components/cluster-workspace-setting.tsx | 4 +- .../components/remove-cluster-button.tsx | 7 +- .../components/+workspaces/workspace-menu.tsx | 4 +- .../components/+workspaces/workspaces.tsx | 21 ++-- .../cluster-manager/clusters-menu.tsx | 5 +- 14 files changed, 211 insertions(+), 55 deletions(-) diff --git a/src/common/__tests__/cluster-store.test.ts b/src/common/__tests__/cluster-store.test.ts index 724a83f0d72c..14b170726051 100644 --- a/src/common/__tests__/cluster-store.test.ts +++ b/src/common/__tests__/cluster-store.test.ts @@ -70,7 +70,7 @@ describe("empty config", () => { describe("with prod and dev clusters added", () => { beforeEach(() => { - clusterStore.addCluster( + clusterStore.addClusters( new Cluster({ id: "prod", contextName: "prod", diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index fddd0f3be6c7..4b3914e40446 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 @@ -78,6 +79,12 @@ export class ClusterStore extends BaseStore { accessPropertiesByDotNotation: false, // To make dots safe in cluster context names migrations: migrations, }); + + if (!ipcRenderer) { + setInterval(() => { + this.pushState() + }, 5000) + } } @observable activeClusterId: ClusterId; @@ -86,11 +93,9 @@ export class ClusterStore extends BaseStore { 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,6 +104,12 @@ export class ClusterStore extends BaseStore { ipcRenderer.removeAllListeners("cluster:state") } + pushState() { + this.clusters.forEach((c) => { + c.pushState() + }) + } + @computed get activeCluster(): Cluster | null { return this.getById(this.activeClusterId); } @@ -107,6 +118,10 @@ export class ClusterStore extends BaseStore { return Array.from(this.clusters.values()); } + @computed get enabledClustersList(): Cluster[] { + return this.clustersList.filter((c) => c.enabled) + } + isActive(id: ClusterId) { return this.activeClusterId === id; } @@ -145,12 +160,29 @@ 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 { + appEventBus.emit({name: "cluster", action: "add"}) + let cluster: Cluster = null + if (model instanceof Cluster === false) { + cluster = new Cluster(model) + } + this.clusters.set(model.id, cluster); + + return cluster + } + + async removeCluster(model: ClusterModel) { + await this.removeById(model.id) } @action @@ -189,6 +221,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); } diff --git a/src/common/ipc.ts b/src/common/ipc.ts index 97c4dd05cd2b..a5dc21da65aa 100644 --- a/src/common/ipc.ts +++ b/src/common/ipc.ts @@ -61,6 +61,9 @@ export interface IpcBroadcastParams { } export function broadcastIpc({ channel, frameId, frameOnly, webContentId, filter, args = [] }: IpcBroadcastParams) { + if (!webContents) { + return + } const singleView = webContentId ? webContents.fromId(webContentId) : null; let views = singleView ? [singleView] : webContents.getAllWebContents(); if (filter) { diff --git a/src/common/workspace-store.ts b/src/common/workspace-store.ts index b7f246701383..bc469c3c61eb 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() + }) + } + } + + 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,34 @@ 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) => { + console.log(workspaceId, state) + 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 +119,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; } @@ -65,7 +152,7 @@ export class WorkspaceStore extends BaseStore { } @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 +169,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 +195,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.ownerRef) { + workspace.enabled = true + } this.workspaces.set(workspace.id, workspace) }) } @@ -112,7 +208,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/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..692ebb2c7c70 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 { } } + 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 {