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

@@ -46,7 +46,6 @@ import { mock } from "jest-mock-extended";
import { waitUntilUsed } from "tcp-port-used";
import type { Readable } from "stream";
import { EventEmitter } from "stream";
import { UserStore } from "../../common/user-store";
import { Console } from "console";
import { stdout, stderr } from "process";
import mockFs from "mock-fs";
@@ -120,12 +119,9 @@ describe("kube auth proxy tests", () => {
createCluster = di.inject(createClusterInjectionToken);
createKubeAuthProxy = di.inject(createKubeAuthProxyInjectable);
UserStore.createInstance();
});
afterEach(() => {
UserStore.resetInstance();
mockFs.restore();
});
@@ -3,16 +3,17 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import packageInfo from "../../../../package.json";
import isDevelopmentInjectable from "../../../common/vars/is-development.injectable";
import productNameInjectable from "./product-name.injectable";
const appNameInjectable = getInjectable({
id: "app-name",
instantiate: (di) => {
const isDevelopment = di.inject(isDevelopmentInjectable);
const productName = di.inject(productNameInjectable);
return `${packageInfo.productName}${isDevelopment ? "Dev" : ""}`;
return `${productName}${isDevelopment ? "Dev" : ""}`;
},
causesSideEffects: true,
@@ -0,0 +1,14 @@
/**
* 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 packageInfo from "../../../../package.json";
const productNameInjectable = getInjectable({
id: "product-name",
instantiate: () => packageInfo.productName,
causesSideEffects: true,
});
export default productNameInjectable;
@@ -0,0 +1,27 @@
/**
* 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 type { RequestChannelListener } from "../../common/utils/channel/request-channel-listener-injection-token";
import { requestChannelListenerInjectionToken } from "../../common/utils/channel/request-channel-listener-injection-token";
import type { AppPathsChannel } from "../../common/app-paths/app-paths-channel.injectable";
import appPathsChannelInjectable from "../../common/app-paths/app-paths-channel.injectable";
import appPathsInjectable from "../../common/app-paths/app-paths.injectable";
const appPathsRequestChannelListenerInjectable = getInjectable({
id: "app-paths-request-channel-listener",
instantiate: (di): RequestChannelListener<AppPathsChannel> => {
const channel = di.inject(appPathsChannelInjectable);
const appPaths = di.inject(appPathsInjectable);
return {
channel,
handler: () => appPaths,
};
},
injectionToken: requestChannelListenerInjectionToken,
});
export default appPathsRequestChannelListenerInjectable;
@@ -6,7 +6,6 @@ import electronAppInjectable from "../../electron-app/electron-app.injectable";
import getElectronAppPathInjectable from "./get-electron-app-path.injectable";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import type { App } from "electron";
import registerChannelInjectable from "../register-channel/register-channel.injectable";
import joinPathsInjectable from "../../../common/path/join-paths.injectable";
import { joinPathsFake } from "../../../common/test-utils/join-paths-fake";
@@ -32,7 +31,6 @@ describe("get-electron-app-path", () => {
} as App;
di.override(electronAppInjectable, () => appStub);
di.override(registerChannelInjectable, () => () => undefined);
di.override(joinPathsInjectable, () => joinPathsFake);
getElectronAppPath = di.inject(getElectronAppPathInjectable) as (name: string) => string;
@@ -1,17 +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 ipcMainInjectable from "./ipc-main/ipc-main.injectable";
import { registerChannel } from "./register-channel";
const registerChannelInjectable = getInjectable({
id: "register-channel",
instantiate: (di) => registerChannel({
ipcMain: di.inject(ipcMainInjectable),
}),
});
export default registerChannelInjectable;
@@ -1,18 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { IpcMain } from "electron";
import type { Channel } from "../../../common/ipc-channel/channel";
interface Dependencies {
ipcMain: IpcMain;
}
export const registerChannel =
({ ipcMain }: Dependencies) =>
<TChannel extends Channel<TInstance>, TInstance>(
channel: TChannel,
getValue: () => TInstance,
) =>
ipcMain.handle(channel.name, getValue);
@@ -12,8 +12,6 @@ import appPathsStateInjectable from "../../common/app-paths/app-paths-state.inje
import { pathNames } from "../../common/app-paths/app-path-names";
import { fromPairs, map } from "lodash/fp";
import { pipeline } from "@ogre-tools/fp";
import { appPathsIpcChannel } from "../../common/app-paths/app-path-injection-token";
import registerChannelInjectable from "./register-channel/register-channel.injectable";
import joinPathsInjectable from "../../common/path/join-paths.injectable";
import { beforeElectronIsReadyInjectionToken } from "../start-main-application/runnable-tokens/before-electron-is-ready-injection-token";
@@ -25,7 +23,6 @@ const setupAppPathsInjectable = getInjectable({
const appName = di.inject(appNameInjectable);
const getAppPath = di.inject(getElectronAppPathInjectable);
const appPathsState = di.inject(appPathsStateInjectable);
const registerChannel = di.inject(registerChannelInjectable);
const directoryForIntegrationTesting = di.inject(directoryForIntegrationTestingInjectable);
const joinPaths = di.inject(joinPathsInjectable);
@@ -46,8 +43,6 @@ const setupAppPathsInjectable = getInjectable({
) as AppPaths;
appPathsState.set(appPaths);
registerChannel(appPathsIpcChannel, () => appPaths);
},
};
},
-133
View File
@@ -1,133 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { UpdateInfo } from "electron-updater";
import { autoUpdater } from "electron-updater";
import logger from "./logger";
import { isPublishConfigured, isTestEnv } from "../common/vars";
import { delay } from "../common/utils";
import type { UpdateAvailableToBackchannel } from "../common/ipc";
import { areArgsUpdateAvailableToBackchannel, AutoUpdateChecking, AutoUpdateLogPrefix, AutoUpdateNoUpdateAvailable, broadcastMessage, onceCorrect, UpdateAvailableChannel } from "../common/ipc";
import { once } from "lodash";
import { ipcMain } from "electron";
import { nextUpdateChannel } from "./utils/update-channel";
import { UserStore } from "../common/user-store";
let installVersion: undefined | string;
export function isAutoUpdateEnabled() {
return autoUpdater.isUpdaterActive() && isPublishConfigured;
}
function handleAutoUpdateBackChannel(event: Electron.IpcMainEvent, ...[arg]: UpdateAvailableToBackchannel) {
if (arg.doUpdate) {
if (arg.now) {
logger.info(`${AutoUpdateLogPrefix}: User chose to update now`);
autoUpdater.quitAndInstall(true, true);
} else {
logger.info(`${AutoUpdateLogPrefix}: User chose to update on quit`);
}
} else {
logger.info(`${AutoUpdateLogPrefix}: User chose not to update, will update on quit anyway`);
}
}
autoUpdater.logger = {
info: message => logger.info(`[AUTO-UPDATE]: electron-updater: %s`, message),
warn: message => logger.warn(`[AUTO-UPDATE]: electron-updater: %s`, message),
error: message => logger.error(`[AUTO-UPDATE]: electron-updater: %s`, message),
debug: message => logger.debug(`[AUTO-UPDATE]: electron-updater: %s`, message),
};
interface Dependencies {
isAutoUpdateEnabled: () => boolean;
}
/**
* starts the automatic update checking
* @param interval milliseconds between interval to check on, defaults to 2h
*/
export const startUpdateChecking = ({ isAutoUpdateEnabled } : Dependencies) => once(function (interval = 1000 * 60 * 60 * 2): void {
if (!isAutoUpdateEnabled() || isTestEnv) {
return;
}
const userStore = UserStore.getInstance();
autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = true;
autoUpdater.channel = userStore.updateChannel;
autoUpdater.allowDowngrade = userStore.isAllowedToDowngrade;
autoUpdater
.on("update-available", (info: UpdateInfo) => {
if (installVersion === info.version) {
// same version, don't broadcast
return;
}
installVersion = info.version;
autoUpdater.downloadUpdate()
.catch(error => logger.error(`${AutoUpdateLogPrefix}: failed to download update`, { error: String(error) }));
})
.on("update-downloaded", (info: UpdateInfo) => {
try {
const backchannel = `auto-update:${info.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: info.version });
broadcastMessage(UpdateAvailableChannel, backchannel, info);
} catch (error) {
logger.error(`${AutoUpdateLogPrefix}: broadcasting failed`, { error });
installVersion = undefined;
}
})
.on("update-not-available", () => {
const nextChannel = nextUpdateChannel(userStore.updateChannel, autoUpdater.channel);
logger.info(`${AutoUpdateLogPrefix}: update not available from ${autoUpdater.channel}, will check ${nextChannel} channel next`);
if (nextChannel !== autoUpdater.channel) {
autoUpdater.channel = nextChannel;
autoUpdater.checkForUpdates()
.catch(error => logger.error(`${AutoUpdateLogPrefix}: failed with an error`, error));
} else {
broadcastMessage(AutoUpdateNoUpdateAvailable);
}
});
async function helper() {
while (true) {
await checkForUpdates();
await delay(interval);
}
}
helper();
});
export async function checkForUpdates(): Promise<void> {
const userStore = UserStore.getInstance();
try {
logger.info(`📡 Checking for app updates`);
autoUpdater.channel = userStore.updateChannel;
autoUpdater.allowDowngrade = userStore.isAllowedToDowngrade;
broadcastMessage(AutoUpdateChecking);
await autoUpdater.checkForUpdates();
} catch (error) {
logger.error(`${AutoUpdateLogPrefix}: failed with an error`, error);
}
}
@@ -0,0 +1,60 @@
/**
* 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 electronUpdaterInjectable from "../../electron-app/features/electron-updater.injectable";
import type { UpdateChannel } from "../../../common/application-update/update-channels";
import loggerInjectable from "../../../common/logger.injectable";
import type { UpdateCheckResult } from "electron-updater";
export type CheckForUpdatesResult = {
updateWasDiscovered: false;
} | {
updateWasDiscovered: true;
version: string;
};
export type CheckForPlatformUpdates = (updateChannel: UpdateChannel, opts: { allowDowngrade: boolean }) => Promise<CheckForUpdatesResult>;
const checkForPlatformUpdatesInjectable = getInjectable({
id: "check-for-platform-updates",
instantiate: (di): CheckForPlatformUpdates => {
const electronUpdater = di.inject(electronUpdaterInjectable);
const logger = di.inject(loggerInjectable);
return async (updateChannel, { allowDowngrade }) => {
electronUpdater.channel = updateChannel.id;
electronUpdater.autoDownload = false;
electronUpdater.allowDowngrade = allowDowngrade;
let result: UpdateCheckResult;
try {
result = await electronUpdater.checkForUpdates();
} catch (error) {
logger.error("[UPDATE-APP/CHECK-FOR-UPDATES]", error);
return {
updateWasDiscovered: false,
};
}
const { updateInfo, cancellationToken } = result;
if (!cancellationToken) {
return {
updateWasDiscovered: false,
};
}
return {
updateWasDiscovered: true,
version: updateInfo.version,
};
};
},
});
export default checkForPlatformUpdatesInjectable;
@@ -0,0 +1,128 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import electronUpdaterInjectable from "../../electron-app/features/electron-updater.injectable";
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import type { AppUpdater, UpdateCheckResult } from "electron-updater";
import type { CheckForPlatformUpdates } from "./check-for-platform-updates.injectable";
import checkForPlatformUpdatesInjectable from "./check-for-platform-updates.injectable";
import type { UpdateChannel, UpdateChannelId } from "../../../common/application-update/update-channels";
import { getPromiseStatus } from "../../../common/test-utils/get-promise-status";
import loggerInjectable from "../../../common/logger.injectable";
import type { Logger } from "../../../common/logger";
describe("check-for-platform-updates", () => {
let checkForPlatformUpdates: CheckForPlatformUpdates;
let electronUpdaterFake: AppUpdater;
let checkForUpdatesMock: AsyncFnMock<() => UpdateCheckResult>;
let logErrorMock: jest.Mock;
beforeEach(() => {
const di = getDiForUnitTesting();
checkForUpdatesMock = asyncFn();
electronUpdaterFake = {
channel: undefined,
autoDownload: undefined,
allowDowngrade: undefined,
checkForUpdates: checkForUpdatesMock,
} as unknown as AppUpdater;
di.override(electronUpdaterInjectable, () => electronUpdaterFake);
logErrorMock = jest.fn();
di.override(loggerInjectable, () => ({ error: logErrorMock }) as unknown as Logger);
checkForPlatformUpdates = di.inject(checkForPlatformUpdatesInjectable);
});
describe("when called", () => {
let actualPromise: Promise<any>;
beforeEach(() => {
const testUpdateChannel: UpdateChannel = {
id: "some-update-channel" as UpdateChannelId,
label: "Some update channel",
moreStableUpdateChannel: null,
};
actualPromise = checkForPlatformUpdates(testUpdateChannel, { allowDowngrade: true });
});
it("sets update channel", () => {
expect(electronUpdaterFake.channel).toBe("some-update-channel");
});
it("sets flag for allowing downgrade", () => {
expect(electronUpdaterFake.allowDowngrade).toBe(true);
});
it("disables auto downloading for being controlled", () => {
expect(electronUpdaterFake.autoDownload).toBe(false);
});
it("checks for updates", () => {
expect(checkForUpdatesMock).toHaveBeenCalled();
});
it("does not resolve yet", async () => {
const promiseStatus = await getPromiseStatus(actualPromise);
expect(promiseStatus.fulfilled).toBe(false);
});
it("when checking for updates resolves with update, resolves with the discovered update", async () => {
await checkForUpdatesMock.resolve({
updateInfo: {
version: "some-version",
},
cancellationToken: "some-cancellation-token",
} as unknown as UpdateCheckResult);
const actual = await actualPromise;
expect(actual).toEqual({ updateWasDiscovered: true, version: "some-version" });
});
it("when checking for updates resolves without update, resolves with update not being discovered", async () => {
await checkForUpdatesMock.resolve({
updateInfo: {
version: "some-version-that-matches-to-current-installed-version",
},
cancellationToken: null,
} as unknown as UpdateCheckResult);
const actual = await actualPromise;
expect(actual).toEqual({ updateWasDiscovered: false });
});
describe("when checking for updates rejects", () => {
let errorStub: Error;
beforeEach(() => {
errorStub = new Error("Some error");
checkForUpdatesMock.reject(errorStub);
});
it("logs errors", () => {
expect(logErrorMock).toHaveBeenCalledWith("[UPDATE-APP/CHECK-FOR-UPDATES]", errorStub);
});
it("resolves with update not being discovered", async () => {
const actual = await actualPromise;
expect(actual).toEqual({ updateWasDiscovered: false });
});
});
});
});
@@ -0,0 +1,79 @@
/**
* 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 updatingIsEnabledInjectable from "./updating-is-enabled.injectable";
import { trayMenuItemInjectionToken } from "../tray/tray-menu-item/tray-menu-item-injection-token";
import showApplicationWindowInjectable from "../start-main-application/lens-window/show-application-window.injectable";
import discoveredUpdateVersionInjectable from "../../common/application-update/discovered-update-version/discovered-update-version.injectable";
import updateIsBeingDownloadedInjectable from "../../common/application-update/update-is-being-downloaded/update-is-being-downloaded.injectable";
import updatesAreBeingDiscoveredInjectable from "../../common/application-update/updates-are-being-discovered/updates-are-being-discovered.injectable";
import progressOfUpdateDownloadInjectable from "../../common/application-update/progress-of-update-download/progress-of-update-download.injectable";
import assert from "assert";
import processCheckingForUpdatesInjectable from "./check-for-updates/process-checking-for-updates.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 checkForUpdatesTrayItemInjectable = getInjectable({
id: "check-for-updates-tray-item",
instantiate: (di) => {
const showApplicationWindow = di.inject(showApplicationWindowInjectable);
const updatingIsEnabled = di.inject(updatingIsEnabledInjectable);
const progressOfUpdateDownload = di.inject(progressOfUpdateDownloadInjectable);
const discoveredVersionState = di.inject(discoveredUpdateVersionInjectable);
const downloadingUpdateState = di.inject(updateIsBeingDownloadedInjectable);
const checkingForUpdatesState = di.inject(updatesAreBeingDiscoveredInjectable);
const processCheckingForUpdates = di.inject(processCheckingForUpdatesInjectable);
const withErrorLoggingFor = di.inject(withErrorLoggingInjectable);
return {
id: "check-for-updates",
parentId: null,
orderNumber: 30,
label: computed(() => {
if (downloadingUpdateState.value.get()) {
const discoveredVersion = discoveredVersionState.value.get();
assert(discoveredVersion);
const roundedPercentage = Math.round(progressOfUpdateDownload.value.get().percentage);
return `Downloading update ${discoveredVersion.version} (${roundedPercentage}%)...`;
}
if (checkingForUpdatesState.value.get()) {
return "Checking for updates...";
}
return "Check for updates";
}),
enabled: computed(() => !checkingForUpdatesState.value.get() && !downloadingUpdateState.value.get()),
visible: computed(() => updatingIsEnabled),
click: pipeline(
async () => {
await processCheckingForUpdates();
await showApplicationWindow();
},
withErrorLoggingFor(() => "[TRAY]: Checking for updates 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 checkForUpdatesTrayItemInjectable;
@@ -0,0 +1,23 @@
/**
* 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 type { ApplicationUpdateStatusChannelMessage } from "../../../common/application-update/application-update-status-channel.injectable";
import { messageToChannelInjectionToken } from "../../../common/utils/channel/message-to-channel-injection-token";
import applicationUpdateStatusChannelInjectable from "../../../common/application-update/application-update-status-channel.injectable";
const broadcastChangeInUpdatingStatusInjectable = getInjectable({
id: "broadcast-change-in-updating-status",
instantiate: (di) => {
const messageToChannel = di.inject(messageToChannelInjectionToken);
const applicationUpdateStatusChannel = di.inject(applicationUpdateStatusChannelInjectable);
return (data: ApplicationUpdateStatusChannelMessage) => {
messageToChannel(applicationUpdateStatusChannel, data);
};
},
});
export default broadcastChangeInUpdatingStatusInjectable;
@@ -0,0 +1,54 @@
/**
* 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 type { UpdateChannel } from "../../../common/application-update/update-channels";
import checkForPlatformUpdatesInjectable from "../check-for-platform-updates/check-for-platform-updates.injectable";
import updateCanBeDowngradedInjectable from "./update-can-be-downgraded.injectable";
export type CheckForUpdatesFromChannelResult = {
updateWasDiscovered: false;
} | {
updateWasDiscovered: true;
version: string;
actualUpdateChannel: UpdateChannel;
};
const checkForUpdatesStartingFromChannelInjectable = getInjectable({
id: "check-for-updates-starting-from-channel",
instantiate: (di) => {
const checkForPlatformUpdates = di.inject(
checkForPlatformUpdatesInjectable,
);
const updateCanBeDowngraded = di.inject(updateCanBeDowngradedInjectable);
const _recursiveCheck = async (
updateChannel: UpdateChannel,
): Promise<CheckForUpdatesFromChannelResult> => {
const result = await checkForPlatformUpdates(updateChannel, {
allowDowngrade: updateCanBeDowngraded.get(),
});
if (result.updateWasDiscovered) {
return {
updateWasDiscovered: true,
version: result.version,
actualUpdateChannel: updateChannel,
};
}
if (updateChannel.moreStableUpdateChannel) {
return await _recursiveCheck(updateChannel.moreStableUpdateChannel);
}
return { updateWasDiscovered: false };
};
return _recursiveCheck;
},
});
export default checkForUpdatesStartingFromChannelInjectable;
@@ -0,0 +1,93 @@
/**
* 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 selectedUpdateChannelInjectable from "../../../common/application-update/selected-update-channel/selected-update-channel.injectable";
import updatesAreBeingDiscoveredInjectable from "../../../common/application-update/updates-are-being-discovered/updates-are-being-discovered.injectable";
import discoveredUpdateVersionInjectable from "../../../common/application-update/discovered-update-version/discovered-update-version.injectable";
import { runInAction } from "mobx";
import askBooleanInjectable from "../../ask-boolean/ask-boolean.injectable";
import quitAndInstallUpdateInjectable from "../../electron-app/features/quit-and-install-update.injectable";
import downloadUpdateInjectable from "../download-update/download-update.injectable";
import broadcastChangeInUpdatingStatusInjectable from "./broadcast-change-in-updating-status.injectable";
import checkForUpdatesStartingFromChannelInjectable from "./check-for-updates-starting-from-channel.injectable";
import withOrphanPromiseInjectable from "../../../common/utils/with-orphan-promise/with-orphan-promise.injectable";
const processCheckingForUpdatesInjectable = getInjectable({
id: "process-checking-for-updates",
instantiate: (di) => {
const askBoolean = di.inject(askBooleanInjectable);
const quitAndInstallUpdate = di.inject(quitAndInstallUpdateInjectable);
const downloadUpdate = di.inject(downloadUpdateInjectable);
const selectedUpdateChannel = di.inject(selectedUpdateChannelInjectable);
const broadcastChangeInUpdatingStatus = di.inject(broadcastChangeInUpdatingStatusInjectable);
const checkingForUpdatesState = di.inject(updatesAreBeingDiscoveredInjectable);
const discoveredVersionState = di.inject(discoveredUpdateVersionInjectable);
const checkForUpdatesStartingFromChannel = di.inject(checkForUpdatesStartingFromChannelInjectable);
const withOrphanPromise = di.inject(withOrphanPromiseInjectable);
return async () => {
broadcastChangeInUpdatingStatus({ eventId: "checking-for-updates" });
runInAction(() => {
checkingForUpdatesState.set(true);
});
const result = await checkForUpdatesStartingFromChannel(selectedUpdateChannel.value.get());
if (!result.updateWasDiscovered) {
broadcastChangeInUpdatingStatus({ eventId: "no-updates-available" });
runInAction(() => {
discoveredVersionState.set(null);
checkingForUpdatesState.set(false);
});
return;
}
const { version, actualUpdateChannel } = result;
broadcastChangeInUpdatingStatus({
eventId: "download-for-update-started",
version,
});
runInAction(() => {
discoveredVersionState.set({
version,
updateChannel: actualUpdateChannel,
});
checkingForUpdatesState.set(false);
});
withOrphanPromise(async () => {
const { downloadWasSuccessful } = await downloadUpdate();
if (!downloadWasSuccessful) {
broadcastChangeInUpdatingStatus({
eventId: "download-for-update-failed",
});
return;
}
const userWantsToInstallUpdate = await askBoolean({
title: "Update Available",
question: `Version ${version} of Lens IDE is available and ready to be installed. Would you like to update now?\n\n` +
`Lens should restart automatically, if it doesn't please restart manually. Installed extensions might require updating.`,
});
if (userWantsToInstallUpdate) {
quitAndInstallUpdate();
}
})();
};
},
});
export default processCheckingForUpdatesInjectable;
@@ -0,0 +1,29 @@
/**
* 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 selectedUpdateChannelInjectable from "../../../common/application-update/selected-update-channel/selected-update-channel.injectable";
import appVersionInjectable from "../../../common/get-configuration-file-model/app-version/app-version.injectable";
import { SemVer } from "semver";
const updateCanBeDowngradedInjectable = getInjectable({
id: "update-can-be-downgraded",
instantiate: (di) => {
const selectedUpdateChannel = di.inject(selectedUpdateChannelInjectable);
const appVersion = di.inject(appVersionInjectable);
return computed(() => {
const semVer = new SemVer(appVersion);
return (
semVer.prerelease[0] !==
selectedUpdateChannel.value.get().id
);
});
},
});
export default updateCanBeDowngradedInjectable;
@@ -0,0 +1,45 @@
/**
* 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 electronUpdaterInjectable from "../../electron-app/features/electron-updater.injectable";
import loggerInjectable from "../../../common/logger.injectable";
import type { ProgressInfo } from "electron-updater";
import type { ProgressOfDownload } from "../../../common/application-update/progress-of-update-download/progress-of-update-download.injectable";
export type DownloadPlatformUpdate = (
onDownloadProgress: (arg: ProgressOfDownload) => void
) => Promise<{ downloadWasSuccessful: boolean }>;
const downloadPlatformUpdateInjectable = getInjectable({
id: "download-platform-update",
instantiate: (di): DownloadPlatformUpdate => {
const electronUpdater = di.inject(electronUpdaterInjectable);
const logger = di.inject(loggerInjectable);
return async (onDownloadProgress) => {
onDownloadProgress({ percentage: 0 });
const updateDownloadProgress = ({ percent: percentage }: ProgressInfo) =>
onDownloadProgress({ percentage });
electronUpdater.on("download-progress", updateDownloadProgress);
try {
await electronUpdater.downloadUpdate();
} catch(error) {
logger.error("[UPDATE-APP/DOWNLOAD]", error);
return { downloadWasSuccessful: false };
} finally {
electronUpdater.off("download-progress", updateDownloadProgress);
}
return { downloadWasSuccessful: true };
};
},
});
export default downloadPlatformUpdateInjectable;
@@ -0,0 +1,155 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import electronUpdaterInjectable from "../../electron-app/features/electron-updater.injectable";
import type { DownloadPlatformUpdate } from "./download-platform-update.injectable";
import downloadPlatformUpdateInjectable from "./download-platform-update.injectable";
import type { AppUpdater } from "electron-updater";
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import { getPromiseStatus } from "../../../common/test-utils/get-promise-status";
import type { DiContainer } from "@ogre-tools/injectable";
import loggerInjectable from "../../../common/logger.injectable";
import type { Logger } from "../../../common/logger";
describe("download-platform-update", () => {
let downloadPlatformUpdate: DownloadPlatformUpdate;
let downloadUpdateMock: AsyncFnMock<() => void>;
let electronUpdaterFake: AppUpdater;
let electronUpdaterOnMock: jest.Mock;
let electronUpdaterOffMock: jest.Mock;
let di: DiContainer;
let logErrorMock: jest.Mock;
beforeEach(() => {
di = getDiForUnitTesting();
downloadUpdateMock = asyncFn();
electronUpdaterOnMock = jest.fn();
electronUpdaterOffMock = jest.fn();
electronUpdaterFake = {
channel: undefined,
autoDownload: undefined,
on: electronUpdaterOnMock,
off: electronUpdaterOffMock,
downloadUpdate: downloadUpdateMock,
} as unknown as AppUpdater;
di.override(electronUpdaterInjectable, () => electronUpdaterFake);
logErrorMock = jest.fn();
di.override(loggerInjectable, () => ({ error: logErrorMock }) as unknown as Logger);
downloadPlatformUpdate = di.inject(downloadPlatformUpdateInjectable);
});
describe("when called", () => {
let actualPromise: Promise<{ downloadWasSuccessful: boolean }>;
let onDownloadProgressMock: jest.Mock;
beforeEach(() => {
onDownloadProgressMock = jest.fn();
actualPromise = downloadPlatformUpdate(onDownloadProgressMock);
});
it("calls for downloading of update", () => {
expect(downloadUpdateMock).toHaveBeenCalled();
});
it("does not resolve yet", async () => {
const promiseStatus = await getPromiseStatus(actualPromise);
expect(promiseStatus.fulfilled).toBe(false);
});
it("starts progress of download from 0", () => {
expect(onDownloadProgressMock).toHaveBeenCalledWith({ percentage: 0 });
});
describe("when downloading progresses", () => {
beforeEach(() => {
onDownloadProgressMock.mockClear();
const [, callback] = electronUpdaterOnMock.mock.calls.find(
([event]) => event === "download-progress",
);
callback({
percent: 42,
total: 0,
delta: 0,
transferred: 0,
bytesPerSecond: 0,
});
});
it("updates progress of the download", () => {
expect(onDownloadProgressMock).toHaveBeenCalledWith({ percentage: 42 });
});
describe("when downloading resolves", () => {
beforeEach(async () => {
onDownloadProgressMock.mockClear();
await downloadUpdateMock.resolve();
});
it("resolves with success", async () => {
const actual = await actualPromise;
expect(actual).toEqual({ downloadWasSuccessful: true });
});
it("does not reset progress of download yet", () => {
expect(onDownloadProgressMock).not.toHaveBeenCalled();
});
it("stops watching for download progress", () => {
expect(electronUpdaterOffMock).toHaveBeenCalledWith(
"download-progress",
expect.any(Function),
);
});
it("when starting download again, resets progress of download", () => {
downloadPlatformUpdate(onDownloadProgressMock);
expect(onDownloadProgressMock).toHaveBeenCalledWith({ percentage: 0 });
});
});
describe("when downloading rejects", () => {
let errorStub: Error;
beforeEach(() => {
errorStub = new Error("Some error");
downloadUpdateMock.reject(errorStub);
});
it("logs error", () => {
expect(logErrorMock).toHaveBeenCalledWith("[UPDATE-APP/DOWNLOAD]", errorStub);
});
it("stops watching for download progress", () => {
expect(electronUpdaterOffMock).toHaveBeenCalledWith(
"download-progress",
expect.any(Function),
);
});
it("resolves with failure", async () => {
const actual = await actualPromise;
expect(actual).toEqual({ downloadWasSuccessful: false });
});
});
});
});
});
@@ -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 downloadPlatformUpdateInjectable from "../download-platform-update/download-platform-update.injectable";
import updateIsBeingDownloadedInjectable from "../../../common/application-update/update-is-being-downloaded/update-is-being-downloaded.injectable";
import discoveredUpdateVersionInjectable from "../../../common/application-update/discovered-update-version/discovered-update-version.injectable";
import { action, runInAction } from "mobx";
import type { ProgressOfDownload } from "../../../common/application-update/progress-of-update-download/progress-of-update-download.injectable";
import progressOfUpdateDownloadInjectable from "../../../common/application-update/progress-of-update-download/progress-of-update-download.injectable";
const downloadUpdateInjectable = getInjectable({
id: "download-update",
instantiate: (di) => {
const downloadPlatformUpdate = di.inject(downloadPlatformUpdateInjectable);
const downloadingUpdateState = di.inject(updateIsBeingDownloadedInjectable);
const discoveredVersionState = di.inject(discoveredUpdateVersionInjectable);
const progressOfUpdateDownload = di.inject(progressOfUpdateDownloadInjectable);
const updateDownloadProgress = action((progressOfDownload: ProgressOfDownload) => {
progressOfUpdateDownload.set(progressOfDownload);
});
return async () => {
runInAction(() => {
progressOfUpdateDownload.set({ percentage: 0 });
downloadingUpdateState.set(true);
});
const { downloadWasSuccessful } = await downloadPlatformUpdate(
updateDownloadProgress,
);
runInAction(() => {
if (!downloadWasSuccessful) {
discoveredVersionState.set(null);
}
downloadingUpdateState.set(false);
});
return { downloadWasSuccessful };
};
},
});
export default downloadUpdateInjectable;
@@ -0,0 +1,56 @@
/**
* 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 { trayMenuItemInjectionToken } from "../tray/tray-menu-item/tray-menu-item-injection-token";
import quitAndInstallUpdateInjectable from "../electron-app/features/quit-and-install-update.injectable";
import discoveredUpdateVersionInjectable from "../../common/application-update/discovered-update-version/discovered-update-version.injectable";
import updateIsBeingDownloadedInjectable from "../../common/application-update/update-is-being-downloaded/update-is-being-downloaded.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 installApplicationUpdateTrayItemInjectable = getInjectable({
id: "install-update-tray-item",
instantiate: (di) => {
const quitAndInstallUpdate = di.inject(quitAndInstallUpdateInjectable);
const discoveredVersionState = di.inject(discoveredUpdateVersionInjectable);
const downloadingUpdateState = di.inject(updateIsBeingDownloadedInjectable);
const withErrorLoggingFor = di.inject(withErrorLoggingInjectable);
return {
id: "install-update",
parentId: null,
orderNumber: 50,
label: computed(() => {
const versionToBeInstalled = discoveredVersionState.value.get()?.version;
return `Install update ${versionToBeInstalled}`;
}),
enabled: computed(() => true),
visible: computed(
() => !!discoveredVersionState.value.get() && !downloadingUpdateState.value.get(),
),
click: pipeline(
quitAndInstallUpdate,
withErrorLoggingFor(() => "[TRAY]: Update installation 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 installApplicationUpdateTrayItemInjectable;
@@ -0,0 +1,35 @@
/**
* 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 processCheckingForUpdatesInjectable from "../check-for-updates/process-checking-for-updates.injectable";
const periodicalCheckForUpdatesInjectable = getInjectable({
id: "periodical-check-for-updates",
instantiate: (di) => {
const processCheckingForUpdates = di.inject(processCheckingForUpdatesInjectable);
return getStartableStoppable("periodical-check-for-updates", () => {
const TWO_HOURS = 1000 * 60 * 60 * 2;
// Note: intentional orphan promise to make checking for updates happen in the background
processCheckingForUpdates();
const intervalId = setInterval(() => {
// Note: intentional orphan promise to make checking for updates happen in the background
processCheckingForUpdates();
}, TWO_HOURS);
return () => {
clearInterval(intervalId);
};
});
},
causesSideEffects: true,
});
export default periodicalCheckForUpdatesInjectable;
@@ -0,0 +1,29 @@
/**
* 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 periodicalCheckForUpdatesInjectable from "./periodical-check-for-updates.injectable";
import { afterRootFrameIsReadyInjectionToken } from "../../start-main-application/runnable-tokens/after-root-frame-is-ready-injection-token";
import updatingIsEnabledInjectable from "../updating-is-enabled.injectable";
const startCheckingForUpdatesInjectable = getInjectable({
id: "start-checking-for-updates",
instantiate: (di) => {
const periodicalCheckForUpdates = di.inject(periodicalCheckForUpdatesInjectable);
const updatingIsEnabled = di.inject(updatingIsEnabledInjectable);
return {
run: async () => {
if (updatingIsEnabled) {
await periodicalCheckForUpdates.start();
}
},
};
},
injectionToken: afterRootFrameIsReadyInjectionToken,
});
export default startCheckingForUpdatesInjectable;
@@ -0,0 +1,27 @@
/**
* 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 { beforeQuitOfFrontEndInjectionToken } from "../../start-main-application/runnable-tokens/before-quit-of-front-end-injection-token";
import periodicalCheckForUpdatesInjectable from "./periodical-check-for-updates.injectable";
const stopCheckingForUpdatesInjectable = getInjectable({
id: "stop-checking-for-updates",
instantiate: (di) => {
const periodicalCheckForUpdates = di.inject(periodicalCheckForUpdatesInjectable);
return {
run: async () => {
if (periodicalCheckForUpdates.started) {
await periodicalCheckForUpdates.stop();
}
},
};
},
injectionToken: beforeQuitOfFrontEndInjectionToken,
});
export default stopCheckingForUpdatesInjectable;
@@ -0,0 +1,20 @@
/**
* 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 packageJsonInjectable from "../../common/vars/package-json.injectable";
import { has } from "lodash/fp";
// TOOO: Rename to something less technical
const publishIsConfiguredInjectable = getInjectable({
id: "publish-is-configured",
instantiate: (di) => {
const packageJson = di.inject(packageJsonInjectable);
return has("build.publish", packageJson);
},
});
export default publishIsConfiguredInjectable;
@@ -0,0 +1,20 @@
/**
* 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 electronUpdaterIsActiveInjectable from "../electron-app/features/electron-updater-is-active.injectable";
import publishIsConfiguredInjectable from "./publish-is-configured.injectable";
const updatingIsEnabledInjectable = getInjectable({
id: "updating-is-enabled",
instantiate: (di) => {
const electronUpdaterIsActive = di.inject(electronUpdaterIsActiveInjectable);
const publishIsConfigured = di.inject(publishIsConfiguredInjectable);
return electronUpdaterIsActive && publishIsConfigured;
},
});
export default updatingIsEnabledInjectable;
@@ -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 watchIfUpdateShouldHappenOnQuitInjectable from "./watch-if-update-should-happen-on-quit.injectable";
const startWatchingIfUpdateShouldHappenOnQuitInjectable = getInjectable({
id: "start-watching-if-update-should-happen-on-quit",
instantiate: (di) => {
const watchIfUpdateShouldHappenOnQuit = di.inject(watchIfUpdateShouldHappenOnQuitInjectable);
return {
run: () => {
watchIfUpdateShouldHappenOnQuit.start();
},
};
},
injectionToken: onLoadOfApplicationInjectionToken,
});
export default startWatchingIfUpdateShouldHappenOnQuitInjectable;
@@ -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 watchIfUpdateShouldHappenOnQuitInjectable from "./watch-if-update-should-happen-on-quit.injectable";
import { beforeQuitOfBackEndInjectionToken } from "../../start-main-application/runnable-tokens/before-quit-of-back-end-injection-token";
const stopWatchingIfUpdateShouldHappenOnQuitInjectable = getInjectable({
id: "stop-watching-if-update-should-happen-on-quit",
instantiate: (di) => {
const watchIfUpdateShouldHappenOnQuit = di.inject(watchIfUpdateShouldHappenOnQuitInjectable);
return {
run: () => {
watchIfUpdateShouldHappenOnQuit.stop();
},
};
},
injectionToken: beforeQuitOfBackEndInjectionToken,
});
export default stopWatchingIfUpdateShouldHappenOnQuitInjectable;
@@ -0,0 +1,54 @@
/**
* 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 { autorun } from "mobx";
import { getStartableStoppable } from "../../../common/utils/get-startable-stoppable";
import setUpdateOnQuitInjectable from "../../electron-app/features/set-update-on-quit.injectable";
import selectedUpdateChannelInjectable from "../../../common/application-update/selected-update-channel/selected-update-channel.injectable";
import type { UpdateChannel } from "../../../common/application-update/update-channels";
import discoveredUpdateVersionInjectable from "../../../common/application-update/discovered-update-version/discovered-update-version.injectable";
const watchIfUpdateShouldHappenOnQuitInjectable = getInjectable({
id: "watch-if-update-should-happen-on-quit",
instantiate: (di) => {
const setUpdateOnQuit = di.inject(setUpdateOnQuitInjectable);
const selectedUpdateChannel = di.inject(selectedUpdateChannelInjectable);
const discoveredVersionState = di.inject(discoveredUpdateVersionInjectable);
return getStartableStoppable("watch-if-update-should-happen-on-quit", () =>
autorun(() => {
const sufficientlyStableUpdateChannels =
getSufficientlyStableUpdateChannels(selectedUpdateChannel.value.get());
const discoveredVersion = discoveredVersionState.value.get();
const updateIsDiscoveredFromChannel = discoveredVersion?.updateChannel;
const updateOnQuit = updateIsDiscoveredFromChannel
? sufficientlyStableUpdateChannels.includes(
updateIsDiscoveredFromChannel,
)
: false;
setUpdateOnQuit(updateOnQuit);
}),
);
},
});
const getSufficientlyStableUpdateChannels = (updateChannel: UpdateChannel): UpdateChannel[] => {
if (!updateChannel.moreStableUpdateChannel) {
return [updateChannel];
}
return [
updateChannel,
...getSufficientlyStableUpdateChannels(updateChannel.moreStableUpdateChannel),
];
};
export default watchIfUpdateShouldHappenOnQuitInjectable;
@@ -0,0 +1,336 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ask-boolean given started when asking multiple questions renders 1`] = `
<body>
<div>
<div
class="Notifications flex column align-flex-end"
>
<div
class="Animate opacity notification flex info enter"
style="--enter-duration: 100ms; --leave-duration: 100ms;"
>
<div
class="box"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="info_outline"
>
info_outline
</span>
</i>
</div>
<div
class="message box grow"
>
<div
class="flex column gaps"
data-testid="ask-boolean-some-random-id-1"
>
<b>
some-title
</b>
<p>
Some question
</p>
<div
class="flex gaps row align-left box grow"
>
<button
class="Button light"
data-testid="ask-boolean-some-random-id-1-button-yes"
type="button"
>
Yes
</button>
<button
class="Button active outlined"
data-testid="ask-boolean-some-random-id-1-button-no"
type="button"
>
No
</button>
</div>
</div>
</div>
<div
class="box"
>
<i
class="Icon close material interactive focusable"
data-testid="close-notification-for-ask-boolean-for-some-random-id-1"
tabindex="0"
>
<span
class="icon"
data-icon-name="close"
>
close
</span>
</i>
</div>
</div>
<div
class="Animate opacity notification flex info enter"
style="--enter-duration: 100ms; --leave-duration: 100ms;"
>
<div
class="box"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="info_outline"
>
info_outline
</span>
</i>
</div>
<div
class="message box grow"
>
<div
class="flex column gaps"
data-testid="ask-boolean-some-random-id-2"
>
<b>
some-other-title
</b>
<p>
Some other question
</p>
<div
class="flex gaps row align-left box grow"
>
<button
class="Button light"
data-testid="ask-boolean-some-random-id-2-button-yes"
type="button"
>
Yes
</button>
<button
class="Button active outlined"
data-testid="ask-boolean-some-random-id-2-button-no"
type="button"
>
No
</button>
</div>
</div>
</div>
<div
class="box"
>
<i
class="Icon close material interactive focusable"
data-testid="close-notification-for-ask-boolean-for-some-random-id-2"
tabindex="0"
>
<span
class="icon"
data-icon-name="close"
>
close
</span>
</i>
</div>
</div>
</div>
</div>
</body>
`;
exports[`ask-boolean given started when asking multiple questions when answering to first question renders 1`] = `
<body>
<div>
<div
class="Notifications flex column align-flex-end"
>
<div
class="Animate opacity notification flex info enter"
style="--enter-duration: 100ms; --leave-duration: 100ms;"
>
<div
class="box"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="info_outline"
>
info_outline
</span>
</i>
</div>
<div
class="message box grow"
>
<div
class="flex column gaps"
data-testid="ask-boolean-some-random-id-2"
>
<b>
some-other-title
</b>
<p>
Some other question
</p>
<div
class="flex gaps row align-left box grow"
>
<button
class="Button light"
data-testid="ask-boolean-some-random-id-2-button-yes"
type="button"
>
Yes
</button>
<button
class="Button active outlined"
data-testid="ask-boolean-some-random-id-2-button-no"
type="button"
>
No
</button>
</div>
</div>
</div>
<div
class="box"
>
<i
class="Icon close material interactive focusable"
data-testid="close-notification-for-ask-boolean-for-some-random-id-2"
tabindex="0"
>
<span
class="icon"
data-icon-name="close"
>
close
</span>
</i>
</div>
</div>
</div>
</div>
</body>
`;
exports[`ask-boolean given started when asking question renders 1`] = `
<body>
<div>
<div
class="Notifications flex column align-flex-end"
>
<div
class="Animate opacity notification flex info enter"
style="--enter-duration: 100ms; --leave-duration: 100ms;"
>
<div
class="box"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="info_outline"
>
info_outline
</span>
</i>
</div>
<div
class="message box grow"
>
<div
class="flex column gaps"
data-testid="ask-boolean-some-random-id-1"
>
<b>
some-title
</b>
<p>
Some question
</p>
<div
class="flex gaps row align-left box grow"
>
<button
class="Button light"
data-testid="ask-boolean-some-random-id-1-button-yes"
type="button"
>
Yes
</button>
<button
class="Button active outlined"
data-testid="ask-boolean-some-random-id-1-button-no"
type="button"
>
No
</button>
</div>
</div>
</div>
<div
class="box"
>
<i
class="Icon close material interactive focusable"
data-testid="close-notification-for-ask-boolean-for-some-random-id-1"
tabindex="0"
>
<span
class="icon"
data-icon-name="close"
>
close
</span>
</i>
</div>
</div>
</div>
</div>
</body>
`;
exports[`ask-boolean given started when asking question when user answers "no" renders 1`] = `
<body>
<div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
</body>
`;
exports[`ask-boolean given started when asking question when user answers "yes" renders 1`] = `
<body>
<div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
</body>
`;
exports[`ask-boolean given started when asking question when user closes notification without answering the question renders 1`] = `
<body>
<div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
</body>
`;
@@ -0,0 +1,29 @@
/**
* 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 type { AskBooleanAnswerChannel } from "../../common/ask-boolean/ask-boolean-answer-channel.injectable";
import askBooleanAnswerChannelInjectable from "../../common/ask-boolean/ask-boolean-answer-channel.injectable";
import askBooleanPromiseInjectable from "./ask-boolean-promise.injectable";
import type { MessageChannelListener } from "../../common/utils/channel/message-channel-listener-injection-token";
import { messageChannelListenerInjectionToken } from "../../common/utils/channel/message-channel-listener-injection-token";
const askBooleanAnswerChannelListenerInjectable = getInjectable({
id: "ask-boolean-answer-channel-listener",
instantiate: (di): MessageChannelListener<AskBooleanAnswerChannel> => ({
channel: di.inject(askBooleanAnswerChannelInjectable),
handler: ({ id, value }) => {
const answerPromise = di.inject(askBooleanPromiseInjectable, id);
answerPromise.resolve(value);
},
}),
injectionToken: messageChannelListenerInjectionToken,
});
export default askBooleanAnswerChannelListenerInjectable;
@@ -0,0 +1,33 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
const askBooleanPromiseInjectable = getInjectable({
id: "ask-boolean-promise",
instantiate: (di, questionId: string) => {
void questionId;
let resolve: (value: boolean) => void;
const promise = new Promise<boolean>(_resolve => {
resolve = _resolve;
});
return ({
promise,
resolve: (value: boolean) => {
resolve(value);
},
});
},
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, questionId: string) => questionId,
}),
});
export default askBooleanPromiseInjectable;
@@ -0,0 +1,39 @@
/**
* 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 { messageToChannelInjectionToken } from "../../common/utils/channel/message-to-channel-injection-token";
import askBooleanQuestionChannelInjectable from "../../common/ask-boolean/ask-boolean-question-channel.injectable";
import askBooleanPromiseInjectable from "./ask-boolean-promise.injectable";
import getRandomIdInjectable from "../../common/utils/get-random-id.injectable";
export type AskBoolean = ({
title,
question,
}: {
title: string;
question: string;
}) => Promise<boolean>;
const askBooleanInjectable = getInjectable({
id: "ask-boolean",
instantiate: (di): AskBoolean => {
const messageToChannel = di.inject(messageToChannelInjectionToken);
const askBooleanChannel = di.inject(askBooleanQuestionChannelInjectable);
const getRandomId = di.inject(getRandomIdInjectable);
return async ({ title, question }) => {
const id = getRandomId();
const returnValuePromise = di.inject(askBooleanPromiseInjectable, id);
await messageToChannel(askBooleanChannel, { id, title, question });
return await returnValuePromise.promise;
};
},
});
export default askBooleanInjectable;
+206
View File
@@ -0,0 +1,206 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import type { AskBoolean } from "./ask-boolean.injectable";
import askBooleanInjectable from "./ask-boolean.injectable";
import { getPromiseStatus } from "../../common/test-utils/get-promise-status";
import type { RenderResult } from "@testing-library/react";
import { fireEvent } from "@testing-library/react";
import getRandomIdInjectable from "../../common/utils/get-random-id.injectable";
describe("ask-boolean", () => {
let applicationBuilder: ApplicationBuilder;
let askBoolean: AskBoolean;
beforeEach(() => {
applicationBuilder = getApplicationBuilder();
const getRandomIdFake = jest
.fn()
.mockReturnValueOnce("some-random-id-1")
.mockReturnValueOnce("some-random-id-2");
applicationBuilder.dis.mainDi.override(getRandomIdInjectable, () => getRandomIdFake);
askBoolean = applicationBuilder.dis.mainDi.inject(askBooleanInjectable);
});
describe("given started", () => {
let rendered: RenderResult;
beforeEach(async () => {
rendered = await applicationBuilder.render();
});
describe("when asking question", () => {
let actualPromise: Promise<boolean>;
beforeEach(() => {
actualPromise = askBoolean({
title: "some-title",
question: "Some question",
});
});
it("does not resolve yet", async () => {
const promiseStatus = await getPromiseStatus(actualPromise);
expect(promiseStatus.fulfilled).toBe(false);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows notification", () => {
const notification = rendered.getByTestId("ask-boolean-some-random-id-1");
expect(notification).not.toBeUndefined();
});
describe('when user answers "yes"', () => {
beforeEach(() => {
const button = rendered.getByTestId("ask-boolean-some-random-id-1-button-yes");
fireEvent.click(button);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not show notification anymore", () => {
const notification = rendered.queryByTestId("ask-boolean-some-random-id-1");
expect(notification).toBeNull();
});
it("resolves", async () => {
const actual = await actualPromise;
expect(actual).toBe(true);
});
});
describe('when user answers "no"', () => {
beforeEach(() => {
const button = rendered.getByTestId("ask-boolean-some-random-id-1-button-no");
fireEvent.click(button);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not show notification anymore", () => {
const notification = rendered.queryByTestId("ask-boolean-some-random-id-1");
expect(notification).toBeNull();
});
it("resolves", async () => {
const actual = await actualPromise;
expect(actual).toBe(false);
});
});
describe("when user closes notification without answering the question", () => {
beforeEach(() => {
const button = rendered.getByTestId("close-notification-for-ask-boolean-for-some-random-id-1");
fireEvent.click(button);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not show notification anymore", () => {
const notification = rendered.queryByTestId("ask-boolean-some-random-id-1");
expect(notification).toBeNull();
});
it("resolves", async () => {
const actual = await actualPromise;
expect(actual).toBe(false);
});
});
});
describe("when asking multiple questions", () => {
let firstQuestionPromise: Promise<boolean>;
let secondQuestionPromise: Promise<boolean>;
beforeEach(() => {
firstQuestionPromise = askBoolean({
title: "some-title",
question: "Some question",
});
secondQuestionPromise = askBoolean({
title: "some-other-title",
question: "Some other question",
});
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows notification for first question", () => {
const notification = rendered.getByTestId("ask-boolean-some-random-id-1");
expect(notification).not.toBeUndefined();
});
it("shows notification for second question", () => {
const notification = rendered.getByTestId("ask-boolean-some-random-id-2");
expect(notification).not.toBeUndefined();
});
describe("when answering to first question", () => {
beforeEach(() => {
const button = rendered.getByTestId("ask-boolean-some-random-id-1-button-no");
fireEvent.click(button);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not show notification for first question anymore", () => {
const notification = rendered.queryByTestId("ask-boolean-some-random-id-1");
expect(notification).toBeNull();
});
it("still shows notification for second question", () => {
const notification = rendered.getByTestId("ask-boolean-some-random-id-2");
expect(notification).not.toBeUndefined();
});
it("resolves first question", async () => {
const actual = await firstQuestionPromise;
expect(actual).toBe(false);
});
it("does not resolve second question yet", async () => {
const promiseStatus = await getPromiseStatus(secondQuestionPromise);
expect(promiseStatus.fulfilled).toBe(false);
});
});
});
});
});
@@ -17,6 +17,8 @@ const catalogSyncToRendererInjectable = getInjectable({
startCatalogSyncToRenderer(catalogEntityRegistry),
);
},
causesSideEffects: true,
});
export default catalogSyncToRendererInjectable;
@@ -0,0 +1,18 @@
/**
* 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 electronUpdaterInjectable from "./electron-updater.injectable";
const electronUpdaterIsActiveInjectable = getInjectable({
id: "electron-updater-is-active",
instantiate: (di) => {
const electronUpdater = di.inject(electronUpdaterInjectable);
return electronUpdater.isUpdaterActive();
},
});
export default electronUpdaterIsActiveInjectable;
@@ -0,0 +1,14 @@
/**
* 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 { autoUpdater } from "electron-updater";
const electronUpdaterInjectable = getInjectable({
id: "electron-updater",
instantiate: () => autoUpdater,
causesSideEffects: true,
});
export default electronUpdaterInjectable;
@@ -0,0 +1,20 @@
/**
* 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 electronUpdaterInjectable from "./electron-updater.injectable";
const quitAndInstallUpdateInjectable = getInjectable({
id: "quit-and-install-update",
instantiate: (di) => {
const electronUpdater = di.inject(electronUpdaterInjectable);
return () => {
electronUpdater.quitAndInstall(true, true);
};
},
});
export default quitAndInstallUpdateInjectable;
@@ -0,0 +1,20 @@
/**
* 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 electronUpdaterInjectable from "./electron-updater.injectable";
const setUpdateOnQuitInjectable = getInjectable({
id: "set-update-on-quit",
instantiate: (di) => {
const electronUpdater = di.inject(electronUpdaterInjectable);
return (updateOnQuit: boolean) => {
electronUpdater.autoInstallOnAppQuit = updateOnQuit;
};
},
});
export default setUpdateOnQuitInjectable;
@@ -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 { afterRootFrameIsReadyInjectionToken } from "../../start-main-application/runnable-tokens/after-root-frame-is-ready-injection-token";
import startUpdateCheckingInjectable from "../../start-update-checking.injectable";
const setupUpdateCheckingInjectable = getInjectable({
id: "setup-update-checking",
instantiate: (di) => {
const startUpdateChecking = di.inject(startUpdateCheckingInjectable);
return {
run: () => {
startUpdateChecking();
},
};
},
injectionToken: afterRootFrameIsReadyInjectionToken,
});
export default setupUpdateCheckingInjectable;
+34 -13
View File
@@ -4,12 +4,11 @@
*/
import glob from "glob";
import { kebabCase, memoize } from "lodash/fp";
import { kebabCase, memoize, noop } from "lodash/fp";
import type { DiContainer } from "@ogre-tools/injectable";
import { createContainer } from "@ogre-tools/injectable";
import { Environments, setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
import appNameInjectable from "./app-paths/app-name/app-name.injectable";
import registerChannelInjectable from "./app-paths/register-channel/register-channel.injectable";
import writeJsonFileInjectable from "../common/fs/write-json-file.injectable";
import readJsonFileInjectable from "../common/fs/read-json-file.injectable";
import readFileInjectable from "../common/fs/read-file.injectable";
@@ -29,8 +28,6 @@ import { getAbsolutePathFake } from "../common/test-utils/get-absolute-path-fake
import joinPathsInjectable from "../common/path/join-paths.injectable";
import { joinPathsFake } from "../common/test-utils/join-paths-fake";
import hotbarStoreInjectable from "../common/hotbars/store.injectable";
import type { GetDiForUnitTestingOptions } from "../test-utils/get-dis-for-unit-testing";
import isAutoUpdateEnabledInjectable from "./is-auto-update-enabled.injectable";
import appEventBusInjectable from "../common/app-event-bus/app-event-bus.injectable";
import { EventEmitter } from "../common/event-emitter";
import type { AppEvent } from "../common/app-event-bus/event-bus";
@@ -45,7 +42,6 @@ import setupSentryInjectable from "./start-main-application/runnables/setup-sent
import setupShellInjectable from "./start-main-application/runnables/setup-shell.injectable";
import setupSyncingOfWeblinksInjectable from "./start-main-application/runnables/setup-syncing-of-weblinks.injectable";
import stopServicesAndExitAppInjectable from "./stop-services-and-exit-app.injectable";
import trayInjectable from "./tray/tray.injectable";
import applicationMenuInjectable from "./menu/application-menu.injectable";
import isDevelopmentInjectable from "../common/vars/is-development.injectable";
import setupSystemCaInjectable from "./start-main-application/runnables/setup-system-ca.injectable";
@@ -67,7 +63,7 @@ import type { ClusterFrameInfo } from "../common/cluster-frames";
import { observable } from "mobx";
import waitForElectronToBeReadyInjectable from "./electron-app/features/wait-for-electron-to-be-ready.injectable";
import setupListenerForCurrentClusterFrameInjectable from "./start-main-application/lens-window/current-cluster-frame/setup-listener-for-current-cluster-frame.injectable";
import ipcMainInjectable from "./app-paths/register-channel/ipc-main/ipc-main.injectable";
import ipcMainInjectable from "./utils/channel/ipc-main/ipc-main.injectable";
import createElectronWindowForInjectable from "./start-main-application/lens-window/application-window/create-electron-window-for.injectable";
import setupRunnablesAfterWindowIsOpenedInjectable from "./electron-app/runnables/setup-runnables-after-window-is-opened.injectable";
import sendToChannelInElectronBrowserWindowInjectable from "./start-main-application/lens-window/application-window/send-to-channel-in-electron-browser-window.injectable";
@@ -75,10 +71,21 @@ import broadcastMessageInjectable from "../common/ipc/broadcast-message.injectab
import getElectronThemeInjectable from "./electron-app/features/get-electron-theme.injectable";
import syncThemeFromOperatingSystemInjectable from "./electron-app/features/sync-theme-from-operating-system.injectable";
import platformInjectable from "../common/vars/platform.injectable";
import { noop } from "../renderer/utils";
import productNameInjectable from "./app-paths/app-name/product-name.injectable";
import quitAndInstallUpdateInjectable from "./electron-app/features/quit-and-install-update.injectable";
import electronUpdaterIsActiveInjectable from "./electron-app/features/electron-updater-is-active.injectable";
import publishIsConfiguredInjectable from "./application-update/publish-is-configured.injectable";
import checkForPlatformUpdatesInjectable from "./application-update/check-for-platform-updates/check-for-platform-updates.injectable";
import baseBundeledBinariesDirectoryInjectable from "../common/vars/base-bundled-binaries-dir.injectable";
import setUpdateOnQuitInjectable from "./electron-app/features/set-update-on-quit.injectable";
import downloadPlatformUpdateInjectable from "./application-update/download-platform-update/download-platform-update.injectable";
import startCatalogSyncInjectable from "./catalog-sync-to-renderer/start-catalog-sync.injectable";
import startKubeConfigSyncInjectable from "./start-main-application/runnables/kube-config-sync/start-kube-config-sync.injectable";
import appVersionInjectable from "../common/get-configuration-file-model/app-version/app-version.injectable";
import getRandomIdInjectable from "../common/utils/get-random-id.injectable";
import periodicalCheckForUpdatesInjectable from "./application-update/periodical-check-for-updates/periodical-check-for-updates.injectable";
export function getDiForUnitTesting(opts: GetDiForUnitTestingOptions = {}) {
export function getDiForUnitTesting(opts: { doGeneralOverrides?: boolean } = {}) {
const {
doGeneralOverrides = false,
} = opts;
@@ -100,10 +107,11 @@ export function getDiForUnitTesting(opts: GetDiForUnitTestingOptions = {}) {
di.preventSideEffects();
if (doGeneralOverrides) {
di.override(getRandomIdInjectable, () => () => "some-irrelevant-random-id");
di.override(hotbarStoreInjectable, () => ({ load: () => {} }));
di.override(userStoreInjectable, () => ({ startMainReactions: () => {} }) as UserStore);
di.override(userStoreInjectable, () => ({ startMainReactions: () => {}, extensionRegistryUrl: { customUrl: "some-custom-url" }}) as UserStore);
di.override(extensionsStoreInjectable, () => ({ isEnabled: (opts) => (void opts, false) }) as ExtensionsStore);
di.override(clusterStoreInjectable, () => ({ getById: (id) => (void id, {}) as Cluster }) as ClusterStore);
di.override(clusterStoreInjectable, () => ({ provideInitialFromMain: () => {}, getById: (id) => (void id, {}) as Cluster }) as ClusterStore);
di.override(fileSystemProvisionerStoreInjectable, () => ({}) as FileSystemProvisionerStore);
overrideOperatingSystem(di);
@@ -114,19 +122,22 @@ export function getDiForUnitTesting(opts: GetDiForUnitTestingOptions = {}) {
di.override(environmentVariablesInjectable, () => ({}));
di.override(commandLineArgumentsInjectable, () => []);
di.override(productNameInjectable, () => "some-product-name");
di.override(appVersionInjectable, () => "1.0.0");
di.override(clusterFramesInjectable, () => observable.map<string, ClusterFrameInfo>());
di.override(stopServicesAndExitAppInjectable, () => () => {});
di.override(lensResourcesDirInjectable, () => "/irrelevant");
di.override(trayInjectable, () => ({ start: () => {}, stop: () => {} }));
di.override(applicationMenuInjectable, () => ({ start: () => {}, stop: () => {} }));
di.override(periodicalCheckForUpdatesInjectable, () => ({ start: () => {}, stop: () => {}, started: false }));
// TODO: Remove usages of globally exported appEventBus to get rid of this
di.override(appEventBusInjectable, () => new EventEmitter<[AppEvent]>());
di.override(appNameInjectable, () => "some-app-name");
di.override(registerChannelInjectable, () => () => undefined);
di.override(broadcastMessageInjectable, () => (channel) => {
throw new Error(`Tried to broadcast message to channel "${channel}" over IPC without explicit override.`);
});
@@ -182,6 +193,8 @@ const overrideRunnablesHavingSideEffects = (di: DiContainer) => {
setupSystemCaInjectable,
setupListenerForCurrentClusterFrameInjectable,
setupRunnablesAfterWindowIsOpenedInjectable,
startCatalogSyncInjectable,
startKubeConfigSyncInjectable,
].forEach((injectable) => {
di.override(injectable, () => ({ run: () => {} }));
});
@@ -215,6 +228,13 @@ const overrideElectronFeatures = (di: DiContainer) => {
di.override(ipcMainInjectable, () => ({}));
di.override(getElectronThemeInjectable, () => () => "dark");
di.override(syncThemeFromOperatingSystemInjectable, () => ({ start: () => {}, stop: () => {} }));
di.override(quitAndInstallUpdateInjectable, () => () => {});
di.override(setUpdateOnQuitInjectable, () => () => {});
di.override(downloadPlatformUpdateInjectable, () => async () => ({ downloadWasSuccessful: true }));
di.override(checkForPlatformUpdatesInjectable, () => () => {
throw new Error("Tried to check for platform updates without explicit override.");
});
di.override(createElectronWindowForInjectable, () => () => async () => ({
show: () => {},
@@ -234,5 +254,6 @@ const overrideElectronFeatures = (di: DiContainer) => {
);
di.override(setElectronAppPathInjectable, () => () => {});
di.override(isAutoUpdateEnabledInjectable, () => () => false);
di.override(publishIsConfiguredInjectable, () => false);
di.override(electronUpdaterIsActiveInjectable, () => false);
};
@@ -1,19 +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 { isPublishConfigured } from "../common/vars";
import { autoUpdater } from "electron-updater";
const isAutoUpdateEnabledInjectable = getInjectable({
id: "is-auto-update-enabled",
instantiate: () => () => {
return autoUpdater.isUpdaterActive() && isPublishConfigured;
},
causesSideEffects: true,
});
export default isAutoUpdateEnabledInjectable;
@@ -3,7 +3,6 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { checkForUpdates } from "../app-updater";
import { docsUrl, productName, supportUrl } from "../../common/vars";
import { broadcastMessage } from "../../common/ipc";
import { openBrowser } from "../../common/utils";
@@ -12,7 +11,7 @@ import { webContents } from "electron";
import loggerInjectable from "../../common/logger.injectable";
import appNameInjectable from "../app-paths/app-name/app-name.injectable";
import electronMenuItemsInjectable from "./electron-menu-items.injectable";
import isAutoUpdateEnabledInjectable from "../is-auto-update-enabled.injectable";
import updatingIsEnabledInjectable from "../application-update/updating-is-enabled.injectable";
import navigateToPreferencesInjectable from "../../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable";
import navigateToExtensionsInjectable from "../../common/front-end-routing/routes/extensions/navigate-to-extensions.injectable";
import navigateToCatalogInjectable from "../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable";
@@ -25,6 +24,7 @@ import showAboutInjectable from "./show-about.injectable";
import applicationWindowInjectable from "../start-main-application/lens-window/application-window/application-window.injectable";
import reloadWindowInjectable from "../start-main-application/lens-window/reload-window.injectable";
import showApplicationWindowInjectable from "../start-main-application/lens-window/show-application-window.injectable";
import processCheckingForUpdatesInjectable from "../application-update/check-for-updates/process-checking-for-updates.injectable";
function ignoreIf(check: boolean, menuItems: MenuItemOpts[]) {
return check ? [] : menuItems;
@@ -41,7 +41,7 @@ const applicationMenuItemsInjectable = getInjectable({
const logger = di.inject(loggerInjectable);
const appName = di.inject(appNameInjectable);
const isMac = di.inject(isMacInjectable);
const isAutoUpdateEnabled = di.inject(isAutoUpdateEnabledInjectable);
const updatingIsEnabled = di.inject(updatingIsEnabledInjectable);
const electronMenuItems = di.inject(electronMenuItemsInjectable);
const showAbout = di.inject(showAboutInjectable);
const applicationWindow = di.inject(applicationWindowInjectable);
@@ -53,12 +53,11 @@ const applicationMenuItemsInjectable = getInjectable({
const navigateToWelcome = di.inject(navigateToWelcomeInjectable);
const navigateToAddCluster = di.inject(navigateToAddClusterInjectable);
const stopServicesAndExitApp = di.inject(stopServicesAndExitAppInjectable);
const processCheckingForUpdates = di.inject(processCheckingForUpdatesInjectable);
logger.info(`[MENU]: autoUpdateEnabled=${updatingIsEnabled}`);
return computed((): MenuItemOpts[] => {
const autoUpdateDisabled = !isAutoUpdateEnabled();
logger.info(`[MENU]: autoUpdateDisabled=${autoUpdateDisabled}`);
const macAppMenu: MenuItemOpts = {
label: appName,
id: "root",
@@ -70,11 +69,11 @@ const applicationMenuItemsInjectable = getInjectable({
showAbout();
},
},
...ignoreIf(autoUpdateDisabled, [
...ignoreIf(!updatingIsEnabled, [
{
label: "Check for updates",
click() {
checkForUpdates().then(() => showApplicationWindow());
processCheckingForUpdates().then(() => showApplicationWindow());
},
},
]),
@@ -282,11 +281,11 @@ const applicationMenuItemsInjectable = getInjectable({
showAbout();
},
},
...ignoreIf(autoUpdateDisabled, [
...ignoreIf(!updatingIsEnabled, [
{
label: "Check for updates",
click() {
checkForUpdates().then(() =>
processCheckingForUpdates().then(() =>
showApplicationWindow(),
);
},
@@ -11,7 +11,7 @@ import appNameInjectable from "../../../app-paths/app-name/app-name.injectable";
import appEventBusInjectable from "../../../../common/app-event-bus/app-event-bus.injectable";
import { delay } from "../../../../common/utils";
import { bundledExtensionsLoaded } from "../../../../common/ipc/extension-handling";
import ipcMainInjectable from "../../../app-paths/register-channel/ipc-main/ipc-main.injectable";
import ipcMainInjectable from "../../../utils/channel/ipc-main/ipc-main.injectable";
const applicationWindowInjectable = getInjectable({
id: "application-window",
@@ -58,9 +58,9 @@ const createLensWindowInjectable = getInjectable({
browserWindow?.close();
browserWindow = undefined;
},
send: async (args: SendToViewArgs) => {
send: (args: SendToViewArgs) => {
if (!browserWindow) {
browserWindow = await createElectronWindow();
throw new Error(`Tried to send message to window "${configuration.id}" but the window was closed`);
}
return browserWindow.send(args);
@@ -14,7 +14,7 @@ export interface SendToViewArgs {
export interface LensWindow {
show: () => Promise<void>;
close: () => void;
send: (args: SendToViewArgs) => Promise<void>;
send: (args: SendToViewArgs) => void;
visible: boolean;
}
@@ -36,7 +36,7 @@ const navigateForExtensionInjectable = getInjectable({
(frameInfo) => frameInfo.frameId === frameId,
);
await applicationWindow.send({
applicationWindow.send({
channel: "extension:navigate",
frameInfo,
data: [extId, pageId, params],
@@ -29,7 +29,7 @@ const navigateInjectable = getInjectable({
? IpcRendererNavigationEvents.NAVIGATE_IN_CLUSTER
: IpcRendererNavigationEvents.NAVIGATE_IN_APP;
await applicationWindow.send({
applicationWindow.send({
channel,
frameInfo,
data: [url],
@@ -25,6 +25,8 @@ const startKubeConfigSyncInjectable = getInjectable({
};
},
causesSideEffects: true,
injectionToken: afterRootFrameIsReadyInjectionToken,
});
@@ -0,0 +1,35 @@
/**
* 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 rootFrameRenderedChannelInjectable from "../../../../common/root-frame-rendered-channel/root-frame-rendered-channel.injectable";
import { runManyFor } from "../../../../common/runnable/run-many-for";
import { afterRootFrameIsReadyInjectionToken } from "../../runnable-tokens/after-root-frame-is-ready-injection-token";
import { messageChannelListenerInjectionToken } from "../../../../common/utils/channel/message-channel-listener-injection-token";
const rootFrameRenderedChannelListenerInjectable = getInjectable({
id: "root-frame-rendered-channel-listener",
instantiate: (di) => {
const channel = di.inject(rootFrameRenderedChannelInjectable);
const runMany = runManyFor(di);
const runRunnablesAfterRootFrameIsReady = runMany(
afterRootFrameIsReadyInjectionToken,
);
return {
channel,
handler: async () => {
await runRunnablesAfterRootFrameIsReady();
},
};
},
injectionToken: messageChannelListenerInjectionToken,
});
export default rootFrameRenderedChannelListenerInjectable;
@@ -5,7 +5,7 @@
import { getInjectable } from "@ogre-tools/injectable";
import { initializeSentryReporting } from "../../../common/sentry";
import { init } from "@sentry/electron/main";
import { onLoadOfApplicationInjectionToken } from "../runnable-tokens/on-load-of-application-injection-token";
import { beforeApplicationIsLoadingInjectionToken } from "../runnable-tokens/before-application-is-loading-injection-token";
const setupSentryInjectable = getInjectable({
id: "setup-sentry",
@@ -18,7 +18,7 @@ const setupSentryInjectable = getInjectable({
causesSideEffects: true,
injectionToken: onLoadOfApplicationInjectionToken,
injectionToken: beforeApplicationIsLoadingInjectionToken,
});
export default setupSentryInjectable;
@@ -1,19 +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 { startUpdateChecking } from "./app-updater";
import isAutoUpdateEnabledInjectable from "./is-auto-update-enabled.injectable";
const startUpdateCheckingInjectable = getInjectable({
id: "start-update-checking",
instantiate: (di) => startUpdateChecking({
isAutoUpdateEnabled: di.inject(isAutoUpdateEnabledInjectable),
}),
causesSideEffects: true,
});
export default startUpdateCheckingInjectable;
@@ -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;
@@ -1,33 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { nextUpdateChannel } from "../update-channel";
describe("nextUpdateChannel", () => {
it("returns latest if current channel is latest", () => {
expect(nextUpdateChannel("latest", "latest")).toEqual("latest");
});
it("returns beta if current channel is alpha", () => {
expect(nextUpdateChannel("alpha", "alpha")).toEqual("beta");
expect(nextUpdateChannel("beta", "alpha")).toEqual("beta");
expect(nextUpdateChannel("rc", "alpha")).toEqual("beta");
expect(nextUpdateChannel("latest", "alpha")).toEqual("beta");
});
it("returns latest if current channel is beta", () => {
expect(nextUpdateChannel("alpha", "beta")).toEqual("latest");
expect(nextUpdateChannel("beta", "beta")).toEqual("latest");
expect(nextUpdateChannel("rc", "beta")).toEqual("latest");
expect(nextUpdateChannel("latest", "beta")).toEqual("latest");
});
it("returns default if current channel is unknown", () => {
expect(nextUpdateChannel("alpha", "rc")).toEqual("alpha");
expect(nextUpdateChannel("beta", "rc")).toEqual("beta");
expect(nextUpdateChannel("rc", "rc")).toEqual("rc");
expect(nextUpdateChannel("latest", "rc")).toEqual("latest");
});
});
@@ -0,0 +1,38 @@
/**
* 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 type { IpcMainEvent } from "electron";
import ipcMainInjectable from "../ipc-main/ipc-main.injectable";
import { enlistMessageChannelListenerInjectionToken } from "../../../../common/utils/channel/enlist-message-channel-listener-injection-token";
import { pipeline } from "@ogre-tools/fp";
import { tentativeParseJson } from "../../../../common/utils/tentative-parse-json";
const enlistMessageChannelListenerInjectable = getInjectable({
id: "enlist-message-channel-listener-for-main",
instantiate: (di) => {
const ipcMain = di.inject(ipcMainInjectable);
return ({ channel, handler }) => {
const nativeOnCallback = (_: IpcMainEvent, message: unknown) => {
pipeline(
message,
tentativeParseJson,
handler,
);
};
ipcMain.on(channel.id, nativeOnCallback);
return () => {
ipcMain.off(channel.id, nativeOnCallback);
};
};
},
injectionToken: enlistMessageChannelListenerInjectionToken,
});
export default enlistMessageChannelListenerInjectable;
@@ -0,0 +1,97 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import ipcMainInjectable from "../ipc-main/ipc-main.injectable";
import type { EnlistMessageChannelListener } from "../../../../common/utils/channel/enlist-message-channel-listener-injection-token";
import { enlistMessageChannelListenerInjectionToken } from "../../../../common/utils/channel/enlist-message-channel-listener-injection-token";
import type { IpcMain, IpcMainEvent } from "electron";
describe("enlist message channel listener in main", () => {
let enlistMessageChannelListener: EnlistMessageChannelListener;
let ipcMainStub: IpcMain;
let onMock: jest.Mock;
let offMock: jest.Mock;
beforeEach(() => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
onMock = jest.fn();
offMock = jest.fn();
ipcMainStub = {
on: onMock,
off: offMock,
} as unknown as IpcMain;
di.override(ipcMainInjectable, () => ipcMainStub);
enlistMessageChannelListener = di.inject(
enlistMessageChannelListenerInjectionToken,
);
});
describe("when called", () => {
let handlerMock: jest.Mock;
let disposer: () => void;
beforeEach(() => {
handlerMock = jest.fn();
disposer = enlistMessageChannelListener({
channel: { id: "some-channel-id" },
handler: handlerMock,
});
});
it("does not call handler yet", () => {
expect(handlerMock).not.toHaveBeenCalled();
});
it("registers the listener", () => {
expect(onMock).toHaveBeenCalledWith(
"some-channel-id",
expect.any(Function),
);
});
it("does not de-register the listener yet", () => {
expect(offMock).not.toHaveBeenCalled();
});
describe("when message arrives", () => {
beforeEach(() => {
onMock.mock.calls[0][1]({} as IpcMainEvent, "some-message");
});
it("calls the handler with the message", () => {
expect(handlerMock).toHaveBeenCalledWith("some-message");
});
it("when disposing the listener, de-registers the listener", () => {
disposer();
expect(offMock).toHaveBeenCalledWith("some-channel-id", expect.any(Function));
});
});
it("given number as message, when message arrives, calls the handler with the message", () => {
onMock.mock.calls[0][1]({} as IpcMainEvent, 42);
expect(handlerMock).toHaveBeenCalledWith(42);
});
it("given boolean as message, when message arrives, calls the handler with the message", () => {
onMock.mock.calls[0][1]({} as IpcMainEvent, true);
expect(handlerMock).toHaveBeenCalledWith(true);
});
it("given stringified object as message, when message arrives, calls the handler with the message", () => {
onMock.mock.calls[0][1]({} as IpcMainEvent, JSON.stringify({ some: "object" }));
expect(handlerMock).toHaveBeenCalledWith({ some: "object" });
});
});
});
@@ -0,0 +1,34 @@
/**
* 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 type { IpcMainInvokeEvent } from "electron";
import ipcMainInjectable from "../ipc-main/ipc-main.injectable";
import { enlistRequestChannelListenerInjectionToken } from "../../../../common/utils/channel/enlist-request-channel-listener-injection-token";
import { pipeline } from "@ogre-tools/fp";
import { tentativeParseJson } from "../../../../common/utils/tentative-parse-json";
import { tentativeStringifyJson } from "../../../../common/utils/tentative-stringify-json";
const enlistRequestChannelListenerInjectable = getInjectable({
id: "enlist-request-channel-listener-for-main",
instantiate: (di) => {
const ipcMain = di.inject(ipcMainInjectable);
return ({ channel, handler }) => {
const nativeHandleCallback = (_: IpcMainInvokeEvent, request: unknown) =>
pipeline(request, tentativeParseJson, handler, tentativeStringifyJson);
ipcMain.handle(channel.id, nativeHandleCallback);
return () => {
ipcMain.off(channel.id, nativeHandleCallback);
};
};
},
injectionToken: enlistRequestChannelListenerInjectionToken,
});
export default enlistRequestChannelListenerInjectable;
@@ -0,0 +1,147 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
import ipcMainInjectable from "../ipc-main/ipc-main.injectable";
import type { IpcMain, IpcMainInvokeEvent } from "electron";
import type { EnlistRequestChannelListener } from "../../../../common/utils/channel/enlist-request-channel-listener-injection-token";
import { enlistRequestChannelListenerInjectionToken } from "../../../../common/utils/channel/enlist-request-channel-listener-injection-token";
import { getPromiseStatus } from "../../../../common/test-utils/get-promise-status";
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
describe("enlist request channel listener in main", () => {
let enlistRequestChannelListener: EnlistRequestChannelListener;
let ipcMainStub: IpcMain;
let handleMock: jest.Mock;
let offMock: jest.Mock;
beforeEach(() => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
handleMock = jest.fn();
offMock = jest.fn();
ipcMainStub = {
handle: handleMock,
off: offMock,
} as unknown as IpcMain;
di.override(ipcMainInjectable, () => ipcMainStub);
enlistRequestChannelListener = di.inject(
enlistRequestChannelListenerInjectionToken,
);
});
describe("when called", () => {
let handlerMock: AsyncFnMock<(message: any) => any>;
let disposer: () => void;
beforeEach(() => {
handlerMock = asyncFn();
disposer = enlistRequestChannelListener({
channel: { id: "some-channel-id" },
handler: handlerMock,
});
});
it("does not call handler yet", () => {
expect(handlerMock).not.toHaveBeenCalled();
});
it("registers the listener", () => {
expect(handleMock).toHaveBeenCalledWith(
"some-channel-id",
expect.any(Function),
);
});
it("does not de-register the listener yet", () => {
expect(offMock).not.toHaveBeenCalled();
});
describe("when request arrives", () => {
let actualPromise: Promise<any>;
beforeEach(() => {
actualPromise = handleMock.mock.calls[0][1](
{} as IpcMainInvokeEvent,
"some-request",
);
});
it("calls the handler with the request", () => {
expect(handlerMock).toHaveBeenCalledWith("some-request");
});
it("does not resolve yet", async () => {
const promiseStatus = await getPromiseStatus(actualPromise);
expect(promiseStatus.fulfilled).toBe(false);
});
describe("when handler resolves with response, listener resolves with the response", () => {
beforeEach(async () => {
await handlerMock.resolve("some-response");
});
it("resolves with the response", async () => {
const actual = await actualPromise;
expect(actual).toBe('"some-response"');
});
it("when disposing the listener, de-registers the listener", () => {
disposer();
expect(offMock).toHaveBeenCalledWith("some-channel-id", expect.any(Function));
});
});
it("given number as response, when handler resolves with response, listener resolves with stringified response", async () => {
await handlerMock.resolve(42);
const actual = await actualPromise;
expect(actual).toBe("42");
});
it("given boolean as response, when handler resolves with response, listener resolves with stringified response", async () => {
await handlerMock.resolve(true);
const actual = await actualPromise;
expect(actual).toBe("true");
});
it("given object as response, when handler resolves with response, listener resolves with stringified response", async () => {
await handlerMock.resolve({ some: "object" });
const actual = await actualPromise;
expect(actual).toBe(JSON.stringify({ some: "object" }));
});
});
it("given number as request, when request arrives, calls the handler with the request", () => {
handleMock.mock.calls[0][1]({} as IpcMainInvokeEvent, 42);
expect(handlerMock).toHaveBeenCalledWith(42);
});
it("given boolean as request, when request arrives, calls the handler with the request", () => {
handleMock.mock.calls[0][1]({} as IpcMainInvokeEvent, true);
expect(handlerMock).toHaveBeenCalledWith(true);
});
it("given stringified object as request, when request arrives, calls the handler with the request", () => {
handleMock.mock.calls[0][1]({} as IpcMainInvokeEvent, JSON.stringify({ some: "object" }));
expect(handlerMock).toHaveBeenCalledWith({ some: "object" });
});
});
});
@@ -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 listeningOfChannelsInjectable from "../../../../common/utils/channel/listening-of-channels.injectable";
const startListeningOfChannelsInjectable = getInjectable({
id: "start-listening-of-channels-main",
instantiate: (di) => {
const listeningOfChannels = di.inject(listeningOfChannelsInjectable);
return {
run: async () => {
await listeningOfChannels.start();
},
};
},
injectionToken: onLoadOfApplicationInjectionToken,
});
export default startListeningOfChannelsInjectable;
@@ -0,0 +1,39 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { lensWindowInjectionToken } from "../../start-main-application/lens-window/application-window/lens-window-injection-token";
import { pipeline } from "@ogre-tools/fp";
import { getInjectable } from "@ogre-tools/injectable";
import { filter } from "lodash/fp";
import { messageToChannelInjectionToken } from "../../../common/utils/channel/message-to-channel-injection-token";
import type { MessageChannel } from "../../../common/utils/channel/message-channel-injection-token";
import { tentativeStringifyJson } from "../../../common/utils/tentative-stringify-json";
const messageToChannelInjectable = getInjectable({
id: "message-to-channel",
instantiate: (di) => {
const getAllLensWindows = () => di.injectMany(lensWindowInjectionToken);
// TODO: Figure out way to improve typing in internals
// Notice that this should be injected using "messageToChannelInjectionToken" which is typed correctly.
return (channel: MessageChannel<any>, message?: unknown) => {
const stringifiedMessage = tentativeStringifyJson(message);
const visibleWindows = pipeline(
getAllLensWindows(),
filter((lensWindow) => !!lensWindow.visible),
);
visibleWindows.forEach((lensWindow) =>
lensWindow.send({ channel: channel.id, data: stringifiedMessage ? [stringifiedMessage] : [] }),
);
};
},
injectionToken: messageToChannelInjectionToken,
});
export default messageToChannelInjectable;
@@ -0,0 +1,167 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { MessageToChannel } from "../../../common/utils/channel/message-to-channel-injection-token";
import { messageToChannelInjectionToken } from "../../../common/utils/channel/message-to-channel-injection-token";
import closeAllWindowsInjectable from "../../start-main-application/lens-window/hide-all-windows/close-all-windows.injectable";
import type { MessageChannel } from "../../../common/utils/channel/message-channel-injection-token";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import createLensWindowInjectable from "../../start-main-application/lens-window/application-window/create-lens-window.injectable";
import type { LensWindow } from "../../start-main-application/lens-window/application-window/lens-window-injection-token";
import { lensWindowInjectionToken } from "../../start-main-application/lens-window/application-window/lens-window-injection-token";
import type { DiContainer } from "@ogre-tools/injectable";
import { getInjectable } from "@ogre-tools/injectable";
import sendToChannelInElectronBrowserWindowInjectable from "../../start-main-application/lens-window/application-window/send-to-channel-in-electron-browser-window.injectable";
describe("message to channel from main", () => {
let messageToChannel: MessageToChannel;
let someTestWindow: LensWindow;
let someOtherTestWindow: LensWindow;
let sendToChannelInBrowserMock: jest.Mock;
beforeEach(() => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
sendToChannelInBrowserMock = jest.fn();
di.override(sendToChannelInElectronBrowserWindowInjectable, () => sendToChannelInBrowserMock);
someTestWindow = createTestWindow(di, "some-test-window-id");
someOtherTestWindow = createTestWindow(di, "some-other-test-window-id");
messageToChannel = di.inject(messageToChannelInjectionToken);
const closeAllWindows = di.inject(closeAllWindowsInjectable);
closeAllWindows();
});
it("given no visible windows, when messaging to channel, does not message to any window", () => {
messageToChannel(someChannel, "some-message");
expect(sendToChannelInBrowserMock).not.toHaveBeenCalled();
});
describe("given visible window", () => {
beforeEach(async () => {
await someTestWindow.show();
});
it("when messaging to channel, messages to window", () => {
messageToChannel(someChannel, "some-message");
expect(sendToChannelInBrowserMock.mock.calls).toEqual([
[
null,
{
channel: "some-channel",
data: ['"some-message"'],
},
],
]);
});
it("given boolean as message, when messaging to channel, messages to window with stringified message", () => {
messageToChannel(someChannel, true);
expect(sendToChannelInBrowserMock.mock.calls).toEqual([
[
null,
{
channel: "some-channel",
data: ["true"],
},
],
]);
});
it("given number as message, when messaging to channel, messages to window with stringified message", () => {
messageToChannel(someChannel, 42);
expect(sendToChannelInBrowserMock.mock.calls).toEqual([
[
null,
{
channel: "some-channel",
data: ["42"],
},
],
]);
});
it("given object as message, when messaging to channel, messages to window with stringified message", () => {
messageToChannel(someChannel, { some: "object" });
expect(sendToChannelInBrowserMock.mock.calls).toEqual([
[
null,
{
channel: "some-channel",
data: [JSON.stringify({ some: "object" })],
},
],
]);
});
});
it("given multiple visible windows, when messaging to channel, messages to window", async () => {
await someTestWindow.show();
await someOtherTestWindow.show();
messageToChannel(someChannel, "some-message");
expect(sendToChannelInBrowserMock.mock.calls).toEqual([
[
null,
{
channel: "some-channel",
data: ['"some-message"'],
},
],
[
null,
{
channel: "some-channel",
data: ['"some-message"'],
},
],
]);
});
});
const someChannel: MessageChannel<any> = { id: "some-channel" };
const createTestWindow = (di: DiContainer, id: string) => {
const testWindowInjectable = getInjectable({
id,
instantiate: (di) => {
const createLensWindow = di.inject(createLensWindowInjectable);
return createLensWindow({
id,
title: "Some test window",
defaultHeight: 42,
defaultWidth: 42,
getContentSource: () => ({ url: "some-content-url" }),
resizable: true,
windowFrameUtilitiesAreShown: false,
centered: false,
});
},
injectionToken: lensWindowInjectionToken,
});
di.register(testWindowInjectable);
return di.inject(testWindowInjectable);
};
@@ -0,0 +1,31 @@
/**
* 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 syncBoxInitialValueChannelInjectable from "../../../common/utils/sync-box/sync-box-initial-value-channel.injectable";
import { syncBoxInjectionToken } from "../../../common/utils/sync-box/sync-box-injection-token";
import { requestChannelListenerInjectionToken } from "../../../common/utils/channel/request-channel-listener-injection-token";
const syncBoxInitialValueChannelListenerInjectable = getInjectable({
id: "sync-box-initial-value-channel-listener",
instantiate: (di) => {
const channel = di.inject(syncBoxInitialValueChannelInjectable);
const syncBoxes = di.injectMany(syncBoxInjectionToken);
return {
channel,
handler: () =>
syncBoxes.map((box) => ({
id: box.id,
value: box.value.get(),
})),
};
},
injectionToken: requestChannelListenerInjectionToken,
});
export default syncBoxInitialValueChannelListenerInjectable;
-21
View File
@@ -1,21 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
/**
* Compute the next update channel from the current updating channel
* @param defaultChannel The default (initial) channel to check
* @param channel The current channel that did not have a new version associated with it
* @returns The channel name of the next release version
*/
export function nextUpdateChannel(defaultChannel: string, channel: string | null): string {
switch (channel) {
case "alpha":
return "beta";
case "beta":
return "latest"; // there is no RC currently
default:
return defaultChannel;
}
}