Introduce way to install update directly from tray. Make application updater unit testable... (#5433)

* Extract product name as injectable

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make tray items comply with Open Closed Principle

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Replace duplicated overrides with global

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Add behaviour for navigating to preferences using tray

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce a tray item for updating application

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Tweak naming

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Tweak more naming

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove redundant indirection

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Tweak more naming

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce injectable for package.json being side-effect

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Relocate file to directory containing feature

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Switch to using injectable for limiting side effect

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Add missing injection token for implementation of tray item

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove resetting state for update is ready to be installed for being unclear

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Kill dead code

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make label of tray item reactive

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Extract updating is enabled to separate injectable

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce competition for tray

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Expand scope of behaviour for updating using tray also contain checking for updates

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove dead code

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Kill dead code

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Implement checking of updates from multiple update channels

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Start installing updates automatically when quitting application

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Show application window when checking of updates has happened

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Show notifications and dialog for downloading update

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Implement naive notifications for version updates

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Implement checking of Electron specific updates as responsibility

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Implement downloading of Electron specific updates as responsibility

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce competition for channel abstraction

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove redundant global override

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Fix typing after enabling strict mode

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce abstraction for a state that is shared between environments

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Extract states of application update to be usable from all environments

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Handle failing download of update

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make code for window visibility actually work

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Consolidate code for sending messages between processes to a window

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Split bloated dependency in smaller pieces

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make state of download progress accessible from all environments

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Rename files for accuracy

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Move channel abstraction to more global directory

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Enhance typing of channels and sync-box

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Consolidate channel abstraction types

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Update asyncFn to support strict mode

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Fix snapshot after rebase

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Add missing global override

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce injection token for channels to allow injecting all of them at once

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Add notifications about change in update status

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Rename property for accuracy

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Tweak code style

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make notifications unit testable in behaviours

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Add implementation for asking boolean over processes

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Reorganize responsibilities for checking updates

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Reorganize tests for installing update under separate scenarios

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make stuff happening when root frame is rendered unit testable

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce periodical check for updates

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Allow downgrading app versions

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Switch to using competition for checking of updates in application menu

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Kill dead code

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make test less prone to fail for wrong reason

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove redundant boilerplate

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make tests for specific migrations less prone to failing for wrong reason

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Move shared stuff under common

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Switch to using single source of truth for selected update channel

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Extract tests for installing update from different update channels to separate scenario

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Add missing global override

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Switch to using release channel of installed application version as default value for selected update channel

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Consolidate usage of channel abstraction to same implementation

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make Channel abstraction support return values

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Fix direct calling of runnables

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Synchronize initial values of sync boxes when window starts

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Add missing global override

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Tweak message of question from user

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Consolidate names of directories

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Add TODO

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove unimplemented scenario from test

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Simplify test

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Improve name of test

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove redundant overrides

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Fix code style

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make Animate deterministic in unit tests

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Simplify naming

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Simplify more naming

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Simplify even more naming

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Simplify more and more naming

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Add todo for cleaning unacceptable code encountered

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Improve name of behaviour

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make unit test more strict

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Enhance name of behaviour

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Introduce dependency to get random IDs

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make asking of boolean value from user not require explicit ID for question

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Simplify code for asking of boolean value from user

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make setting of initial state for sync boxes not trigger irrelevant messaging to main

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make a channel have default type for sent and returned message

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Introduce higher order function to log errors in decorated functions

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Export type for error logging

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Tweak test name

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce higher order function for suppressing errors

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Relocate some explicit error handlings to proper level of abstraction

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make higher order function for logging errors support asynchronous rejecting with non error instance

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make overridden version of application exactly the one required by unit test

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Mark injectable causing side effects

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Revert not required changes

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make code for asserting a promise more strict

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make dependencies readonly

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove duplication for disposers

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Implement initial values for sync-boxes

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Separate concept of message and request channels

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Introduce tests for requesting from channel in renderer

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Implement requesting from renderer in main

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Revert "Implement requesting from renderer in main"

This reverts commit d3e7899d7900516f3dbfacdb317a453202318305.

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Tweak typing of request channel listeners to get rid of unexpected undefined

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove unused variable

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Tweak timing of sentry setup

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Require messages for MessageChannels be JsonValues for serialization

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Require requests and responses for RequestChannels be JsonValues for serialization

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make different MessageChannels not require explicit "extends JsonObject"

Note: Non-escaped lint breaks type here for forcing interface over type. Reasonable effort brought no understanding for what is the relevant difference between the two.

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make a primitive argument an object for readability

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make typing of higher order function for error suppression not lie

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Serialize messages in channels to make IPC not blow up

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce a way to make intentional orphan promises uniform, controllable and deliberate

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Make downloading of update and what follows more deliberate as orphan promise

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>

* Move utility function under directory

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Move another utility function under directory

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Fix incorrect name of file

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove redundant code

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Kill dead code

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Round percentage of update download progress in tray

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Fix rebase conflicts

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Fix CheckForUpdate type errors

Signed-off-by: Sebastian Malton <sebastian@malton.name>

Co-authored-by: Iku-turso <mikko.aspiala@gmail.com>
Co-authored-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
authored and GitHub committed 2022-06-03 11:21:36 +03:00
1 parent e1c1e00a2b
commit b414f9e06d
229 files changed
+8652 -1217

No files matched your search

@@ -0,0 +1,116 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { Menu, Tray } from "electron";
import packageJsonInjectable from "../../../common/vars/package-json.injectable";
import logger from "../../logger";
import { TRAY_LOG_PREFIX } from "../tray";
import showApplicationWindowInjectable from "../../start-main-application/lens-window/show-application-window.injectable";
import type { TrayMenuItem } from "../tray-menu-item/tray-menu-item-injection-token";
import { pipeline } from "@ogre-tools/fp";
import { isEmpty, map, filter } from "lodash/fp";
import isWindowsInjectable from "../../../common/vars/is-windows.injectable";
import loggerInjectable from "../../../common/logger.injectable";
import trayIconPathInjectable from "../tray-icon-path.injectable";
const electronTrayInjectable = getInjectable({
id: "electron-tray",
instantiate: (di) => {
const packageJson = di.inject(packageJsonInjectable);
const showApplicationWindow = di.inject(showApplicationWindowInjectable);
const isWindows = di.inject(isWindowsInjectable);
const logger = di.inject(loggerInjectable);
const trayIconPath = di.inject(trayIconPathInjectable);
let tray: Tray;
return {
start: () => {
tray = new Tray(trayIconPath);
tray.setToolTip(packageJson.description);
tray.setIgnoreDoubleClickEvents(true);
if (isWindows) {
tray.on("click", () => {
showApplicationWindow()
.catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to open lens`, { error }));
});
}
},
stop: () => {
tray.destroy();
},
setMenuItems: (items: TrayMenuItem[]) => {
pipeline(
items,
convertToElectronMenuTemplate,
Menu.buildFromTemplate,
(template) => {
tray.setContextMenu(template);
},
);
},
};
},
causesSideEffects: true,
});
export default electronTrayInjectable;
const convertToElectronMenuTemplate = (trayMenuItems: TrayMenuItem[]) => {
const _toTrayMenuOptions = (parentId: string | null) =>
pipeline(
trayMenuItems,
filter((item) => item.parentId === parentId),
map(
(trayMenuItem: TrayMenuItem): Electron.MenuItemConstructorOptions => {
if (trayMenuItem.separator) {
return { id: trayMenuItem.id, type: "separator" };
}
const childItems = _toTrayMenuOptions(trayMenuItem.id);
return {
id: trayMenuItem.id,
label: trayMenuItem.label?.get(),
enabled: trayMenuItem.enabled.get(),
toolTip: trayMenuItem.tooltip,
...(isEmpty(childItems)
? {
type: "normal",
submenu: _toTrayMenuOptions(trayMenuItem.id),
click: () => {
try {
trayMenuItem.click?.();
} catch (error) {
logger.error(
`${TRAY_LOG_PREFIX}: clicking item "${trayMenuItem.id} failed."`,
{ error },
);
}
},
}
: {
type: "submenu",
submenu: _toTrayMenuOptions(trayMenuItem.id),
}),
};
},
),
);
return _toTrayMenuOptions(null);
};
@@ -0,0 +1,25 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { onLoadOfApplicationInjectionToken } from "../../start-main-application/runnable-tokens/on-load-of-application-injection-token";
import electronTrayInjectable from "./electron-tray.injectable";
const startTrayInjectable = getInjectable({
id: "start-tray",
instantiate: (di) => {
const electronTray = di.inject(electronTrayInjectable);
return {
run: () => {
electronTray.start();
},
};
},
injectionToken: onLoadOfApplicationInjectionToken,
});
export default startTrayInjectable;
@@ -0,0 +1,28 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import electronTrayInjectable from "./electron-tray.injectable";
import { beforeQuitOfBackEndInjectionToken } from "../../start-main-application/runnable-tokens/before-quit-of-back-end-injection-token";
import stopReactiveTrayMenuItemsInjectable from "../reactive-tray-menu-items/stop-reactive-tray-menu-items.injectable";
const stopTrayInjectable = getInjectable({
id: "stop-tray",
instantiate: (di) => {
const electronTray = di.inject(electronTrayInjectable);
return {
run: () => {
electronTray.stop();
},
runAfter: di.inject(stopReactiveTrayMenuItemsInjectable),
};
},
injectionToken: beforeQuitOfBackEndInjectionToken,
});
export default stopTrayInjectable;
-25
View File
@@ -1,25 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import trayInjectable from "./tray.injectable";
import { onLoadOfApplicationInjectionToken } from "../start-main-application/runnable-tokens/on-load-of-application-injection-token";
const installTrayInjectable = getInjectable({
id: "install-tray",
instantiate: (di) => {
const trayInitializer = di.inject(trayInjectable);
return {
run: async () => {
await trayInitializer.start();
},
};
},
injectionToken: onLoadOfApplicationInjectionToken,
});
export default installTrayInjectable;
@@ -0,0 +1,24 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { getStartableStoppable } from "../../../common/utils/get-startable-stoppable";
import { autorun } from "mobx";
import trayMenuItemsInjectable from "../tray-menu-item/tray-menu-items.injectable";
import electronTrayInjectable from "../electron-tray/electron-tray.injectable";
const reactiveTrayMenuItemsInjectable = getInjectable({
id: "reactive-tray-menu-items",
instantiate: (di) => {
const electronTray = di.inject(electronTrayInjectable);
const trayMenuItems = di.inject(trayMenuItemsInjectable);
return getStartableStoppable("reactive-tray-menu-items", () => autorun(() => {
electronTray.setMenuItems(trayMenuItems.get());
}));
},
});
export default reactiveTrayMenuItemsInjectable;
@@ -0,0 +1,28 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import reactiveTrayMenuItemsInjectable from "./reactive-tray-menu-items.injectable";
import { onLoadOfApplicationInjectionToken } from "../../start-main-application/runnable-tokens/on-load-of-application-injection-token";
import startTrayInjectable from "../electron-tray/start-tray.injectable";
const startReactiveTrayMenuItemsInjectable = getInjectable({
id: "start-reactive-tray-menu-items",
instantiate: (di) => {
const reactiveTrayMenuItems = di.inject(reactiveTrayMenuItemsInjectable);
return {
run: async () => {
await reactiveTrayMenuItems.start();
},
runAfter: di.inject(startTrayInjectable),
};
},
injectionToken: onLoadOfApplicationInjectionToken,
});
export default startReactiveTrayMenuItemsInjectable;
@@ -0,0 +1,25 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import reactiveTrayMenuItemsInjectable from "./reactive-tray-menu-items.injectable";
import { beforeQuitOfBackEndInjectionToken } from "../../start-main-application/runnable-tokens/before-quit-of-back-end-injection-token";
const stopReactiveTrayMenuItemsInjectable = getInjectable({
id: "stop-reactive-tray-menu-items",
instantiate: (di) => {
const reactiveTrayMenuItems = di.inject(reactiveTrayMenuItemsInjectable);
return {
run: async () => {
await reactiveTrayMenuItems.stop();
},
};
},
injectionToken: beforeQuitOfBackEndInjectionToken,
});
export default stopReactiveTrayMenuItemsInjectable;
@@ -0,0 +1,51 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import productNameInjectable from "../../../app-paths/app-name/product-name.injectable";
import showApplicationWindowInjectable from "../../../start-main-application/lens-window/show-application-window.injectable";
import showAboutInjectable from "../../../menu/show-about.injectable";
import { trayMenuItemInjectionToken } from "../tray-menu-item-injection-token";
import { computed } from "mobx";
import withErrorLoggingInjectable from "../../../../common/utils/with-error-logging/with-error-logging.injectable";
import { withErrorSuppression } from "../../../../common/utils/with-error-suppression/with-error-suppression";
import { pipeline } from "@ogre-tools/fp";
const aboutAppTrayItemInjectable = getInjectable({
id: "about-app-tray-item",
instantiate: (di) => {
const productName = di.inject(productNameInjectable);
const showApplicationWindow = di.inject(showApplicationWindowInjectable);
const showAbout = di.inject(showAboutInjectable);
const withErrorLoggingFor = di.inject(withErrorLoggingInjectable);
return {
id: "about-app",
parentId: null,
orderNumber: 140,
label: computed(() => `About ${productName}`),
enabled: computed(() => true),
visible: computed(() => true),
click: pipeline(
async () => {
await showApplicationWindow();
await showAbout();
},
withErrorLoggingFor(() => "[TRAY]: Opening of show about failed."),
// TODO: Find out how to improve typing so that instead of
// x => withErrorSuppression(x) there could only be withErrorSuppression
(x) => withErrorSuppression(x),
),
};
},
injectionToken: trayMenuItemInjectionToken,
});
export default aboutAppTrayItemInjectable;
@@ -0,0 +1,47 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { trayMenuItemInjectionToken } from "../tray-menu-item-injection-token";
import productNameInjectable from "../../../app-paths/app-name/product-name.injectable";
import showApplicationWindowInjectable from "../../../start-main-application/lens-window/show-application-window.injectable";
import { computed } from "mobx";
import withErrorLoggingInjectable from "../../../../common/utils/with-error-logging/with-error-logging.injectable";
import { withErrorSuppression } from "../../../../common/utils/with-error-suppression/with-error-suppression";
import { pipeline } from "@ogre-tools/fp";
const openAppTrayItemInjectable = getInjectable({
id: "open-app-tray-item",
instantiate: (di) => {
const productName = di.inject(productNameInjectable);
const showApplicationWindow = di.inject(showApplicationWindowInjectable);
const withErrorLoggingFor = di.inject(withErrorLoggingInjectable);
return {
id: "open-app",
parentId: null,
label: computed(() => `Open ${productName}`),
orderNumber: 10,
enabled: computed(() => true),
visible: computed(() => true),
click: pipeline(
async () => {
await showApplicationWindow();
},
withErrorLoggingFor(() => "[TRAY]: Opening of application window failed."),
// TODO: Find out how to improve typing so that instead of
// x => withErrorSuppression(x) there could only be withErrorSuppression
(x) => withErrorSuppression(x),
),
};
},
injectionToken: trayMenuItemInjectionToken,
});
export default openAppTrayItemInjectable;
@@ -0,0 +1,43 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { trayMenuItemInjectionToken } from "../tray-menu-item-injection-token";
import navigateToPreferencesInjectable from "../../../../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable";
import { computed } from "mobx";
import { withErrorSuppression } from "../../../../common/utils/with-error-suppression/with-error-suppression";
import { pipeline } from "@ogre-tools/fp";
import withErrorLoggingInjectable from "../../../../common/utils/with-error-logging/with-error-logging.injectable";
const openPreferencesTrayItemInjectable = getInjectable({
id: "open-preferences-tray-item",
instantiate: (di) => {
const navigateToPreferences = di.inject(navigateToPreferencesInjectable);
const withErrorLoggingFor = di.inject(withErrorLoggingInjectable);
return {
id: "open-preferences",
parentId: null,
label: computed(() => "Preferences"),
orderNumber: 20,
enabled: computed(() => true),
visible: computed(() => true),
click: pipeline(
navigateToPreferences,
withErrorLoggingFor(() => "[TRAY]: Opening of preferences failed."),
// TODO: Find out how to improve typing so that instead of
// x => withErrorSuppression(x) there could only be withErrorSuppression
(x) => withErrorSuppression(x),
),
};
},
injectionToken: trayMenuItemInjectionToken,
});
export default openPreferencesTrayItemInjectable;
@@ -0,0 +1,24 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { trayMenuItemInjectionToken } from "../tray-menu-item-injection-token";
import { computed } from "mobx";
const quitAppSeparatorTrayItemInjectable = getInjectable({
id: "quit-app-separator-tray-item",
instantiate: () => ({
id: "quit-app-separator",
parentId: null,
orderNumber: 149,
enabled: computed(() => true),
visible: computed(() => true),
separator: true,
}),
injectionToken: trayMenuItemInjectionToken,
});
export default quitAppSeparatorTrayItemInjectable;
@@ -0,0 +1,43 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { trayMenuItemInjectionToken } from "../tray-menu-item-injection-token";
import { computed } from "mobx";
import stopServicesAndExitAppInjectable from "../../../stop-services-and-exit-app.injectable";
import { withErrorSuppression } from "../../../../common/utils/with-error-suppression/with-error-suppression";
import { pipeline } from "@ogre-tools/fp";
import withErrorLoggingInjectable from "../../../../common/utils/with-error-logging/with-error-logging.injectable";
const quitAppTrayItemInjectable = getInjectable({
id: "quit-app-tray-item",
instantiate: (di) => {
const stopServicesAndExitApp = di.inject(stopServicesAndExitAppInjectable);
const withErrorLoggingFor = di.inject(withErrorLoggingInjectable);
return {
id: "quit-app",
parentId: null,
orderNumber: 150,
label: computed(() => "Quit App"),
enabled: computed(() => true),
visible: computed(() => true),
click: pipeline(
stopServicesAndExitApp,
withErrorLoggingFor(() => "[TRAY]: Quitting application failed."),
// TODO: Find out how to improve typing so that instead of
// x => withErrorSuppression(x) there could only be withErrorSuppression
(x) => withErrorSuppression(x),
),
};
},
injectionToken: trayMenuItemInjectionToken,
});
export default quitAppTrayItemInjectable;
@@ -0,0 +1,25 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type { IComputedValue } from "mobx";
import type { LensMainExtension } from "../../../extensions/lens-main-extension";
export interface TrayMenuItem {
id: string;
parentId: string | null;
orderNumber: number;
enabled: IComputedValue<boolean>;
visible: IComputedValue<boolean>;
label?: IComputedValue<string>;
click?: () => Promise<void> | void;
tooltip?: string;
separator?: boolean;
extension?: LensMainExtension;
}
export const trayMenuItemInjectionToken = getInjectionToken<TrayMenuItem>({
id: "tray-menu-item",
});
@@ -0,0 +1,90 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { pipeline } from "@ogre-tools/fp";
import { flatMap, kebabCase } from "lodash/fp";
import type { Injectable } from "@ogre-tools/injectable";
import { getInjectable } from "@ogre-tools/injectable";
import { computed } from "mobx";
import { extensionRegistratorInjectionToken } from "../../../extensions/extension-loader/extension-registrator-injection-token";
import type { LensMainExtension } from "../../../extensions/lens-main-extension";
import type { TrayMenuItem } from "./tray-menu-item-injection-token";
import { trayMenuItemInjectionToken } from "./tray-menu-item-injection-token";
import type { TrayMenuRegistration } from "../tray-menu-registration";
import { withErrorSuppression } from "../../../common/utils/with-error-suppression/with-error-suppression";
import type { WithErrorLoggingFor } from "../../../common/utils/with-error-logging/with-error-logging.injectable";
import withErrorLoggingInjectable from "../../../common/utils/with-error-logging/with-error-logging.injectable";
const trayMenuItemRegistratorInjectable = getInjectable({
id: "tray-menu-item-registrator",
instantiate: (di) => (extension, installationCounter) => {
const mainExtension = extension as LensMainExtension;
const withErrorLoggingFor = di.inject(withErrorLoggingInjectable);
pipeline(
mainExtension.trayMenus,
flatMap(toItemInjectablesFor(mainExtension, installationCounter, withErrorLoggingFor)),
(injectables) => di.register(...injectables),
);
},
injectionToken: extensionRegistratorInjectionToken,
});
export default trayMenuItemRegistratorInjectable;
const toItemInjectablesFor = (extension: LensMainExtension, installationCounter: number, withErrorLoggingFor: WithErrorLoggingFor) => {
const _toItemInjectables = (parentId: string | null) => (registration: TrayMenuRegistration): Injectable<TrayMenuItem, TrayMenuItem, void>[] => {
const trayItemId = registration.id || kebabCase(registration.label || "");
const id = `${trayItemId}-tray-menu-item-for-extension-${extension.sanitizedExtensionId}-instance-${installationCounter}`;
const parentInjectable = getInjectable({
id,
instantiate: () => ({
id,
parentId,
orderNumber: 100,
separator: registration.type === "separator",
label: computed(() => registration.label || ""),
tooltip: registration.toolTip,
click: pipeline(
() => {
registration.click?.(registration);
},
withErrorLoggingFor(() => `[TRAY]: Clicking of tray item "${trayItemId}" from extension "${extension.sanitizedExtensionId}" failed.`),
// TODO: Find out how to improve typing so that instead of
// x => withErrorSuppression(x) there could only be withErrorSuppression
(x) => withErrorSuppression(x),
),
enabled: computed(() => !!registration.enabled),
visible: computed(() => true),
}),
injectionToken: trayMenuItemInjectionToken,
});
const childMenuItems = registration.submenu || [];
const childInjectables = childMenuItems.flatMap(_toItemInjectables(id));
return [
parentInjectable,
...childInjectables,
];
};
return _toItemInjectables(null);
};
@@ -0,0 +1,49 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { computed } from "mobx";
import mainExtensionsInjectable from "../../../extensions/main-extensions.injectable";
import type { TrayMenuItem } from "./tray-menu-item-injection-token";
import { trayMenuItemInjectionToken } from "./tray-menu-item-injection-token";
import { pipeline } from "@ogre-tools/fp";
import { filter, overSome, sortBy } from "lodash/fp";
import type { LensMainExtension } from "../../../extensions/lens-main-extension";
const trayMenuItemsInjectable = getInjectable({
id: "tray-menu-items",
instantiate: (di) => {
const extensions = di.inject(mainExtensionsInjectable);
return computed(() => {
const enabledExtensions = extensions.get();
return pipeline(
di.injectMany(trayMenuItemInjectionToken),
filter((item) =>
overSome([
isNonExtensionItem,
isEnabledExtensionItemFor(enabledExtensions),
])(item),
),
filter(item => item.visible.get()),
items => sortBy("orderNumber", items),
);
});
},
});
const isNonExtensionItem = (item: TrayMenuItem) => !item.extension;
const isEnabledExtensionItemFor =
(enabledExtensions: LensMainExtension[]) => (item: TrayMenuItem) =>
!!enabledExtensions.find((extension) => extension === item.extension);
export default trayMenuItemsInjectable;
-42
View File
@@ -1,42 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { initTray } from "./tray";
import trayMenuItemsInjectable from "./tray-menu-items.injectable";
import navigateToPreferencesInjectable from "../../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable";
import stopServicesAndExitAppInjectable from "../stop-services-and-exit-app.injectable";
import { getStartableStoppable } from "../../common/utils/get-startable-stoppable";
import isAutoUpdateEnabledInjectable from "../is-auto-update-enabled.injectable";
import showAboutInjectable from "../menu/show-about.injectable";
import showApplicationWindowInjectable from "../start-main-application/lens-window/show-application-window.injectable";
import trayIconPathInjectable from "./tray-icon-path.injectable";
const trayInjectable = getInjectable({
id: "tray",
instantiate: (di) => {
const trayMenuItems = di.inject(trayMenuItemsInjectable);
const navigateToPreferences = di.inject(navigateToPreferencesInjectable);
const stopServicesAndExitApp = di.inject(stopServicesAndExitAppInjectable);
const isAutoUpdateEnabled = di.inject(isAutoUpdateEnabledInjectable);
const showApplicationWindow = di.inject(showApplicationWindowInjectable);
const showAboutPopup = di.inject(showAboutInjectable);
const trayIconPath = di.inject(trayIconPathInjectable);
return getStartableStoppable("build-of-tray", () =>
initTray(
trayMenuItems,
navigateToPreferences,
stopServicesAndExitApp,
isAutoUpdateEnabled,
showApplicationWindow,
showAboutPopup,
trayIconPath,
),
);
},
});
export default trayInjectable;
+48 -70
View File
@@ -7,25 +7,22 @@ import packageInfo from "../../../package.json";
import { Menu, Tray } from "electron";
import type { IComputedValue } from "mobx";
import { autorun } from "mobx";
import { checkForUpdates } from "../app-updater";
import logger from "../logger";
import { isWindows, productName } from "../../common/vars";
import { isWindows } from "../../common/vars";
import type { Disposer } from "../../common/utils";
import { disposer, toJS } from "../../common/utils";
import type { TrayMenuRegistration } from "./tray-menu-registration";
import { disposer } from "../../common/utils";
import type { TrayMenuItem } from "./tray-menu-item/tray-menu-item-injection-token";
import { pipeline } from "@ogre-tools/fp";
import { filter, isEmpty, map } from "lodash/fp";
const TRAY_LOG_PREFIX = "[TRAY]";
export const TRAY_LOG_PREFIX = "[TRAY]";
// note: instance of Tray should be saved somewhere, otherwise it disappears
export let tray: Tray | null = null;
export function initTray(
trayMenuItems: IComputedValue<TrayMenuRegistration[]>,
navigateToPreferences: () => void,
stopServicesAndExitApp: () => void,
isAutoUpdateEnabled: () => boolean,
trayMenuItems: IComputedValue<TrayMenuItem[]>,
showApplicationWindow: () => Promise<void>,
showAbout: () => void,
trayIconPath: string,
): Disposer {
tray = new Tray(trayIconPath);
@@ -42,7 +39,9 @@ export function initTray(
return disposer(
autorun(() => {
try {
const menu = createTrayMenu(toJS(trayMenuItems.get()), navigateToPreferences, stopServicesAndExitApp, isAutoUpdateEnabled, showApplicationWindow, showAbout);
const options = toTrayMenuOptions(trayMenuItems.get());
const menu = Menu.buildFromTemplate(options);
tray?.setContextMenu(menu);
} catch (error) {
@@ -56,66 +55,45 @@ export function initTray(
);
}
function getMenuItemConstructorOptions(trayItem: TrayMenuRegistration): Electron.MenuItemConstructorOptions {
return {
...trayItem,
submenu: trayItem.submenu ? trayItem.submenu.map(getMenuItemConstructorOptions) : undefined,
click: trayItem.click ? () => {
trayItem.click?.(trayItem);
} : undefined,
};
}
const toTrayMenuOptions = (trayMenuItems: TrayMenuItem[]) => {
const _toTrayMenuOptions = (parentId: string | null) =>
pipeline(
trayMenuItems,
function createTrayMenu(
extensionTrayItems: TrayMenuRegistration[],
navigateToPreferences: () => void,
stopServicesAndExitApp: () => void,
isAutoUpdateEnabled: () => boolean,
showApplicationWindow: () => Promise<void>,
showAbout: () => void,
): Menu {
let template: Electron.MenuItemConstructorOptions[] = [
{
label: `Open ${productName}`,
click() {
showApplicationWindow().catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to open lens`, { error }));
},
},
{
label: "Preferences",
click() {
navigateToPreferences();
},
},
];
filter((item) => item.parentId === parentId),
if (isAutoUpdateEnabled()) {
template.push({
label: "Check for updates",
click() {
checkForUpdates()
.then(() => showApplicationWindow());
},
});
}
map(
(trayMenuItem: TrayMenuItem): Electron.MenuItemConstructorOptions => {
if (trayMenuItem.separator) {
return { id: trayMenuItem.id, type: "separator" };
}
template = template.concat(extensionTrayItems.map(getMenuItemConstructorOptions));
const childItems = _toTrayMenuOptions(trayMenuItem.id);
return {
id: trayMenuItem.id,
label: trayMenuItem.label?.get(),
enabled: trayMenuItem.enabled.get(),
toolTip: trayMenuItem.tooltip,
...(isEmpty(childItems)
? {
type: "normal",
submenu: _toTrayMenuOptions(trayMenuItem.id),
click: () => {
trayMenuItem.click?.();
},
}
: {
type: "submenu",
submenu: _toTrayMenuOptions(trayMenuItem.id),
}),
};
},
),
);
return _toTrayMenuOptions(null);
};
return Menu.buildFromTemplate(template.concat([
{
label: `About ${productName}`,
click() {
showApplicationWindow()
.then(showAbout)
.catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to show Lens About view`, { error }));
},
},
{ type: "separator" },
{
label: "Quit App",
click() {
stopServicesAndExitApp();
},
},
]));
}
@@ -1,25 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import trayInjectable from "./tray.injectable";
import { beforeQuitOfBackEndInjectionToken } from "../start-main-application/runnable-tokens/before-quit-of-back-end-injection-token";
const uninstallTrayInjectable = getInjectable({
id: "uninstall-tray",
instantiate: (di) => {
const trayInitializer = di.inject(trayInjectable);
return {
run: async () => {
await trayInitializer.stop();
},
};
},
injectionToken: beforeQuitOfBackEndInjectionToken,
});
export default uninstallTrayInjectable;