From a61425124f18b1cc2d8a507084a472029acc3e6b Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Tue, 9 Feb 2021 10:47:24 -0500 Subject: [PATCH] Add auto-update notifications and confirmation (#1941) * add auto-update notifications and confirmation * Show single update notification (#1985) * Moving notification icons to top (#1987) * Switch to EventEmitter (producer&consumer) model * Add `onCorrect` and `onceCorrect` to ipc module for typechecking ipc messages * move type enforced ipc methods to seperate file, add unit tests Signed-off-by: Jari Kolehmainen Signed-off-by: Alex Andreev Signed-off-by: Sebastian Malton --- .../capabilities/color-reference.md | 1 + .../ipc/__tests__/type-enforced-ipc.test.ts | 126 ++++++++++++++++++ src/common/ipc/index.ts | 3 + src/common/{ => ipc}/ipc.ts | 6 +- src/common/ipc/type-enforced-ipc.ts | 71 ++++++++++ src/common/ipc/update-available/index.ts | 48 +++++++ src/common/utils/delay.ts | 12 +- src/common/utils/index.ts | 1 + src/main/app-updater.ts | 90 ++++++++++--- src/main/index.ts | 8 +- src/main/tray.ts | 16 +-- src/main/window-manager.ts | 6 +- .../+workloads-overview/overview-statuses.tsx | 2 +- src/renderer/components/button/button.scss | 6 + src/renderer/components/button/button.tsx | 10 +- .../notifications/notifications.scss | 4 + ...tions.store.ts => notifications.store.tsx} | 1 + .../notifications/notifications.tsx | 13 +- src/renderer/ipc/index.tsx | 61 +++++++++ src/renderer/lens-app.tsx | 3 + src/renderer/themes/lens-dark.json | 1 + src/renderer/themes/lens-light.json | 1 + src/renderer/themes/theme-vars.scss | 3 +- 23 files changed, 440 insertions(+), 53 deletions(-) create mode 100644 src/common/ipc/__tests__/type-enforced-ipc.test.ts create mode 100644 src/common/ipc/index.ts rename src/common/{ => ipc}/ipc.ts (90%) create mode 100644 src/common/ipc/type-enforced-ipc.ts create mode 100644 src/common/ipc/update-available/index.ts rename src/renderer/components/notifications/{notifications.store.ts => notifications.store.tsx} (95%) create mode 100644 src/renderer/ipc/index.tsx diff --git a/docs/extensions/capabilities/color-reference.md b/docs/extensions/capabilities/color-reference.md index 660e0fe067..6a38ba861c 100644 --- a/docs/extensions/capabilities/color-reference.md +++ b/docs/extensions/capabilities/color-reference.md @@ -43,6 +43,7 @@ You can use theme-based CSS Variables to style an extension according to the act ## Button Colors - `--buttonPrimaryBackground`: button background color for primary actions. - `--buttonDefaultBackground`: default button background color. +- `--buttonLightBackground`: light button background color. - `--buttonAccentBackground`: accent button background color. - `--buttonDisabledBackground`: disabled button background color. diff --git a/src/common/ipc/__tests__/type-enforced-ipc.test.ts b/src/common/ipc/__tests__/type-enforced-ipc.test.ts new file mode 100644 index 0000000000..4e20249392 --- /dev/null +++ b/src/common/ipc/__tests__/type-enforced-ipc.test.ts @@ -0,0 +1,126 @@ +import { EventEmitter } from "events"; +import { onCorrect, onceCorrect } from "../type-enforced-ipc"; + +describe("type enforced ipc tests", () => { + describe("onCorrect tests", () => { + it("should call the handler if the args are valid", () => { + let called = false; + const source = new EventEmitter(); + const listener = () => called = true; + const verifier = (args: unknown[]): args is [] => true; + const channel = "foobar"; + + onCorrect({ source, listener, verifier, channel }); + + source.emit(channel); + expect(called).toBe(true); + }); + + it("should not call the handler if the args are not valid", () => { + let called = false; + const source = new EventEmitter(); + const listener = () => called = true; + const verifier = (args: unknown[]): args is [] => false; + const channel = "foobar"; + + onCorrect({ source, listener, verifier, channel }); + + source.emit(channel); + expect(called).toBe(false); + }); + + it("should call the handler twice if the args are valid on two emits", () => { + let called = 0; + const source = new EventEmitter(); + const listener = () => called += 1; + const verifier = (args: unknown[]): args is [] => true; + const channel = "foobar"; + + onCorrect({ source, listener, verifier, channel }); + + source.emit(channel); + source.emit(channel); + expect(called).toBe(2); + }); + + it("should call the handler twice if the args are [valid, invalid, valid]", () => { + let called = 0; + const source = new EventEmitter(); + const listener = () => called += 1; + const results = [true, false, true]; + const verifier = (args: unknown[]): args is [] => results.pop(); + const channel = "foobar"; + + onCorrect({ source, listener, verifier, channel }); + + source.emit(channel); + source.emit(channel); + source.emit(channel); + expect(called).toBe(2); + }); + }); + + describe("onceCorrect tests", () => { + it("should call the handler if the args are valid", () => { + let called = false; + const source = new EventEmitter(); + const listener = () => called = true; + const verifier = (args: unknown[]): args is [] => true; + const channel = "foobar"; + + onceCorrect({ source, listener, verifier, channel }); + + source.emit(channel); + expect(called).toBe(true); + }); + + it("should not call the handler if the args are not valid", () => { + let called = false; + const source = new EventEmitter(); + const listener = () => called = true; + const verifier = (args: unknown[]): args is [] => false; + const channel = "foobar"; + + onceCorrect({ source, listener, verifier, channel }); + + source.emit(channel); + expect(called).toBe(false); + }); + + it("should call the handler only once even if args are valid multiple times", () => { + let called = 0; + const source = new EventEmitter(); + const listener = () => called += 1; + const verifier = (args: unknown[]): args is [] => true; + const channel = "foobar"; + + onceCorrect({ source, listener, verifier, channel }); + + source.emit(channel); + source.emit(channel); + expect(called).toBe(1); + }); + + it("should call the handler on only the first valid set of args", () => { + let called = ""; + let verifierCalled = 0; + const source = new EventEmitter(); + const listener = (info: any, arg: string) => called = arg; + const verifier = (args: unknown[]): args is [string] => (++verifierCalled) % 3 === 0; + const channel = "foobar"; + + onceCorrect({ source, listener, verifier, channel }); + + source.emit(channel, {}, "a"); + source.emit(channel, {}, "b"); + source.emit(channel, {}, "c"); + source.emit(channel, {}, "d"); + source.emit(channel, {}, "e"); + source.emit(channel, {}, "f"); + source.emit(channel, {}, "g"); + source.emit(channel, {}, "h"); + source.emit(channel, {}, "i"); + expect(called).toBe("c"); + }); + }); +}); diff --git a/src/common/ipc/index.ts b/src/common/ipc/index.ts new file mode 100644 index 0000000000..a34890472e --- /dev/null +++ b/src/common/ipc/index.ts @@ -0,0 +1,3 @@ +export * from "./ipc"; +export * from "./update-available"; +export * from "./type-enforced-ipc"; diff --git a/src/common/ipc.ts b/src/common/ipc/ipc.ts similarity index 90% rename from src/common/ipc.ts rename to src/common/ipc/ipc.ts index c2f8562cf7..48b0b89153 100644 --- a/src/common/ipc.ts +++ b/src/common/ipc/ipc.ts @@ -4,12 +4,12 @@ import { ipcMain, ipcRenderer, webContents, remote } from "electron"; import { toJS } from "mobx"; -import logger from "../main/logger"; -import { ClusterFrameInfo, clusterFrameMap } from "./cluster-frames"; +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) { +export function handleRequest(channel: string, listener: (event: Electron.IpcMainInvokeEvent, ...args: any[]) => any) { ipcMain.handle(channel, listener); } diff --git a/src/common/ipc/type-enforced-ipc.ts b/src/common/ipc/type-enforced-ipc.ts new file mode 100644 index 0000000000..be54992008 --- /dev/null +++ b/src/common/ipc/type-enforced-ipc.ts @@ -0,0 +1,71 @@ +import { EventEmitter } from "events"; +import logger from "../../main/logger"; + +export type HandlerEvent = Parameters[1]>[0]; +export type ListVerifier = (args: unknown[]) => args is T; +export type Rest = T extends [any, ...infer R] ? R : []; + +/** + * 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, + L extends (event: HandlerEvent, ...args: any[]) => any +>({ + source, + channel, + listener, + verifier, +}: { + source: EM, + channel: string | symbol, + listener: L, + verifier: ListVerifier>>, +}): void { + function handler(event: HandlerEvent, ...args: unknown[]): void { + if (verifier(args)) { + source.removeListener(channel, handler); // remove immediately + + (async () => (listener(event, ...args)))() // might return a promise, or throw, or reject + .catch((error: any) => logger.error("[IPC]: channel once handler threw error", { channel, error })); + } else { + logger.error("[IPC]: channel was emitted 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, + L extends (event: HandlerEvent, ...args: any[]) => any +>({ + source, + channel, + listener, + verifier, +}: { + source: EM, + channel: string | symbol, + listener: L, + verifier: ListVerifier>>, +}): void { + source.on(channel, (event, ...args: unknown[]) => { + if (verifier(args)) { + (async () => (listener(event, ...args)))() // might return a promise, or throw, or reject + .catch(error => logger.error("[IPC]: channel on handler threw error", { channel, error })); + } else { + logger.error("[IPC]: channel was emitted with invalid data", { channel, args }); + } + }); +} diff --git a/src/common/ipc/update-available/index.ts b/src/common/ipc/update-available/index.ts new file mode 100644 index 0000000000..1e3fcf1268 --- /dev/null +++ b/src/common/ipc/update-available/index.ts @@ -0,0 +1,48 @@ +import { UpdateInfo } from "electron-updater"; + +export const UpdateAvailableChannel = "update-available"; +export const AutoUpdateLogPrefix = "[UPDATE-CHECKER]"; + +/** + * [, ] + */ +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; +} diff --git a/src/common/utils/delay.ts b/src/common/utils/delay.ts index 208e042759..7d0686d29b 100644 --- a/src/common/utils/delay.ts +++ b/src/common/utils/delay.ts @@ -1,6 +1,8 @@ -// Create async delay for provided timeout in milliseconds - -export async function delay(timeoutMs = 1000) { - if (!timeoutMs) return; - await new Promise(resolve => setTimeout(resolve, timeoutMs)); +/** + * Return a promise that will be resolved after at least `timeout` ms have + * passed + * @param timeout The number of milliseconds before resolving + */ +export function delay(timeout = 1000): Promise { + return new Promise(resolve => setTimeout(resolve, timeout)); } diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index 942c675f0a..2b8147fad9 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -18,3 +18,4 @@ export * from "./openExternal"; export * from "./downloadFile"; export * from "./escapeRegExp"; export * from "./tar"; +export * from "./delay"; diff --git a/src/main/app-updater.ts b/src/main/app-updater.ts index dd9ed97e69..618f714b49 100644 --- a/src/main/app-updater.ts +++ b/src/main/app-updater.ts @@ -1,20 +1,78 @@ -import { autoUpdater } from "electron-updater"; +import { autoUpdater, UpdateInfo } from "electron-updater"; 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 { ipcMain } from "electron"; -export class AppUpdater { - static readonly defaultUpdateIntervalMs = 1000 * 60 * 60 * 24; // once a day - - static checkForUpdates() { - return autoUpdater.checkForUpdatesAndNotify(); - } - - constructor(protected updateInterval = AppUpdater.defaultUpdateIntervalMs) { - autoUpdater.logger = logger; - } - - public start() { - setInterval(AppUpdater.checkForUpdates, this.updateInterval); - - return AppUpdater.checkForUpdates(); +function handleAutoUpdateBackChannel(event: Electron.IpcMainEvent, ...[arg]: UpdateAvailableToBackchannel) { + if (arg.doUpdate) { + if (arg.now) { + logger.info(`${AutoUpdateLogPrefix}: User chose to update now`); + autoUpdater.downloadUpdate() + .then(() => autoUpdater.quitAndInstall()) + .catch(error => logger.error(`${AutoUpdateLogPrefix}: Failed to download or install update`, { error })); + } else { + logger.info(`${AutoUpdateLogPrefix}: User chose to update on quit`); + autoUpdater.autoInstallOnAppQuit = true; + autoUpdater.downloadUpdate() + .catch(error => logger.error(`${AutoUpdateLogPrefix}: Failed to download update`, { error })); + } + } else { + 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 { + const backchannel = `auto-update:${args.version}`; + + ipcMain.removeAllListeners(backchannel); // only one handler should be present + + // make sure that the handler is in place before broadcasting (prevent race-condition) + onceCorrect({ + source: ipcMain, + channel: backchannel, + listener: handleAutoUpdateBackChannel, + verifier: 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 { + try { + logger.info(`📡 Checking for app updates`); + + await autoUpdater.checkForUpdates(); + } catch (error) { + logger.error(`${AutoUpdateLogPrefix}: failed with an error`, { error: String(error) }); } } diff --git a/src/main/index.ts b/src/main/index.ts index af24026a5e..50571a8862 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -10,7 +10,6 @@ import path from "path"; import { LensProxy } from "./lens-proxy"; import { WindowManager } from "./window-manager"; import { ClusterManager } from "./cluster-manager"; -import { AppUpdater } from "./app-updater"; import { shellSync } from "./shell-sync"; import { getFreePort } from "./port"; import { mangleProxyEnv } from "./proxy-env"; @@ -28,6 +27,7 @@ import { installDeveloperTools } from "./developer-tools"; import { filesystemProvisionerStore } from "./extension-filesystem"; import { getAppVersion, getAppVersionFromProxyServer } from "../common/utils"; import { bindBroadcastHandlers } from "../common/ipc"; +import { startUpdateChecking } from "./app-updater"; const workingDir = path.join(app.getPath("appData"), appName); let proxyPort: number; @@ -72,11 +72,6 @@ app.on("ready", async () => { app.exit(); }); - logger.info(`📡 Checking for app updates`); - const updater = new AppUpdater(); - - updater.start(); - registerFileProtocol("static", __static); await installDeveloperTools(); @@ -133,6 +128,7 @@ app.on("ready", async () => { logger.info("🖥️ Starting WindowManager"); windowManager = WindowManager.getInstance(proxyPort); + windowManager.whenLoaded.then(() => startUpdateChecking()); logger.info("🧩 Initializing extensions"); diff --git a/src/main/tray.ts b/src/main/tray.ts index 44a22d27bf..3d6d2dd624 100644 --- a/src/main/tray.ts +++ b/src/main/tray.ts @@ -1,9 +1,9 @@ import path from "path"; import packageInfo from "../../package.json"; -import { dialog, Menu, NativeImage, Tray } from "electron"; +import { Menu, NativeImage, Tray } from "electron"; import { autorun } from "mobx"; import { showAbout } from "./menu"; -import { AppUpdater } from "./app-updater"; +import { checkForUpdates } from "./app-updater"; import { WindowManager } from "./window-manager"; import { clusterStore } from "../common/cluster-store"; import { workspaceStore } from "../common/workspace-store"; @@ -102,16 +102,8 @@ function createTrayMenu(windowManager: WindowManager): Menu { { label: "Check for updates", async click() { - const result = await AppUpdater.checkForUpdates(); - - if (!result) { - const browserWindow = await windowManager.ensureMainWindow(); - - dialog.showMessageBoxSync(browserWindow, { - message: "No updates available", - type: "info", - }); - } + await checkForUpdates(); + await windowManager.ensureMainWindow(); }, }, { diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts index 691aa7c66b..c092e186cb 100644 --- a/src/main/window-manager.ts +++ b/src/main/window-manager.ts @@ -1,5 +1,5 @@ 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 windowStateKeeper from "electron-window-state"; import { appEventBus } from "../common/event-bus"; @@ -16,6 +16,9 @@ export class WindowManager extends Singleton { protected windowState: windowStateKeeper.State; protected disposers: Record = {}; + @observable mainViewInitiallyLoaded = false; + whenLoaded = when(() => this.mainViewInitiallyLoaded); + @observable activeClusterId: ClusterId; constructor(protected proxyPort: number) { @@ -101,6 +104,7 @@ export class WindowManager extends Singleton { setTimeout(() => { appEventBus.emit({ name: "app", action: "start" }); }, 1000); + this.mainViewInitiallyLoaded = true; } catch (err) { dialog.showErrorBox("ERROR!", err.toString()); } diff --git a/src/renderer/components/+workloads-overview/overview-statuses.tsx b/src/renderer/components/+workloads-overview/overview-statuses.tsx index 6274a94f30..1441115ed8 100644 --- a/src/renderer/components/+workloads-overview/overview-statuses.tsx +++ b/src/renderer/components/+workloads-overview/overview-statuses.tsx @@ -8,7 +8,7 @@ import { workloadURL, workloadStores } from "../+workloads"; import { namespaceStore } from "../+namespaces/namespace.store"; import { NamespaceSelectFilter } from "../+namespaces/namespace-select"; import { isAllowedResource, KubeResource } from "../../../common/rbac"; -import { ResourceNames } from "../../../renderer/utils/rbac"; +import { ResourceNames } from "../../utils/rbac"; import { autobind } from "../../utils"; const resources: KubeResource[] = [ diff --git a/src/renderer/components/button/button.scss b/src/renderer/components/button/button.scss index b850a3be59..9224b7e5d3 100644 --- a/src/renderer/components/button/button.scss +++ b/src/renderer/components/button/button.scss @@ -21,10 +21,16 @@ &.primary { background: $buttonPrimaryBackground; } + &.accent { background: $buttonAccentBackground; } + &.light { + background-color: $buttonLightBackground; + color: #505050; + } + &.plain { color: inherit; background: transparent; diff --git a/src/renderer/components/button/button.tsx b/src/renderer/components/button/button.tsx index 9fa822b214..8bcb37bad4 100644 --- a/src/renderer/components/button/button.tsx +++ b/src/renderer/components/button/button.tsx @@ -8,6 +8,7 @@ export interface ButtonProps extends ButtonHTMLAttributes, TooltipDecorator waiting?: boolean; primary?: boolean; accent?: boolean; + light?: boolean; plain?: boolean; outlined?: boolean; hidden?: boolean; @@ -24,13 +25,16 @@ export class Button extends React.PureComponent { private button: HTMLButtonElement; render() { - const { className, waiting, label, primary, accent, plain, hidden, active, big, round, outlined, tooltip, children, ...props } = this.props; - const btnProps = props as Partial; + const { + className, waiting, label, primary, accent, plain, hidden, active, big, + round, outlined, tooltip, light, children, ...props + } = this.props; + const btnProps: Partial = props; if (hidden) return null; 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 = ( diff --git a/src/renderer/components/notifications/notifications.scss b/src/renderer/components/notifications/notifications.scss index 37d4990ee5..4b300edda6 100644 --- a/src/renderer/components/notifications/notifications.scss +++ b/src/renderer/components/notifications/notifications.scss @@ -42,5 +42,9 @@ box-shadow: 0 0 20px $boxShadow; } } + + .close { + margin-top: -2px; + } } } diff --git a/src/renderer/components/notifications/notifications.store.ts b/src/renderer/components/notifications/notifications.store.tsx similarity index 95% rename from src/renderer/components/notifications/notifications.store.ts rename to src/renderer/components/notifications/notifications.store.tsx index 55549b9066..45c1eb9a6b 100644 --- a/src/renderer/components/notifications/notifications.store.ts +++ b/src/renderer/components/notifications/notifications.store.tsx @@ -18,6 +18,7 @@ export interface Notification { message: NotificationMessage; status?: NotificationStatus; 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() diff --git a/src/renderer/components/notifications/notifications.tsx b/src/renderer/components/notifications/notifications.tsx index 4ab297e289..0c1ac692cf 100644 --- a/src/renderer/components/notifications/notifications.tsx +++ b/src/renderer/components/notifications/notifications.tsx @@ -72,23 +72,26 @@ export class Notifications extends React.Component { return (
this.elem = e}> {notifications.map(notification => { - const { id, status } = notification; + const { id, status, onClose } = notification; const msgText = this.getMessage(notification); return (
addAutoHideTimer(id)} onMouseEnter={() => removeAutoHideTimer(id)}> -
+
{msgText}
-
+
remove(id))} + onClick={prevDefault(() => { + remove(id); + onClose?.(); + })} />
diff --git a/src/renderer/ipc/index.tsx b/src/renderer/ipc/index.tsx new file mode 100644 index 0000000000..991d9b7616 --- /dev/null +++ b/src/renderer/ipc/index.tsx @@ -0,0 +1,61 @@ +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 } from "../components/button"; +import { isMac } from "../../common/vars"; +import * as uuid from "uuid"; + +function sendToBackchannel(backchannel: string, notificationId: string, data: BackchannelArg): void { + notificationsStore.remove(notificationId); + ipcRenderer.send(backchannel, data); +} + +function RenderYesButtons(props: { backchannel: string, notificationId: string }) { + 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
+ + ), { + id: notificationId, + onClose() { + sendToBackchannel(backchannel, notificationId, { doUpdate: false }); + } + } + ); +} + +export function registerIpcHandlers() { + onCorrect({ + source: ipcRenderer, + channel: UpdateAvailableChannel, + listener: UpdateAvailableHandler, + verifier: areArgsUpdateAvailableFromMain, + }); +} diff --git a/src/renderer/lens-app.tsx b/src/renderer/lens-app.tsx index 5c0dd79a95..963bd43e4e 100644 --- a/src/renderer/lens-app.tsx +++ b/src/renderer/lens-app.tsx @@ -12,6 +12,7 @@ import { ConfirmDialog } from "./components/confirm-dialog"; import { extensionLoader } from "../extensions/extension-loader"; import { broadcastMessage } from "../common/ipc"; import { CommandContainer } from "./components/command-palette/command-container"; +import { registerIpcHandlers } from "./ipc"; @observer export class LensApp extends React.Component { @@ -23,6 +24,8 @@ export class LensApp extends React.Component { window.addEventListener("online", () => { broadcastMessage("network:online"); }); + + registerIpcHandlers(); } render() { diff --git a/src/renderer/themes/lens-dark.json b/src/renderer/themes/lens-dark.json index 4a7a0f2f70..0a56f851ec 100644 --- a/src/renderer/themes/lens-dark.json +++ b/src/renderer/themes/lens-dark.json @@ -25,6 +25,7 @@ "sidebarSubmenuActiveColor": "#ffffff", "buttonPrimaryBackground": "#3d90ce", "buttonDefaultBackground": "#414448", + "buttonLightBackground": "#f1f1f1", "buttonAccentBackground": "#e85555", "buttonDisabledBackground": "#808080", "tableBgcStripe": "#2a2d33", diff --git a/src/renderer/themes/lens-light.json b/src/renderer/themes/lens-light.json index f59d3c9555..e65722b889 100644 --- a/src/renderer/themes/lens-light.json +++ b/src/renderer/themes/lens-light.json @@ -26,6 +26,7 @@ "sidebarBackground": "#e8e8e8", "buttonPrimaryBackground": "#3d90ce", "buttonDefaultBackground": "#414448", + "buttonLightBackground": "#f1f1f1", "buttonAccentBackground": "#e85555", "buttonDisabledBackground": "#808080", "tableBgcStripe": "#f8f8f8", diff --git a/src/renderer/themes/theme-vars.scss b/src/renderer/themes/theme-vars.scss index 6b556bdc88..bb897818f5 100644 --- a/src/renderer/themes/theme-vars.scss +++ b/src/renderer/themes/theme-vars.scss @@ -34,6 +34,7 @@ $sidebarBackground: var(--sidebarBackground); // Elements $buttonPrimaryBackground: var(--buttonPrimaryBackground); $buttonDefaultBackground: var(--buttonDefaultBackground); +$buttonLightBackground: var(--buttonLightBackground); $buttonAccentBackground: var(--buttonAccentBackground); $buttonDisabledBackground: var(--buttonDisabledBackground); @@ -131,4 +132,4 @@ $selectOptionHoveredColor: var(--selectOptionHoveredColor); $lineProgressBackground: var(--lineProgressBackground); $radioActiveBackground: var(--radioActiveBackground); $menuActiveBackground: var(--menuActiveBackground); -$menuSelectedOptionBgc: var(--menuSelectedOptionBgc); \ No newline at end of file +$menuSelectedOptionBgc: var(--menuSelectedOptionBgc);