mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
add auto-update notifications and confirmation
Signed-off-by: Sebastian Malton <sebastian@malton.name> Show single update notification (#1985) Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> Moving notification icons to top (#1987) Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> Switch to EventEmitter (producer&consumer) model - Main now broadcasts messages whenever updates are available and waits for the first response back - Add `onCorrect` and `onceCorrect` to ipc module for typechecking ipc messages - Correctly port styling from 3.6 Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
88c490ae14
commit
b0de85d21a
@ -212,6 +212,7 @@
|
|||||||
"mobx-observable-history": "^1.0.3",
|
"mobx-observable-history": "^1.0.3",
|
||||||
"mobx-react": "^6.2.2",
|
"mobx-react": "^6.2.2",
|
||||||
"mock-fs": "^4.12.0",
|
"mock-fs": "^4.12.0",
|
||||||
|
"moment": "^2.26.0",
|
||||||
"node-pty": "^0.9.0",
|
"node-pty": "^0.9.0",
|
||||||
"npm": "^6.14.8",
|
"npm": "^6.14.8",
|
||||||
"openid-client": "^3.15.2",
|
"openid-client": "^3.15.2",
|
||||||
@ -321,7 +322,6 @@
|
|||||||
"jest-mock-extended": "^1.0.10",
|
"jest-mock-extended": "^1.0.10",
|
||||||
"make-plural": "^6.2.2",
|
"make-plural": "^6.2.2",
|
||||||
"mini-css-extract-plugin": "^0.9.0",
|
"mini-css-extract-plugin": "^0.9.0",
|
||||||
"moment": "^2.26.0",
|
|
||||||
"node-loader": "^0.6.0",
|
"node-loader": "^0.6.0",
|
||||||
"node-sass": "^4.14.1",
|
"node-sass": "^4.14.1",
|
||||||
"nodemon": "^2.0.4",
|
"nodemon": "^2.0.4",
|
||||||
|
|||||||
@ -1,85 +0,0 @@
|
|||||||
// Inter-process communications (main <-> renderer)
|
|
||||||
// https://www.electronjs.org/docs/api/ipc-main
|
|
||||||
// https://www.electronjs.org/docs/api/ipc-renderer
|
|
||||||
|
|
||||||
import { ipcMain, ipcRenderer, webContents, remote } from "electron";
|
|
||||||
import { toJS } from "mobx";
|
|
||||||
import logger from "../main/logger";
|
|
||||||
import { ClusterFrameInfo, clusterFrameMap } from "./cluster-frames";
|
|
||||||
|
|
||||||
const subFramesChannel = "ipc:get-sub-frames";
|
|
||||||
|
|
||||||
export function handleRequest(channel: string, listener: (...args: any[]) => any) {
|
|
||||||
ipcMain.handle(channel, listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function requestMain(channel: string, ...args: any[]) {
|
|
||||||
return ipcRenderer.invoke(channel, ...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSubFrames(): ClusterFrameInfo[] {
|
|
||||||
return toJS(Array.from(clusterFrameMap.values()), { recurseEverything: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function broadcastMessage(channel: string, ...args: any[]) {
|
|
||||||
const views = (webContents || remote?.webContents)?.getAllWebContents();
|
|
||||||
|
|
||||||
if (!views) return;
|
|
||||||
|
|
||||||
if (ipcRenderer) {
|
|
||||||
ipcRenderer.send(channel, ...args);
|
|
||||||
} else {
|
|
||||||
ipcMain.emit(channel, ...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const view of views) {
|
|
||||||
const type = view.getType();
|
|
||||||
|
|
||||||
logger.silly(`[IPC]: broadcasting "${channel}" to ${type}=${view.id}`, { args });
|
|
||||||
view.send(channel, ...args);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const subFrames: ClusterFrameInfo[] = ipcRenderer
|
|
||||||
? await requestMain(subFramesChannel)
|
|
||||||
: getSubFrames();
|
|
||||||
|
|
||||||
for (const frameInfo of subFrames) {
|
|
||||||
view.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...args);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("[IPC]: failed to send IPC message", { error });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function bindBroadcastHandlers() {
|
|
||||||
handleRequest(subFramesChannel, () => {
|
|
||||||
return getSubFrames();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
2
src/common/ipc/index.ts
Normal file
2
src/common/ipc/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./ipc";
|
||||||
|
export * from "./update-available";
|
||||||
146
src/common/ipc/ipc.ts
Normal file
146
src/common/ipc/ipc.ts
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
// Inter-process communications (main <-> renderer)
|
||||||
|
// https://www.electronjs.org/docs/api/ipc-main
|
||||||
|
// https://www.electronjs.org/docs/api/ipc-renderer
|
||||||
|
|
||||||
|
import { ipcMain, ipcRenderer, webContents, remote } from "electron";
|
||||||
|
import { toJS } from "mobx";
|
||||||
|
import { EventEmitter } from "ws";
|
||||||
|
import logger from "../../main/logger";
|
||||||
|
import { ClusterFrameInfo, clusterFrameMap } from "../cluster-frames";
|
||||||
|
|
||||||
|
const subFramesChannel = "ipc:get-sub-frames";
|
||||||
|
|
||||||
|
export type HandlerEvent<EM extends EventEmitter> = Parameters<Parameters<EM["on"]>[1]>[0];
|
||||||
|
export type EventListener<E extends EventEmitter, T extends any[]> = (event: HandlerEvent<E>, ...args: T) => any;
|
||||||
|
|
||||||
|
export function handleRequest(channel: string, listener: (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any) {
|
||||||
|
ipcMain.handle(channel, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function requestMain(channel: string, ...args: any[]) {
|
||||||
|
return ipcRenderer.invoke(channel, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a listener to `source` that waits for the first IPC message with the correct
|
||||||
|
* argument data is sent.
|
||||||
|
* @param channel The channel to be listened on
|
||||||
|
* @param listener The function for the channel to be called if the args of the correct type
|
||||||
|
* @param verifier The function to be called to verify that the args are the correct type
|
||||||
|
*/
|
||||||
|
export function onceCorrect<
|
||||||
|
EM extends EventEmitter,
|
||||||
|
T extends any[],
|
||||||
|
L extends (event: HandlerEvent<EM>, ...args: T) => any
|
||||||
|
>(
|
||||||
|
source: EM,
|
||||||
|
channel: string | symbol,
|
||||||
|
listener: L,
|
||||||
|
verifier: (args: unknown[]) => args is T
|
||||||
|
): void {
|
||||||
|
function handler(event: HandlerEvent<EM>, ...args: unknown[]): void {
|
||||||
|
if (verifier(args)) {
|
||||||
|
source.removeListener(channel, handler); // remove immediately
|
||||||
|
|
||||||
|
Promise.resolve(listener(event, ...args)) // might return a promise
|
||||||
|
.catch(error => logger.error("[IPC]: channel once handler threw error", { channel, error }));
|
||||||
|
} else {
|
||||||
|
logger.error("[IPC]: channel was sent to with invalid data", { channel, args });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
source.on(channel, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a listener to `source` that checks to verify the arguments before calling the handler.
|
||||||
|
* @param channel The channel to be listened on
|
||||||
|
* @param listener The function for the channel to be called if the args of the correct type
|
||||||
|
* @param verifier The function to be called to verify that the args are the correct type
|
||||||
|
*/
|
||||||
|
export function onCorrect<
|
||||||
|
EM extends EventEmitter,
|
||||||
|
T extends any[],
|
||||||
|
L extends (event: HandlerEvent<EM>, ...args: T) => any
|
||||||
|
>(
|
||||||
|
source: EM,
|
||||||
|
channel: string | symbol,
|
||||||
|
listener: L,
|
||||||
|
verifier: (args: unknown[]) => args is T
|
||||||
|
): void {
|
||||||
|
source.on(channel, (event, ...args: unknown[]) => {
|
||||||
|
if (verifier(args)) {
|
||||||
|
Promise.resolve(listener(event, ...args)) // might return a promise
|
||||||
|
.catch(error => logger.error("[IPC]: channel on handler threw error", { channel, error }));
|
||||||
|
} else {
|
||||||
|
logger.error("[IPC]: channel was sent to with invalid data", { channel, args });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSubFrames(): ClusterFrameInfo[] {
|
||||||
|
return toJS(Array.from(clusterFrameMap.values()), { recurseEverything: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function broadcastMessage(channel: string, ...args: any[]) {
|
||||||
|
const views = (webContents || remote?.webContents)?.getAllWebContents();
|
||||||
|
|
||||||
|
if (!views) return;
|
||||||
|
|
||||||
|
if (ipcRenderer) {
|
||||||
|
ipcRenderer.send(channel, ...args);
|
||||||
|
} else {
|
||||||
|
ipcMain.emit(channel, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const view of views) {
|
||||||
|
const type = view.getType();
|
||||||
|
|
||||||
|
logger.silly(`[IPC]: broadcasting "${channel}" to ${type}=${view.id}`, { args });
|
||||||
|
view.send(channel, ...args);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const subFrames: ClusterFrameInfo[] = ipcRenderer
|
||||||
|
? await requestMain(subFramesChannel)
|
||||||
|
: getSubFrames();
|
||||||
|
|
||||||
|
for (const frameInfo of subFrames) {
|
||||||
|
view.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...args);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("[IPC]: failed to send IPC message", { error });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bindBroadcastHandlers() {
|
||||||
|
handleRequest(subFramesChannel, () => {
|
||||||
|
return getSubFrames();
|
||||||
|
});
|
||||||
|
}
|
||||||
48
src/common/ipc/update-available/index.ts
Normal file
48
src/common/ipc/update-available/index.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { UpdateInfo } from "electron-updater";
|
||||||
|
|
||||||
|
export const UpdateAvailableChannel = "update-available";
|
||||||
|
export const AutoUpdateLogPrefix = "[UPDATE-CHECKER]";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [<back-channel>, <update-info>]
|
||||||
|
*/
|
||||||
|
export type UpdateAvailableFromMain = [string, UpdateInfo];
|
||||||
|
|
||||||
|
export function areArgsUpdateAvailableFromMain(args: unknown[]): args is UpdateAvailableFromMain {
|
||||||
|
if (args.length !== 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof args[0] !== "string") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof args[1] !== "object" || args[1] === null) {
|
||||||
|
// TODO: improve this checking
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BackchannelArg = {
|
||||||
|
doUpdate: false;
|
||||||
|
} | {
|
||||||
|
doUpdate: true;
|
||||||
|
now: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateAvailableToBackchannel = [BackchannelArg];
|
||||||
|
|
||||||
|
export function areArgsUpdateAvailableToBackchannel(args: unknown[]): args is UpdateAvailableToBackchannel {
|
||||||
|
if (args.length !== 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof args[0] !== "object" || args[0] === null) {
|
||||||
|
// TODO: improve this checking
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@ -1,6 +1,8 @@
|
|||||||
// Create async delay for provided timeout in milliseconds
|
/**
|
||||||
|
* Return a promise that will be resolved after at least `timeout` ms have
|
||||||
export async function delay(timeoutMs = 1000) {
|
* passed
|
||||||
if (!timeoutMs) return;
|
* @param timeout The number of milliseconds before resolving
|
||||||
await new Promise(resolve => setTimeout(resolve, timeoutMs));
|
*/
|
||||||
|
export function delay(timeout = 1000): Promise<void> {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, timeout));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,3 +18,4 @@ export * from "./openExternal";
|
|||||||
export * from "./downloadFile";
|
export * from "./downloadFile";
|
||||||
export * from "./escapeRegExp";
|
export * from "./escapeRegExp";
|
||||||
export * from "./tar";
|
export * from "./tar";
|
||||||
|
export * from "./delay";
|
||||||
|
|||||||
@ -1,20 +1,71 @@
|
|||||||
import { autoUpdater } from "electron-updater";
|
import { autoUpdater, UpdateInfo } from "electron-updater";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
|
import { isDevelopment, isTestEnv } from "../common/vars";
|
||||||
|
import { delay } from "../common/utils";
|
||||||
|
import { areArgsUpdateAvailableToBackchannel, AutoUpdateLogPrefix, broadcastMessage, onceCorrect, UpdateAvailableChannel, UpdateAvailableToBackchannel } from "../common/ipc";
|
||||||
|
import * as uuid from "uuid";
|
||||||
|
import { ipcMain } from "electron";
|
||||||
|
|
||||||
export class AppUpdater {
|
function handleAutoUpdateBackChannel(event: Electron.IpcMainEvent, ...[arg]: UpdateAvailableToBackchannel) {
|
||||||
static readonly defaultUpdateIntervalMs = 1000 * 60 * 60 * 24; // once a day
|
if (arg.doUpdate) {
|
||||||
|
if (arg.now) {
|
||||||
static checkForUpdates() {
|
logger.info(`${AutoUpdateLogPrefix}: User chose to update now`);
|
||||||
return autoUpdater.checkForUpdatesAndNotify();
|
autoUpdater.downloadUpdate()
|
||||||
}
|
.then(() => autoUpdater.quitAndInstall())
|
||||||
|
.catch(error => logger.error(`${AutoUpdateLogPrefix}: Failed to download or install update`, { error }));
|
||||||
constructor(protected updateInterval = AppUpdater.defaultUpdateIntervalMs) {
|
} else {
|
||||||
autoUpdater.logger = logger;
|
logger.info(`${AutoUpdateLogPrefix}: User chose to update on quit`);
|
||||||
}
|
autoUpdater.autoInstallOnAppQuit = true;
|
||||||
|
autoUpdater.downloadUpdate()
|
||||||
public start() {
|
.catch(error => logger.error(`${AutoUpdateLogPrefix}: Failed to download update`, { error }));
|
||||||
setInterval(AppUpdater.checkForUpdates, this.updateInterval);
|
}
|
||||||
|
} else {
|
||||||
return AppUpdater.checkForUpdates();
|
logger.info(`${AutoUpdateLogPrefix}: User chose not to update`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* starts the automatic update checking
|
||||||
|
* @param interval milliseconds between interval to check on, defaults to 24h
|
||||||
|
*/
|
||||||
|
export function startUpdateChecking(interval = 1000 * 60 * 60 * 24): void {
|
||||||
|
if (isDevelopment || isTestEnv) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
autoUpdater.logger = logger;
|
||||||
|
autoUpdater.autoDownload = false;
|
||||||
|
autoUpdater.autoInstallOnAppQuit = false;
|
||||||
|
|
||||||
|
autoUpdater
|
||||||
|
.on("update-available", (args: UpdateInfo) => {
|
||||||
|
try {
|
||||||
|
// use a UUID so that this back-channel is harder to discover
|
||||||
|
const backchannel = uuid.v4();
|
||||||
|
|
||||||
|
// make sure that the handler is in place before broadcasting (prevent race-condition)
|
||||||
|
onceCorrect(ipcMain, backchannel, handleAutoUpdateBackChannel, areArgsUpdateAvailableToBackchannel);
|
||||||
|
logger.info(`${AutoUpdateLogPrefix}: broadcasting update available`, { backchannel, version: args.version });
|
||||||
|
broadcastMessage(UpdateAvailableChannel, backchannel, args);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`${AutoUpdateLogPrefix}: broadcasting failed`, { error });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function helper() {
|
||||||
|
while (true) {
|
||||||
|
await checkForUpdates();
|
||||||
|
await delay(interval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
helper();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkForUpdates(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await autoUpdater.checkForUpdates();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`${AutoUpdateLogPrefix}: failed with an error`, { error: String(error) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import path from "path";
|
|||||||
import { LensProxy } from "./lens-proxy";
|
import { LensProxy } from "./lens-proxy";
|
||||||
import { WindowManager } from "./window-manager";
|
import { WindowManager } from "./window-manager";
|
||||||
import { ClusterManager } from "./cluster-manager";
|
import { ClusterManager } from "./cluster-manager";
|
||||||
import { AppUpdater } from "./app-updater";
|
|
||||||
import { shellSync } from "./shell-sync";
|
import { shellSync } from "./shell-sync";
|
||||||
import { getFreePort } from "./port";
|
import { getFreePort } from "./port";
|
||||||
import { mangleProxyEnv } from "./proxy-env";
|
import { mangleProxyEnv } from "./proxy-env";
|
||||||
@ -27,6 +26,7 @@ import type { LensExtensionId } from "../extensions/lens-extension";
|
|||||||
import { installDeveloperTools } from "./developer-tools";
|
import { installDeveloperTools } from "./developer-tools";
|
||||||
import { filesystemProvisionerStore } from "./extension-filesystem";
|
import { filesystemProvisionerStore } from "./extension-filesystem";
|
||||||
import { bindBroadcastHandlers } from "../common/ipc";
|
import { bindBroadcastHandlers } from "../common/ipc";
|
||||||
|
import { startUpdateChecking } from "./app-updater";
|
||||||
|
|
||||||
const workingDir = path.join(app.getPath("appData"), appName);
|
const workingDir = path.join(app.getPath("appData"), appName);
|
||||||
let proxyPort: number;
|
let proxyPort: number;
|
||||||
@ -70,10 +70,6 @@ app.on("ready", async () => {
|
|||||||
app.exit();
|
app.exit();
|
||||||
});
|
});
|
||||||
|
|
||||||
const updater = new AppUpdater();
|
|
||||||
|
|
||||||
updater.start();
|
|
||||||
|
|
||||||
registerFileProtocol("static", __static);
|
registerFileProtocol("static", __static);
|
||||||
|
|
||||||
await installDeveloperTools();
|
await installDeveloperTools();
|
||||||
@ -112,6 +108,7 @@ app.on("ready", async () => {
|
|||||||
extensionLoader.init();
|
extensionLoader.init();
|
||||||
extensionDiscovery.init();
|
extensionDiscovery.init();
|
||||||
windowManager = WindowManager.getInstance<WindowManager>(proxyPort);
|
windowManager = WindowManager.getInstance<WindowManager>(proxyPort);
|
||||||
|
windowManager.whenLoaded.then(() => startUpdateChecking());
|
||||||
|
|
||||||
// call after windowManager to see splash earlier
|
// call after windowManager to see splash earlier
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import packageInfo from "../../package.json";
|
import packageInfo from "../../package.json";
|
||||||
import { dialog, Menu, NativeImage, Tray } from "electron";
|
import { Menu, NativeImage, Tray } from "electron";
|
||||||
import { autorun } from "mobx";
|
import { autorun } from "mobx";
|
||||||
import { showAbout } from "./menu";
|
import { showAbout } from "./menu";
|
||||||
import { AppUpdater } from "./app-updater";
|
import { checkForUpdates } from "./app-updater";
|
||||||
import { WindowManager } from "./window-manager";
|
import { WindowManager } from "./window-manager";
|
||||||
import { clusterStore } from "../common/cluster-store";
|
import { clusterStore } from "../common/cluster-store";
|
||||||
import { workspaceStore } from "../common/workspace-store";
|
import { workspaceStore } from "../common/workspace-store";
|
||||||
@ -112,16 +112,8 @@ function createTrayMenu(windowManager: WindowManager): Menu {
|
|||||||
{
|
{
|
||||||
label: "Check for updates",
|
label: "Check for updates",
|
||||||
async click() {
|
async click() {
|
||||||
const result = await AppUpdater.checkForUpdates();
|
await checkForUpdates();
|
||||||
|
await windowManager.ensureMainWindow();
|
||||||
if (!result) {
|
|
||||||
const browserWindow = await windowManager.ensureMainWindow();
|
|
||||||
|
|
||||||
dialog.showMessageBoxSync(browserWindow, {
|
|
||||||
message: "No updates available",
|
|
||||||
type: "info",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ type: "separator" },
|
{ type: "separator" },
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type { ClusterId } from "../common/cluster-store";
|
import type { ClusterId } from "../common/cluster-store";
|
||||||
import { observable } from "mobx";
|
import { observable, when } from "mobx";
|
||||||
import { app, BrowserWindow, dialog, 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 { appEventBus } from "../common/event-bus";
|
import { appEventBus } from "../common/event-bus";
|
||||||
@ -15,6 +15,9 @@ export class WindowManager extends Singleton {
|
|||||||
protected windowState: windowStateKeeper.State;
|
protected windowState: windowStateKeeper.State;
|
||||||
protected disposers: Record<string, Function> = {};
|
protected disposers: Record<string, Function> = {};
|
||||||
|
|
||||||
|
@observable mainViewInitiallyLoaded = false;
|
||||||
|
whenLoaded = when(() => this.mainViewInitiallyLoaded);
|
||||||
|
|
||||||
@observable activeClusterId: ClusterId;
|
@observable activeClusterId: ClusterId;
|
||||||
|
|
||||||
constructor(protected proxyPort: number) {
|
constructor(protected proxyPort: number) {
|
||||||
@ -91,6 +94,7 @@ export class WindowManager extends Singleton {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
appEventBus.emit({ name: "app", action: "start" });
|
appEventBus.emit({ name: "app", action: "start" });
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
this.mainViewInitiallyLoaded = true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
dialog.showErrorBox("ERROR!", err.toString());
|
dialog.showErrorBox("ERROR!", err.toString());
|
||||||
}
|
}
|
||||||
|
|||||||
3
src/renderer/components/button/button-panel.scss
Normal file
3
src/renderer/components/button/button-panel.scss
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.ButtonPannel button:not(:last-of-type) {
|
||||||
|
margin-right: $margin;
|
||||||
|
}
|
||||||
13
src/renderer/components/button/button-panel.tsx
Normal file
13
src/renderer/components/button/button-panel.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import "./button-panel.scss";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export function ButtonPannel(props: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<br />
|
||||||
|
<div className="ButtonPannel flex row align-right box grow">
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -21,10 +21,16 @@
|
|||||||
&.primary {
|
&.primary {
|
||||||
background: $buttonPrimaryBackground;
|
background: $buttonPrimaryBackground;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.accent {
|
&.accent {
|
||||||
background: $buttonAccentBackground;
|
background: $buttonAccentBackground;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.light {
|
||||||
|
background-color: white;
|
||||||
|
color: #505050;
|
||||||
|
}
|
||||||
|
|
||||||
&.plain {
|
&.plain {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@ -45,6 +51,7 @@
|
|||||||
&.outlined {
|
&.outlined {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
border: 1px solid;
|
||||||
|
|
||||||
&.active,
|
&.active,
|
||||||
&:focus {
|
&:focus {
|
||||||
|
|||||||
@ -8,6 +8,7 @@ export interface ButtonProps extends ButtonHTMLAttributes<any>, TooltipDecorator
|
|||||||
waiting?: boolean;
|
waiting?: boolean;
|
||||||
primary?: boolean;
|
primary?: boolean;
|
||||||
accent?: boolean;
|
accent?: boolean;
|
||||||
|
light?: boolean;
|
||||||
plain?: boolean;
|
plain?: boolean;
|
||||||
outlined?: boolean;
|
outlined?: boolean;
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
@ -24,13 +25,16 @@ export class Button extends React.PureComponent<ButtonProps, {}> {
|
|||||||
private button: HTMLButtonElement;
|
private button: HTMLButtonElement;
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { className, waiting, label, primary, accent, plain, hidden, active, big, round, outlined, tooltip, children, ...props } = this.props;
|
const {
|
||||||
const btnProps = props as Partial<ButtonProps>;
|
className, waiting, label, primary, accent, plain, hidden, active, big,
|
||||||
|
round, outlined, tooltip, light, children, ...props
|
||||||
|
} = this.props;
|
||||||
|
const btnProps: Partial<ButtonProps> = props;
|
||||||
|
|
||||||
if (hidden) return null;
|
if (hidden) return null;
|
||||||
|
|
||||||
btnProps.className = cssNames("Button", className, {
|
btnProps.className = cssNames("Button", className, {
|
||||||
waiting, primary, accent, plain, active, big, round, outlined
|
waiting, primary, accent, plain, active, big, round, outlined, light,
|
||||||
});
|
});
|
||||||
|
|
||||||
const btnContent: ReactNode = (
|
const btnContent: ReactNode = (
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
export * from "./button";
|
export * from "./button";
|
||||||
|
export * from "./button-panel";
|
||||||
|
|||||||
@ -42,5 +42,9 @@
|
|||||||
box-shadow: 0 0 20px $boxShadow;
|
box-shadow: 0 0 20px $boxShadow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
margin-top: -2px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,7 @@ export interface Notification {
|
|||||||
message: NotificationMessage;
|
message: NotificationMessage;
|
||||||
status?: NotificationStatus;
|
status?: NotificationStatus;
|
||||||
timeout?: number; // auto-hiding timeout in milliseconds, 0 = no hide
|
timeout?: number; // auto-hiding timeout in milliseconds, 0 = no hide
|
||||||
|
onClose?(): void; // additonal logic on when the notification times out or is closed by the "x"
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
@ -72,23 +72,26 @@ export class Notifications extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<div className="Notifications flex column align-flex-end" ref={e => this.elem = e}>
|
<div className="Notifications flex column align-flex-end" ref={e => this.elem = e}>
|
||||||
{notifications.map(notification => {
|
{notifications.map(notification => {
|
||||||
const { id, status } = notification;
|
const { id, status, onClose } = notification;
|
||||||
const msgText = this.getMessage(notification);
|
const msgText = this.getMessage(notification);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animate key={id}>
|
<Animate key={id}>
|
||||||
<div
|
<div
|
||||||
className={cssNames("notification flex align-center", status)}
|
className={cssNames("notification flex", status)}
|
||||||
onMouseLeave={() => addAutoHideTimer(id)}
|
onMouseLeave={() => addAutoHideTimer(id)}
|
||||||
onMouseEnter={() => removeAutoHideTimer(id)}>
|
onMouseEnter={() => removeAutoHideTimer(id)}>
|
||||||
<div className="box center">
|
<div className="box">
|
||||||
<Icon material="info_outline"/>
|
<Icon material="info_outline"/>
|
||||||
</div>
|
</div>
|
||||||
<div className="message box grow">{msgText}</div>
|
<div className="message box grow">{msgText}</div>
|
||||||
<div className="box center">
|
<div className="box">
|
||||||
<Icon
|
<Icon
|
||||||
material="close" className="close"
|
material="close" className="close"
|
||||||
onClick={prevDefault(() => remove(id))}
|
onClick={prevDefault(() => {
|
||||||
|
remove(id);
|
||||||
|
onClose?.();
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
57
src/renderer/ipc/index.tsx
Normal file
57
src/renderer/ipc/index.tsx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { ipcRenderer, IpcRendererEvent } from "electron";
|
||||||
|
import { areArgsUpdateAvailableFromMain, UpdateAvailableChannel, onCorrect, UpdateAvailableFromMain, BackchannelArg } from "../../common/ipc";
|
||||||
|
import { Notifications, notificationsStore } from "../components/notifications";
|
||||||
|
import { Button, ButtonPannel } from "../components/button";
|
||||||
|
import { isMac } from "../../common/vars";
|
||||||
|
import * as uuid from "uuid";
|
||||||
|
|
||||||
|
function UpdateAvailableHandler(event: IpcRendererEvent, ...[backchannel, updateInfo]: UpdateAvailableFromMain): void {
|
||||||
|
const notificationId = uuid.v4();
|
||||||
|
|
||||||
|
function sendToBackchannel(data: BackchannelArg): void {
|
||||||
|
notificationsStore.remove(notificationId);
|
||||||
|
console.log("sending to backchanel", { backchannel, data });
|
||||||
|
ipcRenderer.send(backchannel, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderYesButtons() {
|
||||||
|
if (isMac) {
|
||||||
|
/**
|
||||||
|
* auto-updater's "installOnQuit" is not applicable for macOS as per their docs.
|
||||||
|
*
|
||||||
|
* See: https://github.com/electron-userland/electron-builder/blob/master/packages/electron-updater/src/AppUpdater.ts#L27-L32
|
||||||
|
*/
|
||||||
|
return <Button light label="Yes" onClick={() => sendToBackchannel({ doUpdate: true, now: true })} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button light label="Yes, now" onClick={() => sendToBackchannel({ doUpdate: true, now: true })} />
|
||||||
|
<Button primary outlined label="Yes, later" onClick={() => sendToBackchannel({ doUpdate: true, now: false })} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Notifications.info(
|
||||||
|
(
|
||||||
|
<>
|
||||||
|
<b>Update Available</b>
|
||||||
|
<p>Version {updateInfo.version} of Lens IDE is now available. Would you like to update?</p>
|
||||||
|
<ButtonPannel>
|
||||||
|
{renderYesButtons()}
|
||||||
|
<Button primary outlined label="No" onClick={() => sendToBackchannel({ doUpdate: false })} />
|
||||||
|
</ButtonPannel>
|
||||||
|
</>
|
||||||
|
), {
|
||||||
|
id: notificationId,
|
||||||
|
onClose() {
|
||||||
|
sendToBackchannel({ doUpdate: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerIpcHandlers() {
|
||||||
|
onCorrect(ipcRenderer, UpdateAvailableChannel, UpdateAvailableHandler, areArgsUpdateAvailableFromMain);
|
||||||
|
}
|
||||||
@ -12,6 +12,7 @@ import { ConfirmDialog } from "./components/confirm-dialog";
|
|||||||
import { extensionLoader } from "../extensions/extension-loader";
|
import { extensionLoader } from "../extensions/extension-loader";
|
||||||
import { broadcastMessage } from "../common/ipc";
|
import { broadcastMessage } from "../common/ipc";
|
||||||
import { CommandContainer } from "./components/command-palette/command-container";
|
import { CommandContainer } from "./components/command-palette/command-container";
|
||||||
|
import { registerIpcHandlers } from "./ipc";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class LensApp extends React.Component {
|
export class LensApp extends React.Component {
|
||||||
@ -23,6 +24,8 @@ export class LensApp extends React.Component {
|
|||||||
window.addEventListener("online", () => {
|
window.addEventListener("online", () => {
|
||||||
broadcastMessage("network:online");
|
broadcastMessage("network:online");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
registerIpcHandlers();
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@ -92,10 +92,6 @@ $terminalBrightMagenta: var(--terminalBrightMagenta);
|
|||||||
$terminalBrightCyan: var(--terminalBrightCyan);
|
$terminalBrightCyan: var(--terminalBrightCyan);
|
||||||
$terminalBrightWhite: var(--terminalBrightWhite);
|
$terminalBrightWhite: var(--terminalBrightWhite);
|
||||||
|
|
||||||
// Logs
|
|
||||||
$logsBackground: var(--logsBackground);
|
|
||||||
$logRowHoverBackground: var(--logRowHoverBackground);
|
|
||||||
|
|
||||||
// Dialogs
|
// Dialogs
|
||||||
$dialogTextColor: var(--dialogTextColor);
|
$dialogTextColor: var(--dialogTextColor);
|
||||||
$dialogBackground: var(--dialogBackground);
|
$dialogBackground: var(--dialogBackground);
|
||||||
@ -131,4 +127,4 @@ $selectOptionHoveredColor: var(--selectOptionHoveredColor);
|
|||||||
$lineProgressBackground: var(--lineProgressBackground);
|
$lineProgressBackground: var(--lineProgressBackground);
|
||||||
$radioActiveBackground: var(--radioActiveBackground);
|
$radioActiveBackground: var(--radioActiveBackground);
|
||||||
$menuActiveBackground: var(--menuActiveBackground);
|
$menuActiveBackground: var(--menuActiveBackground);
|
||||||
$menuSelectedOptionBgc: var(--menuSelectedOptionBgc);
|
$menuSelectedOptionBgc: var(--menuSelectedOptionBgc);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user