diff --git a/src/common/cluster-ipc.ts b/src/common/cluster-ipc.ts index 6cc9219805..5e1b86992b 100644 --- a/src/common/cluster-ipc.ts +++ b/src/common/cluster-ipc.ts @@ -5,20 +5,24 @@ import { tracker } from "./tracker"; export const clusterIpc = { init: createIpcChannel({ channel: "cluster:init", - handle: async (clusterId: ClusterId) => { - return clusterStore.getById(clusterId)?.pushState(); + handle: async (clusterId: ClusterId, frameId: number) => { + const cluster = clusterStore.getById(clusterId); + if (cluster) { + cluster.frameId = frameId; // save cluster's webFrame.routingId to be able to send push-updates + return cluster.pushState(); + } }, }), activate: createIpcChannel({ channel: "cluster:activate", - handle: async (clusterId: ClusterId = clusterStore.activeClusterId) => { + handle: (clusterId: ClusterId) => { return clusterStore.getById(clusterId)?.activate(); }, }), disconnect: createIpcChannel({ channel: "cluster:disconnect", - handle: (clusterId: ClusterId = clusterStore.activeClusterId) => { + handle: (clusterId: ClusterId) => { tracker.event("cluster", "stop"); return clusterStore.getById(clusterId)?.disconnect(); }, @@ -29,7 +33,6 @@ export const clusterIpc = { handle: async (clusterId: ClusterId, feature: string, config?: any) => { tracker.event("cluster", "install", feature); const cluster = clusterStore.getById(clusterId); - if (cluster) { await cluster.installFeature(feature, config) } else { diff --git a/src/common/ipc.ts b/src/common/ipc.ts index ea0633c34e..428a11c933 100644 --- a/src/common/ipc.ts +++ b/src/common/ipc.ts @@ -2,106 +2,46 @@ // https://www.electronjs.org/docs/api/ipc-main // https://www.electronjs.org/docs/api/ipc-renderer -import { ipcMain, ipcRenderer, IpcRendererEvent, WebContents, webContents } from "electron" +import { ipcMain, ipcRenderer, WebContents, webContents } from "electron" import logger from "../main/logger"; -import { getRandId } from "./utils"; export type IpcChannel = string; -export enum IpcMode { - SYNC = "sync", - ASYNC = "async", -} - -export interface IpcChannelRequest { - msgId: string; - args: A; -} - -export interface IpcChannelResponse { - msgId: string; - data?: T; - error?: E; -} - export interface IpcChannelOptions { channel: IpcChannel; // main <-> renderer communication channel name - mode?: IpcMode; // default: "async", use "sync" as last resort: https://www.electronjs.org/docs/api/ipc-renderer#ipcrenderersendsyncchannel-args - handle?: (...args: any[]) => any; // main-process message handler + handle?: (...args: any[]) => void; // message handler autoBind?: boolean; // auto-bind message handler in main-process, default: true timeout?: number; // timeout for waiting response from the sender - once?: boolean; // todo: add support + once?: boolean; // one-time event } -export function createIpcChannel({ autoBind = true, mode = IpcMode.ASYNC, timeout = 0, handle, channel }: IpcChannelOptions) { - channel = `${mode}:${channel}` - +export function createIpcChannel({ autoBind = true, once, timeout = 0, handle, channel }: IpcChannelOptions) { const ipcChannel = { channel: channel, handleInMain: () => { logger.info(`[IPC]: setup channel "${channel}"`); - - ipcMain.on(channel, async (event, req: IpcChannelRequest) => { - let resolved = false; + const ipcHandler = once ? ipcMain.handleOnce : ipcMain.handle; + ipcHandler(channel, async (event, ...args) => { let timerId: any; - - function resolve(res: Partial) { - if (resolved) return; - res.msgId = req.msgId; // return back to sender to be able to handle response - resolved = true - logger.debug(`[IPC]: sending response to "${channel}"`, res); - if (mode === IpcMode.SYNC) { - event.returnValue = res; - } else { - event.reply(channel, res); - } - } - - if (timeout > 0) { - timerId = setTimeout(() => { - const timeoutError = new Error(`[IPC]: response timeout in ${timeout}ms`); - resolve({ error: timeoutError }) - }, timeout); - } - try { - const data = await handle(...req.args); // todo: maybe exec in separate thread/worker - resolve({ data }) + if (timeout > 0) { + timerId = setTimeout(() => { + throw new Error(`[IPC]: response timeout in ${timeout}ms`) + }, timeout); + } + return await handle(...args); // todo: maybe exec in separate thread/worker } catch (error) { - resolve({ - error: String(error) - }) + throw error } finally { clearTimeout(timerId); } }) }, - invokeFromRenderer: async (...args: any[]) => { - const req: IpcChannelRequest = { - msgId: getRandId({ prefix: "ipc-msg-id" }), - args: args, - } - if (mode === IpcMode.SYNC) { - ipcRenderer.sendSync(channel, req) - } else { - ipcRenderer.send(channel, req) - } - return new Promise(async (resolve, reject) => { - ipcRenderer.on(channel, function waitResponseHandler(event: IpcRendererEvent, res: IpcChannelResponse) { - if (req.msgId === res.msgId) { - const meta = { ...req, ...res }; - if (res.data) { - logger.debug(`[IPC]: "${channel}" resolve`, meta); - resolve(res.data); - } - if (res.error) { - logger.error(`[IPC]: "${channel}" reject`, meta); - reject(res.error); - } - ipcRenderer.off(channel, waitResponseHandler); // unsubscribe since handled - } - }); - }) + removeHandler() { + ipcMain.removeHandler(channel); + }, + invokeFromRenderer: async (...args: any[]): Promise => { + return ipcRenderer.invoke(channel, ...args); }, } if (autoBind && ipcMain) { @@ -112,13 +52,14 @@ export function createIpcChannel({ autoBind = true, mode = IpcMode.ASYNC, timeou export interface IpcBroadcastParams { channel: IpcChannel - webContentId?: number; // sends to single webContents view + webContentId?: number; // send to single webContents view + frameId?: number; // send to inner frame of webContents filter?: (webContent: WebContents) => boolean timeout?: number; // todo: add support args?: A; } -export function broadcastIpc({ channel, webContentId, filter, args = [] }: IpcBroadcastParams) { +export function broadcastIpc({ channel, frameId, webContentId, filter, args = [] }: IpcBroadcastParams) { const singleView = webContentId ? webContents.fromId(webContentId) : null; let views = singleView ? [singleView] : webContents.getAllWebContents(); if (filter) { @@ -127,6 +68,9 @@ export function broadcastIpc({ channel, webContentId, filter, args = [] }: IpcBr views.forEach(webContent => { const type = webContent.getType(); logger.debug(`[IPC]: broadcasting "${channel}" to ${type}=${webContent.id}`, { args }); - webContent.send(channel, ...[args].flat()); + webContent.send(channel, ...args); + if (frameId) { + webContent.sendToFrame(frameId, channel, ...args) + } }) } diff --git a/src/main/cluster.ts b/src/main/cluster.ts index 5d28d4ba5a..6ba0fb1f48 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -40,6 +40,7 @@ export interface ClusterState extends ClusterModel { export class Cluster implements ClusterModel { public id: ClusterId; + public frameId: number; public kubeCtl: Kubectl public contextHandler: ContextHandler; protected kubeconfigManager: KubeconfigManager; @@ -389,8 +390,8 @@ export class Cluster implements ClusterModel { pushState = (state = this.getState()): ClusterState => { logger.debug(`[CLUSTER]: push-state`, state); broadcastIpc({ - // webContentId: viewId, // todo: send to cluster-view only channel: "cluster:state", + frameId: this.frameId, args: [state], }); return state; diff --git a/src/main/menu.ts b/src/main/menu.ts index f8c4b41ac0..202f61475e 100644 --- a/src/main/menu.ts +++ b/src/main/menu.ts @@ -1,5 +1,4 @@ -import type { ClusterId } from "../common/cluster-store"; -import { app, BrowserWindow, dialog, Menu, MenuItem, MenuItemConstructorOptions, shell } from "electron" +import { app, BrowserWindow, dialog, Menu, MenuItem, MenuItemConstructorOptions, shell, webContents } from "electron" import { autorun } from "mobx"; import { WindowManager } from "./window-manager"; import { appName, isMac, issuesTrackerUrl, isWindows, slackUrl } from "../common/vars"; @@ -30,12 +29,11 @@ export function buildMenu(windowManager: WindowManager) { return menuItems; } - function navigate(url: string, clusterId?: ClusterId) { + function navigate(url: string) { logger.info(`[MENU]: navigating to ${url}`); windowManager.navigate({ channel: "menu:navigate", url: url, - clusterId: clusterId, }) } @@ -146,33 +144,24 @@ export function buildMenu(windowManager: WindowManager) { label: 'Back', accelerator: 'CmdOrCtrl+[', click() { - windowManager.getActiveClusterView()?.goBack(); + webContents.getFocusedWebContents()?.goBack(); } }, { label: 'Forward', accelerator: 'CmdOrCtrl+]', click() { - windowManager.getActiveClusterView()?.goForward(); + webContents.getFocusedWebContents()?.goForward(); } }, { label: 'Reload', accelerator: 'CmdOrCtrl+R', click() { - windowManager.getActiveClusterView()?.reload(); + webContents.getFocusedWebContents()?.reload(); } }, { role: 'toggleDevTools' }, - ...activeClusterOnly([ - { - accelerator: "CmdOrCtrl+Shift+I", - label: "Toggle Dashboard DevTools", - click() { - windowManager.getActiveClusterView()?.toggleDevTools(); - } - } - ]), { type: 'separator' }, { role: 'resetZoom' }, { role: 'zoomIn' }, diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts index 7051db1055..9fcfa26931 100644 --- a/src/main/window-manager.ts +++ b/src/main/window-manager.ts @@ -28,8 +28,8 @@ export class WindowManager { backgroundColor: "#1e2124", webPreferences: { nodeIntegration: true, + nodeIntegrationInSubFrames: true, enableRemoteModule: true, - webviewTag: true, }, }); this.windowState.manage(this.mainView); @@ -50,18 +50,14 @@ export class WindowManager { initMenu(this); } - navigate({ url, channel, clusterId }: { url: string, channel: string, clusterId?: ClusterId }) { - if (clusterId) { - this.getClusterView(clusterId)?.send(channel, url); + navigate({ url, channel, frameId }: { url: string, channel: string, frameId?: number }) { + if (frameId) { + this.mainView.webContents.sendToFrame(frameId, channel, url); } else { this.mainView.webContents.send(channel, url); } } - getActiveClusterView() { - return this.getClusterView(this.activeClusterId) - } - getClusterView(clusterId: ClusterId): WebContents { return webContents.getAllWebContents().find(view => { return new URL(view.getURL()).host.split(".")[0] === clusterId; diff --git a/src/renderer/components/app.tsx b/src/renderer/components/app.tsx index af91aa6891..0c3044fc12 100755 --- a/src/renderer/components/app.tsx +++ b/src/renderer/components/app.tsx @@ -32,13 +32,17 @@ import { ErrorBoundary } from "./error-boundary"; import { Terminal } from "./dock/terminal"; import { getHostedCluster, getHostedClusterId } from "../../common/cluster-store"; import logger from "../../main/logger"; +import { clusterIpc } from "../../common/cluster-ipc"; +import { webFrame } from "electron"; @observer export class App extends React.Component { static async init() { - logger.info(`[APP]: Init dashboard, clusterId=${getHostedClusterId()}`) + const clusterId = getHostedClusterId(); + logger.info(`[APP]: Init dashboard, clusterId=${clusterId}`) await Terminal.preloadFonts() - await getHostedCluster().whenInitialized; // wait for cluster-state before initial render + await clusterIpc.init.invokeFromRenderer(clusterId, webFrame.routingId); + await getHostedCluster().whenInitialized; } get startURL() { diff --git a/src/renderer/components/cluster-manager/lens-views.ts b/src/renderer/components/cluster-manager/lens-views.ts index 9fa42993cd..67a0ac68e2 100644 --- a/src/renderer/components/cluster-manager/lens-views.ts +++ b/src/renderer/components/cluster-manager/lens-views.ts @@ -1,62 +1,37 @@ -import { ipcRenderer, WebviewTag } from "electron"; -import { observable, when } from "mobx"; +import { observable } from "mobx"; import { ClusterId, clusterStore } from "../../../common/cluster-store"; -import { clusterIpc } from "../../../common/cluster-ipc"; -import { clusterViewURL, getMatchedCluster, getMatchedClusterId } from "./cluster-view.route" -import { navigate } from "../../navigation"; +import { getMatchedCluster } from "./cluster-view.route" import logger from "../../../main/logger"; export interface LensView { isLoaded?: boolean clusterId: ClusterId; - view: WebviewTag + view: HTMLIFrameElement } export const lensViews = observable.map(); -export async function navigateInClusterView(path: string, clusterId: ClusterId) { - // select active cluster in common view - if (clusterId !== getMatchedClusterId()) { - clusterStore.setActive(clusterId); - navigate(clusterViewURL({ params: { clusterId } })); - } - // navigate in cluster-view when ready - await when(() => hasLoadedView(clusterId)) - ipcRenderer.sendTo(getViewId(clusterId), "menu:navigate", path); -} - export function hasLoadedView(clusterId: ClusterId): boolean { return !!lensViews.get(clusterId)?.isLoaded; } -export function getViewId(clusterId: ClusterId): number { - const webview = lensViews.get(clusterId)?.view - if (webview) { - return webview.getWebContentsId() - } -} - -// todo: figure out how to replace -tag to