mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
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 <jari.kolehmainen@gmail.com> Signed-off-by: Alex Andreev <alex.andreev.email@gmail.com> Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
741973dd29
commit
a61425124f
@ -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.
|
||||
|
||||
|
||||
126
src/common/ipc/__tests__/type-enforced-ipc.test.ts
Normal file
126
src/common/ipc/__tests__/type-enforced-ipc.test.ts
Normal file
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
3
src/common/ipc/index.ts
Normal file
3
src/common/ipc/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./ipc";
|
||||
export * from "./update-available";
|
||||
export * from "./type-enforced-ipc";
|
||||
@ -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);
|
||||
}
|
||||
|
||||
71
src/common/ipc/type-enforced-ipc.ts
Normal file
71
src/common/ipc/type-enforced-ipc.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { EventEmitter } from "events";
|
||||
import logger from "../../main/logger";
|
||||
|
||||
export type HandlerEvent<EM extends EventEmitter> = Parameters<Parameters<EM["on"]>[1]>[0];
|
||||
export type ListVerifier<T extends any[]> = (args: unknown[]) => args is T;
|
||||
export type Rest<T> = 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<EM>, ...args: any[]) => any
|
||||
>({
|
||||
source,
|
||||
channel,
|
||||
listener,
|
||||
verifier,
|
||||
}: {
|
||||
source: EM,
|
||||
channel: string | symbol,
|
||||
listener: L,
|
||||
verifier: ListVerifier<Rest<Parameters<L>>>,
|
||||
}): void {
|
||||
function handler(event: HandlerEvent<EM>, ...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<EM>, ...args: any[]) => any
|
||||
>({
|
||||
source,
|
||||
channel,
|
||||
listener,
|
||||
verifier,
|
||||
}: {
|
||||
source: EM,
|
||||
channel: string | symbol,
|
||||
listener: L,
|
||||
verifier: ListVerifier<Rest<Parameters<L>>>,
|
||||
}): 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 });
|
||||
}
|
||||
});
|
||||
}
|
||||
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
|
||||
|
||||
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<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, timeout));
|
||||
}
|
||||
|
||||
@ -18,3 +18,4 @@ export * from "./openExternal";
|
||||
export * from "./downloadFile";
|
||||
export * from "./escapeRegExp";
|
||||
export * from "./tar";
|
||||
export * from "./delay";
|
||||
|
||||
@ -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();
|
||||
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;
|
||||
}
|
||||
|
||||
constructor(protected updateInterval = AppUpdater.defaultUpdateIntervalMs) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public start() {
|
||||
setInterval(AppUpdater.checkForUpdates, this.updateInterval);
|
||||
helper();
|
||||
}
|
||||
|
||||
return AppUpdater.checkForUpdates();
|
||||
export async function checkForUpdates(): Promise<void> {
|
||||
try {
|
||||
logger.info(`📡 Checking for app updates`);
|
||||
|
||||
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 { 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<WindowManager>(proxyPort);
|
||||
windowManager.whenLoaded.then(() => startUpdateChecking());
|
||||
|
||||
logger.info("🧩 Initializing extensions");
|
||||
|
||||
|
||||
@ -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();
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -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<string, Function> = {};
|
||||
|
||||
@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());
|
||||
}
|
||||
|
||||
@ -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[] = [
|
||||
|
||||
@ -21,10 +21,16 @@
|
||||
&.primary {
|
||||
background: $buttonPrimaryBackground;
|
||||
}
|
||||
|
||||
&.accent {
|
||||
background: $buttonAccentBackground;
|
||||
}
|
||||
|
||||
&.light {
|
||||
background-color: $buttonLightBackground;
|
||||
color: #505050;
|
||||
}
|
||||
|
||||
&.plain {
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
|
||||
@ -8,6 +8,7 @@ export interface ButtonProps extends ButtonHTMLAttributes<any>, 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<ButtonProps, {}> {
|
||||
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<ButtonProps>;
|
||||
const {
|
||||
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;
|
||||
|
||||
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 = (
|
||||
|
||||
@ -42,5 +42,9 @@
|
||||
box-shadow: 0 0 20px $boxShadow;
|
||||
}
|
||||
}
|
||||
|
||||
.close {
|
||||
margin-top: -2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
@ -72,23 +72,26 @@ export class Notifications extends React.Component {
|
||||
return (
|
||||
<div className="Notifications flex column align-flex-end" ref={e => this.elem = e}>
|
||||
{notifications.map(notification => {
|
||||
const { id, status } = notification;
|
||||
const { id, status, onClose } = notification;
|
||||
const msgText = this.getMessage(notification);
|
||||
|
||||
return (
|
||||
<Animate key={id}>
|
||||
<div
|
||||
className={cssNames("notification flex align-center", status)}
|
||||
className={cssNames("notification flex", status)}
|
||||
onMouseLeave={() => addAutoHideTimer(id)}
|
||||
onMouseEnter={() => removeAutoHideTimer(id)}>
|
||||
<div className="box center">
|
||||
<div className="box">
|
||||
<Icon material="info_outline"/>
|
||||
</div>
|
||||
<div className="message box grow">{msgText}</div>
|
||||
<div className="box center">
|
||||
<div className="box">
|
||||
<Icon
|
||||
material="close" className="close"
|
||||
onClick={prevDefault(() => remove(id))}
|
||||
onClick={prevDefault(() => {
|
||||
remove(id);
|
||||
onClose?.();
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
61
src/renderer/ipc/index.tsx
Normal file
61
src/renderer/ipc/index.tsx
Normal file
@ -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 <Button light label="Yes" onClick={() => sendToBackchannel(props.backchannel, props.notificationId, { doUpdate: true, now: true })} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button light label="Yes, now" onClick={() => sendToBackchannel(props.backchannel, props.notificationId, { doUpdate: true, now: true })} />
|
||||
<Button active outlined label="Yes, later" onClick={() => sendToBackchannel(props.backchannel, props.notificationId, { doUpdate: true, now: false })} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function UpdateAvailableHandler(event: IpcRendererEvent, ...[backchannel, updateInfo]: UpdateAvailableFromMain): void {
|
||||
const notificationId = uuid.v4();
|
||||
|
||||
Notifications.info(
|
||||
(
|
||||
<>
|
||||
<b>Update Available</b>
|
||||
<p>Version {updateInfo.version} of Lens IDE is now available. Would you like to update?</p>
|
||||
<div className="flex gaps row align-left box grow">
|
||||
<RenderYesButtons backchannel={backchannel} notificationId={notificationId} />
|
||||
<Button active outlined label="No" onClick={() => sendToBackchannel(backchannel, notificationId, { doUpdate: false })} />
|
||||
</div>
|
||||
</>
|
||||
), {
|
||||
id: notificationId,
|
||||
onClose() {
|
||||
sendToBackchannel(backchannel, notificationId, { doUpdate: false });
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function registerIpcHandlers() {
|
||||
onCorrect({
|
||||
source: ipcRenderer,
|
||||
channel: UpdateAvailableChannel,
|
||||
listener: UpdateAvailableHandler,
|
||||
verifier: areArgsUpdateAvailableFromMain,
|
||||
});
|
||||
}
|
||||
@ -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() {
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
"sidebarSubmenuActiveColor": "#ffffff",
|
||||
"buttonPrimaryBackground": "#3d90ce",
|
||||
"buttonDefaultBackground": "#414448",
|
||||
"buttonLightBackground": "#f1f1f1",
|
||||
"buttonAccentBackground": "#e85555",
|
||||
"buttonDisabledBackground": "#808080",
|
||||
"tableBgcStripe": "#2a2d33",
|
||||
|
||||
@ -26,6 +26,7 @@
|
||||
"sidebarBackground": "#e8e8e8",
|
||||
"buttonPrimaryBackground": "#3d90ce",
|
||||
"buttonDefaultBackground": "#414448",
|
||||
"buttonLightBackground": "#f1f1f1",
|
||||
"buttonAccentBackground": "#e85555",
|
||||
"buttonDisabledBackground": "#808080",
|
||||
"tableBgcStripe": "#f8f8f8",
|
||||
|
||||
@ -34,6 +34,7 @@ $sidebarBackground: var(--sidebarBackground);
|
||||
// Elements
|
||||
$buttonPrimaryBackground: var(--buttonPrimaryBackground);
|
||||
$buttonDefaultBackground: var(--buttonDefaultBackground);
|
||||
$buttonLightBackground: var(--buttonLightBackground);
|
||||
$buttonAccentBackground: var(--buttonAccentBackground);
|
||||
$buttonDisabledBackground: var(--buttonDisabledBackground);
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user