diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index 3d3366fe1e..083031eba1 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -65,7 +65,7 @@ export class ClusterStore extends BaseStore { if (ipcRenderer) { ipcRenderer.on("cluster:state", (event, clusterState: ClusterState) => { this.applyWithoutSync(() => { - logger.info(`[CLUSTER-STORE]: received cluster(${clusterState.id}) update`, clusterState); + logger.debug(`[CLUSTER-STORE]: received state update for cluster=${clusterState.id}`, clusterState); const cluster = this.getById(clusterState.id); if (cluster) cluster.updateModel(clusterState) }) @@ -85,6 +85,10 @@ export class ClusterStore extends BaseStore { return Array.from(this.clusters.values()); } + hasContext(name: string) { + return this.clustersList.some(cluster => cluster.contextName === name); + } + getById(id: ClusterId): Cluster { return this.clusters.get(id); } @@ -94,18 +98,23 @@ export class ClusterStore extends BaseStore { } @action - addCluster(model: ClusterModel): Cluster { + async addCluster(model: ClusterModel, activate = true): Promise { const cluster = new Cluster(model); this.clusters.set(model.id, cluster); + if (activate) this.activeClusterId = model.id; return cluster; } @action - removeById(clusterId: ClusterId): void { - if (this.activeClusterId === clusterId) { - this.activeClusterId = null; + async removeById(clusterId: ClusterId) { + const cluster = this.getById(clusterId); + if (cluster) { + this.clusters.delete(clusterId); + if (this.activeClusterId === clusterId) { + this.activeClusterId = null; + } + unlink(cluster.kubeConfigPath).catch(() => null); } - this.clusters.delete(clusterId); } @action @@ -116,7 +125,7 @@ export class ClusterStore extends BaseStore { } @action - protected async uploadClusterIcon({ clusterId, ...upload }: ClusterIconUpload): Promise { + protected async uploadIcon({ clusterId, ...upload }: ClusterIconUpload): Promise { const cluster = this.getById(clusterId); if (cluster) { tracker.event("cluster", "upload-icon"); @@ -129,7 +138,7 @@ export class ClusterStore extends BaseStore { } @action - protected resetClusterIcon(clusterId: ClusterId) { + protected resetIcon(clusterId: ClusterId) { const cluster = this.getById(clusterId); if (cluster) { tracker.event("cluster", "reset-icon") @@ -167,7 +176,7 @@ export class ClusterStore extends BaseStore { this.clusters.replace(newClusters); this.removedClusters.replace(removedClusters); - // "auto-select" first cluster if not available or invalid from config file + // "auto-select" first cluster if available if (!this.activeClusterId && newClusters.size) { this.activeClusterId = Array.from(newClusters.values())[0].id; } diff --git a/src/common/ipc.ts b/src/common/ipc.ts index de2126e6d7..704ce9a9ba 100644 --- a/src/common/ipc.ts +++ b/src/common/ipc.ts @@ -31,14 +31,14 @@ export function sendMessage({ channel, webContentId, filter, args = [] }: IpcMes } views.forEach(webContent => { const type = webContent.getType(); - logger.info(`[IPC]: sending message "${channel}" to ${type}=${webContent.id}`); + logger.debug(`[IPC]: sending message "${channel}" to ${type}=${webContent.id}`, { args }); webContent.send(channel, ...[args].flat()); }) } // todo: support timeout + merge with sendMessage? export async function invokeMessage(channel: IpcChannel, ...args: T): Promise { - logger.debug(`[IPC]: invoke channel "${channel}"`, args); + logger.debug(`[IPC]: invoke channel "${channel}"`, { args }); return ipcRenderer.invoke(channel, ...args); } @@ -46,7 +46,7 @@ export async function invokeMessage(channel: IpcChanne export function handleMessage(channel: IpcChannel, handler: IpcMessageHandler, options: IpcHandleOpts = {}) { const { timeout = 0 } = options; ipcMain.handle(channel, async (event, ...args: T) => { - logger.info(`[IPC]: handle "${channel}"`, { event, args }); + logger.debug(`[IPC]: handle "${channel}"`, { args }); return new Promise(async (resolve, reject) => { let timerId; if (timeout) { @@ -60,7 +60,7 @@ export function handleMessage(channel: IpcChannel, handler: Ipc clearTimeout(timerId); return result; } catch (err) { - logger.debug(`[IPC]: handling "${channel}" error`, err); + logger.debug(`[IPC]: handling "${channel}" error`, { err }); } }) }) diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts index d2c99d2b18..c6254c6ae7 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster-manager.ts @@ -5,23 +5,31 @@ import { ClusterId, clusterStore } from "../common/cluster-store" import { handleMessage } from "../common/ipc"; import { tracker } from "../common/tracker"; import { Cluster, ClusterIpcEvent } from "./cluster" +import logger from "./logger"; export class ClusterManager { constructor(public readonly port: number) { // auto-init clusters autorun(() => { - clusterStore.clustersList - .filter(cluster => !cluster.initialized) - .forEach(cluster => cluster.init(port)); + clusterStore.clusters.forEach(cluster => { + if (cluster.initialized) return; + cluster.init(port); + logger.info(`[CLUSTER-MANAGER]: initializing cluster`, cluster.getMeta()); + }); }); // auto-stop removed clusters autorun(() => { - clusterStore.removedClusters.forEach(cluster => cluster.stop()); - clusterStore.removedClusters.clear(); + const { removedClusters } = clusterStore; + const meta = Array.from(removedClusters.values()).map(cluster => cluster.getMeta()); + logger.info(`[CLUSTER-MANAGER]: removing clusters`, meta); + removedClusters.forEach(cluster => cluster.destroy()); + removedClusters.clear(); + }, { + delay: 250 }); - // listen ipc-events which could be handled *only* in main-process (nodeIntegration=true) + // listen for ipc-events that must be handled *only* in main-process (nodeIntegration=true) handleMessage(ClusterIpcEvent.STOP, this.stopCluster.bind(this)); } @@ -37,7 +45,7 @@ export class ClusterManager { protected stopCluster(clusterId: ClusterId) { tracker.event("cluster", "stop"); - this.getCluster(clusterId)?.stop(); + this.getCluster(clusterId)?.destroy(); } getClusterForRequest(req: http.IncomingMessage): Cluster { diff --git a/src/main/cluster.ts b/src/main/cluster.ts index d11060fcfd..e03a307cf3 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -24,6 +24,7 @@ export enum ClusterStatus { } export interface ClusterState extends ClusterModel { + initialized?: boolean; apiUrl: string; online?: boolean; accessible?: boolean; @@ -98,12 +99,13 @@ export class Cluster implements ClusterModel { bindEvents(viewId: number) { if (!this.initialized) return; + logger.info(`[CLUSTER]: bind events`, this.getMeta()); const refreshStatusTimer = setInterval(() => this.refreshStatus(), 30000); // every 30s const refreshEventsTimer = setInterval(() => this.refreshEvents(), 3000); // every 3s this.disposers.push( - () => clearTimeout(refreshStatusTimer), - () => clearTimeout(refreshEventsTimer), + () => clearInterval(refreshStatusTimer), + () => clearInterval(refreshEventsTimer), reaction(() => this.getState(), clusterState => { sendMessage({ @@ -118,15 +120,24 @@ export class Cluster implements ClusterModel { } unbindEvents() { + if (!this.initialized) return; + logger.info(`[CLUSTER]: unbind events`, this.getMeta()); this.disposers.forEach(dispose => dispose()); this.disposers.length = 0; } stop() { - if (!this.initialized) return; this.contextHandler.stopServer(); - this.kubeconfigManager.unlink(); - this.unbindEvents(); + } + + destroy() { + try { + this.stop(); + this.unbindEvents(); + this.kubeconfigManager.unlink(); + } catch (err) { + logger.error(`[CLUSTER]: destroy() throws: ${err}`, this.getMeta()); + } } @action @@ -318,6 +329,7 @@ export class Cluster implements ClusterModel { getState(): ClusterState { const state: ClusterState = { ...this.toJSON(), + initialized: this.initialized, apiUrl: this.apiUrl, online: this.online, accessible: this.accessible, @@ -333,4 +345,12 @@ export class Cluster implements ClusterModel { recurseEverything: true }) } + + // get cluster system meta, e.g. use in "logger" + getMeta() { + return { + id: this.id, + name: this.contextName, + } + } } diff --git a/src/main/kubeconfig-manager.ts b/src/main/kubeconfig-manager.ts index 9d3fcd1892..5f75899389 100644 --- a/src/main/kubeconfig-manager.ts +++ b/src/main/kubeconfig-manager.ts @@ -69,7 +69,7 @@ export class KubeconfigManager { } unlink() { - logger.debug('Deleting temporary kubeconfig: ' + this.tempFile) + logger.info('Deleting temporary kubeconfig: ' + this.tempFile) fs.unlinkSync(this.tempFile) } } diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts index 674a61517d..fde815dd7f 100644 --- a/src/main/window-manager.ts +++ b/src/main/window-manager.ts @@ -51,7 +51,6 @@ export class WindowManager { } }, { fireImmediately: true, - delay: 250, }), // auto-destroy views for removed clusters diff --git a/src/renderer/_vue/components/AddClusterPage.vue b/src/renderer/_vue/components/AddClusterPage.vue deleted file mode 100644 index 25bff611fd..0000000000 --- a/src/renderer/_vue/components/AddClusterPage.vue +++ /dev/null @@ -1,261 +0,0 @@ - - - - - diff --git a/src/renderer/components/+add-cluster/add-cluster.tsx b/src/renderer/components/+add-cluster/add-cluster.tsx index 13419b71b0..0570b45fe0 100644 --- a/src/renderer/components/+add-cluster/add-cluster.tsx +++ b/src/renderer/components/+add-cluster/add-cluster.tsx @@ -16,16 +16,17 @@ import { tracker } from "../../../common/tracker"; import { clusterStore } from "../../../common/cluster-store"; import { workspaceStore } from "../../../common/workspace-store"; import { v4 as uuid } from "uuid" +import { navigate } from "../../navigation"; @observer export class AddCluster extends React.Component { readonly custom: any = "custom" @observable.ref clusterConfig: KubeConfig; @observable.ref kubeConfig: KubeConfig; // local ~/.kube/config (if available) + @observable.ref error: React.ReactNode; @observable isWaiting = false @observable showSettings = false - @observable error = "" @observable proxyServer = "" @observable customConfig = "" @@ -49,13 +50,15 @@ export class AddCluster extends React.Component { @computed get clusterOptions() { const options: SelectOption[] = []; if (this.kubeConfig) { - splitConfig(this.kubeConfig).forEach(kubeConfig => { - const contextName = kubeConfig.getCurrentContext(); + const contexts = splitConfig(this.kubeConfig) + .filter(kc => !clusterStore.hasContext(kc.currentContext)); + + contexts.forEach(kubeConfig => { const isNew = false; // fixme: detect new context since last visit options.push({ value: kubeConfig, label: <> - {contextName} + {kubeConfig.currentContext} {isNew && (new)} , }) @@ -72,16 +75,16 @@ export class AddCluster extends React.Component { tracker.event("cluster", "add"); const { clusterConfig, customConfig, proxyServer } = this; const clusterId = uuid(); + this.isWaiting = true + this.error = "" try { const config = this.isCustom ? loadConfig(customConfig) : clusterConfig; if (!config) { - this.error = "Please select kubeconfig" + this.error = Please select kubeconfig return; } - this.error = "" - this.isWaiting = true validateConfig(config); - clusterStore.addCluster({ + await clusterStore.addCluster({ id: clusterId, kubeConfigPath: saveConfigToAppFiles(clusterId, config), workspace: workspaceStore.currentWorkspaceId, @@ -90,7 +93,8 @@ export class AddCluster extends React.Component { clusterName: config.currentContext, httpsProxy: proxyServer || undefined, }, - }) + }); + navigate("/"); } catch (err) { this.error = String(err); } finally { diff --git a/src/renderer/components/cluster-manager/clusters-menu.scss b/src/renderer/components/cluster-manager/clusters-menu.scss index 01973ec058..051a615686 100644 --- a/src/renderer/components/cluster-manager/clusters-menu.scss +++ b/src/renderer/components/cluster-manager/clusters-menu.scss @@ -33,7 +33,7 @@ } > .clusters { - @include hidden-scrollbar; + //@include hidden-scrollbar; // fixme: uncomment after refactoring tooltip.tsx --flex-gap: #{$padding * 2}; padding: $padding; diff --git a/src/renderer/components/cluster-manager/clusters-menu.tsx b/src/renderer/components/cluster-manager/clusters-menu.tsx index d34079b7d3..c3c66b697b 100644 --- a/src/renderer/components/cluster-manager/clusters-menu.tsx +++ b/src/renderer/components/cluster-manager/clusters-menu.tsx @@ -18,6 +18,7 @@ import { addClusterURL } from "../+add-cluster"; import { clusterSettingsURL } from "../+cluster-settings"; import { landingURL } from "../+landing-page"; import { Tooltip, TooltipContent } from "../tooltip"; +import { ConfirmDialog } from "../confirm-dialog"; // fixme: allow to rearrange clusters with drag&drop // fixme: disconnect cluster from context-menu @@ -50,7 +51,6 @@ export class ClustersMenu extends React.Component { label: _i18n._(t`Settings`), click: () => navigate(clusterSettingsURL()) })); - if (cluster.initialized) { menu.append(new MenuItem({ label: _i18n._(t`Disconnect`), @@ -59,6 +59,16 @@ export class ClustersMenu extends React.Component { } })) } + menu.append(new MenuItem({ + label: _i18n._(t`Remove`), + click: () => { + ConfirmDialog.open({ + ok: () => clusterStore.removeById(cluster.id), + labelOk: _i18n._(t`Remove`), + message:

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

, + }) + } + })); menu.popup({ window: remote.getCurrentWindow() })