mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
refactor ipc module
Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
parent
02b9ac6b8b
commit
cae880ad85
@ -6,7 +6,7 @@ import { action, observable, reaction, runInAction, toJS, when } from "mobx";
|
|||||||
import Singleton from "./utils/singleton";
|
import Singleton from "./utils/singleton";
|
||||||
import { getAppVersion } from "./utils/app-version";
|
import { getAppVersion } from "./utils/app-version";
|
||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
import { broadcastIpc, IpcBroadcastParams } from "./ipc";
|
import { broadcastMessage, subscribeToBroadcast, unsubscribeFromBroadcast } from "./ipc";
|
||||||
import isEqual from "lodash/isEqual";
|
import isEqual from "lodash/isEqual";
|
||||||
|
|
||||||
export interface BaseStoreParams<T = any> extends ConfOptions<T> {
|
export interface BaseStoreParams<T = any> extends ConfOptions<T> {
|
||||||
@ -36,8 +36,12 @@ export class BaseStore<T = any> extends Singleton {
|
|||||||
return path.basename(this.storeConfig.path);
|
return path.basename(this.storeConfig.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
get syncChannel() {
|
get syncRendererChannel() {
|
||||||
return `store-sync:${this.name}`
|
return `store-sync-renderer:${this.name}`
|
||||||
|
}
|
||||||
|
|
||||||
|
get syncMainChannel() {
|
||||||
|
return `store-sync-main:${this.name}`
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async init() {
|
protected async init() {
|
||||||
@ -84,16 +88,16 @@ export class BaseStore<T = any> extends Singleton {
|
|||||||
logger.silly(`[STORE]: SYNC ${this.name} from renderer`, { model });
|
logger.silly(`[STORE]: SYNC ${this.name} from renderer`, { model });
|
||||||
this.onSync(model);
|
this.onSync(model);
|
||||||
};
|
};
|
||||||
ipcMain.on(this.syncChannel, callback);
|
subscribeToBroadcast(this.syncMainChannel, callback)
|
||||||
this.syncDisposers.push(() => ipcMain.off(this.syncChannel, callback));
|
this.syncDisposers.push(() => unsubscribeFromBroadcast(this.syncMainChannel, callback));
|
||||||
}
|
}
|
||||||
if (ipcRenderer) {
|
if (ipcRenderer) {
|
||||||
const callback = (event: IpcRendererEvent, model: T) => {
|
const callback = (event: IpcRendererEvent, model: T) => {
|
||||||
logger.silly(`[STORE]: SYNC ${this.name} from main`, { model });
|
logger.silly(`[STORE]: SYNC ${this.name} from main`, { model });
|
||||||
this.onSyncFromMain(model);
|
this.onSyncFromMain(model);
|
||||||
};
|
};
|
||||||
ipcRenderer.on(this.syncChannel, callback);
|
subscribeToBroadcast(this.syncRendererChannel, callback)
|
||||||
this.syncDisposers.push(() => ipcRenderer.off(this.syncChannel, callback));
|
this.syncDisposers.push(() => unsubscribeFromBroadcast(this.syncRendererChannel, callback));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,7 +108,8 @@ export class BaseStore<T = any> extends Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
unregisterIpcListener() {
|
unregisterIpcListener() {
|
||||||
ipcRenderer.removeAllListeners(this.syncChannel)
|
ipcRenderer.removeAllListeners(this.syncMainChannel)
|
||||||
|
ipcRenderer.removeAllListeners(this.syncRendererChannel)
|
||||||
}
|
}
|
||||||
|
|
||||||
disableSync() {
|
disableSync() {
|
||||||
@ -130,41 +135,10 @@ export class BaseStore<T = any> extends Singleton {
|
|||||||
protected async onModelChange(model: T) {
|
protected async onModelChange(model: T) {
|
||||||
if (ipcMain) {
|
if (ipcMain) {
|
||||||
this.saveToFile(model); // save config file
|
this.saveToFile(model); // save config file
|
||||||
this.syncToWebViews(model); // send update to renderer views
|
broadcastMessage(this.syncRendererChannel, model)
|
||||||
|
} else {
|
||||||
|
broadcastMessage(this.syncMainChannel, model)
|
||||||
}
|
}
|
||||||
// send "update-request" to main-process
|
|
||||||
if (ipcRenderer) {
|
|
||||||
ipcRenderer.send(this.syncChannel, model);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async syncToWebViews(model: T) {
|
|
||||||
const msg: IpcBroadcastParams = {
|
|
||||||
channel: this.syncChannel,
|
|
||||||
args: [model],
|
|
||||||
}
|
|
||||||
broadcastIpc(msg); // send to all windows (BrowserWindow, webContents)
|
|
||||||
const frames = await this.getSubFrames();
|
|
||||||
frames.forEach(frameId => {
|
|
||||||
// send to all sub-frames (e.g. cluster-view managed in iframe)
|
|
||||||
broadcastIpc({
|
|
||||||
...msg,
|
|
||||||
frameId: frameId,
|
|
||||||
frameOnly: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo: refactor?
|
|
||||||
protected async getSubFrames(): Promise<number[]> {
|
|
||||||
const subFrames: number[] = [];
|
|
||||||
const { clusterStore } = await import("./cluster-store");
|
|
||||||
clusterStore.clustersList.forEach(cluster => {
|
|
||||||
if (cluster.frameId) {
|
|
||||||
subFrames.push(cluster.frameId)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return subFrames;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
|||||||
@ -1,59 +1,43 @@
|
|||||||
import { createIpcChannel } from "./ipc";
|
import { handleRequest } from "./ipc";
|
||||||
import { ClusterId, clusterStore } from "./cluster-store";
|
import { ClusterId, clusterStore } from "./cluster-store";
|
||||||
import { extensionLoader } from "../extensions/extension-loader"
|
|
||||||
import { appEventBus } from "./event-bus"
|
import { appEventBus } from "./event-bus"
|
||||||
import { ResourceApplier } from "../main/resource-applier";
|
import { ResourceApplier } from "../main/resource-applier";
|
||||||
|
import { ipcMain } from "electron";
|
||||||
|
|
||||||
export const clusterIpc = {
|
if (ipcMain) {
|
||||||
activate: createIpcChannel({
|
handleRequest("cluster:activate", (event, clusterId: ClusterId, force = false) => {
|
||||||
channel: "cluster:activate",
|
const cluster = clusterStore.getById(clusterId);
|
||||||
handle: (clusterId: ClusterId, force = false) => {
|
if (cluster) {
|
||||||
const cluster = clusterStore.getById(clusterId);
|
return cluster.activate(force);
|
||||||
if (cluster) {
|
|
||||||
return cluster.activate(force);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
|
|
||||||
setFrameId: createIpcChannel({
|
|
||||||
channel: "cluster:set-frame-id",
|
|
||||||
handle: (clusterId: ClusterId, frameId?: number) => {
|
|
||||||
const cluster = clusterStore.getById(clusterId);
|
|
||||||
if (cluster) {
|
|
||||||
if (frameId) cluster.frameId = frameId; // save cluster's webFrame.routingId to be able to send push-updates
|
|
||||||
extensionLoader.broadcastExtensions(frameId)
|
|
||||||
return cluster.pushState();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
|
|
||||||
refresh: createIpcChannel({
|
|
||||||
channel: "cluster:refresh",
|
|
||||||
handle: (clusterId: ClusterId) => {
|
|
||||||
const cluster = clusterStore.getById(clusterId);
|
|
||||||
if (cluster) return cluster.refresh({ refreshMetadata: true })
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
|
|
||||||
disconnect: createIpcChannel({
|
|
||||||
channel: "cluster:disconnect",
|
|
||||||
handle: (clusterId: ClusterId) => {
|
|
||||||
appEventBus.emit({name: "cluster", action: "stop"});
|
|
||||||
return clusterStore.getById(clusterId)?.disconnect();
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
|
|
||||||
kubectlApplyAll: createIpcChannel({
|
|
||||||
channel: "cluster:kubectl-apply-all",
|
|
||||||
handle: (clusterId: ClusterId, resources: string[]) => {
|
|
||||||
appEventBus.emit({name: "cluster", action: "kubectl-apply-all"})
|
|
||||||
const cluster = clusterStore.getById(clusterId);
|
|
||||||
if (cluster) {
|
|
||||||
const applier = new ResourceApplier(cluster)
|
|
||||||
applier.kubectlApplyAll(resources)
|
|
||||||
} else {
|
|
||||||
throw `${clusterId} is not a valid cluster id`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
|
|
||||||
|
handleRequest("cluster:set-frame-id", (event, clusterId: ClusterId, frameId?: number) => {
|
||||||
|
const cluster = clusterStore.getById(clusterId);
|
||||||
|
if (cluster) {
|
||||||
|
if (frameId) cluster.frameId = frameId; // save cluster's webFrame.routingId to be able to send push-updates
|
||||||
|
return cluster.pushState();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
handleRequest("cluster:refresh", (event, clusterId: ClusterId) => {
|
||||||
|
const cluster = clusterStore.getById(clusterId);
|
||||||
|
if (cluster) return cluster.refresh({ refreshMetadata: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
handleRequest("cluster:disconnect", (event, clusterId: ClusterId) => {
|
||||||
|
appEventBus.emit({name: "cluster", action: "stop"});
|
||||||
|
return clusterStore.getById(clusterId)?.disconnect();
|
||||||
|
})
|
||||||
|
|
||||||
|
handleRequest("cluster:kubectl-apply-all", (event, clusterId: ClusterId, resources: string[]) => {
|
||||||
|
appEventBus.emit({name: "cluster", action: "kubectl-apply-all"})
|
||||||
|
const cluster = clusterStore.getById(clusterId);
|
||||||
|
if (cluster) {
|
||||||
|
const applier = new ResourceApplier(cluster)
|
||||||
|
applier.kubectlApplyAll(resources)
|
||||||
|
} else {
|
||||||
|
throw `${clusterId} is not a valid cluster id`;
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import type { WorkspaceId } from "./workspace-store";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import { app, ipcRenderer, remote, webFrame } from "electron";
|
import { app, ipcRenderer, remote, webFrame } from "electron";
|
||||||
import { unlink } from "fs-extra";
|
import { unlink } from "fs-extra";
|
||||||
import { action, computed, observable, toJS } from "mobx";
|
import { action, computed, observable, reaction, toJS } from "mobx";
|
||||||
import { BaseStore } from "./base-store";
|
import { BaseStore } from "./base-store";
|
||||||
import { Cluster, ClusterState } from "../main/cluster";
|
import { Cluster, ClusterState } from "../main/cluster";
|
||||||
import migrations from "../migrations/cluster-store"
|
import migrations from "../migrations/cluster-store"
|
||||||
@ -13,6 +13,7 @@ import { saveToAppFiles } from "./utils/saveToAppFiles";
|
|||||||
import { KubeConfig } from "@kubernetes/client-node";
|
import { KubeConfig } from "@kubernetes/client-node";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import move from "array-move";
|
import move from "array-move";
|
||||||
|
import { subscribeToBroadcast, unsubscribeAllFromBroadcast } from "./ipc";
|
||||||
|
|
||||||
export interface ClusterIconUpload {
|
export interface ClusterIconUpload {
|
||||||
clusterId: string;
|
clusterId: string;
|
||||||
@ -84,21 +85,20 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
migrations: migrations,
|
migrations: migrations,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.pushStateToViewsPeriodically()
|
this.pushStateToViewsAutomatically()
|
||||||
}
|
}
|
||||||
|
|
||||||
protected pushStateToViewsPeriodically() {
|
protected pushStateToViewsAutomatically() {
|
||||||
if (!ipcRenderer) {
|
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
|
reaction(() => this.connectedClustersList, () => {
|
||||||
setInterval(() => {
|
|
||||||
this.pushState()
|
this.pushState()
|
||||||
}, 5000)
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
registerIpcListener() {
|
registerIpcListener() {
|
||||||
logger.info(`[CLUSTER-STORE] start to listen (${webFrame.routingId})`)
|
logger.info(`[CLUSTER-STORE] start to listen (${webFrame.routingId})`)
|
||||||
ipcRenderer.on("cluster:state", (event, clusterId: string, state: ClusterState) => {
|
subscribeToBroadcast("cluster:state", (event, clusterId: string, state: ClusterState) => {
|
||||||
logger.silly(`[CLUSTER-STORE]: received push-state at ${location.host} (${webFrame.routingId})`, clusterId, state);
|
logger.silly(`[CLUSTER-STORE]: received push-state at ${location.host} (${webFrame.routingId})`, clusterId, state);
|
||||||
this.getById(clusterId)?.setState(state)
|
this.getById(clusterId)?.setState(state)
|
||||||
})
|
})
|
||||||
@ -106,7 +106,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
|
|
||||||
unregisterIpcListener() {
|
unregisterIpcListener() {
|
||||||
super.unregisterIpcListener()
|
super.unregisterIpcListener()
|
||||||
ipcRenderer.removeAllListeners("cluster:state")
|
unsubscribeAllFromBroadcast("cluster:state")
|
||||||
}
|
}
|
||||||
|
|
||||||
pushState() {
|
pushState() {
|
||||||
@ -131,6 +131,10 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
return this.getById(this.activeCluster);
|
return this.getById(this.activeCluster);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@computed get connectedClustersList(): Cluster[] {
|
||||||
|
return this.clustersList.filter((c) => !c.disconnected)
|
||||||
|
}
|
||||||
|
|
||||||
isActive(id: ClusterId) {
|
isActive(id: ClusterId) {
|
||||||
return this.activeCluster === id;
|
return this.activeCluster === id;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,79 +1,70 @@
|
|||||||
// Inter-protocol communications (main <-> renderer)
|
// Inter-process communications (main <-> renderer)
|
||||||
// https://www.electronjs.org/docs/api/ipc-main
|
// https://www.electronjs.org/docs/api/ipc-main
|
||||||
// https://www.electronjs.org/docs/api/ipc-renderer
|
// https://www.electronjs.org/docs/api/ipc-renderer
|
||||||
|
|
||||||
import { ipcMain, ipcRenderer, WebContents, webContents } from "electron"
|
import { ipcMain, ipcRenderer, webContents, remote } from "electron"
|
||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
|
|
||||||
export type IpcChannel = string;
|
export function handleRequest(channel: string, listener: (...args: any[]) => any) {
|
||||||
|
ipcMain.handle(channel, listener)
|
||||||
export interface IpcChannelOptions {
|
|
||||||
channel: IpcChannel; // main <-> renderer communication channel name
|
|
||||||
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; // one-time event
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createIpcChannel({ autoBind = true, once, timeout = 0, handle, channel }: IpcChannelOptions) {
|
export async function requestMain(channel: string, ...args: any[]) {
|
||||||
const ipcChannel = {
|
return await ipcRenderer.invoke(channel, ...args)
|
||||||
channel: channel,
|
|
||||||
handleInMain: () => {
|
|
||||||
logger.info(`[IPC]: setup channel "${channel}"`);
|
|
||||||
const ipcHandler = once ? ipcMain.handleOnce : ipcMain.handle;
|
|
||||||
ipcHandler(channel, async (event, ...args) => {
|
|
||||||
let timerId: any;
|
|
||||||
try {
|
|
||||||
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) {
|
|
||||||
throw error
|
|
||||||
} finally {
|
|
||||||
clearTimeout(timerId);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
removeHandler() {
|
|
||||||
ipcMain.removeHandler(channel);
|
|
||||||
},
|
|
||||||
invokeFromRenderer: async <T>(...args: any[]): Promise<T> => {
|
|
||||||
return ipcRenderer.invoke(channel, ...args);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if (autoBind && ipcMain) {
|
|
||||||
ipcChannel.handleInMain();
|
|
||||||
}
|
|
||||||
return ipcChannel;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IpcBroadcastParams<A extends any[] = any> {
|
async function getSubFrames(): Promise<number[]> {
|
||||||
channel: IpcChannel
|
const subFrames: number[] = [];
|
||||||
webContentId?: number; // send to single webContents view
|
const { clusterStore } = await import("./cluster-store");
|
||||||
frameId?: number; // send to inner frame of webContents
|
clusterStore.clustersList.forEach(cluster => {
|
||||||
frameOnly?: boolean; // send message only to view with provided `frameId`
|
if (cluster.frameId) {
|
||||||
filter?: (webContent: WebContents) => boolean
|
subFrames.push(cluster.frameId)
|
||||||
timeout?: number; // todo: add support
|
}
|
||||||
args?: A;
|
});
|
||||||
|
return subFrames;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function broadcastIpc({ channel, frameId, frameOnly, webContentId, filter, args = [] }: IpcBroadcastParams) {
|
export function broadcastMessage(channel: string, ...args: any[]) {
|
||||||
const singleView = webContentId ? webContents.fromId(webContentId) : null;
|
const views = (webContents || remote.webContents).getAllWebContents();
|
||||||
let views = singleView ? [singleView] : webContents.getAllWebContents();
|
|
||||||
if (filter) {
|
|
||||||
views = views.filter(filter);
|
|
||||||
}
|
|
||||||
views.forEach(webContent => {
|
views.forEach(webContent => {
|
||||||
const type = webContent.getType();
|
const type = webContent.getType();
|
||||||
logger.silly(`[IPC]: broadcasting "${channel}" to ${type}=${webContent.id}`, { args });
|
logger.silly(`[IPC]: broadcasting "${channel}" to ${type}=${webContent.id}`, { args });
|
||||||
if (!frameOnly) {
|
webContent.send(channel, ...args);
|
||||||
webContent.send(channel, ...args);
|
getSubFrames().then((frames) => {
|
||||||
}
|
frames.map((frameId) => {
|
||||||
if (frameId) {
|
webContent.sendToFrame(frameId, channel, ...args)
|
||||||
webContent.sendToFrame(frameId, channel, ...args)
|
})
|
||||||
}
|
}).catch((e) => e)
|
||||||
})
|
})
|
||||||
|
if (ipcRenderer) {
|
||||||
|
ipcRenderer.send(channel, ...args)
|
||||||
|
} else {
|
||||||
|
ipcMain.emit(channel, ...args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function subscribeToBroadcast(channel: string, listener: (...args: any[]) => any) {
|
||||||
|
if (ipcRenderer) {
|
||||||
|
ipcRenderer.on(channel, listener)
|
||||||
|
} else {
|
||||||
|
ipcMain.on(channel, listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
return listener
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unsubscribeFromBroadcast(channel: string, listener: (...args: any[]) => any) {
|
||||||
|
if (ipcRenderer) {
|
||||||
|
ipcRenderer.off(channel, listener)
|
||||||
|
} else {
|
||||||
|
ipcMain.off(channel, listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unsubscribeAllFromBroadcast(channel: string) {
|
||||||
|
if (ipcRenderer) {
|
||||||
|
ipcRenderer.removeAllListeners(channel)
|
||||||
|
} else {
|
||||||
|
ipcMain.removeAllListeners(channel)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { action, computed, observable, toJS, reaction } from "mobx";
|
|||||||
import { BaseStore } from "./base-store";
|
import { BaseStore } from "./base-store";
|
||||||
import { clusterStore } from "./cluster-store"
|
import { clusterStore } from "./cluster-store"
|
||||||
import { appEventBus } from "./event-bus";
|
import { appEventBus } from "./event-bus";
|
||||||
import { broadcastIpc } from "../common/ipc";
|
import { broadcastMessage } from "../common/ipc";
|
||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
|
|
||||||
export type WorkspaceId = string;
|
export type WorkspaceId = string;
|
||||||
@ -53,10 +53,7 @@ export class Workspace implements WorkspaceModel, WorkspaceState {
|
|||||||
|
|
||||||
pushState(state = this.getState()) {
|
pushState(state = this.getState()) {
|
||||||
logger.silly("[WORKSPACE] pushing state", {...state, id: this.id})
|
logger.silly("[WORKSPACE] pushing state", {...state, id: this.id})
|
||||||
broadcastIpc({
|
broadcastMessage("workspace:state", this.id, toJS(state))
|
||||||
channel: "workspace:state",
|
|
||||||
args: [this.id, toJS(state)],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { ResourceApplier } from "../main/resource-applier"
|
|||||||
import { Cluster } from "../main/cluster";
|
import { Cluster } from "../main/cluster";
|
||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
import { app } from "electron"
|
import { app } from "electron"
|
||||||
import { clusterIpc } from "../common/cluster-ipc"
|
import { requestMain } from "../common/ipc";
|
||||||
|
|
||||||
export interface ClusterFeatureStatus {
|
export interface ClusterFeatureStatus {
|
||||||
currentVersion: string;
|
currentVersion: string;
|
||||||
@ -39,7 +39,7 @@ export abstract class ClusterFeature {
|
|||||||
if (app) {
|
if (app) {
|
||||||
await new ResourceApplier(cluster).kubectlApplyAll(resources)
|
await new ResourceApplier(cluster).kubectlApplyAll(resources)
|
||||||
} else {
|
} else {
|
||||||
await clusterIpc.kubectlApplyAll.invokeFromRenderer(cluster.id, resources)
|
await requestMain("cluster:kubectl-apply-all", cluster.id, resources)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,10 +2,12 @@ import type { ExtensionId, ExtensionManifest, ExtensionModel, LensExtension } fr
|
|||||||
import type { LensMainExtension } from "./lens-main-extension"
|
import type { LensMainExtension } from "./lens-main-extension"
|
||||||
import type { LensRendererExtension } from "./lens-renderer-extension"
|
import type { LensRendererExtension } from "./lens-renderer-extension"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { broadcastIpc } from "../common/ipc"
|
import { broadcastMessage, subscribeToBroadcast } from "../common/ipc"
|
||||||
import { observable, reaction, toJS, } from "mobx"
|
import { observable, reaction, toJS, } from "mobx"
|
||||||
import logger from "../main/logger"
|
import logger from "../main/logger"
|
||||||
import { app, ipcRenderer, remote } from "electron"
|
import { app, ipcRenderer, remote } from "electron"
|
||||||
|
import { appEventBus } from "./core-api/event-bus"
|
||||||
|
import { clusterStore } from "./core-api/stores"
|
||||||
import {
|
import {
|
||||||
appPreferenceRegistry, clusterFeatureRegistry, clusterPageRegistry, globalPageRegistry,
|
appPreferenceRegistry, clusterFeatureRegistry, clusterPageRegistry, globalPageRegistry,
|
||||||
kubeObjectDetailRegistry, kubeObjectMenuRegistry, menuRegistry, statusBarRegistry
|
kubeObjectDetailRegistry, kubeObjectMenuRegistry, menuRegistry, statusBarRegistry
|
||||||
@ -27,13 +29,25 @@ export class ExtensionLoader {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
if (ipcRenderer) {
|
if (ipcRenderer) {
|
||||||
ipcRenderer.on("extensions:loaded", (event, extensions: InstalledExtension[]) => {
|
subscribeToBroadcast("extensions:loaded", (event, extensions: InstalledExtension[]) => {
|
||||||
extensions.forEach((ext) => {
|
extensions.forEach((ext) => {
|
||||||
if (!this.getById(ext.manifestPath)) {
|
if (!this.getById(ext.manifestPath)) {
|
||||||
this.extensions.set(ext.manifestPath, ext)
|
this.extensions.set(ext.manifestPath, ext)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
reaction(() => this.extensions.toJS(), () => {
|
||||||
|
this.broadcastExtensions()
|
||||||
|
})
|
||||||
|
appEventBus.addListener((ev) => {
|
||||||
|
if (ev.name === "app" && ev.action === "dom-ready") {
|
||||||
|
this.broadcastExtensions()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
reaction(() => clusterStore.connectedClustersList, () => {
|
||||||
|
this.broadcastExtensions()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,13 +134,8 @@ export class ExtensionLoader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
broadcastExtensions(frameId?: number) {
|
broadcastExtensions() {
|
||||||
broadcastIpc({
|
broadcastMessage("extensions:loaded", this.toJSON().extensions)
|
||||||
channel: "extensions:loaded",
|
|
||||||
frameId: frameId,
|
|
||||||
frameOnly: !!frameId,
|
|
||||||
args: [this.toJSON().extensions],
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api";
|
|||||||
import type { WorkspaceId } from "../common/workspace-store";
|
import type { WorkspaceId } from "../common/workspace-store";
|
||||||
import { action, computed, observable, reaction, toJS, when } from "mobx";
|
import { action, computed, observable, reaction, toJS, when } from "mobx";
|
||||||
import { apiKubePrefix } from "../common/vars";
|
import { apiKubePrefix } from "../common/vars";
|
||||||
import { broadcastIpc } from "../common/ipc";
|
import { broadcastMessage } from "../common/ipc";
|
||||||
import { ContextHandler } from "./context-handler"
|
import { ContextHandler } from "./context-handler"
|
||||||
import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"
|
import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"
|
||||||
import { Kubectl } from "./kubectl";
|
import { Kubectl } from "./kubectl";
|
||||||
@ -405,11 +405,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
|
|
||||||
pushState(state = this.getState()) {
|
pushState(state = this.getState()) {
|
||||||
logger.silly(`[CLUSTER]: push-state`, state);
|
logger.silly(`[CLUSTER]: push-state`, state);
|
||||||
broadcastIpc({
|
broadcastMessage("cluster:state", this.id, state)
|
||||||
channel: "cluster:state",
|
|
||||||
frameId: this.frameId,
|
|
||||||
args: [this.id, state],
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// get cluster system meta, e.g. use in "logger"
|
// get cluster system meta, e.g. use in "logger"
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { ChildProcess, spawn } from "child_process"
|
import { ChildProcess, spawn } from "child_process"
|
||||||
import { waitUntilUsed } from "tcp-port-used";
|
import { waitUntilUsed } from "tcp-port-used";
|
||||||
import { broadcastIpc } from "../common/ipc";
|
import { broadcastMessage } from "../common/ipc";
|
||||||
import type { Cluster } from "./cluster"
|
import type { Cluster } from "./cluster"
|
||||||
import { Kubectl } from "./kubectl"
|
import { Kubectl } from "./kubectl"
|
||||||
import logger from "./logger"
|
import logger from "./logger"
|
||||||
@ -88,7 +88,7 @@ export class KubeAuthProxy {
|
|||||||
protected async sendIpcLogMessage(res: KubeAuthProxyLog) {
|
protected async sendIpcLogMessage(res: KubeAuthProxyLog) {
|
||||||
const channel = `kube-auth:${this.cluster.id}`
|
const channel = `kube-auth:${this.cluster.id}`
|
||||||
logger.info(`[KUBE-AUTH]: out-channel "${channel}"`, { ...res, meta: this.cluster.getMeta() });
|
logger.info(`[KUBE-AUTH]: out-channel "${channel}"`, { ...res, meta: this.cluster.getMeta() });
|
||||||
broadcastIpc({ channel: channel, args: [res] });
|
broadcastMessage(channel, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
public exit() {
|
public exit() {
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import type { ClusterId } from "../common/cluster-store";
|
import type { ClusterId } from "../common/cluster-store";
|
||||||
import { clusterStore } from "../common/cluster-store";
|
import { clusterStore } from "../common/cluster-store";
|
||||||
import { observable } from "mobx";
|
import { observable } from "mobx";
|
||||||
import { app, BrowserWindow, dialog, ipcMain, shell, webContents } from "electron"
|
import { app, BrowserWindow, dialog, shell, webContents } from "electron"
|
||||||
import windowStateKeeper from "electron-window-state"
|
import windowStateKeeper from "electron-window-state"
|
||||||
import { extensionLoader } from "../extensions/extension-loader";
|
|
||||||
import { appEventBus } from "../common/event-bus"
|
import { appEventBus } from "../common/event-bus"
|
||||||
|
import { subscribeToBroadcast } from "../common/ipc"
|
||||||
import { initMenu } from "./menu";
|
import { initMenu } from "./menu";
|
||||||
import { initTray } from "./tray";
|
import { initTray } from "./tray";
|
||||||
|
|
||||||
@ -61,7 +61,7 @@ export class WindowManager {
|
|||||||
shell.openExternal(url);
|
shell.openExternal(url);
|
||||||
});
|
});
|
||||||
this.mainWindow.webContents.on("dom-ready", () => {
|
this.mainWindow.webContents.on("dom-ready", () => {
|
||||||
extensionLoader.broadcastExtensions()
|
appEventBus.emit({name: "app", action: "dom-ready"})
|
||||||
})
|
})
|
||||||
this.mainWindow.on("focus", () => {
|
this.mainWindow.on("focus", () => {
|
||||||
appEventBus.emit({name: "app", action: "focus"})
|
appEventBus.emit({name: "app", action: "focus"})
|
||||||
@ -98,9 +98,9 @@ export class WindowManager {
|
|||||||
|
|
||||||
protected bindEvents() {
|
protected bindEvents() {
|
||||||
// track visible cluster from ui
|
// track visible cluster from ui
|
||||||
ipcMain.on("cluster-view:current-id", (event, clusterId: ClusterId) => {
|
subscribeToBroadcast("cluster-view:current-id", (event, clusterId: ClusterId) => {
|
||||||
this.activeClusterId = clusterId;
|
this.activeClusterId = clusterId;
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async ensureMainWindow(): Promise<BrowserWindow> {
|
async ensureMainWindow(): Promise<BrowserWindow> {
|
||||||
|
|||||||
@ -12,8 +12,8 @@ import { Cluster } from "../../../main/cluster";
|
|||||||
import { ClusterIcon } from "../cluster-icon";
|
import { ClusterIcon } from "../cluster-icon";
|
||||||
import { IClusterSettingsRouteParams } from "./cluster-settings.route";
|
import { IClusterSettingsRouteParams } from "./cluster-settings.route";
|
||||||
import { clusterStore } from "../../../common/cluster-store";
|
import { clusterStore } from "../../../common/cluster-store";
|
||||||
import { clusterIpc } from "../../../common/cluster-ipc";
|
|
||||||
import { PageLayout } from "../layout/page-layout";
|
import { PageLayout } from "../layout/page-layout";
|
||||||
|
import { requestMain } from "../../../common/ipc";
|
||||||
|
|
||||||
interface Props extends RouteComponentProps<IClusterSettingsRouteParams> {
|
interface Props extends RouteComponentProps<IClusterSettingsRouteParams> {
|
||||||
}
|
}
|
||||||
@ -41,8 +41,8 @@ export class ClusterSettings extends React.Component<Props> {
|
|||||||
|
|
||||||
refreshCluster = async () => {
|
refreshCluster = async () => {
|
||||||
if (this.cluster) {
|
if (this.cluster) {
|
||||||
await clusterIpc.activate.invokeFromRenderer(this.cluster.id);
|
await requestMain("cluster:activate", this.cluster.id)
|
||||||
await clusterIpc.refresh.invokeFromRenderer(this.cluster.id);
|
await requestMain("cluster:refresh", this.cluster.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Trans } from "@lingui/macro";
|
import { Trans } from "@lingui/macro";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { clusterIpc } from "../../../../common/cluster-ipc";
|
|
||||||
import { clusterStore } from "../../../../common/cluster-store";
|
import { clusterStore } from "../../../../common/cluster-store";
|
||||||
import { Cluster } from "../../../../main/cluster";
|
import { Cluster } from "../../../../main/cluster";
|
||||||
import { autobind } from "../../../utils";
|
import { autobind } from "../../../utils";
|
||||||
|
|||||||
@ -33,12 +33,12 @@ import { ErrorBoundary } from "./error-boundary";
|
|||||||
import { Terminal } from "./dock/terminal";
|
import { Terminal } from "./dock/terminal";
|
||||||
import { getHostedCluster, getHostedClusterId } from "../../common/cluster-store";
|
import { getHostedCluster, getHostedClusterId } from "../../common/cluster-store";
|
||||||
import logger from "../../main/logger";
|
import logger from "../../main/logger";
|
||||||
import { clusterIpc } from "../../common/cluster-ipc";
|
|
||||||
import { webFrame } from "electron";
|
import { webFrame } from "electron";
|
||||||
import { clusterPageRegistry } from "../../extensions/registries/page-registry";
|
import { clusterPageRegistry } from "../../extensions/registries/page-registry";
|
||||||
import { DynamicPage } from "../../extensions/dynamic-page";
|
import { DynamicPage } from "../../extensions/dynamic-page";
|
||||||
import { extensionLoader } from "../../extensions/extension-loader";
|
import { extensionLoader } from "../../extensions/extension-loader";
|
||||||
import { appEventBus } from "../../common/event-bus"
|
import { appEventBus } from "../../common/event-bus"
|
||||||
|
import { requestMain } from "../../common/ipc";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class App extends React.Component {
|
export class App extends React.Component {
|
||||||
@ -48,7 +48,7 @@ export class App extends React.Component {
|
|||||||
logger.info(`[APP]: Init dashboard, clusterId=${clusterId}, frameId=${frameId}`)
|
logger.info(`[APP]: Init dashboard, clusterId=${clusterId}, frameId=${frameId}`)
|
||||||
await Terminal.preloadFonts()
|
await Terminal.preloadFonts()
|
||||||
|
|
||||||
await clusterIpc.setFrameId.invokeFromRenderer(clusterId, frameId);
|
await requestMain("cluster:set-frame-id", clusterId, frameId)
|
||||||
await getHostedCluster().whenReady; // cluster.activate() is done at this point
|
await getHostedCluster().whenReady; // cluster.activate() is done at this point
|
||||||
extensionLoader.loadOnClusterRenderer();
|
extensionLoader.loadOnClusterRenderer();
|
||||||
appEventBus.emit({name: "cluster", action: "open", params: {
|
appEventBus.emit({name: "cluster", action: "open", params: {
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import React from "react";
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { ipcRenderer } from "electron";
|
import { ipcRenderer } from "electron";
|
||||||
import { computed, observable } from "mobx";
|
import { computed, observable } from "mobx";
|
||||||
import { clusterIpc } from "../../../common/cluster-ipc";
|
import { requestMain, subscribeToBroadcast } from "../../../common/ipc";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
import { cssNames, IClassName } from "../../utils";
|
import { cssNames, IClassName } from "../../utils";
|
||||||
@ -32,7 +32,7 @@ export class ClusterStatus extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
ipcRenderer.on(`kube-auth:${this.cluster.id}`, (evt, res: KubeAuthProxyLog) => {
|
subscribeToBroadcast(`kube-auth:${this.cluster.id}`, (evt, res: KubeAuthProxyLog) => {
|
||||||
this.authOutput.push({
|
this.authOutput.push({
|
||||||
data: res.data.trimRight(),
|
data: res.data.trimRight(),
|
||||||
error: res.error,
|
error: res.error,
|
||||||
@ -48,7 +48,7 @@ export class ClusterStatus extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
activateCluster = async (force = false) => {
|
activateCluster = async (force = false) => {
|
||||||
await clusterIpc.activate.invokeFromRenderer(this.props.clusterId, force);
|
await requestMain("cluster:activate", this.props.clusterId, force)
|
||||||
}
|
}
|
||||||
|
|
||||||
reconnect = async () => {
|
reconnect = async () => {
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import "./clusters-menu.scss"
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { remote } from "electron"
|
import { remote } from "electron"
|
||||||
|
import { requestMain } from "../../../common/ipc";
|
||||||
import type { Cluster } from "../../../main/cluster";
|
import type { Cluster } from "../../../main/cluster";
|
||||||
import { DragDropContext, Draggable, DraggableProvided, Droppable, DroppableProvided, DropResult } from "react-beautiful-dnd";
|
import { DragDropContext, Draggable, DraggableProvided, Droppable, DroppableProvided, DropResult } from "react-beautiful-dnd";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
@ -20,7 +21,6 @@ import { clusterSettingsURL } from "../+cluster-settings";
|
|||||||
import { landingURL } from "../+landing-page";
|
import { landingURL } from "../+landing-page";
|
||||||
import { Tooltip } from "../tooltip";
|
import { Tooltip } from "../tooltip";
|
||||||
import { ConfirmDialog } from "../confirm-dialog";
|
import { ConfirmDialog } from "../confirm-dialog";
|
||||||
import { clusterIpc } from "../../../common/cluster-ipc";
|
|
||||||
import { clusterViewURL } from "./cluster-view.route";
|
import { clusterViewURL } from "./cluster-view.route";
|
||||||
import { globalPageRegistry } from "../../../extensions/registries/page-registry";
|
import { globalPageRegistry } from "../../../extensions/registries/page-registry";
|
||||||
|
|
||||||
@ -60,7 +60,7 @@ export class ClustersMenu extends React.Component<Props> {
|
|||||||
navigate(landingURL());
|
navigate(landingURL());
|
||||||
clusterStore.setActive(null);
|
clusterStore.setActive(null);
|
||||||
}
|
}
|
||||||
await clusterIpc.disconnect.invokeFromRenderer(cluster.id);
|
await requestMain("cluster:disconnect", cluster.id)
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -72,9 +72,15 @@ export class Sidebar extends React.Component<Props> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@computed
|
||||||
|
get pageRegistryItems() {
|
||||||
|
return clusterPageRegistry.getItems()
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { toggle, isPinned, className } = this.props;
|
const { toggle, isPinned, className } = this.props;
|
||||||
const query = namespaceStore.getContextParams();
|
const query = namespaceStore.getContextParams();
|
||||||
|
const registryItems = this.pageRegistryItems
|
||||||
return (
|
return (
|
||||||
<SidebarContext.Provider value={{ pinned: isPinned }}>
|
<SidebarContext.Provider value={{ pinned: isPinned }}>
|
||||||
<div className={cssNames("Sidebar flex column", className, { pinned: isPinned })}>
|
<div className={cssNames("Sidebar flex column", className, { pinned: isPinned })}>
|
||||||
@ -184,7 +190,7 @@ export class Sidebar extends React.Component<Props> {
|
|||||||
>
|
>
|
||||||
{this.renderCustomResources()}
|
{this.renderCustomResources()}
|
||||||
</SidebarNavItem>
|
</SidebarNavItem>
|
||||||
{clusterPageRegistry.getItems().map(({ path, title, url = String(path), hideInMenu, components: { MenuIcon } }) => {
|
{registryItems.map(({ path, title, url = String(path), hideInMenu, components: { MenuIcon } }) => {
|
||||||
if (!MenuIcon || hideInMenu) {
|
if (!MenuIcon || hideInMenu) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,16 +13,17 @@ import { WhatsNew, whatsNewRoute } from "./components/+whats-new";
|
|||||||
import { Notifications } from "./components/notifications";
|
import { Notifications } from "./components/notifications";
|
||||||
import { ConfirmDialog } from "./components/confirm-dialog";
|
import { ConfirmDialog } from "./components/confirm-dialog";
|
||||||
import { extensionLoader } from "../extensions/extension-loader";
|
import { extensionLoader } from "../extensions/extension-loader";
|
||||||
|
import { broadcastMessage } from "../common/ipc";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class LensApp extends React.Component {
|
export class LensApp extends React.Component {
|
||||||
static async init() {
|
static async init() {
|
||||||
extensionLoader.loadOnClusterManagerRenderer();
|
extensionLoader.loadOnClusterManagerRenderer();
|
||||||
window.addEventListener("offline", () => {
|
window.addEventListener("offline", () => {
|
||||||
ipcRenderer.send("network:offline")
|
broadcastMessage("network:offline")
|
||||||
})
|
})
|
||||||
window.addEventListener("online", () => {
|
window.addEventListener("online", () => {
|
||||||
ipcRenderer.send("network:online")
|
broadcastMessage("network:online")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { createObservableHistory } from "mobx-observable-history";
|
|||||||
import { createBrowserHistory, createMemoryHistory, LocationDescriptor } from "history";
|
import { createBrowserHistory, createMemoryHistory, LocationDescriptor } from "history";
|
||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
import { clusterViewRoute, IClusterViewRouteParams } from "./components/cluster-manager/cluster-view.route";
|
import { clusterViewRoute, IClusterViewRouteParams } from "./components/cluster-manager/cluster-view.route";
|
||||||
|
import { broadcastMessage, subscribeToBroadcast } from "../common/ipc";
|
||||||
|
|
||||||
export const history = typeof window !== "undefined" ? createBrowserHistory() : createMemoryHistory();
|
export const history = typeof window !== "undefined" ? createBrowserHistory() : createMemoryHistory();
|
||||||
export const navigation = createObservableHistory(history);
|
export const navigation = createObservableHistory(history);
|
||||||
@ -94,19 +95,19 @@ export function getMatchedClusterId(): string {
|
|||||||
if (process.isMainFrame) {
|
if (process.isMainFrame) {
|
||||||
// Keep track of active cluster-id for handling IPC/menus/etc.
|
// Keep track of active cluster-id for handling IPC/menus/etc.
|
||||||
reaction(() => getMatchedClusterId(), clusterId => {
|
reaction(() => getMatchedClusterId(), clusterId => {
|
||||||
ipcRenderer.send("cluster-view:current-id", clusterId);
|
broadcastMessage("cluster-view:current-id", clusterId)
|
||||||
}, {
|
}, {
|
||||||
fireImmediately: true
|
fireImmediately: true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle navigation via IPC (e.g. from top menu)
|
// Handle navigation via IPC (e.g. from top menu)
|
||||||
ipcRenderer.on("menu:navigate", (event, location: LocationDescriptor) => {
|
subscribeToBroadcast("menu:navigate", (event, location: LocationDescriptor) => {
|
||||||
logger.info(`[IPC]: ${event.type} ${JSON.stringify(location)}`, event);
|
logger.info(`[IPC]: ${event.type} ${JSON.stringify(location)}`, event);
|
||||||
navigate(location);
|
navigate(location);
|
||||||
});
|
})
|
||||||
|
|
||||||
// Reload dashboard window
|
// Reload dashboard window
|
||||||
ipcRenderer.on("menu:reload", () => {
|
subscribeToBroadcast("menu:reload", () => {
|
||||||
location.reload();
|
location.reload();
|
||||||
});
|
})
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user