1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Replace webview to iframe (#701)

* replace webview-tag to iframe with nodeIntegrationInSubFrames=true

Signed-off-by: Roman <ixrock@gmail.com>

* clean up

Signed-off-by: Roman <ixrock@gmail.com>

* chore: fix type ipc#handle

Signed-off-by: Roman <ixrock@gmail.com>
Signed-off-by: Lauri Nevala <lauri.nevala@gmail.com>
This commit is contained in:
Roman 2020-08-17 21:31:20 +03:00 committed by Lauri Nevala
parent fb124ada7d
commit 0353ee737b
7 changed files with 66 additions and 160 deletions

View File

@ -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 {

View File

@ -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<A extends any[] = any> {
msgId: string;
args: A;
}
export interface IpcChannelResponse<T extends any[] = any, E = any> {
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[]) => Promise<any> | any; // 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<IpcChannelResponse>) {
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 <T>(...args: any[]): Promise<T> => {
return ipcRenderer.invoke(channel, ...args);
},
}
if (autoBind && ipcMain) {
@ -112,13 +52,14 @@ export function createIpcChannel({ autoBind = true, mode = IpcMode.ASYNC, timeou
export interface IpcBroadcastParams<A extends any[] = any> {
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)
}
})
}

View File

@ -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;

View File

@ -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' },

View File

@ -28,8 +28,8 @@ export class WindowManager {
backgroundColor: "#1e2124",
webPreferences: {
nodeIntegration: true,
nodeIntegrationInSubFrames: true,
enableRemoteModule: true,
webviewTag: true,
},
});
this.windowState.manage(this.mainView);
@ -50,24 +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;
})
}
async showMain() {
try {
await this.showSplash();

View File

@ -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() {

View File

@ -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<ClusterId, LensView>();
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 <webview>-tag to <iframe> with nodeIntegration=true
export function initView(clusterId: ClusterId) {
export async function initView(clusterId: ClusterId) {
if (!clusterId || lensViews.has(clusterId)) {
return;
}
logger.info(`[CLUSTER-VIEW]: init dashboard, clusterId=${clusterId}`)
const parentElem = document.getElementById("lens-views"); // defined in cluster-manager's css-grid
const webview = document.createElement("webview");
webview.setAttribute("src", `//${clusterId}.${location.host}`)
webview.setAttribute("nodeintegration", "true")
webview.setAttribute("enableremotemodule", "true")
webview.addEventListener("did-finish-load", () => {
logger.info(`[CLUSTER-VIEW]: loaded, clusterId=${clusterId}`)
clusterIpc.init.invokeFromRenderer(clusterId); // push cluster-state to webview and init render
logger.info(`[LENS-VIEW]: init dashboard, clusterId=${clusterId}`)
const cluster = clusterStore.getById(clusterId);
await cluster.whenInitialized;
const parentElem = document.getElementById("lens-views");
const iframe = document.createElement("iframe");
iframe.name = cluster.preferences.clusterName;
iframe.setAttribute("src", `//${clusterId}.${location.host}`)
iframe.addEventListener("load", async () => {
logger.info(`[LENS-VIEW]: loaded from ${iframe.src}`)
lensViews.get(clusterId).isLoaded = true;
});
webview.addEventListener("did-fail-load", (event) => {
logger.error(`[CLUSTER-VIEW]: failed to load, clusterId=${clusterId}`, event)
});
lensViews.set(clusterId, { clusterId, view: webview });
parentElem.appendChild(webview); // add to dom and init cluster-page loading
})
lensViews.set(clusterId, { clusterId, view: iframe });
parentElem.appendChild(iframe);
}
export function refreshViews() {