Make starting of application modular and unit testable (#5324)

* Introduce injection token as competition for injectable setup to have better control of timing of different setups

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

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

* Switch to using competition to setup app paths

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

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

* Switch to using competition for setupping IPC channel listeners

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

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

* Stop running setups in unit tests without need

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

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

* Switch to running injection token based setups over legacy DI setups in unit tests

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

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

* Consolidate naming for running setups

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

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

* Switch to running injection token based setups over legacy DI setups in application roots

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

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

* Adapt to typing changes in injectable

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

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

* Update injectable

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

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

* Introduce concept of runnable as a way to delegate runs to Open Closed Principle compliant runnables

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

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

* Deprecate vars in favor of injectables

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

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

* Consolidate loading of extensions to injectable instead of setup-code

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

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

* Flag injectable causing side effects

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

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

* Adapt injectable to auto register using a plugin instead of internal feature that no longer exists

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

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

* Simplify late registrations

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

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

* Introduce tokens for runnables of specific application events for Open Closed Principle

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

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

* Consolidate setup events to more granular timeslots

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

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

* Reimplement setup for catalog syncing using runnables instead of non-OCP in index.ts

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

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

* Reimplement setup for application menu using runnables instead of non-OCP in index.ts

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

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

* Reimplement setup for electron application name using runnables instead of non-OCP in index.ts

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

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

* Reimplement setup for immer using runnables instead of non-OCP in index.ts

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

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

* Reimplement setup for MobX strictness using runnables instead of non-OCP in index.ts

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

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

* Reimplement setup for application proxy using runnables instead of non-OCP in index.ts

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

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

* Reimplement setup for system certifications using runnables instead of non-OCP in index.ts

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

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

* Reimplement setup for system shutdown using runnables instead of non-OCP in index.ts

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

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

* Reimplement setup for main window visibility using runnables instead of non-OCP in index.ts

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

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

* Reimplement setup for application quit using runnables instead of non-OCP in index.ts

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

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

* Reimplement setup for after application is ready using runnables instead of non-OCP in index.ts

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

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

* Reimplement setup for application tray using runnables instead of non-OCP in index.ts

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

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

* Reimplement setup for deep linking using runnables instead of non-OCP in index.ts

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

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

* Reimplement setup for root frame using runnables instead of non-OCP in index.ts

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

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

* Remove multiple usages of shared global state

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

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

* Remove recently reimplemented stuff from index.ts

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

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

* Extract implementation for intent over technical phenomena (here electron events)

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

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

* Consolidate naming of event timing window

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

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

* Move directories for timing windows among peers

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

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

* Consolidate event naming

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

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

* Introduce function to remove duplication from things that are startable and stoppable

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

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

* Consolidate implementation of something startable and stoppable

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

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

* Consolidate naming

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

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

* Consolidate more startables and stoppables

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

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

* Introduce abstractions for electron application events

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

* Introduce more abstractions for electron

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

* Abstract even more Electron specifics to make them overridable in unit tests

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

* Override dependency for causing side effects

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

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

* Make running of many delegatees able to be hierarchical

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

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

* Bump async-fn

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

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

* Make startable-stoppable also restartable

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

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

* Extract "isIntegrationTesting" as dependency for having a side-effect

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

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

* Make temporal dependency apparent

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

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

* Consolidate all setupping related to single feature to same runnable

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

* Abstract command line arguments to make them overridable in tests

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

* Deprecate some globals

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

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

* Introduce injectable for __static directory to eliminate side effect on import

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

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

* Extract responsibilities from electron

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

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

* Extract async initialization from sync-injectable

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

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

* Accumulate more global overrides

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

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

* Make lifecycle of injectable store work as expected

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

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

* Extract responsibilities to delegatees

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

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

* Rename delegate for accuracy

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

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

* Rename another delegate for accuracy

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

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

* Rename yet another delegate for accuracy

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

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

* Add new global overrides

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

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

* Consolidate variable naming

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

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

* Rename injectable for accuracy

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

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

* Make difference between soft and hard quit of application apparent

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

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

* Make quit triggered by auto update manifest as hard quit instead of just soft quit

Soft quit is the stand-by quit where only the renderer quits. Hard quit is the full quit of also main.

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

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

* Introduce abstraction for publishing and subscribing between processes

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

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

* Revert "Introduce abstraction for publishing and subscribing between processes"

This reverts commit 46f2d5a5f28bddcf5ffe124b1c590b19e7b9a15f.

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

* Adapt code and unit tests to recent changes in application setup and event handling

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

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

* Group overrides by category

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

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

* Abstract event of application activation from electron

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

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

* Abstract event of device shutdown from electron

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

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

* Consolidate runnables related to Electron

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

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

* Make startableStoppables have ID for better error logging

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

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

* Make navigating to Helm Charts not blow up

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

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

* Add general override for behavioural tests

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

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

* Update snapshot

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

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

* Adapt vars after merge

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

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

* Fix code style

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

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

* Fix error about multiple states for React Router being present at once

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

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

* Kludge around test setup which is difficult because of circular dependencies

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

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

* Fix setupping of sentry after rebase

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

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

* Ensure that LensProxy is setupped before starting the main window

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

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

* Remove redundant import

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

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

* Consolidate to use injectable instead of var for static file directory

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

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

* Deprecate another var with injectable

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

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

* Tweak timing of runnables

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

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

* Extract tray icon path as injectable

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

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

* Make splash screen not blow up by providing compile-time environment variables

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

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

* Introduce a way to run many synchronous runnables

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

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

* Make runnables for before application is ready synchronous as this is technical limitation of Electron

See: https://github.com/electron/electron/issues/21370

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>

* Fix application quit by calling Electron's event.preventDefault in a very specific way and preventing async

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

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

* Fix missing injectable postfix in file name

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

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

* Tweak timeslot of a runnable

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

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

* Make it possible to not stop something that was never started

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

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

* Make quit of hidden Lens not blow up

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

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

* Extract responsibilities of WindowManager as injectables

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

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

* Consolidate code for application window

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

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

* Simplify directory structures for runnables

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

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

* Flag injectable causing side-effect

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

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

* Extract helpers for testing promises to test-utils

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

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

* Make script for running unit tests support targeting

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

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

* Make startable stoppable support async starting

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

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

* Introduce reactive way to get theme from operating system

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

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

* Update yarn.lock after rebase

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

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

* Fix code style

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

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

* Remove code-style changes that are not welcome

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

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

* Switch to using dependency over using explicit side-effect

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

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

* Simplify naming

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>

* Add empty mocks comply to lint

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

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

* Remove global state

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

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

* Extract responsibility of setting dependencies to own injectable

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

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

* Flag injectable causing side effects

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

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

* Extract responsibility to injectable

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

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

* Tweak typing

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

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

* Tweak naming of runnables

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

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

* Revert code-style changes

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

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

* Tweak types

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

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

* Extract injectable with side-effect for exact overriding

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

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

* Tweak name of timeslot to make it more apparent that new content for application load can be added

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

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

* Tweak name of the splash window for loading to avoid confusion with similarly named phenomenon

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

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

* Tweak comment

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

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

* Rename injectable for brevity

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

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

* Consolidate LensProxy related stuff to directory

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

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

* Rename injectables for accuracy

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

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

* Remove usage of extension for injectable for being YAGNI

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

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

* Prevent restarting a startableStoppable when already being started

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

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

* Rename callback in ApplicationBuilder for accuracy

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

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

* Fix conflicts after rebase

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

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

* Fix merge conflicts

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

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

* Revert switch to react-router-dom by installing history as dependency

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

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

* Make test more strict

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

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

* Make runnable give friendly error about incorrect hierarchy for easier debugging

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

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

* Fix code-style

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

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

* Fix code style

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

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

* Switch to using dependency instead of legacy-di

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

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

* Fix timing of injects

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

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

Co-authored-by: Iku-turso <mikko.aspiala@gmail.com>
This commit is contained in:
Janne SavolainenandMikko Aspiala authored and GitHub committed 2022-05-18 16:18:02 +03:00
1 parent b57d48e39e
commit 54b92aa9b6
302 files changed
+6388 -2025

No files matched your search

+5 -3
View File
@@ -19,6 +19,8 @@ import listNamespacesInjectable from "../../common/cluster/list-namespaces.injec
import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable";
import type { ClusterContextHandler } from "../context-handler/context-handler";
import { parse } from "url";
import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable";
console = new Console(process.stdout, process.stderr); // fix mockFS
@@ -26,7 +28,7 @@ describe("create clusters", () => {
let cluster: Cluster;
let createCluster: (model: ClusterModel) => Cluster;
beforeEach(async () => {
beforeEach(() => {
jest.clearAllMocks();
const di = getDiForUnitTesting({ doGeneralOverrides: true });
@@ -55,8 +57,8 @@ describe("create clusters", () => {
}),
});
await di.runSetups();
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
di.override(directoryForTempInjectable, () => "some-directory-for-temp");
di.override(authorizationReviewInjectable, () => () => () => Promise.resolve(true));
di.override(listNamespacesInjectable, () => () => () => Promise.resolve([ "default" ]));
di.override(createContextHandlerInjectable, () => (cluster) => ({
+18 -24
View File
@@ -5,13 +5,14 @@
import { UserStore } from "../../common/user-store";
import type { ClusterContextHandler } from "../context-handler/context-handler";
import type { PrometheusService } from "../prometheus";
import { PrometheusProvider, PrometheusProviderRegistry } from "../prometheus";
import type { PrometheusService, PrometheusProviderRegistry } from "../prometheus";
import { PrometheusProvider } from "../prometheus";
import mockFs from "mock-fs";
import { getDiForUnitTesting } from "../getDiForUnitTesting";
import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable";
import type { Cluster } from "../../common/cluster/cluster";
import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
import prometheusProviderRegistryInjectable from "../prometheus/prometheus-provider-registry.injectable";
jest.mock("electron", () => ({
app: {
@@ -74,8 +75,9 @@ const clusterStub = {
describe("ContextHandler", () => {
let createContextHandler: (cluster: Cluster) => ClusterContextHandler | undefined;
let prometheusProviderRegistry: PrometheusProviderRegistry;
beforeEach(async () => {
beforeEach(() => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
mockFs({
@@ -84,15 +86,12 @@ describe("ContextHandler", () => {
di.override(createKubeAuthProxyInjectable, () => ({} as any));
await di.runSetups();
prometheusProviderRegistry = di.inject(prometheusProviderRegistryInjectable);
createContextHandler = di.inject(createContextHandlerInjectable);
PrometheusProviderRegistry.createInstance();
});
afterEach(() => {
PrometheusProviderRegistry.resetInstance();
UserStore.resetInstance();
mockFs.restore();
});
@@ -104,17 +103,16 @@ describe("ContextHandler", () => {
[0, 2],
[0, 3],
])("should throw from %d success(es) after %d failure(s)", async (successes, failures) => {
const reg = PrometheusProviderRegistry.getInstance();
let count = 0;
for (let i = 0; i < failures; i += 1) {
const serviceResult = i % 2 === 0 ? ServiceResult.Failure : ServiceResult.Undefined;
reg.registerProvider(new TestProvider(`id_${count++}`, serviceResult));
prometheusProviderRegistry.registerProvider(new TestProvider(`id_${count++}`, serviceResult));
}
for (let i = 0; i < successes; i += 1) {
reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
prometheusProviderRegistry.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
}
expect(() => {
@@ -135,17 +133,16 @@ describe("ContextHandler", () => {
[2, 2],
[2, 3],
])("should pick the first provider of %d success(es) after %d failure(s)", async (successes, failures) => {
const reg = PrometheusProviderRegistry.getInstance();
let count = 0;
for (let i = 0; i < failures; i += 1) {
const serviceResult = i % 2 === 0 ? ServiceResult.Failure : ServiceResult.Undefined;
reg.registerProvider(new TestProvider(`id_${count++}`, serviceResult));
prometheusProviderRegistry.registerProvider(new TestProvider(`id_${count++}`, serviceResult));
}
for (let i = 0; i < successes; i += 1) {
reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
prometheusProviderRegistry.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
}
// TODO: Unit test shouldn't access protected or private methods
@@ -166,17 +163,16 @@ describe("ContextHandler", () => {
[2, 2],
[2, 3],
])("should pick the first provider of %d success(es) before %d failure(s)", async (successes, failures) => {
const reg = PrometheusProviderRegistry.getInstance();
let count = 0;
for (let i = 0; i < successes; i += 1) {
reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
prometheusProviderRegistry.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
}
for (let i = 0; i < failures; i += 1) {
const serviceResult = i % 2 === 0 ? ServiceResult.Failure : ServiceResult.Undefined;
reg.registerProvider(new TestProvider(`id_${count++}`, serviceResult));
prometheusProviderRegistry.registerProvider(new TestProvider(`id_${count++}`, serviceResult));
}
// TODO: Unit test shouldn't access protected or private methods
@@ -197,23 +193,22 @@ describe("ContextHandler", () => {
[2, 2],
[2, 3],
])("should pick the first provider of %d success(es) between %d failure(s)", async (successes, failures) => {
const reg = PrometheusProviderRegistry.getInstance();
let count = 0;
const beforeSuccesses = Math.floor(successes / 2);
const afterSuccesses = successes - beforeSuccesses;
for (let i = 0; i < beforeSuccesses; i += 1) {
reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
prometheusProviderRegistry.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
}
for (let i = 0; i < failures; i += 1) {
const serviceResult = i % 2 === 0 ? ServiceResult.Failure : ServiceResult.Undefined;
reg.registerProvider(new TestProvider(`id_${count++}`, serviceResult));
prometheusProviderRegistry.registerProvider(new TestProvider(`id_${count++}`, serviceResult));
}
for (let i = 0; i < afterSuccesses; i += 1) {
reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
prometheusProviderRegistry.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
}
// TODO: Unit test shouldn't access protected or private methods
@@ -225,12 +220,11 @@ describe("ContextHandler", () => {
});
it("shouldn't pick the second provider of 2 success(es) after 1 failure(s)", async () => {
const reg = PrometheusProviderRegistry.getInstance();
let count = 0;
reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Failure));
reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
reg.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
prometheusProviderRegistry.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Failure));
prometheusProviderRegistry.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
prometheusProviderRegistry.registerProvider(new TestProvider(`id_${count++}`, ServiceResult.Success));
// TODO: Unit test shouldn't access protected or private methods
const contextHandler = createContextHandler(clusterStub) as unknown as { getPrometheusService(): Promise<PrometheusService> };
+5 -2
View File
@@ -57,6 +57,8 @@ import path from "path";
import spawnInjectable from "../child-process/spawn.injectable";
import getConfigurationFileModelInjectable from "../../common/get-configuration-file-model/get-configuration-file-model.injectable";
import appVersionInjectable from "../../common/get-configuration-file-model/app-version/app-version.injectable";
import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import directoryForTempInjectable from "../../common/app-paths/directory-for-temp/directory-for-temp.injectable";
console = new Console(stdout, stderr);
@@ -99,6 +101,9 @@ describe("kube auth proxy tests", () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
di.override(directoryForTempInjectable, () => "some-directory-for-temp");
di.override(spawnInjectable, () => mockSpawn);
di.permitSideEffects(getConfigurationFileModelInjectable);
@@ -106,8 +111,6 @@ describe("kube auth proxy tests", () => {
mockFs(mockMinikubeConfig);
await di.runSetups();
createCluster = di.inject(createClusterInjectionToken);
createKubeAuthProxy = di.inject(createKubeAuthProxyInjectable);
+2 -3
View File
@@ -2,7 +2,6 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getDiForUnitTesting } from "../getDiForUnitTesting";
import { KubeconfigManager } from "../kubeconfig-manager/kubeconfig-manager";
import mockFs from "mock-fs";
@@ -20,6 +19,7 @@ import { parse } from "url";
import loggerInjectable from "../../common/logger.injectable";
import type { Logger } from "../../common/logger";
import assert from "assert";
import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
console = new Console(process.stdout, process.stderr); // fix mockFS
@@ -33,6 +33,7 @@ describe("kubeconfig manager tests", () => {
di = getDiForUnitTesting({ doGeneralOverrides: true });
di.override(directoryForTempInjectable, () => "some-directory-for-temp");
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
loggerMock = {
warn: jest.fn(),
@@ -68,8 +69,6 @@ describe("kubeconfig manager tests", () => {
}),
});
await di.runSetups();
di.override(createContextHandlerInjectable, () => (cluster) => ({
restartServer: jest.fn(),
stopServer: jest.fn(),
+1 -1
View File
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { isLongRunningRequest } from "../lens-proxy";
import { isLongRunningRequest } from "../lens-proxy/lens-proxy";
describe("isLongRunningRequest", () => {
it("returns true on watches", () => {
+1 -3
View File
@@ -26,11 +26,9 @@ jest.mock("electron", () => ({
describe("static-file-route", () => {
let handleStaticFileRoute: Route<Buffer, "/{path*}">;
beforeEach(async () => {
beforeEach(() => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
await di.runSetups();
handleStaticFileRoute = di.inject(staticFileRouteInjectable);
});
@@ -3,11 +3,19 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import electronAppInjectable from "../get-electron-app-path/electron-app/electron-app.injectable";
import packageInfo from "../../../../package.json";
import isDevelopmentInjectable from "../../../common/vars/is-development.injectable";
const appNameInjectable = getInjectable({
id: "app-name",
instantiate: (di) => di.inject(electronAppInjectable).name,
instantiate: (di) => {
const isDevelopment = di.inject(isDevelopmentInjectable);
return `${packageInfo.productName}${isDevelopment ? "Dev" : ""}`;
},
causesSideEffects: true,
});
export default appNameInjectable;
@@ -1,71 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type {
DiContainerForSetup } from "@ogre-tools/injectable";
import {
getInjectable,
} from "@ogre-tools/injectable";
import {
appPathsInjectionToken,
appPathsIpcChannel,
} from "../../common/app-paths/app-path-injection-token";
import registerChannelInjectable from "./register-channel/register-channel.injectable";
import { getAppPaths } from "./get-app-paths";
import getElectronAppPathInjectable from "./get-electron-app-path/get-electron-app-path.injectable";
import setElectronAppPathInjectable from "./set-electron-app-path/set-electron-app-path.injectable";
import appNameInjectable from "./app-name/app-name.injectable";
import directoryForIntegrationTestingInjectable from "./directory-for-integration-testing/directory-for-integration-testing.injectable";
import joinPathsInjectable from "../../common/path/join-paths.injectable";
const appPathsInjectable = getInjectable({
id: "app-paths",
setup: async (di) => {
const directoryForIntegrationTesting = await di.inject(
directoryForIntegrationTestingInjectable,
);
if (directoryForIntegrationTesting) {
await setupPathForAppDataInIntegrationTesting(di, directoryForIntegrationTesting);
}
await setupPathForUserData(di);
await registerAppPathsChannel(di);
},
instantiate: (di) =>
getAppPaths({ getAppPath: di.inject(getElectronAppPathInjectable) }),
injectionToken: appPathsInjectionToken,
});
export default appPathsInjectable;
const registerAppPathsChannel = async (di: DiContainerForSetup) => {
const registerChannel = await di.inject(registerChannelInjectable);
const appPaths = await di.inject(appPathsInjectable);
registerChannel(appPathsIpcChannel, () => appPaths);
};
const setupPathForUserData = async (di: DiContainerForSetup) => {
const setElectronAppPath = await di.inject(setElectronAppPathInjectable);
const appName = await di.inject(appNameInjectable);
const getAppPath = await di.inject(getElectronAppPathInjectable);
const joinPaths = await di.inject(joinPathsInjectable);
const appDataPath = getAppPath("appData");
setElectronAppPath("userData", joinPaths(appDataPath, appName));
};
// Todo: this kludge is here only until we have a proper place to setup integration testing.
const setupPathForAppDataInIntegrationTesting = async (di: DiContainerForSetup, appDataPath: string) => {
const setElectronAppPath = await di.inject(setElectronAppPathInjectable);
setElectronAppPath("appData", appDataPath);
};
@@ -3,10 +3,16 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import environmentVariablesInjectable from "../../../common/utils/environment-variables.injectable";
const directoryForIntegrationTestingInjectable = getInjectable({
id: "directory-for-integration-testing",
instantiate: () => process.env.CICD,
instantiate: (di) => {
const environmentVariables = di.inject(environmentVariablesInjectable);
return environmentVariables.CICD;
},
});
export default directoryForIntegrationTestingInjectable;
@@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import electronAppInjectable from "./electron-app/electron-app.injectable";
import electronAppInjectable from "../../electron-app/electron-app.injectable";
import { getElectronAppPath } from "./get-electron-app-path";
const getElectronAppPathInjectable = getInjectable({
@@ -2,7 +2,7 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import electronAppInjectable from "./electron-app/electron-app.injectable";
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";
@@ -13,7 +13,7 @@ import { joinPathsFake } from "../../../common/test-utils/join-paths-fake";
describe("get-electron-app-path", () => {
let getElectronAppPath: (name: string) => string;
beforeEach(async () => {
beforeEach(() => {
const di = getDiForUnitTesting({ doGeneralOverrides: false });
const appStub = {
@@ -35,8 +35,6 @@ describe("get-electron-app-path", () => {
di.override(registerChannelInjectable, () => () => undefined);
di.override(joinPathsInjectable, () => joinPathsFake);
await di.runSetups();
getElectronAppPath = di.inject(getElectronAppPathInjectable) as (name: string) => string;
});
@@ -4,7 +4,7 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { PathName } from "../../../common/app-paths/app-path-names";
import electronAppInjectable from "../get-electron-app-path/electron-app/electron-app.injectable";
import electronAppInjectable from "../../electron-app/electron-app.injectable";
const setElectronAppPathInjectable = getInjectable({
id: "set-electron-app-path",
@@ -0,0 +1,58 @@
/**
* 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 { AppPaths } from "../../common/app-paths/app-path-injection-token";
import getElectronAppPathInjectable from "./get-electron-app-path/get-electron-app-path.injectable";
import setElectronAppPathInjectable from "./set-electron-app-path/set-electron-app-path.injectable";
import appNameInjectable from "./app-name/app-name.injectable";
import directoryForIntegrationTestingInjectable from "./directory-for-integration-testing/directory-for-integration-testing.injectable";
import appPathsStateInjectable from "../../common/app-paths/app-paths-state.injectable";
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";
const setupAppPathsInjectable = getInjectable({
id: "setup-app-paths",
instantiate: (di) => {
const setElectronAppPath = di.inject(setElectronAppPathInjectable);
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);
return {
run: () => {
if (directoryForIntegrationTesting) {
setElectronAppPath("appData", directoryForIntegrationTesting);
}
const appDataPath = getAppPath("appData");
setElectronAppPath("userData", joinPaths(appDataPath, appName));
const appPaths = pipeline(
pathNames,
map(name => [name, getAppPath(name)]),
fromPairs,
) as AppPaths;
appPathsState.set(appPaths);
registerChannel(appPathsIpcChannel, () => appPaths);
},
};
},
injectionToken: beforeElectronIsReadyInjectionToken,
});
export default setupAppPathsInjectable;
+5 -1
View File
@@ -41,11 +41,15 @@ autoUpdater.logger = {
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 = once(function (interval = 1000 * 60 * 60 * 2): void {
export const startUpdateChecking = ({ isAutoUpdateEnabled } : Dependencies) => once(function (interval = 1000 * 60 * 60 * 2): void {
if (!isAutoUpdateEnabled() || isTestEnv) {
return;
}
@@ -10,16 +10,16 @@ import type { Cluster } from "../../../common/cluster/cluster";
import { computeDiff as computeDiffFor, configToModels } from "../kubeconfig-sync/manager";
import mockFs from "mock-fs";
import fs from "fs";
import { ClusterManager } from "../../cluster-manager";
import clusterStoreInjectable from "../../../common/cluster-store/cluster-store.injectable";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import { createClusterInjectionToken } from "../../../common/cluster/create-cluster-injection-token";
import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import { ClusterStore } from "../../../common/cluster-store/cluster-store";
import getConfigurationFileModelInjectable
from "../../../common/get-configuration-file-model/get-configuration-file-model.injectable";
import appVersionInjectable
from "../../../common/get-configuration-file-model/app-version/app-version.injectable";
import getConfigurationFileModelInjectable from "../../../common/get-configuration-file-model/get-configuration-file-model.injectable";
import appVersionInjectable from "../../../common/get-configuration-file-model/app-version/app-version.injectable";
import clusterManagerInjectable from "../../cluster-manager.injectable";
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import directoryForTempInjectable from "../../../common/app-paths/directory-for-temp/directory-for-temp.injectable";
jest.mock("electron", () => ({
app: {
@@ -45,12 +45,8 @@ describe("kubeconfig-sync.source tests", () => {
mockFs();
await di.runSetups();
computeDiff = computeDiffFor({
directoryForKubeConfigs: di.inject(directoryForKubeConfigsInjectable),
createCluster: di.inject(createClusterInjectionToken),
});
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
di.override(directoryForTempInjectable, () => "some-directory-for-temp");
di.override(clusterStoreInjectable, () =>
ClusterStore.createInstance({ createCluster: () => null as never }),
@@ -59,14 +55,17 @@ describe("kubeconfig-sync.source tests", () => {
di.permitSideEffects(getConfigurationFileModelInjectable);
di.permitSideEffects(appVersionInjectable);
di.inject(clusterStoreInjectable);
computeDiff = computeDiffFor({
directoryForKubeConfigs: di.inject(directoryForKubeConfigsInjectable),
createCluster: di.inject(createClusterInjectionToken),
clusterManager: di.inject(clusterManagerInjectable),
});
ClusterManager.createInstance();
di.inject(clusterStoreInjectable);
});
afterEach(() => {
mockFs.restore();
ClusterManager.resetInstance();
ClusterStore.resetInstance();
});
@@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import { KubeconfigSyncManager } from "./manager";
import { createClusterInjectionToken } from "../../../common/cluster/create-cluster-injection-token";
import clusterManagerInjectable from "../../cluster-manager.injectable";
import catalogEntityRegistryInjectable from "../../catalog/entity-registry.injectable";
const kubeconfigSyncManagerInjectable = getInjectable({
@@ -14,6 +15,7 @@ const kubeconfigSyncManagerInjectable = getInjectable({
instantiate: (di) => new KubeconfigSyncManager({
directoryForKubeConfigs: di.inject(directoryForKubeConfigsInjectable),
createCluster: di.inject(createClusterInjectionToken),
clusterManager: di.inject(clusterManagerInjectable),
entityRegistry: di.inject(catalogEntityRegistryInjectable),
}),
});
@@ -17,7 +17,8 @@ import { bytesToUnits, getOrInsertWith, iter, noop } from "../../../common/utils
import logger from "../../logger";
import type { KubeConfig } from "@kubernetes/client-node";
import { loadConfigFromString, splitConfig } from "../../../common/kube-helpers";
import { catalogEntityFromCluster, ClusterManager } from "../../cluster-manager";
import type { ClusterManager } from "../../cluster-manager";
import { catalogEntityFromCluster } from "../../cluster-manager";
import { UserStore } from "../../../common/user-store";
import { ClusterStore } from "../../../common/cluster-store/cluster-store";
import { createHash } from "crypto";
@@ -51,9 +52,10 @@ const ignoreGlobs = [
const folderSyncMaxAllowedFileReadSize = 2 * 1024 * 1024; // 2 MiB
const fileSyncMaxAllowedFileReadSize = 16 * folderSyncMaxAllowedFileReadSize; // 32 MiB
interface Dependencies {
interface KubeconfigSyncManagerDependencies {
readonly directoryForKubeConfigs: string;
readonly entityRegistry: CatalogEntityRegistry;
readonly clusterManager: ClusterManager;
createCluster: (model: ClusterModel) => Cluster;
}
@@ -64,7 +66,7 @@ export class KubeconfigSyncManager {
protected syncing = false;
protected syncListDisposer?: Disposer;
constructor(protected readonly dependencies: Dependencies) {
constructor(protected readonly dependencies: KubeconfigSyncManagerDependencies) {
makeObservable(this);
}
@@ -165,8 +167,14 @@ export function configToModels(rootConfig: KubeConfig, filePath: string): Update
type RootSourceValue = [Cluster, CatalogEntity];
type RootSource = ObservableMap<string, RootSourceValue>;
interface ComputeDiffDependencies {
directoryForKubeConfigs: string;
createCluster: (model: ClusterModel) => Cluster;
clusterManager: ClusterManager;
}
// exported for testing
export const computeDiff = ({ directoryForKubeConfigs, createCluster }: Pick<Dependencies, "createCluster" | "directoryForKubeConfigs">) => (contents: string, source: RootSource, filePath: string): void => {
export const computeDiff = ({ directoryForKubeConfigs, createCluster, clusterManager }: ComputeDiffDependencies) => (contents: string, source: RootSource, filePath: string): void => {
runInAction(() => {
try {
const { config, error } = loadConfigFromString(contents);
@@ -186,7 +194,7 @@ export const computeDiff = ({ directoryForKubeConfigs, createCluster }: Pick<Dep
// remove and disconnect clusters that were removed from the config
if (!model) {
// remove from the deleting set, so that if a new context of the same name is added, it isn't marked as deleting
ClusterManager.getInstance().deleting.delete(value[0].id);
clusterManager.deleting.delete(value[0].id);
value[0].disconnect();
source.delete(contextName);
@@ -241,7 +249,7 @@ interface DiffChangedConfigArgs {
maxAllowedFileReadSize: number;
}
const diffChangedConfigFor = (dependencies: Dependencies) => ({ filePath, source, stats, maxAllowedFileReadSize }: DiffChangedConfigArgs): Disposer => {
const diffChangedConfigFor = (dependencies: ComputeDiffDependencies) => ({ filePath, source, stats, maxAllowedFileReadSize }: DiffChangedConfigArgs): Disposer => {
logger.debug(`${logPrefix} file changed`, { filePath });
if (stats.size >= maxAllowedFileReadSize) {
@@ -297,7 +305,7 @@ const diffChangedConfigFor = (dependencies: Dependencies) => ({ filePath, source
return cleanup;
};
const watchFileChanges = (filePath: string, dependencies: Dependencies): [IComputedValue<CatalogEntity[]>, Disposer] => {
const watchFileChanges = (filePath: string, dependencies: ComputeDiffDependencies): [IComputedValue<CatalogEntity[]>, Disposer] => {
const rootSource = observable.map<string, ObservableMap<string, RootSourceValue>>();
const derivedSource = computed(() => Array.from(iter.flatMap(rootSource.values(), from => iter.map(from.values(), child => child[1]))));
@@ -0,0 +1,19 @@
/**
* 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 { syncWeblinks } from "./weblinks";
import weblinkStoreInjectable from "../../common/weblink-store.injectable";
import catalogEntityRegistryInjectable from "../catalog/entity-registry.injectable";
const syncWeblinksInjectable = getInjectable({
id: "sync-weblinks",
instantiate: (di) => syncWeblinks({
weblinkStore: di.inject(weblinkStoreInjectable),
catalogEntityRegistry: di.inject(catalogEntityRegistryInjectable),
}),
});
export default syncWeblinksInjectable;
+8 -5
View File
@@ -4,9 +4,9 @@
*/
import { computed, observable, reaction } from "mobx";
import { WeblinkStore } from "../../common/weblink-store";
import type { WeblinkStore } from "../../common/weblink-store";
import { WebLink } from "../../common/catalog-entities";
import { catalogEntityRegistry } from "../catalog";
import type { CatalogEntityRegistry } from "../catalog";
import got from "got";
import type { Disposer } from "../../common/utils";
import { random } from "lodash";
@@ -28,9 +28,12 @@ async function validateLink(link: WebLink) {
}
}
interface Dependencies {
weblinkStore: WeblinkStore;
catalogEntityRegistry: CatalogEntityRegistry;
}
export function syncWeblinks() {
const weblinkStore = WeblinkStore.getInstance();
export const syncWeblinks = ({ weblinkStore, catalogEntityRegistry }: Dependencies) => () => {
const webLinkEntities = observable.map<string, [WebLink, Disposer]>();
function periodicallyCheckLink(link: WebLink): Disposer {
@@ -87,4 +90,4 @@ export function syncWeblinks() {
}, { fireImmediately: true });
catalogEntityRegistry.addComputedSource("weblinks", computed(() => Array.from(webLinkEntities.values(), ([link]) => link)));
}
};
@@ -0,0 +1,22 @@
/**
* 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 { startCatalogSyncToRenderer } from "../catalog-pusher";
import { getStartableStoppable } from "../../common/utils/get-startable-stoppable";
import catalogEntityRegistryInjectable from "../catalog/entity-registry.injectable";
const catalogSyncToRendererInjectable = getInjectable({
id: "catalog-sync-to-renderer",
instantiate: (di) => {
const catalogEntityRegistry = di.inject(catalogEntityRegistryInjectable);
return getStartableStoppable("catalog-sync", () =>
startCatalogSyncToRenderer(catalogEntityRegistry),
);
},
});
export default catalogSyncToRendererInjectable;
@@ -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 { afterRootFrameIsReadyInjectionToken } from "../start-main-application/runnable-tokens/after-root-frame-is-ready-injection-token";
import catalogSyncToRendererInjectable from "./catalog-sync-to-renderer.injectable";
const startCatalogSyncInjectable = getInjectable({
id: "start-catalog-sync",
instantiate: (di) => {
const catalogSyncToRenderer = di.inject(catalogSyncToRendererInjectable);
return {
run: async () => {
await catalogSyncToRenderer.start();
},
};
},
injectionToken: afterRootFrameIsReadyInjectionToken,
});
export default startCatalogSyncInjectable;
@@ -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 catalogSyncToRendererInjectable from "./catalog-sync-to-renderer.injectable";
import { beforeQuitOfFrontEndInjectionToken } from "../start-main-application/runnable-tokens/before-quit-of-front-end-injection-token";
const stopCatalogSyncInjectable = getInjectable({
id: "stop-catalog-sync",
instantiate: (di) => {
const catalogSyncToRenderer = di.inject(catalogSyncToRendererInjectable);
return {
run: async () => {
if (catalogSyncToRenderer.started) {
await catalogSyncToRenderer.stop();
}
},
};
},
injectionToken: beforeQuitOfFrontEndInjectionToken,
});
export default stopCatalogSyncInjectable;
@@ -8,6 +8,7 @@ import { CatalogEntityRegistry } from "./entity-registry";
const catalogEntityRegistryInjectable = getInjectable({
id: "catalog-entity-registry",
instantiate: (di) => new CatalogEntityRegistry({
hasCategoryForEntity: di.inject(hasCategoryForEntityInjectable),
}),
@@ -5,7 +5,7 @@
import type { RequestPromiseOptions } from "request-promise-native";
import type { Cluster } from "../../common/cluster/cluster";
import { k8sRequest } from "../k8s-request";
import type { K8sRequest } from "../k8s-request.injectable";
export interface ClusterDetectionResult {
value: string | number | boolean;
@@ -15,11 +15,11 @@ export interface ClusterDetectionResult {
export abstract class BaseClusterDetector {
abstract readonly key: string;
constructor(public readonly cluster: Cluster) {}
constructor(public readonly cluster: Cluster, private _k8sRequest: K8sRequest) {}
abstract detect(): Promise<ClusterDetectionResult | null>;
protected async k8sRequest<T = any>(path: string, options: RequestPromiseOptions = {}): Promise<T> {
return k8sRequest(this.cluster, path, options);
return this._k8sRequest(this.cluster, path, options);
}
}
@@ -0,0 +1,21 @@
/**
* 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 { VersionDetector } from "./version-detector";
import k8sRequestInjectable from "../k8s-request.injectable";
import type { Cluster } from "../../common/cluster/cluster";
const createVersionDetectorInjectable = getInjectable({
id: "create-version-detector",
instantiate: (di) => {
const k8sRequest = di.inject(k8sRequestInjectable);
return (cluster: Cluster) =>
new VersionDetector(cluster, k8sRequest);
},
});
export default createVersionDetectorInjectable;
@@ -0,0 +1,16 @@
/**
* 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 { DetectorRegistry } from "./detector-registry";
import k8sRequestInjectable from "../k8s-request.injectable";
const detectorRegistryInjectable = getInjectable({
id: "detector-registry",
instantiate: (di) =>
new DetectorRegistry({ k8sRequest: di.inject(k8sRequestInjectable) }),
});
export default detectorRegistryInjectable;
@@ -5,13 +5,19 @@
import { observable } from "mobx";
import type { ClusterMetadata } from "../../common/cluster-types";
import { Singleton } from "../../common/utils";
import type { Cluster } from "../../common/cluster/cluster";
import type { BaseClusterDetector, ClusterDetectionResult } from "./base-cluster-detector";
import type { K8sRequest } from "../k8s-request.injectable";
export type DetectorConstructor = new (cluster: Cluster) => BaseClusterDetector;
interface Dependencies {
k8sRequest: K8sRequest;
}
export type DetectorConstructor = new (cluster: Cluster, k8sRequest: K8sRequest) => BaseClusterDetector;
export class DetectorRegistry {
constructor(private dependencies: Dependencies) {}
export class DetectorRegistry extends Singleton {
registry = observable.array<DetectorConstructor>([], { deep: false });
add(detectorClass: DetectorConstructor): this {
@@ -24,7 +30,7 @@ export class DetectorRegistry extends Singleton {
const results: { [key: string]: ClusterDetectionResult } = {};
for (const detectorClass of this.registry) {
const detector = new detectorClass(cluster);
const detector = new detectorClass(cluster, this.dependencies.k8sRequest);
try {
const data = await detector.detect();
+25
View File
@@ -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 { ClusterManager } from "./cluster-manager";
import clusterStoreInjectable from "../common/cluster-store/cluster-store.injectable";
import catalogEntityRegistryInjectable from "./catalog/entity-registry.injectable";
const clusterManagerInjectable = getInjectable({
id: "cluster-manager",
instantiate: (di) => {
const clusterManager = new ClusterManager({
store: di.inject(clusterStoreInjectable),
catalogEntityRegistry: di.inject(catalogEntityRegistryInjectable),
});
clusterManager.init();
return clusterManager;
},
});
export default clusterManagerInjectable;
+29 -26
View File
@@ -9,52 +9,55 @@ import { action, makeObservable, observable, observe, reaction, toJS } from "mob
import type { Cluster } from "../common/cluster/cluster";
import logger from "./logger";
import { apiKubePrefix } from "../common/vars";
import { getClusterIdFromHost, isErrnoException, Singleton } from "../common/utils";
import { catalogEntityRegistry } from "./catalog";
import { getClusterIdFromHost, isErrnoException } from "../common/utils";
import type { KubernetesClusterPrometheusMetrics } from "../common/catalog-entities/kubernetes-cluster";
import { isKubernetesCluster, KubernetesCluster, LensKubernetesClusterStatus } from "../common/catalog-entities/kubernetes-cluster";
import { ipcMainOn } from "../common/ipc";
import { once } from "lodash";
import { ClusterStore } from "../common/cluster-store/cluster-store";
import type { ClusterStore } from "../common/cluster-store/cluster-store";
import type { ClusterId } from "../common/cluster-types";
import type { CatalogEntityRegistry } from "./catalog";
const logPrefix = "[CLUSTER-MANAGER]:";
const lensSpecificClusterStatuses: Set<string> = new Set(Object.values(LensKubernetesClusterStatus));
export class ClusterManager extends Singleton {
private store = ClusterStore.getInstance();
interface Dependencies {
store: ClusterStore;
catalogEntityRegistry: CatalogEntityRegistry;
}
export class ClusterManager {
deleting = observable.set<ClusterId>();
@observable visibleCluster: ClusterId | undefined = undefined;
constructor() {
super();
constructor(private dependencies: Dependencies) {
makeObservable(this);
}
init = once(() => {
// reacting to every cluster's state change and total amount of items
reaction(
() => this.store.clustersList.map(c => c.getState()),
() => this.updateCatalog(this.store.clustersList),
() => this.dependencies.store.clustersList.map(c => c.getState()),
() => this.updateCatalog(this.dependencies.store.clustersList),
{ fireImmediately: false },
);
// reacting to every cluster's preferences change and total amount of items
reaction(
() => this.store.clustersList.map(c => toJS(c.preferences)),
() => this.updateCatalog(this.store.clustersList),
() => this.dependencies.store.clustersList.map(c => toJS(c.preferences)),
() => this.updateCatalog(this.dependencies.store.clustersList),
{ fireImmediately: false },
);
reaction(
() => catalogEntityRegistry.filterItemsByPredicate(isKubernetesCluster),
() => this.dependencies.catalogEntityRegistry.filterItemsByPredicate(isKubernetesCluster),
entities => this.syncClustersFromCatalog(entities),
);
reaction(() => [
catalogEntityRegistry.filterItemsByPredicate(isKubernetesCluster),
this.dependencies.catalogEntityRegistry.filterItemsByPredicate(isKubernetesCluster),
this.visibleCluster,
] as const, ([entities, visibleCluster]) => {
for (const entity of entities) {
@@ -68,7 +71,7 @@ export class ClusterManager extends Singleton {
observe(this.deleting, change => {
if (change.type === "add") {
this.updateEntityStatus(catalogEntityRegistry.findById(change.newValue) as KubernetesCluster);
this.updateEntityStatus(this.dependencies.catalogEntityRegistry.findById(change.newValue) as KubernetesCluster);
}
});
@@ -86,13 +89,13 @@ export class ClusterManager extends Singleton {
}
protected updateEntityFromCluster(cluster: Cluster) {
const index = catalogEntityRegistry.items.findIndex((entity) => entity.getId() === cluster.id);
const index = this.dependencies.catalogEntityRegistry.items.findIndex((entity) => entity.getId() === cluster.id);
if (index === -1) {
return;
}
const entity = catalogEntityRegistry.items[index] as KubernetesCluster;
const entity = this.dependencies.catalogEntityRegistry.items[index] as KubernetesCluster;
this.updateEntityStatus(entity, cluster);
@@ -133,7 +136,7 @@ export class ClusterManager extends Singleton {
cluster.preferences.icon = undefined;
}
catalogEntityRegistry.items.splice(index, 1, entity);
this.dependencies.catalogEntityRegistry.items.splice(index, 1, entity);
}
@action
@@ -180,7 +183,7 @@ export class ClusterManager extends Singleton {
@action
protected syncClustersFromCatalog(entities: KubernetesCluster[]) {
for (const entity of entities) {
const cluster = this.store.getById(entity.getId());
const cluster = this.dependencies.store.getById(entity.getId());
if (!cluster) {
const model = {
@@ -195,7 +198,7 @@ export class ClusterManager extends Singleton {
* Add the bare minimum of data to ClusterStore. And especially no
* preferences, as those might be configured by the entity's source
*/
this.store.addCluster(model);
this.dependencies.store.addCluster(model);
} catch (error) {
if (isErrnoException(error) && error.code === "ENOENT" && error.path === entity.spec.kubeconfigPath) {
logger.warn(`${logPrefix} kubeconfig file disappeared`, model);
@@ -234,7 +237,7 @@ export class ClusterManager extends Singleton {
protected onNetworkOffline = () => {
logger.info(`${logPrefix} network is offline`);
this.store.clustersList.forEach((cluster) => {
this.dependencies.store.clustersList.forEach((cluster) => {
if (!cluster.disconnected) {
cluster.online = false;
cluster.accessible = false;
@@ -245,7 +248,7 @@ export class ClusterManager extends Singleton {
protected onNetworkOnline = () => {
logger.info(`${logPrefix} network is online`);
this.store.clustersList.forEach((cluster) => {
this.dependencies.store.clustersList.forEach((cluster) => {
if (!cluster.disconnected) {
cluster.refreshConnectionStatus().catch((e) => e);
}
@@ -253,12 +256,12 @@ export class ClusterManager extends Singleton {
};
stop() {
this.store.clusters.forEach((cluster: Cluster) => {
this.dependencies.store.clusters.forEach((cluster: Cluster) => {
cluster.disconnect();
});
}
getClusterForRequest(req: http.IncomingMessage): Cluster | undefined {
getClusterForRequest = (req: http.IncomingMessage): Cluster | undefined => {
if (!req.headers.host) {
return undefined;
}
@@ -266,7 +269,7 @@ export class ClusterManager extends Singleton {
// lens-server is connecting to 127.0.0.1:<port>/<uid>
if (req.url && req.headers.host.startsWith("127.0.0.1")) {
const clusterId = req.url.split("/")[1];
const cluster = this.store.getById(clusterId);
const cluster = this.dependencies.store.getById(clusterId);
if (cluster) {
// we need to swap path prefix so that request is proxied to kube api
@@ -276,8 +279,8 @@ export class ClusterManager extends Singleton {
return cluster;
}
return this.store.getById(getClusterIdFromHost(req.headers.host));
}
return this.dependencies.store.getById(getClusterIdFromHost(req.headers.host));
};
}
export function catalogEntityFromCluster(cluster: Cluster) {
+6 -6
View File
@@ -3,8 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { PrometheusProvider, PrometheusService } from "../prometheus/provider-registry";
import { PrometheusProviderRegistry } from "../prometheus/provider-registry";
import type { PrometheusProvider, PrometheusService, PrometheusProviderRegistry } from "../prometheus/provider-registry";
import type { ClusterPrometheusPreferences } from "../../common/cluster-types";
import type { Cluster } from "../../common/cluster/cluster";
import type httpProxy from "http-proxy";
@@ -28,8 +27,9 @@ interface PrometheusServicePreferences {
}
interface Dependencies {
createKubeAuthProxy: CreateKubeAuthProxy;
authProxyCa: string;
readonly createKubeAuthProxy: CreateKubeAuthProxy;
readonly authProxyCa: string;
readonly prometheusProviderRegistry: PrometheusProviderRegistry;
}
export interface ClusterContextHandler {
@@ -79,11 +79,11 @@ export class ContextHandler implements ClusterContextHandler {
this.prometheusProvider = service.id;
}
return PrometheusProviderRegistry.getInstance().getByKind(this.prometheusProvider);
return this.dependencies.prometheusProviderRegistry.getByKind(this.prometheusProvider);
}
protected listPotentialProviders(): PrometheusProvider[] {
const registry = PrometheusProviderRegistry.getInstance();
const registry = this.dependencies.prometheusProviderRegistry;
const provider = this.prometheusProvider && registry.getByKind(this.prometheusProvider);
if (provider) {
@@ -10,23 +10,25 @@ import { ContextHandler } from "./context-handler";
import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-proxy.injectable";
import { getKubeAuthProxyCertificate } from "../kube-auth-proxy/get-kube-auth-proxy-certificate";
import URLParse from "url-parse";
import prometheusProviderRegistryInjectable from "../prometheus/prometheus-provider-registry.injectable";
const createContextHandlerInjectable = getInjectable({
id: "create-context-handler",
instantiate: (di) => {
const createKubeAuthProxy = di.inject(createKubeAuthProxyInjectable);
const prometheusProviderRegistry = di.inject(prometheusProviderRegistryInjectable);
return (cluster: Cluster): ClusterContextHandler | undefined => {
return (cluster: Cluster): ClusterContextHandler => {
const clusterUrl = new URLParse(cluster.apiUrl);
return new ContextHandler(
{
createKubeAuthProxy,
authProxyCa: getKubeAuthProxyCertificate(clusterUrl.hostname, selfsigned.generate).cert,
},
cluster,
);
const dependencies = {
createKubeAuthProxy,
prometheusProviderRegistry,
authProxyCa: getKubeAuthProxyCertificate(clusterUrl.hostname, selfsigned.generate).cert,
};
return new ContextHandler(dependencies, cluster);
};
},
});
@@ -13,6 +13,8 @@ import { createClusterInjectionToken } from "../../common/cluster/create-cluster
import authorizationReviewInjectable from "../../common/cluster/authorization-review.injectable";
import listNamespacesInjectable from "../../common/cluster/list-namespaces.injectable";
import loggerInjectable from "../../common/logger.injectable";
import detectorRegistryInjectable from "../cluster-detectors/detector-registry.injectable";
import createVersionDetectorInjectable from "../cluster-detectors/create-version-detector.injectable";
const createClusterInjectable = getInjectable({
id: "create-cluster",
@@ -26,6 +28,8 @@ const createClusterInjectable = getInjectable({
createAuthorizationReview: di.inject(authorizationReviewInjectable),
createListNamespaces: di.inject(listNamespacesInjectable),
logger: di.inject(loggerInjectable),
detectorRegistry: di.inject(detectorRegistryInjectable),
createVersionDetector: di.inject(createVersionDetectorInjectable),
};
return (model) => new Cluster(dependencies, model);
-20
View File
@@ -1,20 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import logger from "./logger";
/**
* Installs Electron developer tools in the development build.
* The dependency is not bundled to the production build.
*/
export const installDeveloperTools = () => {
if (process.env.NODE_ENV === "development") {
logger.info("🤓 Installing developer tools");
import("electron-devtools-installer")
.then(({ default: devToolsInstaller, REACT_DEVELOPER_TOOLS }) => devToolsInstaller([REACT_DEVELOPER_TOOLS]))
.then((name) => logger.info(`[DEVTOOLS-INSTALLER]: installed ${name}`))
.catch(error => logger.error(`[DEVTOOLS-INSTALLER]: failed`, { error }));
}
};
@@ -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";
const autoUpdaterInjectable = getInjectable({
id: "auto-updater",
instantiate: () => autoUpdater,
causesSideEffects: true,
});
export default autoUpdaterInjectable;
@@ -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 electronAppInjectable from "../electron-app.injectable";
const disableHardwareAccelerationInjectable = getInjectable({
id: "disable-hardware-acceleration",
instantiate: (di) => {
const app = di.inject(electronAppInjectable);
return () => {
app.disableHardwareAcceleration();
};
},
});
export default disableHardwareAccelerationInjectable;
@@ -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 { dialog } from "electron";
const electronDialogInjectable = getInjectable({
id: "electron-dialog",
instantiate: () => dialog,
causesSideEffects: true,
});
export default electronDialogInjectable;
@@ -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 electronAppInjectable from "../electron-app.injectable";
const exitAppInjectable = getInjectable({
id: "exit-app",
instantiate: (di) => () => {
const app = di.inject(electronAppInjectable);
app.exit(0);
},
});
export default exitAppInjectable;
@@ -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 electronAppInjectable from "../electron-app.injectable";
const getCommandLineSwitchInjectable = getInjectable({
id: "get-command-line-switch",
instantiate: (di) => {
const app = di.inject(electronAppInjectable);
return (name: string) => app.commandLine.getSwitchValue(name);
},
});
export default getCommandLineSwitchInjectable;
@@ -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 nativeThemeInjectable from "./native-theme.injectable";
const getElectronThemeInjectable = getInjectable({
id: "get-electron-theme",
instantiate: (di) => {
const nativeTheme = di.inject(nativeThemeInjectable);
return () => nativeTheme.shouldUseDarkColors ? "dark" : "light";
},
});
export default getElectronThemeInjectable;
@@ -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 { nativeTheme } from "electron";
const nativeThemeInjectable = getInjectable({
id: "native-theme",
instantiate: () => nativeTheme,
causesSideEffects: true,
});
export default nativeThemeInjectable;
@@ -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 { powerMonitor } from "electron";
const powerMonitorInjectable = getInjectable({
id: "power-monitor",
instantiate: () => powerMonitor,
causesSideEffects: true,
});
export default powerMonitorInjectable;
@@ -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 { protocol } from "electron";
import getAbsolutePathInjectable from "../../../common/path/get-absolute-path.injectable";
const registerFileProtocolInjectable = getInjectable({
id: "register-file-protocol",
instantiate: (di) => {
const getAbsolutePath = di.inject(getAbsolutePathInjectable);
return (name: string, basePath: string) => {
protocol.registerFileProtocol(name, (request, callback) => {
const filePath = request.url.replace(`${name}://`, "");
const absPath = getAbsolutePath(basePath, filePath);
callback({ path: absPath });
});
};
},
causesSideEffects: true,
});
export default registerFileProtocolInjectable;
@@ -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 electronAppInjectable from "../electron-app.injectable";
const requestSingleInstanceLockInjectable = getInjectable({
id: "request-single-instance-lock",
instantiate: (di) => {
const app = di.inject(electronAppInjectable);
return () => app.requestSingleInstanceLock();
},
});
export default requestSingleInstanceLockInjectable;
@@ -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 electronAppInjectable from "../electron-app.injectable";
import isMacInjectable from "../../../common/vars/is-mac.injectable";
import commandLineArgumentsInjectable from "../../utils/command-line-arguments.injectable";
const shouldStartHiddenInjectable = getInjectable({
id: "should-start-hidden",
instantiate: (di) => {
const app = di.inject(electronAppInjectable);
const isMac = di.inject(isMacInjectable);
const commandLineArguments = di.inject(commandLineArgumentsInjectable);
// Start the app without showing the main window when auto starting on login
// (On Windows and Linux, we get a flag. On MacOS, we get special API.)
return (
commandLineArguments.includes("--hidden") ||
(isMac && app.getLoginItemSettings().wasOpenedAsHidden)
);
},
});
export default shouldStartHiddenInjectable;
@@ -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 electronDialogInjectable from "./electron-dialog.injectable";
const showErrorPopupInjectable = getInjectable({
id: "show-error-popup",
instantiate: (di) => {
const dialog = di.inject(electronDialogInjectable);
return (heading: string, message: string) => {
dialog.showErrorBox(heading, message);
};
},
});
export default showErrorPopupInjectable;
@@ -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 electronDialogInjectable from "./electron-dialog.injectable";
export type ShowMessagePopup = (title: string, message: string, detail: string) => void;
const showMessagePopupInjectable = getInjectable({
id: "show-message-popup",
instantiate: (di): ShowMessagePopup => {
const dialog = di.inject(electronDialogInjectable);
return async (title, message, detail) => {
await dialog.showMessageBox({
title,
message,
detail,
type: "info",
buttons: ["Close"],
});
};
},
});
export default showMessagePopupInjectable;
@@ -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 operatingSystemThemeStateInjectable from "../../theme/operating-system-theme-state.injectable";
import nativeThemeInjectable from "./native-theme.injectable";
import getElectronThemeInjectable from "./get-electron-theme.injectable";
const syncThemeFromOperatingSystemInjectable = getInjectable({
id: "sync-theme-from-operating-system",
instantiate: (di) => {
const currentThemeState = di.inject(operatingSystemThemeStateInjectable);
const nativeTheme = di.inject(nativeThemeInjectable);
const getElectronTheme = di.inject(getElectronThemeInjectable);
return getStartableStoppable("sync-theme-from-operating-system", () => {
const updateThemeState = () => {
const newTheme = getElectronTheme();
currentThemeState.set(newTheme);
};
nativeTheme.on("updated", updateThemeState);
return () => {
nativeTheme.off("updated", updateThemeState);
};
});
},
});
export default syncThemeFromOperatingSystemInjectable;
@@ -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 electronAppInjectable from "../electron-app.injectable";
const waitForElectronToBeReadyInjectable = getInjectable({
id: "wait-for-electron-to-be-ready",
instantiate: (di) => () => di.inject(electronAppInjectable).whenReady(),
});
export default waitForElectronToBeReadyInjectable;
@@ -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 { beforeQuitOfBackEndInjectionToken } from "../../start-main-application/runnable-tokens/before-quit-of-back-end-injection-token";
import lensProtocolRouterMainInjectable from "../../protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable";
const cleanUpDeepLinkingInjectable = getInjectable({
id: "clean-up-deep-linking",
instantiate: (di) => {
const lensProtocolRouterMain = di.inject(lensProtocolRouterMainInjectable);
return {
run: () => {
lensProtocolRouterMain.cleanup();
},
};
},
injectionToken: beforeQuitOfBackEndInjectionToken,
});
export default cleanUpDeepLinkingInjectable;
@@ -0,0 +1,36 @@
/**
* 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 electronAppInjectable from "../../electron-app.injectable";
import { lensWindowInjectionToken } from "../../../start-main-application/lens-window/application-window/lens-window-injection-token";
import { pipeline } from "@ogre-tools/fp";
import { filter, isEmpty } from "lodash/fp";
const hideDockForLastClosedWindowInjectable = getInjectable({
id: "hide-dock-when-there-are-no-windows",
instantiate: (di) => {
const app = di.inject(electronAppInjectable);
const getLensWindows = () => di.injectMany(lensWindowInjectionToken);
return {
run: () => {
const visibleWindows = pipeline(
getLensWindows(),
filter(window => !!window.visible),
);
if (isEmpty(visibleWindows)) {
app.dock?.hide();
}
},
};
},
injectionToken: beforeQuitOfFrontEndInjectionToken,
});
export default hideDockForLastClosedWindowInjectable;
@@ -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 electronAppInjectable from "../../electron-app.injectable";
import { afterWindowIsOpenedInjectionToken } from "../../../start-main-application/runnable-tokens/after-window-is-opened-injection-token";
const showDockForFirstOpenedWindowInjectable = getInjectable({
id: "show-dock-for-first-opened-window",
instantiate: (di) => {
const app = di.inject(electronAppInjectable);
return {
run: () => {
app.dock?.show();
},
};
},
injectionToken: afterWindowIsOpenedInjectionToken,
});
export default showDockForFirstOpenedWindowInjectable;
@@ -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 { beforeElectronIsReadyInjectionToken } from "../../start-main-application/runnable-tokens/before-electron-is-ready-injection-token";
import requestSingleInstanceLockInjectable from "../features/request-single-instance-lock.injectable";
import exitAppInjectable from "../features/exit-app.injectable";
const enforceSingleApplicationInstanceInjectable = getInjectable({
id: "enforce-single-application-instance",
instantiate: (di) => {
const requestSingleInstanceLock = di.inject(requestSingleInstanceLockInjectable);
const exitApp = di.inject(exitAppInjectable);
return {
run: () => {
if (!requestSingleInstanceLock()) {
exitApp();
}
},
};
},
injectionToken: beforeElectronIsReadyInjectionToken,
});
export default enforceSingleApplicationInstanceInjectable;
@@ -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 appNameInjectable from "../../app-paths/app-name/app-name.injectable";
import { beforeElectronIsReadyInjectionToken } from "../../start-main-application/runnable-tokens/before-electron-is-ready-injection-token";
import electronAppInjectable from "../electron-app.injectable";
const setupApplicationNameInjectable = getInjectable({
id: "setup-application-name",
instantiate: (di) => {
const app = di.inject(electronAppInjectable);
const appName = di.inject(appNameInjectable);
return {
run: () => {
app.setName(appName);
},
};
},
injectionToken: beforeElectronIsReadyInjectionToken,
});
export default setupApplicationNameInjectable;
@@ -0,0 +1,73 @@
/**
* 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 electronAppInjectable from "../electron-app.injectable";
import openDeepLinkInjectable from "../../protocol-handler/lens-protocol-router-main/open-deep-link-for-url/open-deep-link.injectable";
import loggerInjectable from "../../../common/logger.injectable";
import commandLineArgumentsInjectable from "../../utils/command-line-arguments.injectable";
import { pipeline } from "@ogre-tools/fp";
import { find, startsWith, toLower, map } from "lodash/fp";
import { onLoadOfApplicationInjectionToken } from "../../start-main-application/runnable-tokens/on-load-of-application-injection-token";
import showApplicationWindowInjectable from "../../start-main-application/lens-window/show-application-window.injectable";
const setupDeepLinkingInjectable = getInjectable({
id: "setup-deep-linking",
instantiate: (di) => {
const app = di.inject(electronAppInjectable);
const logger = di.inject(loggerInjectable);
const openDeepLinkForUrl = di.inject(openDeepLinkInjectable);
const showApplicationWindow = di.inject(showApplicationWindowInjectable);
const firstInstanceCommandLineArguments = di.inject(
commandLineArgumentsInjectable,
);
return {
run: async () => {
logger.info(`📟 Setting protocol client for lens://`);
if (app.setAsDefaultProtocolClient("lens")) {
logger.info("📟 Protocol client register succeeded ✅");
} else {
logger.info("📟 Protocol client register failed ❗");
}
const url = getDeepLinkUrl(firstInstanceCommandLineArguments);
if (url) {
await openDeepLinkForUrl(url);
}
app.on("open-url", async (event, url) => {
event.preventDefault();
await openDeepLinkForUrl(url);
});
app.on(
"second-instance",
async (_, secondInstanceCommandLineArguments) => {
const url = getDeepLinkUrl(secondInstanceCommandLineArguments);
await showApplicationWindow();
if (url) {
await openDeepLinkForUrl(url);
}
},
);
},
};
},
injectionToken: onLoadOfApplicationInjectionToken,
});
export default setupDeepLinkingInjectable;
const getDeepLinkUrl = (commandLineArguments: string[]) =>
pipeline(commandLineArguments, map(toLower), find(startsWith("lens://")));
@@ -0,0 +1,40 @@
/**
* 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 loggerInjectable from "../../../common/logger.injectable";
import { onLoadOfApplicationInjectionToken } from "../../start-main-application/runnable-tokens/on-load-of-application-injection-token";
const setupDeveloperToolsInDevelopmentEnvironmentInjectable = getInjectable({
id: "setup-developer-tools-in-development-environment",
instantiate: (di) => {
const logger = di.inject(loggerInjectable);
return {
run: () => {
if (process.env.NODE_ENV !== "development") {
return;
}
logger.info("🤓 Installing developer tools");
import("electron-devtools-installer")
.then(({ default: devToolsInstaller, REACT_DEVELOPER_TOOLS }) =>
devToolsInstaller([REACT_DEVELOPER_TOOLS]),
)
.then((name) =>
logger.info(`[DEVTOOLS-INSTALLER]: installed ${name}`),
)
.catch((error) =>
logger.error(`[DEVTOOLS-INSTALLER]: failed`, { error }),
);
},
};
},
injectionToken: onLoadOfApplicationInjectionToken,
});
export default setupDeveloperToolsInDevelopmentEnvironmentInjectable;
@@ -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 powerMonitorInjectable from "../features/power-monitor.injectable";
import exitAppInjectable from "../features/exit-app.injectable";
import { onLoadOfApplicationInjectionToken } from "../../start-main-application/runnable-tokens/on-load-of-application-injection-token";
const setupDeviceShutdownInjectable = getInjectable({
id: "setup-device-shutdown",
instantiate: (di) => {
const powerMonitor = di.inject(powerMonitorInjectable);
const exitApp = di.inject(exitAppInjectable);
return {
run: () => {
powerMonitor.on("shutdown", async () => {
exitApp();
});
},
};
},
injectionToken: onLoadOfApplicationInjectionToken,
});
export default setupDeviceShutdownInjectable;
@@ -0,0 +1,58 @@
/**
* 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 directoryForLensLocalStorageInjectable from "../../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
import { setupIpcMainHandlers } from "./setup-ipc-main-handlers";
import loggerInjectable from "../../../../common/logger.injectable";
import clusterManagerInjectable from "../../../cluster-manager.injectable";
import applicationMenuItemsInjectable from "../../../menu/application-menu-items.injectable";
import getAbsolutePathInjectable from "../../../../common/path/get-absolute-path.injectable";
import clusterStoreInjectable from "../../../../common/cluster-store/cluster-store.injectable";
import { onLoadOfApplicationInjectionToken } from "../../../start-main-application/runnable-tokens/on-load-of-application-injection-token";
import operatingSystemThemeInjectable from "../../../theme/operating-system-theme.injectable";
import catalogEntityRegistryInjectable from "../../../catalog/entity-registry.injectable";
import askUserForFilePathsInjectable from "../../../ipc/ask-user-for-file-paths.injectable";
const setupIpcMainHandlersInjectable = getInjectable({
id: "setup-ipc-main-handlers",
instantiate: (di) => {
const logger = di.inject(loggerInjectable);
const directoryForLensLocalStorage = di.inject(
directoryForLensLocalStorageInjectable,
);
const clusterManager = di.inject(clusterManagerInjectable);
const applicationMenuItems = di.inject(applicationMenuItemsInjectable);
const getAbsolutePath = di.inject(getAbsolutePathInjectable);
const catalogEntityRegistry = di.inject(catalogEntityRegistryInjectable);
const clusterStore = di.inject(clusterStoreInjectable);
const operatingSystemTheme = di.inject(operatingSystemThemeInjectable);
const askUserForFilePaths = di.inject(askUserForFilePathsInjectable);
return {
run: () => {
logger.debug("[APP-MAIN] initializing ipc main handlers");
setupIpcMainHandlers({
applicationMenuItems,
getAbsolutePath,
directoryForLensLocalStorage,
clusterManager,
catalogEntityRegistry,
clusterStore,
operatingSystemTheme,
askUserForFilePaths,
});
},
};
},
injectionToken: onLoadOfApplicationInjectionToken,
causesSideEffects: true,
});
export default setupIpcMainHandlersInjectable;
@@ -2,37 +2,41 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { IpcMainInvokeEvent } from "electron";
import { BrowserWindow, Menu } from "electron";
import { clusterFrameMap } from "../../../common/cluster-frames";
import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler, clusterSetDeletingHandler, clusterClearDeletingHandler } from "../../../common/ipc/cluster";
import type { ClusterId } from "../../../common/cluster-types";
import { ClusterStore } from "../../../common/cluster-store/cluster-store";
import { appEventBus } from "../../../common/app-event-bus/event-bus";
import { broadcastMainChannel, broadcastMessage, ipcMainHandle, ipcMainOn } from "../../../common/ipc";
import { catalogEntityRegistry } from "../../catalog";
import { pushCatalogToRenderer } from "../../catalog-pusher";
import { ClusterManager } from "../../cluster-manager";
import { ResourceApplier } from "../../resource-applier";
import { clusterFrameMap } from "../../../../common/cluster-frames";
import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler, clusterDeleteHandler, clusterSetDeletingHandler, clusterClearDeletingHandler } from "../../../../common/ipc/cluster";
import type { ClusterId } from "../../../../common/cluster-types";
import { ClusterStore } from "../../../../common/cluster-store/cluster-store";
import { appEventBus } from "../../../../common/app-event-bus/event-bus";
import { broadcastMainChannel, broadcastMessage, ipcMainHandle, ipcMainOn } from "../../../../common/ipc";
import type { CatalogEntityRegistry } from "../../../catalog";
import { pushCatalogToRenderer } from "../../../catalog-pusher";
import type { ClusterManager } from "../../../cluster-manager";
import { ResourceApplier } from "../../../resource-applier";
import { remove } from "fs-extra";
import { onLocationChange, handleWindowAction } from "../../ipc/window";
import { openFilePickingDialogChannel } from "../../../common/ipc/dialog";
import { showOpenDialog } from "../../ipc/dialog";
import { windowActionHandleChannel, windowLocationChangedChannel, windowOpenAppMenuAsContextMenuChannel } from "../../../common/ipc/window";
import { getNativeColorTheme } from "../../native-theme";
import { getNativeThemeChannel } from "../../../common/ipc/native-theme";
import type { GetAbsolutePath } from "../../../common/path/get-absolute-path.injectable";
import type { IComputedValue } from "mobx";
import type { MenuItemOpts } from "../../menu/application-menu-items.injectable";
import type { GetAbsolutePath } from "../../../../common/path/get-absolute-path.injectable";
import type { MenuItemOpts } from "../../../menu/application-menu-items.injectable";
import { windowActionHandleChannel, windowLocationChangedChannel, windowOpenAppMenuAsContextMenuChannel } from "../../../../common/ipc/window";
import { handleWindowAction, onLocationChange } from "../../../ipc/window";
import { openFilePickingDialogChannel } from "../../../../common/ipc/dialog";
import { getNativeThemeChannel } from "../../../../common/ipc/native-theme";
import type { Theme } from "../../../theme/operating-system-theme-state.injectable";
import type { AskUserForFilePaths } from "../../../ipc/ask-user-for-file-paths.injectable";
interface Dependencies {
directoryForLensLocalStorage: string;
getAbsolutePath: GetAbsolutePath;
applicationMenuItems: IComputedValue<MenuItemOpts[]>;
clusterManager: ClusterManager;
catalogEntityRegistry: CatalogEntityRegistry;
clusterStore: ClusterStore;
operatingSystemTheme: IComputedValue<Theme>;
askUserForFilePaths: AskUserForFilePaths;
}
export const initIpcMainHandlers = ({ applicationMenuItems, directoryForLensLocalStorage, getAbsolutePath }: Dependencies) => () => {
export const setupIpcMainHandlers = ({ applicationMenuItems, directoryForLensLocalStorage, getAbsolutePath, clusterManager, catalogEntityRegistry, clusterStore, operatingSystemTheme, askUserForFilePaths }: Dependencies) => {
ipcMainHandle(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => {
return ClusterStore.getInstance()
.getById(clusterId)
@@ -51,7 +55,7 @@ export const initIpcMainHandlers = ({ applicationMenuItems, directoryForLensLoca
});
ipcMainOn(clusterVisibilityHandler, (event, clusterId?: ClusterId) => {
ClusterManager.getInstance().visibleCluster = clusterId;
clusterManager.visibleCluster = clusterId;
});
ipcMainHandle(clusterRefreshHandler, (event, clusterId: ClusterId) => {
@@ -97,11 +101,11 @@ export const initIpcMainHandlers = ({ applicationMenuItems, directoryForLensLoca
});
ipcMainHandle(clusterSetDeletingHandler, (event, clusterId: string) => {
ClusterManager.getInstance().deleting.add(clusterId);
clusterManager.deleting.add(clusterId);
});
ipcMainHandle(clusterClearDeletingHandler, (event, clusterId: string) => {
ClusterManager.getInstance().deleting.delete(clusterId);
clusterManager.deleting.delete(clusterId);
});
ipcMainHandle(clusterKubectlApplyAllHandler, async (event, clusterId: ClusterId, resources: string[], extraArgs: string[]) => {
@@ -146,7 +150,7 @@ export const initIpcMainHandlers = ({ applicationMenuItems, directoryForLensLoca
ipcMainOn(windowLocationChangedChannel, () => onLocationChange());
ipcMainHandle(openFilePickingDialogChannel, (event, opts) => showOpenDialog(opts));
ipcMainHandle(openFilePickingDialogChannel, (event, opts) => askUserForFilePaths(opts));
ipcMainHandle(broadcastMainChannel, (event, channel, ...args) => broadcastMessage(channel, ...args));
@@ -164,6 +168,8 @@ export const initIpcMainHandlers = ({ applicationMenuItems, directoryForLensLoca
});
ipcMainHandle(getNativeThemeChannel, () => {
return getNativeColorTheme();
return operatingSystemTheme.get();
});
clusterStore.provideInitialFromMain();
};
@@ -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 electronAppInjectable from "../electron-app.injectable";
import loggerInjectable from "../../../common/logger.injectable";
import { onLoadOfApplicationInjectionToken } from "../../start-main-application/runnable-tokens/on-load-of-application-injection-token";
import showApplicationWindowInjectable from "../../start-main-application/lens-window/show-application-window.injectable";
const setupMainWindowVisibilityAfterActivationInjectable = getInjectable({
id: "setup-main-window-visibility-after-activation",
instantiate: (di) => {
const app = di.inject(electronAppInjectable);
const showApplicationWindow = di.inject(showApplicationWindowInjectable);
const logger = di.inject(loggerInjectable);
return {
run: () => {
app.on("activate", async (_, windowIsVisible) => {
logger.info("APP:ACTIVATE", { hasVisibleWindows: windowIsVisible });
if (!windowIsVisible) {
await showApplicationWindow();
}
});
},
};
},
injectionToken: onLoadOfApplicationInjectionToken,
});
export default setupMainWindowVisibilityAfterActivationInjectable;
@@ -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 { beforeElectronIsReadyInjectionToken } from "../../start-main-application/runnable-tokens/before-electron-is-ready-injection-token";
import electronAppInjectable from "../electron-app.injectable";
import { runManyFor } from "../../../common/runnable/run-many-for";
import { afterWindowIsOpenedInjectionToken } from "../../start-main-application/runnable-tokens/after-window-is-opened-injection-token";
const setupRunnablesAfterWindowIsOpenedInjectable = getInjectable({
id: "setup-runnables-after-window-is-opened",
instantiate: (di) => {
const afterWindowIsOpened = runManyFor(di)(afterWindowIsOpenedInjectionToken);
return {
run: () => {
const app = di.inject(electronAppInjectable);
app.on("browser-window-created", async () => {
await afterWindowIsOpened();
});
},
};
},
injectionToken: beforeElectronIsReadyInjectionToken,
});
export default setupRunnablesAfterWindowIsOpenedInjectable;
@@ -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 { beforeElectronIsReadyInjectionToken } from "../../start-main-application/runnable-tokens/before-electron-is-ready-injection-token";
import { beforeQuitOfFrontEndInjectionToken } from "../../start-main-application/runnable-tokens/before-quit-of-front-end-injection-token";
import { beforeQuitOfBackEndInjectionToken } from "../../start-main-application/runnable-tokens/before-quit-of-back-end-injection-token";
import electronAppInjectable from "../electron-app.injectable";
import isIntegrationTestingInjectable from "../../../common/vars/is-integration-testing.injectable";
import autoUpdaterInjectable from "../features/auto-updater.injectable";
import { runManySyncFor } from "../../../common/runnable/run-many-sync-for";
const setupRunnablesBeforeClosingOfApplicationInjectable = getInjectable({
id: "setup-closing-of-application",
instantiate: (di) => {
const runMany = runManySyncFor(di);
const runRunnablesBeforeQuitOfFrontEnd = runMany(
beforeQuitOfFrontEndInjectionToken,
);
const runRunnablesBeforeQuitOfBackEnd = runMany(
beforeQuitOfBackEndInjectionToken,
);
return {
run: () => {
const app = di.inject(electronAppInjectable);
const isIntegrationTesting = di.inject(isIntegrationTestingInjectable);
const autoUpdater = di.inject(autoUpdaterInjectable);
let isAutoUpdating = false;
autoUpdater.on("before-quit-for-update", () => {
isAutoUpdating = true;
});
app.on("will-quit", (event) => {
runRunnablesBeforeQuitOfFrontEnd();
const shouldQuitBackEnd = isIntegrationTesting || isAutoUpdating;
if (shouldQuitBackEnd) {
runRunnablesBeforeQuitOfBackEnd();
} else {
// IMPORTANT: This cannot be destructured as it would break binding of "this" for the Electron event
event.preventDefault();
}
});
},
};
},
injectionToken: beforeElectronIsReadyInjectionToken,
});
export default setupRunnablesBeforeClosingOfApplicationInjectable;
@@ -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 { 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;
-23
View File
@@ -1,23 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { app } from "electron";
import { WindowManager } from "./window-manager";
import { appEventBus } from "../common/app-event-bus/event-bus";
import { ClusterManager } from "./cluster-manager";
import logger from "./logger";
export function exitApp() {
const windowManager = WindowManager.getInstance(false);
const clusterManager = ClusterManager.getInstance(false);
appEventBus.emit({ name: "service", action: "close" });
windowManager?.hide();
clusterManager?.stop();
logger.info("SERVICE:QUIT");
setTimeout(() => {
app.exit();
}, 1000);
}
@@ -9,6 +9,7 @@ import { lensExtensionDependencies } from "../../extensions/lens-extension";
import type { LensMainExtensionDependencies } from "../../extensions/lens-extension-set-dependencies";
import type { LensMainExtension } from "../../extensions/lens-main-extension";
import catalogEntityRegistryInjectable from "../catalog/entity-registry.injectable";
import navigateForExtensionInjectable from "../start-main-application/lens-window/navigate-for-extension.injectable";
const createExtensionInstanceInjectable = getInjectable({
id: "create-extension-instance",
@@ -16,6 +17,7 @@ const createExtensionInstanceInjectable = getInjectable({
const deps: LensMainExtensionDependencies = {
fileSystemProvisionerStore: di.inject(fileSystemProvisionerStoreInjectable),
entityRegistry: di.inject(catalogEntityRegistryInjectable),
navigate: di.inject(navigateForExtensionInjectable),
};
return (ExtensionClass, extension) => {
+37
View File
@@ -0,0 +1,37 @@
/**
* 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 { Cluster } from "../common/cluster/cluster";
import type { IMetricsReqParams } from "../common/k8s-api/endpoints/metrics.api";
import k8sRequestInjectable from "./k8s-request.injectable";
export type GetMetrics = (cluster: Cluster, prometheusPath: string, queryParams: IMetricsReqParams & { query: string }) => Promise<any>;
const getMetricsInjectable = getInjectable({
id: "get-metrics",
instantiate: (di): GetMetrics => {
const k8sRequest = di.inject(k8sRequestInjectable);
return async (
cluster,
prometheusPath,
queryParams,
) => {
const prometheusPrefix = cluster.preferences.prometheus?.prefix || "";
const metricsPath = `/api/v1/namespaces/${prometheusPath}/proxy${prometheusPrefix}/api/v1/query_range`;
return k8sRequest(cluster, metricsPath, {
timeout: 0,
resolveWithFullResponse: false,
json: true,
method: "POST",
form: queryParams,
});
};
},
});
export default getMetricsInjectable;
+11 -14
View File
@@ -4,25 +4,22 @@
*/
import { createContainer } from "@ogre-tools/injectable";
import { autoRegister } from "@ogre-tools/injectable-extension-for-auto-registration";
import { Environments, setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
export const getDi = () => {
const di = createContainer(
getRequireContextForMainCode,
getRequireContextForCommonExtensionCode,
getRequireContextForCommonCode,
);
const di = createContainer();
autoRegister({
di,
requireContexts: [
require.context("./", true, /\.injectable\.(ts|tsx)$/),
require.context("../extensions", true, /\.injectable\.(ts|tsx)$/),
require.context("../common", true, /\.injectable\.(ts|tsx)$/),
],
});
setLegacyGlobalDiForExtensionApi(di, Environments.main);
return di;
};
const getRequireContextForMainCode = () =>
require.context("./", true, /\.injectable\.(ts|tsx)$/);
const getRequireContextForCommonExtensionCode = () =>
require.context("../extensions", true, /\.injectable\.(ts|tsx)$/);
const getRequireContextForCommonCode = () =>
require.context("../common", true, /\.injectable\.(ts|tsx)$/);
+143 -21
View File
@@ -4,12 +4,10 @@
*/
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 getElectronAppPathInjectable from "./app-paths/get-electron-app-path/get-electron-app-path.injectable";
import setElectronAppPathInjectable from "./app-paths/set-electron-app-path/set-electron-app-path.injectable";
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";
@@ -26,16 +24,59 @@ import clusterStoreInjectable from "../common/cluster-store/cluster-store.inject
import type { ClusterStore } from "../common/cluster-store/cluster-store";
import type { Cluster } from "../common/cluster/cluster";
import userStoreInjectable from "../common/user-store/user-store.injectable";
import isMacInjectable from "../common/vars/is-mac.injectable";
import isWindowsInjectable from "../common/vars/is-windows.injectable";
import isLinuxInjectable from "../common/vars/is-linux.injectable";
import type { UserStore } from "../common/user-store";
import getAbsolutePathInjectable from "../common/path/get-absolute-path.injectable";
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 { noop } from "../renderer/utils";
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";
import commandLineArgumentsInjectable from "./utils/command-line-arguments.injectable";
import initializeExtensionsInjectable from "./start-main-application/runnables/initialize-extensions.injectable";
import lensResourcesDirInjectable from "../common/vars/lens-resources-dir.injectable";
import registerFileProtocolInjectable from "./electron-app/features/register-file-protocol.injectable";
import environmentVariablesInjectable from "../common/utils/environment-variables.injectable";
import setupIpcMainHandlersInjectable from "./electron-app/runnables/setup-ipc-main-handlers/setup-ipc-main-handlers.injectable";
import setupLensProxyInjectable from "./start-main-application/runnables/setup-lens-proxy.injectable";
import setupRunnablesForAfterRootFrameIsReadyInjectable from "./start-main-application/runnables/setup-runnables-for-after-root-frame-is-ready.injectable";
import setupSentryInjectable from "./start-main-application/runnables/setup-sentry.injectable";
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";
import setupDeepLinkingInjectable from "./electron-app/runnables/setup-deep-linking.injectable";
import exitAppInjectable from "./electron-app/features/exit-app.injectable";
import getCommandLineSwitchInjectable from "./electron-app/features/get-command-line-switch.injectable";
import requestSingleInstanceLockInjectable from "./electron-app/features/request-single-instance-lock.injectable";
import disableHardwareAccelerationInjectable from "./electron-app/features/disable-hardware-acceleration.injectable";
import shouldStartHiddenInjectable from "./electron-app/features/should-start-hidden.injectable";
import getElectronAppPathInjectable from "./app-paths/get-electron-app-path/get-electron-app-path.injectable";
import setElectronAppPathInjectable from "./app-paths/set-electron-app-path/set-electron-app-path.injectable";
import setupMainWindowVisibilityAfterActivationInjectable from "./electron-app/runnables/setup-main-window-visibility-after-activation.injectable";
import setupDeviceShutdownInjectable from "./electron-app/runnables/setup-device-shutdown.injectable";
import setupApplicationNameInjectable from "./electron-app/runnables/setup-application-name.injectable";
import setupRunnablesBeforeClosingOfApplicationInjectable from "./electron-app/runnables/setup-runnables-before-closing-of-application.injectable";
import showMessagePopupInjectable from "./electron-app/features/show-message-popup.injectable";
import clusterFramesInjectable from "../common/cluster-frames.injectable";
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 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";
import broadcastMessageInjectable from "../common/ipc/broadcast-message.injectable";
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";
export function getDiForUnitTesting(opts: GetDiForUnitTestingOptions = {}) {
const {
@@ -59,27 +100,39 @@ export function getDiForUnitTesting(opts: GetDiForUnitTestingOptions = {}) {
di.preventSideEffects();
if (doGeneralOverrides) {
di.override(hotbarStoreInjectable, () => ({}));
di.override(userStoreInjectable, () => ({}));
di.override(hotbarStoreInjectable, () => ({ load: () => {} }));
di.override(userStoreInjectable, () => ({ startMainReactions: () => {} }) 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(fileSystemProvisionerStoreInjectable, () => ({}) as FileSystemProvisionerStore);
di.override(isMacInjectable, () => true);
di.override(isWindowsInjectable, () => false);
di.override(isLinuxInjectable, () => false);
di.override(getAbsolutePathInjectable, () => getAbsolutePathFake);
di.override(joinPathsInjectable, () => joinPathsFake);
overrideOperatingSystem(di);
overrideRunnablesHavingSideEffects(di);
overrideElectronFeatures(di);
di.override(
getElectronAppPathInjectable,
() => (name: string) => `some-electron-app-path-for-${kebabCase(name)}`,
);
di.override(isDevelopmentInjectable, () => false);
di.override(environmentVariablesInjectable, () => ({}));
di.override(commandLineArgumentsInjectable, () => []);
di.override(setElectronAppPathInjectable, () => () => undefined);
di.override(appNameInjectable, () => "some-electron-app-name");
di.override(clusterFramesInjectable, () => observable.map<string, ClusterFrameInfo>());
di.override(stopServicesAndExitAppInjectable, () => () => {});
di.override(lensResourcesDirInjectable, () => "/irrelevant");
di.override(trayInjectable, () => ({ start: () => {}, stop: () => {} }));
di.override(applicationMenuInjectable, () => ({ start: () => {}, stop: () => {} }));
// 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(directoryForBundledBinariesInjectable, () => "some-bin-directory");
di.override(broadcastMessageInjectable, () => (channel) => {
throw new Error(`Tried to broadcast message to channel "${channel}" over IPC without explicit override.`);
});
di.override(spawnInjectable, () => () => {
return {
stderr: { on: jest.fn(), removeAllListeners: jest.fn() },
@@ -117,3 +170,72 @@ const getInjectableFilePaths = memoize(() => [
...glob.sync("../extensions/**/*.injectable.{ts,tsx}", { cwd: __dirname }),
...glob.sync("../common/**/*.injectable.{ts,tsx}", { cwd: __dirname }),
]);
// TODO: Reorganize code in Runnables to get rid of requirement for override
const overrideRunnablesHavingSideEffects = (di: DiContainer) => {
[
initializeExtensionsInjectable,
setupIpcMainHandlersInjectable,
setupLensProxyInjectable,
setupRunnablesForAfterRootFrameIsReadyInjectable,
setupSentryInjectable,
setupShellInjectable,
setupSyncingOfWeblinksInjectable,
setupSystemCaInjectable,
setupListenerForCurrentClusterFrameInjectable,
setupRunnablesAfterWindowIsOpenedInjectable,
].forEach((injectable) => {
di.override(injectable, () => ({ run: () => {} }));
});
};
const overrideOperatingSystem = (di: DiContainer) => {
di.override(platformInjectable, () => "darwin");
di.override(getAbsolutePathInjectable, () => getAbsolutePathFake);
di.override(joinPathsInjectable, () => joinPathsFake);
};
const overrideElectronFeatures = (di: DiContainer) => {
di.override(setupMainWindowVisibilityAfterActivationInjectable, () => ({
run: () => {},
}));
di.override(setupDeviceShutdownInjectable, () => ({
run: () => {},
}));
di.override(setupDeepLinkingInjectable, () => ({ run: () => {} }));
di.override(exitAppInjectable, () => () => {});
di.override(setupApplicationNameInjectable, () => ({ run: () => {} }));
di.override(setupRunnablesBeforeClosingOfApplicationInjectable, () => ({ run: () => {} }));
di.override(getCommandLineSwitchInjectable, () => () => "irrelevant");
di.override(requestSingleInstanceLockInjectable, () => () => true);
di.override(disableHardwareAccelerationInjectable, () => () => {});
di.override(shouldStartHiddenInjectable, () => true);
di.override(showMessagePopupInjectable, () => () => {});
di.override(waitForElectronToBeReadyInjectable, () => () => Promise.resolve());
di.override(ipcMainInjectable, () => ({}));
di.override(getElectronThemeInjectable, () => () => "dark");
di.override(syncThemeFromOperatingSystemInjectable, () => ({ start: () => {}, stop: () => {} }));
di.override(createElectronWindowForInjectable, () => () => async () => ({
show: () => {},
close: () => {},
send: (arg) => {
const sendFake = di.inject(sendToChannelInElectronBrowserWindowInjectable) as any;
sendFake(null, arg);
},
}));
di.override(
getElectronAppPathInjectable,
() => (name: string) => `some-electron-app-path-for-${kebabCase(name)}`,
);
di.override(setElectronAppPathInjectable, () => () => {});
di.override(isAutoUpdateEnabledInjectable, () => () => false);
di.override(registerFileProtocolInjectable, () => () => {});
};
+6 -10
View File
@@ -17,14 +17,11 @@ describe("Helm Service tests", () => {
it("list charts with deprecated entries", async () => {
mockHelmRepoManager.mockReturnValue({
init: jest.fn(),
repositories: jest.fn().mockImplementation(async () => {
return [
{ name: "stable", url: "stableurl" },
{ name: "experiment", url: "experimenturl" },
];
}),
});
repositories: jest.fn().mockImplementation(async () => [
{ name: "stable", url: "stableurl" },
{ name: "experiment", url: "experimenturl" },
]),
} as any);
const charts = await helmService.listCharts();
@@ -127,13 +124,12 @@ describe("Helm Service tests", () => {
it("list charts sorted by version in descending order", async () => {
mockHelmRepoManager.mockReturnValue({
init: jest.fn(),
repositories: jest.fn().mockImplementation(async () => {
return [
{ name: "bitnami", url: "bitnamiurl" },
];
}),
});
} as any);
const charts = await helmService.listCharts();
@@ -0,0 +1,172 @@
/**
* 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 yaml from "js-yaml";
import { readFile } from "fs-extra";
import { customRequestPromise } from "../../common/request";
import orderBy from "lodash/orderBy";
import logger from "../logger";
import { execHelm } from "./exec";
import type { HelmEnv, HelmRepo, HelmRepoConfig } from "./helm-repo-manager";
interface EnsuredHelmRepoManagerData {
helmEnv: HelmEnv;
didUpdateOnce: boolean;
}
export class HelmRepoManager {
protected helmEnv?: HelmEnv;
protected didUpdateOnce?: boolean;
public async loadAvailableRepos(): Promise<HelmRepo[]> {
const res = await customRequestPromise({
uri: "https://github.com/lensapp/artifact-hub-repositories/releases/download/latest/repositories.json",
json: true,
resolveWithFullResponse: true,
timeout: 10000,
});
return orderBy(res.body as HelmRepo[], repo => repo.name);
}
private async ensureInitialized(): Promise<EnsuredHelmRepoManagerData> {
this.helmEnv ??= await this.parseHelmEnv();
const repos = await this.list(this.helmEnv);
if (repos.length === 0) {
await this.addRepo({
name: "bitnami",
url: "https://charts.bitnami.com/bitnami",
});
}
if (!this.didUpdateOnce) {
await this.update();
this.didUpdateOnce = true;
}
return {
didUpdateOnce: this.didUpdateOnce,
helmEnv: this.helmEnv,
};
}
protected async parseHelmEnv() {
const output = await execHelm(["env"]);
const lines = output.split(/\r?\n/); // split by new line feed
const env: Partial<Record<string, string>> = {};
lines.forEach((line: string) => {
const [key, value] = line.split("=");
if (key && value) {
env[key] = value.replace(/"/g, ""); // strip quotas
}
});
return env as HelmEnv;
}
public async repo(name: string): Promise<HelmRepo | undefined> {
const repos = await this.repositories();
return repos.find(repo => repo.name === name);
}
private async list(helmEnv: HelmEnv): Promise<HelmRepo[]> {
try {
const rawConfig = await readFile(helmEnv.HELM_REPOSITORY_CONFIG, "utf8");
const parsedConfig = yaml.load(rawConfig) as HelmRepoConfig;
if (typeof parsedConfig === "object" && parsedConfig) {
return parsedConfig.repositories;
}
} catch {
// ignore error
}
return [];
}
public async repositories(): Promise<HelmRepo[]> {
try {
const { helmEnv } = await this.ensureInitialized();
const repos = await this.list(helmEnv);
return repos.map(repo => ({
...repo,
cacheFilePath: `${helmEnv.HELM_REPOSITORY_CACHE}/${repo.name}-index.yaml`,
}));
} catch (error) {
logger.error(`[HELM]: repositories listing error`, error);
return [];
}
}
public async update() {
return execHelm([
"repo",
"update",
]);
}
public async addRepo({ name, url, insecureSkipTlsVerify, username, password, caFile, keyFile, certFile }: HelmRepo) {
logger.info(`[HELM]: adding repo ${name} from ${url}`);
const args = [
"repo",
"add",
name,
url,
];
if (insecureSkipTlsVerify) {
args.push("--insecure-skip-tls-verify");
}
if (username) {
args.push("--username", username);
}
if (password) {
args.push("--password", password);
}
if (caFile) {
args.push("--ca-file", caFile);
}
if (keyFile) {
args.push("--key-file", keyFile);
}
if (certFile) {
args.push("--cert-file", certFile);
}
return execHelm(args);
}
public async removeRepo({ name, url }: HelmRepo): Promise<string> {
logger.info(`[HELM]: removing repo ${name} (${url})`);
return execHelm([
"repo",
"remove",
name,
]);
}
}
const helmRepoManagerInjectable = getInjectable({
id: "helm-repo-manager",
instantiate: () => new HelmRepoManager(),
});
export default helmRepoManagerInjectable;
+6 -157
View File
@@ -3,13 +3,11 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import yaml from "js-yaml";
import { readFile } from "fs-extra";
import { Singleton } from "../../common/utils/singleton";
import { customRequestPromise } from "../../common/request";
import orderBy from "lodash/orderBy";
import logger from "../logger";
import { execHelm } from "./exec";
import {
asLegacyGlobalSingletonForExtensionApi,
} from "../../extensions/as-legacy-globals-for-extension-api/as-legacy-global-singleton-object-for-extension-api";
import helmRepoManagerInjectable from "./helm-repo-manager.injectable";
export type HelmEnv = Partial<Record<string, string>> & {
HELM_REPOSITORY_CACHE: string;
@@ -32,153 +30,4 @@ export interface HelmRepo {
password?: string;
}
interface EnsuredHelmRepoManagerData {
helmEnv: HelmEnv;
didUpdateOnce: boolean;
}
export class HelmRepoManager extends Singleton {
protected helmEnv?: HelmEnv;
protected didUpdateOnce?: boolean;
public async loadAvailableRepos(): Promise<HelmRepo[]> {
const res = await customRequestPromise({
uri: "https://github.com/lensapp/artifact-hub-repositories/releases/download/latest/repositories.json",
json: true,
resolveWithFullResponse: true,
timeout: 10000,
});
return orderBy(res.body as HelmRepo[], repo => repo.name);
}
private async ensureInitialized(): Promise<EnsuredHelmRepoManagerData> {
this.helmEnv ??= await this.parseHelmEnv();
const repos = await this.list(this.helmEnv);
if (repos.length === 0) {
await this.addRepo({
name: "bitnami",
url: "https://charts.bitnami.com/bitnami",
});
}
if (!this.didUpdateOnce) {
await this.update();
this.didUpdateOnce = true;
}
return {
didUpdateOnce: this.didUpdateOnce,
helmEnv: this.helmEnv,
};
}
protected async parseHelmEnv() {
const output = await execHelm(["env"]);
const lines = output.split(/\r?\n/); // split by new line feed
const env: Partial<Record<string, string>> = {};
lines.forEach((line: string) => {
const [key, value] = line.split("=");
if (key && value) {
env[key] = value.replace(/"/g, ""); // strip quotas
}
});
return env as HelmEnv;
}
public async repo(name: string): Promise<HelmRepo | undefined> {
const repos = await this.repositories();
return repos.find(repo => repo.name === name);
}
private async list(helmEnv: HelmEnv): Promise<HelmRepo[]> {
try {
const rawConfig = await readFile(helmEnv.HELM_REPOSITORY_CONFIG, "utf8");
const parsedConfig = yaml.load(rawConfig) as HelmRepoConfig;
if (typeof parsedConfig === "object" && parsedConfig) {
return parsedConfig.repositories;
}
} catch {
// ignore error
}
return [];
}
public async repositories(): Promise<HelmRepo[]> {
try {
const { helmEnv } = await this.ensureInitialized();
const repos = await this.list(helmEnv);
return repos.map(repo => ({
...repo,
cacheFilePath: `${helmEnv.HELM_REPOSITORY_CACHE}/${repo.name}-index.yaml`,
}));
} catch (error) {
logger.error(`[HELM]: repositories listing error`, error);
return [];
}
}
public async update() {
return execHelm([
"repo",
"update",
]);
}
public async addRepo({ name, url, insecureSkipTlsVerify, username, password, caFile, keyFile, certFile }: HelmRepo) {
logger.info(`[HELM]: adding repo ${name} from ${url}`);
const args = [
"repo",
"add",
name,
url,
];
if (insecureSkipTlsVerify) {
args.push("--insecure-skip-tls-verify");
}
if (username) {
args.push("--username", username);
}
if (password) {
args.push("--password", password);
}
if (caFile) {
args.push("--ca-file", caFile);
}
if (keyFile) {
args.push("--key-file", keyFile);
}
if (certFile) {
args.push("--cert-file", certFile);
}
return execHelm(args);
}
public async removeRepo({ name, url }: HelmRepo): Promise<string> {
logger.info(`[HELM]: removing repo ${name} (${url})`);
return execHelm([
"repo",
"remove",
name,
]);
}
}
export const HelmRepoManager = asLegacyGlobalSingletonForExtensionApi(helmRepoManagerInjectable);
+7 -357
View File
@@ -5,366 +5,19 @@
// Main process
import { injectSystemCAs } from "../common/system-ca";
import * as Mobx from "mobx";
import httpProxy from "http-proxy";
import * as LensExtensionsCommonApi from "../extensions/common-api";
import * as LensExtensionsMainApi from "../extensions/main-api";
import { app, autoUpdater, dialog, powerMonitor } from "electron";
import { appName, isIntegrationTesting, isMac, isWindows, productName, staticFilesDirectory } from "../common/vars";
import { LensProxy } from "./lens-proxy";
import { WindowManager } from "./window-manager";
import { ClusterManager } from "./cluster-manager";
import { shellSync } from "./shell-sync";
import { mangleProxyEnv } from "./proxy-env";
import { registerFileProtocol } from "../common/register-protocol";
import logger from "./logger";
import { appEventBus } from "../common/app-event-bus/event-bus";
import type { InstalledExtension } from "../extensions/extension-discovery/extension-discovery";
import type { LensExtensionId } from "../extensions/lens-extension";
import { installDeveloperTools } from "./developer-tools";
import { disposer, getAppVersion, getAppVersionFromProxyServer } from "../common/utils";
import { ipcMainOn } from "../common/ipc";
import { startUpdateChecking } from "./app-updater";
import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
import { startCatalogSyncToRenderer } from "./catalog-pusher";
import { catalogEntityRegistry } from "./catalog";
import { HelmRepoManager } from "./helm/helm-repo-manager";
import { syncWeblinks } from "./catalog-sources";
import configurePackages from "../common/configure-packages";
import { PrometheusProviderRegistry } from "./prometheus";
import * as initializers from "./initializers";
import { WeblinkStore } from "../common/weblink-store";
import { initializeSentryReporting } from "../common/sentry";
import { ensureDir } from "fs-extra";
import { initMenu } from "./menu/menu";
import { kubeApiUpgradeRequest } from "./proxy-functions";
import { initTray } from "./tray/tray";
import { ShellSession } from "./shell-session/shell-session";
import { getDi } from "./getDi";
import extensionLoaderInjectable from "../extensions/extension-loader/extension-loader.injectable";
import lensProtocolRouterMainInjectable from "./protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable";
import extensionDiscoveryInjectable from "../extensions/extension-discovery/extension-discovery.injectable";
import directoryForExesInjectable from "../common/app-paths/directory-for-exes/directory-for-exes.injectable";
import initIpcMainHandlersInjectable from "./initializers/init-ipc-main-handlers/init-ipc-main-handlers.injectable";
import directoryForKubeConfigsInjectable from "../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import kubeconfigSyncManagerInjectable from "./catalog-sources/kubeconfig-sync/manager.injectable";
import clusterStoreInjectable from "../common/cluster-store/cluster-store.injectable";
import routerInjectable from "./router/router.injectable";
import shellApiRequestInjectable from "./proxy-functions/shell-api-request/shell-api-request.injectable";
import userStoreInjectable from "../common/user-store/user-store.injectable";
import trayMenuItemsInjectable from "./tray/tray-menu-items.injectable";
import { broadcastNativeThemeOnUpdate } from "./native-theme";
import assert from "assert";
import windowManagerInjectable from "./window-manager.injectable";
import navigateToPreferencesInjectable from "../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable";
import syncGeneralCatalogEntitiesInjectable from "./catalog-sources/sync-general-catalog-entities.injectable";
import hotbarStoreInjectable from "../common/hotbars/store.injectable";
import applicationMenuItemsInjectable from "./menu/application-menu-items.injectable";
import type { DiContainer } from "@ogre-tools/injectable";
import { init } from "@sentry/electron/main";
import startMainApplicationInjectable from "./start-main-application/start-main-application.injectable";
async function main(di: DiContainer) {
app.setName(appName);
const di = getDi();
/**
* Note: this MUST be called before electron's "ready" event has been emitted.
*/
initializeSentryReporting(init);
const startApplication = di.inject(startMainApplicationInjectable);
await di.runSetups();
await app.whenReady();
injectSystemCAs();
const onCloseCleanup = disposer();
const onQuitCleanup = disposer();
logger.info(`📟 Setting ${productName} as protocol client for lens://`);
if (app.setAsDefaultProtocolClient("lens")) {
logger.info("📟 Protocol client register succeeded ✅");
} else {
logger.info("📟 Protocol client register failed ❗");
}
if (process.env.LENS_DISABLE_GPU) {
app.disableHardwareAcceleration();
}
logger.debug("[APP-MAIN] configuring packages");
configurePackages();
mangleProxyEnv();
const initIpcMainHandlers = di.inject(initIpcMainHandlersInjectable);
logger.debug("[APP-MAIN] initializing ipc main handlers");
initIpcMainHandlers();
if (app.commandLine.getSwitchValue("proxy-server") !== "") {
process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server");
}
logger.debug("[APP-MAIN] Lens protocol routing main");
const lensProtocolRouterMain = di.inject(lensProtocolRouterMainInjectable);
if (!app.requestSingleInstanceLock()) {
app.exit();
} else {
for (const arg of process.argv) {
if (arg.toLowerCase().startsWith("lens://")) {
lensProtocolRouterMain.route(arg);
}
}
}
broadcastNativeThemeOnUpdate();
app.on("second-instance", (event, argv) => {
logger.debug("second-instance message");
for (const arg of argv) {
if (arg.toLowerCase().startsWith("lens://")) {
lensProtocolRouterMain.route(arg);
}
}
WindowManager.getInstance(false)?.ensureMainWindow();
});
app.on("activate", (event, hasVisibleWindows) => {
logger.info("APP:ACTIVATE", { hasVisibleWindows });
if (!hasVisibleWindows) {
WindowManager.getInstance(false)?.ensureMainWindow(false);
}
});
/**
* This variable should is used so that `autoUpdater.installAndQuit()` works
*/
let blockQuit = !isIntegrationTesting;
autoUpdater.on("before-quit-for-update", () => {
logger.debug("Unblocking quit for update");
blockQuit = false;
});
app.on("will-quit", (event) => {
logger.debug("will-quit message");
// This is called when the close button of the main window is clicked
logger.info("APP:QUIT");
appEventBus.emit({ name: "app", action: "close" });
ClusterManager.getInstance(false)?.stop(); // close cluster connections
const kubeConfigSyncManager = di.inject(kubeconfigSyncManagerInjectable);
kubeConfigSyncManager.stopSync();
onCloseCleanup();
// This is set to false here so that LPRM can wait to send future lens://
// requests until after it loads again
lensProtocolRouterMain.rendererLoaded = false;
if (blockQuit) {
// Quit app on Cmd+Q (MacOS)
event.preventDefault(); // prevent app's default shutdown (e.g. required for telemetry, etc.)
return; // skip exit to make tray work, to quit go to app's global menu or tray's menu
}
lensProtocolRouterMain.cleanup();
onQuitCleanup();
});
app.on("open-url", (event, rawUrl) => {
logger.debug("open-url message");
// lens:// protocol handler
event.preventDefault();
lensProtocolRouterMain.route(rawUrl);
});
const directoryForExes = di.inject(directoryForExesInjectable);
logger.info(`🚀 Starting ${productName} from "${directoryForExes}"`);
logger.info("🐚 Syncing shell environment");
await shellSync();
powerMonitor.on("shutdown", () => app.exit());
registerFileProtocol("static", staticFilesDirectory);
PrometheusProviderRegistry.createInstance();
initializers.initPrometheusProviderRegistry();
/**
* The following sync MUST be done before HotbarStore creation, because that
* store has migrations that will remove items that previous migrations add
* if this is not present
*/
const syncGeneralCatalogEntities = di.inject(syncGeneralCatalogEntitiesInjectable);
syncGeneralCatalogEntities();
logger.info("💾 Loading stores");
const userStore = di.inject(userStoreInjectable);
userStore.startMainReactions();
// ClusterStore depends on: UserStore
const clusterStore = di.inject(clusterStoreInjectable);
clusterStore.provideInitialFromMain();
// HotbarStore depends on: ClusterStore
di.inject(hotbarStoreInjectable);
WeblinkStore.createInstance();
syncWeblinks();
HelmRepoManager.createInstance(); // create the instance
const router = di.inject(routerInjectable);
const shellApiRequest = di.inject(shellApiRequestInjectable);
const lensProxy = LensProxy.createInstance(router, httpProxy.createProxy(), {
getClusterForRequest: (req) => ClusterManager.getInstance().getClusterForRequest(req),
kubeApiUpgradeRequest,
shellApiRequest,
});
ClusterManager.createInstance().init();
initializers.initClusterMetadataDetectors();
try {
logger.info("🔌 Starting LensProxy");
await lensProxy.listen(); // lensProxy.port available
} catch (error) {
dialog.showErrorBox("Lens Error", `Could not start proxy: ${error ? String(error) : "unknown error"}`);
return app.exit();
}
assert(lensProxy.port, "Lens Proxy failed to start");
// test proxy connection
try {
logger.info("🔎 Testing LensProxy connection ...");
const versionFromProxy = await getAppVersionFromProxyServer(lensProxy.port);
if (getAppVersion() !== versionFromProxy) {
logger.error("Proxy server responded with invalid response");
return app.exit();
}
logger.info("⚡ LensProxy connection OK");
} catch (error) {
logger.error(`🛑 LensProxy: failed connection test: ${error}`);
const hostsPath = isWindows
? "C:\\windows\\system32\\drivers\\etc\\hosts"
: "/etc/hosts";
const message = [
`Failed connection test: ${error}`,
"Check to make sure that no other versions of Lens are running",
`Check ${hostsPath} to make sure that it is clean and that the localhost loopback is at the top and set to 127.0.0.1`,
"If you have HTTP_PROXY or http_proxy set in your environment, make sure that the localhost and the ipv4 loopback address 127.0.0.1 are added to the NO_PROXY environment variable.",
];
dialog.showErrorBox("Lens Proxy Error", message.join("\n\n"));
return app.exit();
}
const extensionLoader = di.inject(extensionLoaderInjectable);
extensionLoader.init();
const extensionDiscovery = di.inject(extensionDiscoveryInjectable);
extensionDiscovery.init();
// Start the app without showing the main window when auto starting on login
// (On Windows and Linux, we get a flag. On MacOS, we get special API.)
const startHidden = process.argv.includes("--hidden") || (isMac && app.getLoginItemSettings().wasOpenedAsHidden);
logger.info("🖥️ Starting WindowManager");
const windowManager = di.inject(windowManagerInjectable);
const applicationMenuItems = di.inject(applicationMenuItemsInjectable);
const trayMenuItems = di.inject(trayMenuItemsInjectable);
const navigateToPreferences = di.inject(navigateToPreferencesInjectable);
onQuitCleanup.push(
initMenu(applicationMenuItems),
initTray(windowManager, trayMenuItems, navigateToPreferences),
() => ShellSession.cleanup(),
);
installDeveloperTools();
if (!startHidden) {
windowManager.ensureMainWindow();
}
ipcMainOn(IpcRendererNavigationEvents.LOADED, async () => {
onCloseCleanup.push(startCatalogSyncToRenderer(catalogEntityRegistry));
const directoryForKubeConfigs = di.inject(directoryForKubeConfigsInjectable);
await ensureDir(directoryForKubeConfigs);
const kubeConfigSyncManager = di.inject(kubeconfigSyncManagerInjectable);
kubeConfigSyncManager.startSync();
startUpdateChecking();
lensProtocolRouterMain.rendererLoaded = true;
});
logger.info("🧩 Initializing extensions");
// call after windowManager to see splash earlier
try {
const extensions = await extensionDiscovery.load();
// Start watching after bundled extensions are loaded
extensionDiscovery.watchExtensions();
// Subscribe to extensions that are copied or deleted to/from the extensions folder
extensionDiscovery.events
.on("add", (extension: InstalledExtension) => {
extensionLoader.addExtension(extension);
})
.on("remove", (lensExtensionId: LensExtensionId) => {
extensionLoader.removeExtension(lensExtensionId);
});
extensionLoader.initExtensions(extensions);
} catch (error) {
dialog.showErrorBox("Lens Error", `Could not load extensions${error ? `: ${String(error)}` : ""}`);
console.error(error);
console.trace();
}
setTimeout(() => {
appEventBus.emit({ name: "service", action: "start" });
}, 1000);
}
main(getDi());
(async () => {
await startApplication();
})();
/**
* Exports for virtual package "@k8slens/extensions" for main-process.
@@ -376,7 +29,4 @@ const LensExtensions = {
Main: LensExtensionsMainApi,
};
export {
Mobx,
LensExtensions,
};
export { Mobx, LensExtensions };
@@ -1,20 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { ClusterIdDetector } from "../cluster-detectors/cluster-id-detector";
import { DetectorRegistry } from "../cluster-detectors/detector-registry";
import { DistributionDetector } from "../cluster-detectors/distribution-detector";
import { LastSeenDetector } from "../cluster-detectors/last-seen-detector";
import { NodesCountDetector } from "../cluster-detectors/nodes-count-detector";
import { VersionDetector } from "../cluster-detectors/version-detector";
export function initClusterMetadataDetectors() {
DetectorRegistry.createInstance()
.add(ClusterIdDetector)
.add(LastSeenDetector)
.add(VersionDetector)
.add(DistributionDetector)
.add(NodesCountDetector);
}
-6
View File
@@ -1,6 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export * from "./metrics-providers";
export * from "./cluster-metadata-detectors";
@@ -1,21 +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 directoryForLensLocalStorageInjectable from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
import { initIpcMainHandlers } from "./init-ipc-main-handlers";
import getAbsolutePathInjectable from "../../../common/path/get-absolute-path.injectable";
import applicationMenuItemsInjectable from "../../menu/application-menu-items.injectable";
const initIpcMainHandlersInjectable = getInjectable({
id: "init-ipc-main-handlers",
instantiate: (di) => initIpcMainHandlers({
applicationMenuItems: di.inject(applicationMenuItemsInjectable),
directoryForLensLocalStorage: di.inject(directoryForLensLocalStorageInjectable),
getAbsolutePath: di.inject(getAbsolutePathInjectable),
}),
});
export default initIpcMainHandlersInjectable;
@@ -1,19 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { PrometheusProviderRegistry } from "../prometheus";
import { PrometheusHelm } from "../prometheus/helm";
import { PrometheusLens } from "../prometheus/lens";
import { PrometheusOperator } from "../prometheus/operator";
import { PrometheusStacklight } from "../prometheus/stacklight";
export function initPrometheusProviderRegistry() {
PrometheusProviderRegistry
.getInstance()
.registerProvider(new PrometheusLens())
.registerProvider(new PrometheusHelm())
.registerProvider(new PrometheusOperator())
.registerProvider(new PrometheusStacklight());
}
@@ -0,0 +1,36 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { OpenDialogOptions } from "electron";
import { dialog } from "electron";
import { getInjectable } from "@ogre-tools/injectable";
import showApplicationWindowInjectable from "../start-main-application/lens-window/show-application-window.injectable";
// TODO: Replace leaking electron with abstraction
export type AskUserForFilePaths = (
dialogOptions: OpenDialogOptions
) => Promise<{ canceled: boolean; filePaths: string[] }>;
const askUserForFilePathsInjectable = getInjectable({
id: "ask-user-for-file-paths",
instantiate: (di): AskUserForFilePaths => {
const showApplicationWindow = di.inject(showApplicationWindowInjectable);
return async (dialogOptions) => {
await showApplicationWindow();
const { canceled, filePaths } = await dialog.showOpenDialog(
dialogOptions,
);
return { canceled, filePaths };
};
},
causesSideEffects: true,
});
export default askUserForFilePathsInjectable;
-15
View File
@@ -1,15 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { OpenDialogOptions } from "electron";
import { dialog } from "electron";
import { WindowManager } from "../window-manager";
export async function showOpenDialog(dialogOptions: OpenDialogOptions): Promise<{ canceled: boolean; filePaths: string[] }> {
const window = await WindowManager.getInstance().ensureMainWindow();
const { canceled, filePaths } = await dialog.showOpenDialog(window, dialogOptions);
return { canceled, filePaths };
}
@@ -3,11 +3,16 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { isAutoUpdateEnabled } from "./app-updater";
import { isPublishConfigured } from "../common/vars";
import { autoUpdater } from "electron-updater";
const isAutoUpdateEnabledInjectable = getInjectable({
id: "is-auto-update-enabled",
instantiate: () => isAutoUpdateEnabled,
instantiate: () => () => {
return autoUpdater.isUpdaterActive() && isPublishConfigured;
},
causesSideEffects: true,
});
+37
View File
@@ -0,0 +1,37 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { RequestPromiseOptions } from "request-promise-native";
import request from "request-promise-native";
import { apiKubePrefix } from "../common/vars";
import type { Cluster } from "../common/cluster/cluster";
import { getInjectable } from "@ogre-tools/injectable";
import lensProxyPortInjectable from "./lens-proxy/lens-proxy-port.injectable";
export type K8sRequest = (cluster: Cluster, path: string, options?: RequestPromiseOptions) => Promise<any>;
const k8SRequestInjectable = getInjectable({
id: "k8s-request",
instantiate: (di) => {
const lensProxyPort = di.inject(lensProxyPortInjectable);
return async (
cluster: Cluster,
path: string,
options: RequestPromiseOptions = {},
) => {
const kubeProxyUrl = `http://localhost:${lensProxyPort.get()}${apiKubePrefix}`;
options.headers ??= {};
options.json ??= true;
options.timeout ??= 30000;
options.headers.Host = `${cluster.id}.${new URL(kubeProxyUrl).host}`; // required in ClusterManager.getClusterForRequest()
return request(kubeProxyUrl + path, options);
};
},
});
export default k8SRequestInjectable;
-35
View File
@@ -1,35 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { RequestPromiseOptions } from "request-promise-native";
import request from "request-promise-native";
import { apiKubePrefix } from "../common/vars";
import type { IMetricsReqParams } from "../common/k8s-api/endpoints/metrics.api";
import { LensProxy } from "./lens-proxy";
import type { Cluster } from "../common/cluster/cluster";
export async function k8sRequest<T = any>(cluster: Cluster, path: string, options: RequestPromiseOptions = {}): Promise<T> {
const kubeProxyUrl = `http://localhost:${LensProxy.getInstance().port}${apiKubePrefix}`;
options.headers ??= {};
options.json ??= true;
options.timeout ??= 30000;
options.headers.Host = `${cluster.id}.${new URL(kubeProxyUrl).host}`; // required in ClusterManager.getClusterForRequest()
return request(kubeProxyUrl + path, options);
}
export async function getMetrics(cluster: Cluster, prometheusPath: string, queryParams: IMetricsReqParams & { query: string }): Promise<any> {
const prometheusPrefix = cluster.preferences.prometheus?.prefix || "";
const metricsPath = `/api/v1/namespaces/${prometheusPath}/proxy${prometheusPrefix}/api/v1/query_range`;
return k8sRequest(cluster, metricsPath, {
timeout: 0,
resolveWithFullResponse: false,
json: true,
method: "POST",
form: queryParams,
});
}
@@ -8,12 +8,13 @@ import directoryForTempInjectable from "../../common/app-paths/directory-for-tem
import type { KubeconfigManagerDependencies } from "./kubeconfig-manager";
import { KubeconfigManager } from "./kubeconfig-manager";
import loggerInjectable from "../../common/logger.injectable";
import lensProxyPortInjectable from "../lens-proxy/lens-proxy-port.injectable";
export interface KubeConfigManagerInstantiationParameter {
cluster: Cluster;
}
export type CreateKubeconfigManager = (cluster: Cluster) => KubeconfigManager | undefined;
export type CreateKubeconfigManager = (cluster: Cluster) => KubeconfigManager;
const createKubeconfigManagerInjectable = getInjectable({
id: "create-kubeconfig-manager",
@@ -22,6 +23,7 @@ const createKubeconfigManagerInjectable = getInjectable({
const dependencies: KubeconfigManagerDependencies = {
directoryForTemp: di.inject(directoryForTempInjectable),
logger: di.inject(loggerInjectable),
lensProxyPort: di.inject(lensProxyPortInjectable),
};
return (cluster) => new KubeconfigManager(dependencies, cluster);
@@ -9,8 +9,6 @@ import type { ClusterContextHandler } from "../context-handler/context-handler";
import path from "path";
import fs from "fs-extra";
import { dumpConfigYaml } from "../../common/kube-helpers";
import logger from "../logger";
import { LensProxy } from "../lens-proxy";
import { isErrnoException } from "../../common/utils";
import type { PartialDeep } from "type-fest";
import type { Logger } from "../../common/logger";
@@ -18,6 +16,7 @@ import type { Logger } from "../../common/logger";
export interface KubeconfigManagerDependencies {
readonly directoryForTemp: string;
readonly logger: Logger;
lensProxyPort: { get: () => number };
}
export class KubeconfigManager {
@@ -60,7 +59,7 @@ export class KubeconfigManager {
return;
}
logger.info(`[KUBECONFIG-MANAGER]: Deleting temporary kubeconfig: ${this.tempFilePath}`);
this.dependencies.logger.info(`[KUBECONFIG-MANAGER]: Deleting temporary kubeconfig: ${this.tempFilePath}`);
try {
await fs.unlink(this.tempFilePath);
@@ -84,7 +83,7 @@ export class KubeconfigManager {
}
get resolveProxyUrl() {
return `http://127.0.0.1:${LensProxy.getInstance().port}/${this.cluster.id}`;
return `http://127.0.0.1:${this.dependencies.lensProxyPort.get()}/${this.cluster.id}`;
}
/**
@@ -124,7 +123,7 @@ export class KubeconfigManager {
await fs.ensureDir(path.dirname(tempFile));
await fs.writeFile(tempFile, configYaml, { mode: 0o600 });
logger.debug(`[KUBECONFIG-MANAGER]: Created temp kubeconfig "${contextName}" at "${tempFile}": \n${configYaml}`);
this.dependencies.logger.debug(`[KUBECONFIG-MANAGER]: Created temp kubeconfig "${contextName}" at "${tempFile}": \n${configYaml}`);
return tempFile;
}
@@ -0,0 +1,37 @@
/**
* 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";
const lensProxyPortInjectable = getInjectable({
id: "lens-proxy-port",
instantiate: () => {
let _portNumber: number;
return {
get: () => {
if (!_portNumber) {
throw new Error(
"Tried to access port number of LensProxy while it has not been set yet.",
);
}
return _portNumber;
},
set: (portNumber: number) => {
if (_portNumber) {
throw new Error(
"Tried to set port number for LensProxy when it has already been set.",
);
}
_portNumber = portNumber;
},
};
},
});
export default lensProxyPortInjectable;
@@ -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 { LensProxy } from "./lens-proxy";
import { kubeApiUpgradeRequest } from "./proxy-functions";
import routerInjectable from "../router/router.injectable";
import httpProxy from "http-proxy";
import clusterManagerInjectable from "../cluster-manager.injectable";
import shellApiRequestInjectable from "./proxy-functions/shell-api-request/shell-api-request.injectable";
import lensProxyPortInjectable from "./lens-proxy-port.injectable";
const lensProxyInjectable = getInjectable({
id: "lens-proxy",
instantiate: (di) => {
const clusterManager = di.inject(clusterManagerInjectable);
const router = di.inject(routerInjectable);
const shellApiRequest = di.inject(shellApiRequestInjectable);
const proxy = httpProxy.createProxy();
const lensProxyPort = di.inject(lensProxyPortInjectable);
return new LensProxy({
router,
proxy,
kubeApiUpgradeRequest,
shellApiRequest,
getClusterForRequest: clusterManager.getClusterForRequest,
lensProxyPort,
});
},
});
export default lensProxyInjectable;
@@ -7,15 +7,14 @@ import net from "net";
import type http from "http";
import spdy from "spdy";
import type httpProxy from "http-proxy";
import { apiPrefix, apiKubePrefix } from "../common/vars";
import type { Router } from "./router/router";
import type { ClusterContextHandler } from "./context-handler/context-handler";
import logger from "./logger";
import { Singleton } from "../common/utils";
import type { Cluster } from "../common/cluster/cluster";
import { apiPrefix, apiKubePrefix } from "../../common/vars";
import type { Router } from "../router/router";
import type { ClusterContextHandler } from "../context-handler/context-handler";
import logger from "../logger";
import type { Cluster } from "../../common/cluster/cluster";
import type { ProxyApiRequestArgs } from "./proxy-functions";
import { appEventBus } from "../common/app-event-bus/event-bus";
import { getBoolean } from "./utils/parse-query";
import { appEventBus } from "../../common/app-event-bus/event-bus";
import { getBoolean } from "../utils/parse-query";
import assert from "assert";
import type { SetRequired } from "type-fest";
@@ -23,10 +22,13 @@ type GetClusterForRequest = (req: http.IncomingMessage) => Cluster | undefined;
export type ServerIncomingMessage = SetRequired<http.IncomingMessage, "url" | "method">;
export interface LensProxyFunctions {
interface Dependencies {
getClusterForRequest: GetClusterForRequest;
shellApiRequest: (args: ProxyApiRequestArgs) => void | Promise<void>;
kubeApiUpgradeRequest: (args: ProxyApiRequestArgs) => void | Promise<void>;
router: Router;
proxy: httpProxy;
lensProxyPort: { set: (portNumber: number) => void };
}
const watchParam = "watch";
@@ -56,20 +58,13 @@ const disallowedPorts = new Set([
10080,
]);
export class LensProxy extends Singleton {
export class LensProxy {
protected proxyServer: http.Server;
protected closed = false;
protected retryCounters = new Map<string, number>();
protected getClusterForRequest: GetClusterForRequest;
public port?: number;
constructor(protected router: Router, protected proxy: httpProxy, { shellApiRequest, kubeApiUpgradeRequest, getClusterForRequest }: LensProxyFunctions) {
super();
this.configureProxy(proxy);
this.getClusterForRequest = getClusterForRequest;
constructor(private dependencies: Dependencies) {
this.configureProxy(dependencies.proxy);
this.proxyServer = spdy.createServer({
spdy: {
@@ -82,14 +77,14 @@ export class LensProxy extends Singleton {
this.proxyServer
.on("upgrade", (req: ServerIncomingMessage, socket: net.Socket, head: Buffer) => {
const cluster = getClusterForRequest(req);
const cluster = dependencies.getClusterForRequest(req);
if (!cluster) {
logger.error(`[LENS-PROXY]: Could not find cluster for upgrade request from url=${req.url}`);
socket.destroy();
} else {
const isInternal = req.url.startsWith(`${apiPrefix}?`);
const reqHandler = isInternal ? shellApiRequest : kubeApiUpgradeRequest;
const reqHandler = isInternal ? dependencies.shellApiRequest : dependencies.kubeApiUpgradeRequest;
(async () => reqHandler({ req, socket, head, cluster }))()
.catch(error => logger.error("[LENS-PROXY]: failed to handle proxy upgrade", error));
@@ -113,13 +108,14 @@ export class LensProxy extends Singleton {
const { address, port } = this.proxyServer.address() as net.AddressInfo;
this.dependencies.lensProxyPort.set(port);
logger.info(`[LENS-PROXY]: Proxy server has started at ${address}:${port}`);
this.proxyServer.on("error", (error) => {
logger.info(`[LENS-PROXY]: Subsequent error: ${error}`);
});
this.port = port;
appEventBus.emit({ name: "lens-proxy", action: "listen", params: { port }});
resolve(port);
})
@@ -233,16 +229,16 @@ export class LensProxy extends Singleton {
}
protected async handleRequest(req: ServerIncomingMessage, res: http.ServerResponse) {
const cluster = this.getClusterForRequest(req);
const cluster = this.dependencies.getClusterForRequest(req);
if (cluster) {
const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler);
if (proxyTarget) {
return this.proxy.web(req, res, proxyTarget);
return this.dependencies.proxy.web(req, res, proxyTarget);
}
}
this.router.route(cluster, req, res);
this.dependencies.router.route(cluster, req, res);
}
}
@@ -7,7 +7,7 @@ import { chunk } from "lodash";
import type { ConnectionOptions } from "tls";
import { connect } from "tls";
import url from "url";
import { apiKubePrefix } from "../../common/vars";
import { apiKubePrefix } from "../../../common/vars";
import type { ProxyApiRequestArgs } from "./types";
const skipRawHeaders = new Set(["Host", "Authorization"]);
@@ -4,9 +4,9 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import { shellApiRequest } from "./shell-api-request";
import createShellSessionInjectable from "../../shell-session/create-shell-session.injectable";
import shellRequestAuthenticatorInjectable
from "./shell-request-authenticator/shell-request-authenticator.injectable";
import createShellSessionInjectable from "../../../shell-session/create-shell-session.injectable";
import shellRequestAuthenticatorInjectable from "./shell-request-authenticator/shell-request-authenticator.injectable";
import clusterManagerInjectable from "../../../cluster-manager.injectable";
const shellApiRequestInjectable = getInjectable({
id: "shell-api-request",
@@ -14,6 +14,7 @@ const shellApiRequestInjectable = getInjectable({
instantiate: (di) => shellApiRequest({
createShellSession: di.inject(createShellSessionInjectable),
authenticateRequest: di.inject(shellRequestAuthenticatorInjectable).authenticate,
clusterManager: di.inject(clusterManagerInjectable),
}),
});
@@ -3,14 +3,14 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import logger from "../../logger";
import logger from "../../../logger";
import type WebSocket from "ws";
import { Server as WebSocketServer } from "ws";
import type { ProxyApiRequestArgs } from "../types";
import { ClusterManager } from "../../cluster-manager";
import type { ClusterManager } from "../../../cluster-manager";
import URLParse from "url-parse";
import type { Cluster } from "../../../common/cluster/cluster";
import type { ClusterId } from "../../../common/cluster-types";
import type { Cluster } from "../../../../common/cluster/cluster";
import type { ClusterId } from "../../../../common/cluster-types";
interface Dependencies {
authenticateRequest: (clusterId: ClusterId, tabId: string, shellToken: string | undefined) => boolean;
@@ -21,10 +21,12 @@ interface Dependencies {
tabId: string;
nodeName?: string;
}) => { open: () => Promise<void> };
clusterManager: ClusterManager;
}
export const shellApiRequest = ({ createShellSession, authenticateRequest }: Dependencies) => ({ req, socket, head }: ProxyApiRequestArgs): void => {
const cluster = ClusterManager.getInstance().getClusterForRequest(req);
export const shellApiRequest = ({ createShellSession, authenticateRequest, clusterManager }: Dependencies) => ({ req, socket, head }: ProxyApiRequestArgs): void => {
const cluster = clusterManager.getClusterForRequest(req);
const { query: { node: nodeName, shellToken, id: tabId }} = new URLParse(req.url, true);
if (!tabId || !cluster || !authenticateRequest(cluster.id, tabId, shellToken)) {
@@ -2,9 +2,9 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getOrInsertMap } from "../../../../common/utils";
import type { ClusterId } from "../../../../common/cluster-types";
import { ipcMainHandle } from "../../../../common/ipc";
import { getOrInsertMap } from "../../../../../common/utils";
import type { ClusterId } from "../../../../../common/cluster-types";
import { ipcMainHandle } from "../../../../../common/ipc";
import crypto from "crypto";
import { promisify } from "util";
@@ -6,7 +6,7 @@
import type http from "http";
import type net from "net";
import type { SetRequired } from "type-fest";
import type { Cluster } from "../../common/cluster/cluster";
import type { Cluster } from "../../../common/cluster/cluster";
export interface ProxyApiRequestArgs {
req: SetRequired<http.IncomingMessage, "url" | "method">;
@@ -5,11 +5,8 @@
import { getInjectable } from "@ogre-tools/injectable";
import { checkForUpdates } from "../app-updater";
import { docsUrl, productName, supportUrl } from "../../common/vars";
import { exitApp } from "../exit-app";
import { broadcastMessage } from "../../common/ipc";
import { openBrowser } from "../../common/utils";
import { showAbout } from "./menu";
import windowManagerInjectable from "../window-manager.injectable";
import type { MenuItemConstructorOptions } from "electron";
import { webContents } from "electron";
import loggerInjectable from "../../common/logger.injectable";
@@ -21,8 +18,13 @@ import navigateToExtensionsInjectable from "../../common/front-end-routing/route
import navigateToCatalogInjectable from "../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable";
import navigateToWelcomeInjectable from "../../common/front-end-routing/routes/welcome/navigate-to-welcome.injectable";
import navigateToAddClusterInjectable from "../../common/front-end-routing/routes/add-cluster/navigate-to-add-cluster.injectable";
import stopServicesAndExitAppInjectable from "../stop-services-and-exit-app.injectable";
import isMacInjectable from "../../common/vars/is-mac.injectable";
import { computed } from "mobx";
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";
function ignoreIf(check: boolean, menuItems: MenuItemOpts[]) {
return check ? [] : menuItems;
@@ -41,18 +43,18 @@ const applicationMenuItemsInjectable = getInjectable({
const isMac = di.inject(isMacInjectable);
const isAutoUpdateEnabled = di.inject(isAutoUpdateEnabledInjectable);
const electronMenuItems = di.inject(electronMenuItemsInjectable);
const showAbout = di.inject(showAboutInjectable);
const applicationWindow = di.inject(applicationWindowInjectable);
const showApplicationWindow = di.inject(showApplicationWindowInjectable);
const reloadApplicationWindow = di.inject(reloadWindowInjectable, applicationWindow);
const navigateToPreferences = di.inject(navigateToPreferencesInjectable);
const navigateToExtensions = di.inject(navigateToExtensionsInjectable);
const navigateToCatalog = di.inject(navigateToCatalogInjectable);
const navigateToWelcome = di.inject(navigateToWelcomeInjectable);
const navigateToAddCluster = di.inject(navigateToAddClusterInjectable);
const stopServicesAndExitApp = di.inject(stopServicesAndExitAppInjectable);
return computed((): MenuItemOpts[] => {
// TODO: These injects should happen outside of the computed.
// TODO: Remove temporal dependencies in WindowManager to make sure timing is correct.
const windowManager = di.inject(windowManagerInjectable);
const navigateToPreferences = di.inject(navigateToPreferencesInjectable);
const navigateToExtensions = di.inject(navigateToExtensionsInjectable);
const navigateToCatalog = di.inject(navigateToCatalogInjectable);
const navigateToWelcome = di.inject(navigateToWelcomeInjectable);
const navigateToAddCluster = di.inject(navigateToAddClusterInjectable);
const autoUpdateDisabled = !isAutoUpdateEnabled();
logger.info(`[MENU]: autoUpdateDisabled=${autoUpdateDisabled}`);
@@ -72,7 +74,7 @@ const applicationMenuItemsInjectable = getInjectable({
{
label: "Check for updates",
click() {
checkForUpdates().then(() => windowManager.ensureMainWindow());
checkForUpdates().then(() => showApplicationWindow());
},
},
]),
@@ -105,7 +107,7 @@ const applicationMenuItemsInjectable = getInjectable({
accelerator: "Cmd+Q",
id: "quit",
click() {
exitApp();
stopServicesAndExitApp();
},
},
],
@@ -154,7 +156,7 @@ const applicationMenuItemsInjectable = getInjectable({
accelerator: "Alt+F4",
id: "quit",
click() {
exitApp();
stopServicesAndExitApp();
},
},
]),
@@ -231,7 +233,7 @@ const applicationMenuItemsInjectable = getInjectable({
accelerator: "CmdOrCtrl+R",
id: "reload",
click() {
windowManager.reload();
reloadApplicationWindow();
},
},
{ role: "toggleDevTools" },
@@ -285,7 +287,7 @@ const applicationMenuItemsInjectable = getInjectable({
label: "Check for updates",
click() {
checkForUpdates().then(() =>
windowManager.ensureMainWindow(),
showApplicationWindow(),
);
},
},
@@ -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 { autorun } from "mobx";
import { buildMenu } from "./menu";
import applicationMenuItemsInjectable from "./application-menu-items.injectable";
import { getStartableStoppable } from "../../common/utils/get-startable-stoppable";
const applicationMenuInjectable = getInjectable({
id: "application-menu",
instantiate: (di) => {
const applicationMenuItems = di.inject(applicationMenuItemsInjectable);
return getStartableStoppable("build-of-application-menu", () =>
autorun(() => buildMenu(applicationMenuItems.get()), {
delay: 100,
}),
);
},
});
export default applicationMenuInjectable;
+1 -3
View File
@@ -16,7 +16,7 @@ describe("electron-menu-items", () => {
let electronMenuItems: IComputedValue<MenuRegistration[]>;
let extensionsStub: ObservableMap<string, LensMainExtension>;
beforeEach(async () => {
beforeEach(() => {
di = getDiForUnitTesting({ doGeneralOverrides: true });
extensionsStub = new ObservableMap();
@@ -26,8 +26,6 @@ describe("electron-menu-items", () => {
() => computed(() => [...extensionsStub.values()]),
);
await di.runSetups();
electronMenuItems = di.inject(electronMenuItemsInjectable);
});
+11 -18
View File
@@ -2,24 +2,19 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { app, dialog, Menu } from "electron";
import type { IComputedValue } from "mobx";
import { autorun } from "mobx";
import { app, Menu } from "electron";
import { appName, isWindows, productName } from "../../common/vars";
import packageJson from "../../../package.json";
import type { MenuItemOpts } from "./application-menu-items.injectable";
import type { ShowMessagePopup } from "../electron-app/features/show-message-popup.injectable";
export type MenuTopId = "mac" | "file" | "edit" | "view" | "help";
export function initMenu(
applicationMenuItems: IComputedValue<MenuItemOpts[]>,
) {
return autorun(() => buildMenu(applicationMenuItems.get()), {
delay: 100,
});
interface Dependencies {
showMessagePopup: ShowMessagePopup;
}
export function showAbout() {
export const showAbout = ({ showMessagePopup }: Dependencies) => async () => {
const appInfo = [
`${appName}: ${app.getVersion()}`,
`Electron: ${process.versions.electron}`,
@@ -28,14 +23,12 @@ export function showAbout() {
packageJson.copyright,
];
dialog.showMessageBoxSync({
title: `${isWindows ? " ".repeat(2) : ""}${appName}`,
type: "info",
buttons: ["Close"],
message: productName,
detail: appInfo.join("\r\n"),
});
}
await showMessagePopup(
`${isWindows ? " ".repeat(2) : ""}${appName}`,
productName,
appInfo.join("\r\n"),
);
};
export function buildMenu(applicationMenuItems: MenuItemOpts[]) {
Menu.setApplicationMenu(
+16
View File
@@ -0,0 +1,16 @@
/**
* 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 { showAbout } from "./menu";
import showMessagePopupInjectable from "../electron-app/features/show-message-popup.injectable";
const showAboutInjectable = getInjectable({
id: "show-about",
instantiate: (di) =>
showAbout({ showMessagePopup: di.inject(showMessagePopupInjectable) }),
});
export default showAboutInjectable;
@@ -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 applicationMenuInjectable from "./application-menu.injectable";
import { onLoadOfApplicationInjectionToken } from "../start-main-application/runnable-tokens/on-load-of-application-injection-token";
const startApplicationMenuInjectable = getInjectable({
id: "start-application-menu",
instantiate: (di) => {
const applicationMenu = di.inject(
applicationMenuInjectable,
);
return {
run: async () => {
await applicationMenu.start();
},
};
},
injectionToken: onLoadOfApplicationInjectionToken,
});
export default startApplicationMenuInjectable;
@@ -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 applicationMenuInjectable from "./application-menu.injectable";
import { beforeQuitOfBackEndInjectionToken } from "../start-main-application/runnable-tokens/before-quit-of-back-end-injection-token";
const stopApplicationMenuInjectable = getInjectable({
id: "stop-application-menu",
instantiate: (di) => {
const applicationMenu = di.inject(
applicationMenuInjectable,
);
return {
run: async () => {
await applicationMenu.stop();
},
};
},
injectionToken: beforeQuitOfBackEndInjectionToken,
});
export default stopApplicationMenuInjectable;
Loaded 100 of 302 files, more files were not shown because too many files have changed in this diff. Show more