From 54b92aa9b6d0958446184c8e15e5e5281d0465c5 Mon Sep 17 00:00:00 2001 From: Janne Savolainen Date: Wed, 18 May 2022 16:18:02 +0300 Subject: [PATCH] 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 Signed-off-by: Janne Savolainen * Switch to using competition to setup app paths Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Switch to using competition for setupping IPC channel listeners Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Stop running setups in unit tests without need Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Switch to running injection token based setups over legacy DI setups in unit tests Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Consolidate naming for running setups Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Switch to running injection token based setups over legacy DI setups in application roots Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Adapt to typing changes in injectable Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Update injectable Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Introduce concept of runnable as a way to delegate runs to Open Closed Principle compliant runnables Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Deprecate vars in favor of injectables Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Consolidate loading of extensions to injectable instead of setup-code Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Flag injectable causing side effects Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Adapt injectable to auto register using a plugin instead of internal feature that no longer exists Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Simplify late registrations Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Introduce tokens for runnables of specific application events for Open Closed Principle Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Consolidate setup events to more granular timeslots Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Reimplement setup for catalog syncing using runnables instead of non-OCP in index.ts Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Reimplement setup for application menu using runnables instead of non-OCP in index.ts Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Reimplement setup for electron application name using runnables instead of non-OCP in index.ts Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Reimplement setup for immer using runnables instead of non-OCP in index.ts Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Reimplement setup for MobX strictness using runnables instead of non-OCP in index.ts Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Reimplement setup for application proxy using runnables instead of non-OCP in index.ts Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Reimplement setup for system certifications using runnables instead of non-OCP in index.ts Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Reimplement setup for system shutdown using runnables instead of non-OCP in index.ts Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Reimplement setup for main window visibility using runnables instead of non-OCP in index.ts Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Reimplement setup for application quit using runnables instead of non-OCP in index.ts Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Reimplement setup for after application is ready using runnables instead of non-OCP in index.ts Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Reimplement setup for application tray using runnables instead of non-OCP in index.ts Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Reimplement setup for deep linking using runnables instead of non-OCP in index.ts Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Reimplement setup for root frame using runnables instead of non-OCP in index.ts Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Remove multiple usages of shared global state Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Remove recently reimplemented stuff from index.ts Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Extract implementation for intent over technical phenomena (here electron events) Co-authored-by: Mikko Aspiala Signed-off-by: Iku-turso * Consolidate naming of event timing window Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Move directories for timing windows among peers Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Consolidate event naming Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Introduce function to remove duplication from things that are startable and stoppable Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Consolidate implementation of something startable and stoppable Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Consolidate naming Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Consolidate more startables and stoppables Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Introduce abstractions for electron application events Signed-off-by: Janne Savolainen * Introduce more abstractions for electron Signed-off-by: Janne Savolainen * Abstract even more Electron specifics to make them overridable in unit tests Signed-off-by: Janne Savolainen * Override dependency for causing side effects Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Make running of many delegatees able to be hierarchical Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Bump async-fn Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Make startable-stoppable also restartable Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Extract "isIntegrationTesting" as dependency for having a side-effect Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Make temporal dependency apparent Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Consolidate all setupping related to single feature to same runnable Signed-off-by: Janne Savolainen * Abstract command line arguments to make them overridable in tests Signed-off-by: Janne Savolainen * Deprecate some globals Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Introduce injectable for __static directory to eliminate side effect on import Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Extract responsibilities from electron Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Extract async initialization from sync-injectable Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Accumulate more global overrides Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Make lifecycle of injectable store work as expected Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Extract responsibilities to delegatees Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Rename delegate for accuracy Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Rename another delegate for accuracy Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Rename yet another delegate for accuracy Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Add new global overrides Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Consolidate variable naming Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Rename injectable for accuracy Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Make difference between soft and hard quit of application apparent Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * 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 Signed-off-by: Iku-turso * Introduce abstraction for publishing and subscribing between processes Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Revert "Introduce abstraction for publishing and subscribing between processes" This reverts commit 46f2d5a5f28bddcf5ffe124b1c590b19e7b9a15f. Signed-off-by: Janne Savolainen * Adapt code and unit tests to recent changes in application setup and event handling Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Group overrides by category Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Abstract event of application activation from electron Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Abstract event of device shutdown from electron Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Consolidate runnables related to Electron Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Make startableStoppables have ID for better error logging Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Make navigating to Helm Charts not blow up Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Add general override for behavioural tests Co-authored-by: Janne Savolainen Signed-off-by: Iku-turso * Update snapshot Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Adapt vars after merge Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Fix code style Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Fix error about multiple states for React Router being present at once Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Kludge around test setup which is difficult because of circular dependencies Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Fix setupping of sentry after rebase Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Ensure that LensProxy is setupped before starting the main window Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Remove redundant import Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Consolidate to use injectable instead of var for static file directory Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Deprecate another var with injectable Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Tweak timing of runnables Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Extract tray icon path as injectable Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Make splash screen not blow up by providing compile-time environment variables Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Introduce a way to run many synchronous runnables Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * 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 Signed-off-by: Janne Savolainen * Kill dead code Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Fix application quit by calling Electron's event.preventDefault in a very specific way and preventing async Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Fix missing injectable postfix in file name Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Tweak timeslot of a runnable Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Make it possible to not stop something that was never started Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Make quit of hidden Lens not blow up Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Extract responsibilities of WindowManager as injectables Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Consolidate code for application window Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Simplify directory structures for runnables Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Flag injectable causing side-effect Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Extract helpers for testing promises to test-utils Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Make script for running unit tests support targeting Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Make startable stoppable support async starting Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Introduce reactive way to get theme from operating system Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Update yarn.lock after rebase Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Fix code style Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Remove code-style changes that are not welcome Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Switch to using dependency over using explicit side-effect Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Simplify naming Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Kill dead code Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Add empty mocks comply to lint Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Remove global state Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Extract responsibility of setting dependencies to own injectable Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Flag injectable causing side effects Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Extract responsibility to injectable Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Tweak typing Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Tweak naming of runnables Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Revert code-style changes Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Tweak types Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Extract injectable with side-effect for exact overriding Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Tweak name of timeslot to make it more apparent that new content for application load can be added Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Tweak name of the splash window for loading to avoid confusion with similarly named phenomenon Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Tweak comment Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Rename injectable for brevity Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Consolidate LensProxy related stuff to directory Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Rename injectables for accuracy Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Remove usage of extension for injectable for being YAGNI Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Prevent restarting a startableStoppable when already being started Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Rename callback in ApplicationBuilder for accuracy Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Fix conflicts after rebase Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Fix merge conflicts Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Revert switch to react-router-dom by installing history as dependency Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Make test more strict Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Make runnable give friendly error about incorrect hierarchy for easier debugging Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Fix code-style Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Fix code style Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Switch to using dependency instead of legacy-di Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen * Fix timing of injects Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen Co-authored-by: Iku-turso --- .../@sentry/electron/main.ts | 3 +- __mocks__/@sentry/electron/renderer.ts | 5 + package.json | 12 +- ...navigation-using-application-menu.test.tsx | 6 +- .../cluster/order-of-sidebar-items.test.tsx | 2 +- ...debar-and-tab-navigation-for-core.test.tsx | 8 +- ...and-tab-navigation-for-extensions.test.tsx | 2 +- .../visibility-of-sidebar-items.test.tsx | 2 +- .../navigation-using-application-menu.test.ts | 6 +- .../navigation-to-helm-charts.test.ts.snap | 458 ++++++++++++++++++ .../navigation-to-helm-charts.test.ts | 37 ++ .../navigating-between-routes.test.tsx | 4 +- .../preferences/closing-preferences.test.tsx | 19 +- ...igation-to-application-preferences.test.ts | 15 - .../navigation-to-editor-preferences.test.ts | 16 - ...to-extension-specific-preferences.test.tsx | 15 - ...vigation-to-kubernetes-preferences.test.ts | 17 - .../navigation-to-proxy-preferences.test.ts | 15 - ...vigation-to-telemetry-preferences.test.tsx | 19 +- ...navigation-to-terminal-preferences.test.ts | 17 +- .../navigation-using-application-menu.test.ts | 22 +- .../navigation-using-application-menu.test.ts | 6 +- src/common/__tests__/base-store.test.ts | 4 +- src/common/__tests__/cluster-store.test.ts | 29 +- src/common/__tests__/hotbar-store.test.ts | 19 +- src/common/__tests__/user-store.test.ts | 10 +- .../app-event-bus/app-event-bus.injectable.ts | 1 + .../app-paths/app-paths-state.injectable.ts | 34 ++ src/common/app-paths/app-paths.injectable.ts | 15 + src/common/app-paths/app-paths.test.ts | 62 +-- .../has-category-for-entity.injectable.ts | 1 + .../cluster-frames.injectable.ts} | 5 +- .../cluster-store/cluster-store.injectable.ts | 9 +- src/common/cluster/cluster.ts | 14 +- ...at-all-routes-have-route-component.test.ts | 2 +- src/common/fs/ensure-dir.injectable.ts | 18 + src/common/hotbars/store.ts | 2 +- .../ipc/broadcast-message.injectable.ts | 14 + src/common/k8s-api/__tests__/kube-api.test.ts | 8 +- src/common/runnable/run-many-for.test.ts | 267 ++++++++++ src/common/runnable/run-many-for.ts | 51 ++ src/common/runnable/run-many-sync-for.test.ts | 197 ++++++++ src/common/runnable/run-many-sync-for.ts | 50 ++ .../throw-with-incorrect-hierarchy-for.ts | 16 + src/common/test-utils/flush-promises.ts | 5 + src/common/test-utils/get-promise-status.ts | 17 + .../utils/environment-variables.injectable.ts | 31 ++ .../utils/get-startable-stoppable.test.ts | 235 +++++++++ src/common/utils/get-startable-stoppable.ts | 60 +++ src/common/vars.ts | 41 +- src/common/vars/is-development.injectable.ts | 11 +- .../vars/is-integration-testing.injectable.ts | 18 + src/common/vars/is-linux.injectable.ts | 10 +- src/common/vars/is-mac.injectable.ts | 10 +- src/common/vars/is-production.injectable.ts | 18 + src/common/vars/is-test-env.injectable.ts | 18 + src/common/vars/is-windows.injectable.ts | 10 +- .../vars/lens-resources-dir.injectable.ts | 22 + src/common/vars/platform.injectable.ts | 13 + .../vars/static-files-directory.injectable.ts | 20 + src/common/weblink-store.injectable.ts | 18 + .../__tests__/extension-loader.test.ts | 12 +- ...egacy-global-function-for-extension-api.ts | 2 +- ...-legacy-global-object-for-extension-api.ts | 2 +- .../legacy-global-di-for-extension-api.ts | 8 +- .../extension-discovery.injectable.ts | 3 + .../extension-discovery.test.ts | 4 +- .../extension-discovery.ts | 5 +- .../lens-extension-set-dependencies.ts | 2 + src/extensions/lens-main-extension.ts | 3 +- src/extensions/main-api/navigation.ts | 13 +- src/main/__test__/cluster.test.ts | 8 +- src/main/__test__/context-handler.test.ts | 42 +- src/main/__test__/kube-auth-proxy.test.ts | 7 +- src/main/__test__/kubeconfig-manager.test.ts | 5 +- src/main/__test__/lens-proxy.test.ts | 2 +- src/main/__test__/static-file-route.test.ts | 4 +- .../app-paths/app-name/app-name.injectable.ts | 12 +- src/main/app-paths/app-paths.injectable.ts | 71 --- ...tory-for-integration-testing.injectable.ts | 8 +- .../get-electron-app-path.injectable.ts | 2 +- .../get-electron-app-path.test.ts | 6 +- .../set-electron-app-path.injectable.ts | 2 +- .../app-paths/setup-app-paths.injectable.ts | 58 +++ src/main/app-updater.ts | 6 +- .../__test__/kubeconfig-sync.test.ts | 27 +- .../kubeconfig-sync/manager.injectable.ts | 2 + .../kubeconfig-sync/manager.ts | 22 +- .../sync-weblinks.injectable.ts | 19 + src/main/catalog-sources/weblinks.ts | 13 +- .../catalog-sync-to-renderer.injectable.ts | 22 + .../start-catalog-sync.injectable.ts | 25 + .../stop-catalog-sync.injectable.ts | 27 ++ .../catalog/entity-registry.injectable.ts | 1 + .../base-cluster-detector.ts | 6 +- .../create-version-detector.injectable.ts | 21 + .../detector-registry.injectable.ts | 16 + .../cluster-detectors/detector-registry.ts | 14 +- src/main/cluster-manager.injectable.ts | 25 + src/main/cluster-manager.ts | 55 ++- src/main/context-handler/context-handler.ts | 12 +- .../create-context-handler.injectable.ts | 18 +- .../create-cluster.injectable.ts | 4 + src/main/developer-tools.ts | 20 - .../electron-app/electron-app.injectable.ts | 0 .../features/auto-updater.injectable.ts | 14 + ...isable-hardware-acceleration.injectable.ts | 20 + .../features/electron-dialog.injectable.ts | 14 + .../features/exit-app.injectable.ts | 18 + .../get-command-line-switch.injectable.ts | 18 + .../features/get-electron-theme.injectable.ts | 18 + .../features/native-theme.injectable.ts | 14 + .../features/power-monitor.injectable.ts | 14 + .../register-file-protocol.injectable.ts | 28 ++ ...request-single-instance-lock.injectable.ts | 18 + .../should-start-hidden.injectable.ts | 27 ++ .../features/show-error-popup.injectable.ts | 20 + .../features/show-message-popup.injectable.ts | 28 ++ ...-theme-from-operating-system.injectable.ts | 35 ++ ...ait-for-electron-to-be-ready.injectable.ts | 14 + .../clean-up-deep-linking.injectable.ts | 25 + ...-dock-for-last-closed-window.injectable.ts | 36 ++ ...dock-for-first-opened-window.injectable.ts | 25 + ...-single-application-instance.injectable.ts | 29 ++ .../setup-application-name.injectable.ts | 27 ++ .../setup-deep-linking.injectable.ts | 73 +++ ...s-in-development-environment.injectable.ts | 40 ++ .../setup-device-shutdown.injectable.ts | 29 ++ .../setup-ipc-main-handlers.injectable.ts | 58 +++ .../setup-ipc-main-handlers.ts} | 56 ++- ...-visibility-after-activation.injectable.ts | 35 ++ ...ables-after-window-is-opened.injectable.ts | 31 ++ ...efore-closing-of-application.injectable.ts | 60 +++ .../setup-update-checking.injectable.ts | 25 + src/main/exit-app.ts | 23 - .../create-extension-instance.injectable.ts | 2 + src/main/get-metrics.injectable.ts | 37 ++ src/main/getDi.ts | 25 +- src/main/getDiForUnitTesting.ts | 164 ++++++- src/main/helm/__tests__/helm-service.test.ts | 16 +- src/main/helm/helm-repo-manager.injectable.ts | 172 +++++++ src/main/helm/helm-repo-manager.ts | 163 +------ src/main/index.ts | 364 +------------- .../cluster-metadata-detectors.ts | 20 - .../init-ipc-main-handlers.injectable.ts | 21 - src/main/initializers/metrics-providers.ts | 19 - .../ipc/ask-user-for-file-paths.injectable.ts | 36 ++ src/main/ipc/dialog.ts | 15 - src/main/is-auto-update-enabled.injectable.ts | 9 +- src/main/k8s-request.injectable.ts | 37 ++ src/main/k8s-request.ts | 35 -- .../create-kubeconfig-manager.injectable.ts | 4 +- .../kubeconfig-manager/kubeconfig-manager.ts | 9 +- .../lens-proxy/lens-proxy-port.injectable.ts | 37 ++ src/main/lens-proxy/lens-proxy.injectable.ts | 35 ++ src/main/{ => lens-proxy}/lens-proxy.ts | 46 +- .../{ => lens-proxy}/proxy-functions/index.ts | 0 .../kube-api-upgrade-request.ts | 2 +- .../shell-api-request.injectable.ts | 7 +- .../shell-api-request/shell-api-request.ts | 14 +- .../shell-request-authenticator.injectable.ts | 0 .../shell-request-authenticator.ts | 6 +- .../{ => lens-proxy}/proxy-functions/types.ts | 2 +- .../menu/application-menu-items.injectable.ts | 38 +- src/main/menu/application-menu.injectable.ts | 25 + src/main/menu/electron-menu-items.test.ts | 4 +- src/main/menu/menu.ts | 29 +- src/main/menu/show-about.injectable.ts | 16 + .../menu/start-application-menu.injectable.ts | 27 ++ .../menu/stop-application-menu.injectable.ts | 27 ++ src/main/native-theme.ts | 18 - .../navigate-to-route.injectable.ts | 4 +- .../navigate-to-url.injectable.ts | 8 +- ...prometheus-provider-registry.injectable.ts | 13 + src/main/prometheus/provider-registry.ts | 4 +- .../protocol-handler/__test__/router.test.ts | 3 +- .../lens-protocol-router-main.injectable.ts | 2 + .../lens-protocol-router-main.ts | 5 +- .../open-deep-link.injectable.ts | 20 + src/main/proxy-env.ts | 23 - src/main/router/router.test.ts | 21 +- src/main/router/router.ts | 2 +- .../metrics/add-metrics-route.injectable.ts | 12 +- .../get-metric-providers-route.injectable.ts | 29 +- .../routes/static-file-route.injectable.ts | 22 +- .../terminal-shell-env-modifiers.ts | 5 +- .../terminal-shell-env-modify.injectable.ts | 2 + .../application-window-state.injectable.ts | 32 ++ .../application-window.injectable.ts | 68 +++ .../create-electron-window-for.injectable.ts | 186 +++++++ .../create-lens-window.injectable.ts | 89 ++++ .../lens-window-injection-token.ts | 23 + ...l-in-electron-browser-window.injectable.ts | 32 ++ ...uster-frame-cluster-id-state.injectable.ts | 15 + .../current-cluster-frame.injectable.ts | 25 + ...er-for-current-cluster-frame.injectable.ts | 34 ++ .../close-all-windows.injectable.ts | 20 + .../navigate-for-extension.injectable.ts | 48 ++ .../lens-window/navigate.injectable.ts | 41 ++ .../reload-all-windows.injectable.ts | 24 + .../lens-window/reload-window.injectable.ts | 33 ++ .../show-application-window.injectable.ts | 33 ++ .../splash-window/splash-window.injectable.ts | 30 ++ ...r-application-is-loaded-injection-token.ts | 10 + ...ter-root-frame-is-ready-injection-token.ts | 10 + .../after-window-is-opened-injection-token.ts | 10 + ...-application-is-loading-injection-token.ts | 10 + ...efore-electron-is-ready-injection-token.ts | 11 + ...before-quit-of-back-end-injection-token.ts | 11 + ...efore-quit-of-front-end-injection-token.ts | 11 + .../on-load-of-application-injection-token.ts | 10 + .../clean-up-shell-sessions.injectable.ts | 21 + .../emit-close-to-event-bus.injectable.ts | 25 + ...t-service-start-to-event-bus.injectable.ts | 25 + .../flag-renderer-as-loaded.injectable.ts | 29 ++ .../flag-renderer-as-not-loaded.injectable.ts | 29 ++ .../initialize-extensions.injectable.ts | 67 +++ .../start-kube-config-sync.injectable.ts | 31 ++ .../stop-kube-config-sync.injectable.ts | 25 + .../setup-detector-registry.injectable.ts | 35 ++ .../setup-file-protocol.injectable.ts | 27 ++ .../setup-hardware-acceleration.injectable.ts | 29 ++ .../setup-hotbar-store.injectable.ts | 26 + .../runnables/setup-immer.injectable.ts | 24 + .../runnables/setup-lens-proxy.injectable.ts | 77 +++ .../runnables/setup-mobx.injectable.ts | 29 ++ .../setup-prometheus-registry.injectable.ts | 33 ++ .../runnables/setup-proxy-env.injectable.ts | 43 ++ ...etup-reactions-in-user-store.injectable.ts | 25 + ...or-after-root-frame-is-ready.injectable.ts | 37 ++ .../runnables/setup-sentry.injectable.ts | 24 + .../runnables/setup-shell.injectable.ts | 29 ++ ...-of-general-catalog-entities.injectable.ts | 27 ++ .../setup-syncing-of-weblinks.injectable.ts | 25 + .../runnables/setup-system-ca.injectable.ts | 23 + .../stop-cluster-manager.injectable.ts | 27 ++ .../start-main-application.injectable.ts | 76 +++ src/main/start-update-checking.injectable.ts | 19 + .../stop-services-and-exit-app.injectable.ts | 32 ++ .../broadcast-theme-change.injectable.ts | 27 ++ ...rt-broadcasting-theme-change.injectable.ts | 25 + ...op-broadcasting-theme-change.injectable.ts | 25 + ...operating-system-theme-state.injectable.ts | 24 + .../operating-system-theme.injectable.ts | 19 + ...-theme-from-operating-system.injectable.ts | 25 + ...-theme-from-operating-system.injectable.ts | 25 + src/main/tray/install-tray.injectable.ts | 25 + src/main/tray/tray-icon-path.injectable.ts | 26 + src/main/tray/tray-menu-items.test.ts | 2 - src/main/tray/tray.injectable.ts | 42 ++ src/main/tray/tray.ts | 47 +- src/main/tray/uninstall-tray.injectable.ts | 25 + .../command-line-arguments.injectable.ts | 13 + src/main/window-manager.injectable.ts | 20 - src/main/window-manager.ts | 287 ----------- .../api/setup-on-api-errors.injectable.ts | 14 +- .../app-paths/app-paths.injectable.ts | 28 -- .../app-paths/setup-app-paths.injectable.ts | 31 ++ .../before-frame-starts-injection-token.ts | 10 + src/renderer/bootstrap.tsx | 12 +- .../components/+catalog/catalog.test.tsx | 8 +- .../+extensions/__tests__/extensions.test.tsx | 4 +- .../components/+helm-charts/helm-charts.tsx | 2 + .../+helm-releases/releases.injectable.ts | 5 +- ...-preference-item-registrator.injectable.ts | 2 +- ...-preference-item-registrator.injectable.ts | 2 +- .../__tests__/dialog.test.tsx | 4 +- .../+role-bindings/__tests__/dialog.test.tsx | 4 +- .../+welcome/__test__/welcome.test.tsx | 2 +- .../__tests__/pod-tolerations.test.tsx | 2 +- .../cluster-frame-handler.injectable.ts | 13 + ...lens-views.ts => cluster-frame-handler.ts} | 0 .../cluster-manager/cluster-view.tsx | 6 +- .../__tests__/delete-cluster-dialog.test.tsx | 261 ++++++---- .../dock/__test__/dock-store.test.ts | 2 +- .../dock/__test__/dock-tabs.test.tsx | 2 - .../lens-templates.injectable.ts | 6 +- .../__test__/log-resource-selector.test.tsx | 4 +- .../dock/logs/__test__/log-search.test.tsx | 4 +- .../__tests__/hotbar-remove-command.test.tsx | 8 +- .../components/item-object-list/content.tsx | 9 +- .../kube-object-menu.test.tsx | 2 +- .../kube-object-status-icon.test.tsx | 4 +- src/renderer/components/layout/tab-layout.tsx | 2 +- .../layout/top-bar/top-bar.test.tsx | 4 +- src/renderer/components/menu/menu-actions.tsx | 1 + .../components/select/select.test.tsx | 2 +- .../components/status-bar/status-bar.test.tsx | 4 +- .../test-utils/get-application-builder.tsx | 79 ++- .../create-cluster.injectable.ts | 11 +- src/renderer/getDi.tsx | 25 +- src/renderer/getDiForUnitTesting.tsx | 41 +- ...gister-ipc-channel-listeners.injectable.ts | 23 +- src/renderer/navigation/history.injectable.ts | 3 +- .../observable-history.injectable.ts | 1 + ...extension-route-registrator.injectable.tsx | 6 +- .../routes/navigate-to-url.injectable.ts | 3 +- .../search-store/search-store.test.ts | 4 +- .../start-frame/start-frame.injectable.ts | 20 + src/test-utils/get-dis-for-unit-testing.ts | 1 - src/test-utils/override-ipc-bridge.ts | 38 +- yarn.lock | 47 +- 302 files changed, 6388 insertions(+), 2025 deletions(-) rename src/main/initializers/index.ts => __mocks__/@sentry/electron/main.ts (63%) create mode 100644 __mocks__/@sentry/electron/renderer.ts create mode 100644 src/behaviours/helm-charts/__snapshots__/navigation-to-helm-charts.test.ts.snap create mode 100644 src/behaviours/helm-charts/navigation-to-helm-charts.test.ts create mode 100644 src/common/app-paths/app-paths-state.injectable.ts create mode 100644 src/common/app-paths/app-paths.injectable.ts rename src/{renderer/components/cluster-manager/lens-views.injectable.ts => common/cluster-frames.injectable.ts} (73%) create mode 100644 src/common/fs/ensure-dir.injectable.ts create mode 100644 src/common/ipc/broadcast-message.injectable.ts create mode 100644 src/common/runnable/run-many-for.test.ts create mode 100644 src/common/runnable/run-many-for.ts create mode 100644 src/common/runnable/run-many-sync-for.test.ts create mode 100644 src/common/runnable/run-many-sync-for.ts create mode 100644 src/common/runnable/throw-with-incorrect-hierarchy-for.ts create mode 100644 src/common/test-utils/flush-promises.ts create mode 100644 src/common/test-utils/get-promise-status.ts create mode 100644 src/common/utils/environment-variables.injectable.ts create mode 100644 src/common/utils/get-startable-stoppable.test.ts create mode 100644 src/common/utils/get-startable-stoppable.ts create mode 100644 src/common/vars/is-integration-testing.injectable.ts create mode 100644 src/common/vars/is-production.injectable.ts create mode 100644 src/common/vars/is-test-env.injectable.ts create mode 100644 src/common/vars/lens-resources-dir.injectable.ts create mode 100644 src/common/vars/platform.injectable.ts create mode 100644 src/common/vars/static-files-directory.injectable.ts create mode 100644 src/common/weblink-store.injectable.ts delete mode 100644 src/main/app-paths/app-paths.injectable.ts create mode 100644 src/main/app-paths/setup-app-paths.injectable.ts create mode 100644 src/main/catalog-sources/sync-weblinks.injectable.ts create mode 100644 src/main/catalog-sync-to-renderer/catalog-sync-to-renderer.injectable.ts create mode 100644 src/main/catalog-sync-to-renderer/start-catalog-sync.injectable.ts create mode 100644 src/main/catalog-sync-to-renderer/stop-catalog-sync.injectable.ts create mode 100644 src/main/cluster-detectors/create-version-detector.injectable.ts create mode 100644 src/main/cluster-detectors/detector-registry.injectable.ts create mode 100644 src/main/cluster-manager.injectable.ts delete mode 100644 src/main/developer-tools.ts rename src/main/{app-paths/get-electron-app-path => }/electron-app/electron-app.injectable.ts (100%) create mode 100644 src/main/electron-app/features/auto-updater.injectable.ts create mode 100644 src/main/electron-app/features/disable-hardware-acceleration.injectable.ts create mode 100644 src/main/electron-app/features/electron-dialog.injectable.ts create mode 100644 src/main/electron-app/features/exit-app.injectable.ts create mode 100644 src/main/electron-app/features/get-command-line-switch.injectable.ts create mode 100644 src/main/electron-app/features/get-electron-theme.injectable.ts create mode 100644 src/main/electron-app/features/native-theme.injectable.ts create mode 100644 src/main/electron-app/features/power-monitor.injectable.ts create mode 100644 src/main/electron-app/features/register-file-protocol.injectable.ts create mode 100644 src/main/electron-app/features/request-single-instance-lock.injectable.ts create mode 100644 src/main/electron-app/features/should-start-hidden.injectable.ts create mode 100644 src/main/electron-app/features/show-error-popup.injectable.ts create mode 100644 src/main/electron-app/features/show-message-popup.injectable.ts create mode 100644 src/main/electron-app/features/sync-theme-from-operating-system.injectable.ts create mode 100644 src/main/electron-app/features/wait-for-electron-to-be-ready.injectable.ts create mode 100644 src/main/electron-app/runnables/clean-up-deep-linking.injectable.ts create mode 100644 src/main/electron-app/runnables/dock-visibility/hide-dock-for-last-closed-window.injectable.ts create mode 100644 src/main/electron-app/runnables/dock-visibility/show-dock-for-first-opened-window.injectable.ts create mode 100644 src/main/electron-app/runnables/enforce-single-application-instance.injectable.ts create mode 100644 src/main/electron-app/runnables/setup-application-name.injectable.ts create mode 100644 src/main/electron-app/runnables/setup-deep-linking.injectable.ts create mode 100644 src/main/electron-app/runnables/setup-developer-tools-in-development-environment.injectable.ts create mode 100644 src/main/electron-app/runnables/setup-device-shutdown.injectable.ts create mode 100644 src/main/electron-app/runnables/setup-ipc-main-handlers/setup-ipc-main-handlers.injectable.ts rename src/main/{initializers/init-ipc-main-handlers/init-ipc-main-handlers.ts => electron-app/runnables/setup-ipc-main-handlers/setup-ipc-main-handlers.ts} (69%) create mode 100644 src/main/electron-app/runnables/setup-main-window-visibility-after-activation.injectable.ts create mode 100644 src/main/electron-app/runnables/setup-runnables-after-window-is-opened.injectable.ts create mode 100644 src/main/electron-app/runnables/setup-runnables-before-closing-of-application.injectable.ts create mode 100644 src/main/electron-app/runnables/setup-update-checking.injectable.ts delete mode 100644 src/main/exit-app.ts create mode 100644 src/main/get-metrics.injectable.ts create mode 100644 src/main/helm/helm-repo-manager.injectable.ts delete mode 100644 src/main/initializers/cluster-metadata-detectors.ts delete mode 100644 src/main/initializers/init-ipc-main-handlers/init-ipc-main-handlers.injectable.ts delete mode 100644 src/main/initializers/metrics-providers.ts create mode 100644 src/main/ipc/ask-user-for-file-paths.injectable.ts delete mode 100644 src/main/ipc/dialog.ts create mode 100644 src/main/k8s-request.injectable.ts delete mode 100644 src/main/k8s-request.ts create mode 100644 src/main/lens-proxy/lens-proxy-port.injectable.ts create mode 100644 src/main/lens-proxy/lens-proxy.injectable.ts rename src/main/{ => lens-proxy}/lens-proxy.ts (86%) rename src/main/{ => lens-proxy}/proxy-functions/index.ts (100%) rename src/main/{ => lens-proxy}/proxy-functions/kube-api-upgrade-request.ts (97%) rename src/main/{ => lens-proxy}/proxy-functions/shell-api-request/shell-api-request.injectable.ts (62%) rename src/main/{ => lens-proxy}/proxy-functions/shell-api-request/shell-api-request.ts (74%) rename src/main/{ => lens-proxy}/proxy-functions/shell-api-request/shell-request-authenticator/shell-request-authenticator.injectable.ts (100%) rename src/main/{ => lens-proxy}/proxy-functions/shell-api-request/shell-request-authenticator/shell-request-authenticator.ts (89%) rename src/main/{ => lens-proxy}/proxy-functions/types.ts (86%) create mode 100644 src/main/menu/application-menu.injectable.ts create mode 100644 src/main/menu/show-about.injectable.ts create mode 100644 src/main/menu/start-application-menu.injectable.ts create mode 100644 src/main/menu/stop-application-menu.injectable.ts delete mode 100644 src/main/native-theme.ts create mode 100644 src/main/prometheus/prometheus-provider-registry.injectable.ts create mode 100644 src/main/protocol-handler/lens-protocol-router-main/open-deep-link-for-url/open-deep-link.injectable.ts delete mode 100644 src/main/proxy-env.ts create mode 100644 src/main/start-main-application/lens-window/application-window/application-window-state.injectable.ts create mode 100644 src/main/start-main-application/lens-window/application-window/application-window.injectable.ts create mode 100644 src/main/start-main-application/lens-window/application-window/create-electron-window-for.injectable.ts create mode 100644 src/main/start-main-application/lens-window/application-window/create-lens-window.injectable.ts create mode 100644 src/main/start-main-application/lens-window/application-window/lens-window-injection-token.ts create mode 100644 src/main/start-main-application/lens-window/application-window/send-to-channel-in-electron-browser-window.injectable.ts create mode 100644 src/main/start-main-application/lens-window/current-cluster-frame/current-cluster-frame-cluster-id-state.injectable.ts create mode 100644 src/main/start-main-application/lens-window/current-cluster-frame/current-cluster-frame.injectable.ts create mode 100644 src/main/start-main-application/lens-window/current-cluster-frame/setup-listener-for-current-cluster-frame.injectable.ts create mode 100644 src/main/start-main-application/lens-window/hide-all-windows/close-all-windows.injectable.ts create mode 100644 src/main/start-main-application/lens-window/navigate-for-extension.injectable.ts create mode 100644 src/main/start-main-application/lens-window/navigate.injectable.ts create mode 100644 src/main/start-main-application/lens-window/reload-all-windows.injectable.ts create mode 100644 src/main/start-main-application/lens-window/reload-window.injectable.ts create mode 100644 src/main/start-main-application/lens-window/show-application-window.injectable.ts create mode 100644 src/main/start-main-application/lens-window/splash-window/splash-window.injectable.ts create mode 100644 src/main/start-main-application/runnable-tokens/after-application-is-loaded-injection-token.ts create mode 100644 src/main/start-main-application/runnable-tokens/after-root-frame-is-ready-injection-token.ts create mode 100644 src/main/start-main-application/runnable-tokens/after-window-is-opened-injection-token.ts create mode 100644 src/main/start-main-application/runnable-tokens/before-application-is-loading-injection-token.ts create mode 100644 src/main/start-main-application/runnable-tokens/before-electron-is-ready-injection-token.ts create mode 100644 src/main/start-main-application/runnable-tokens/before-quit-of-back-end-injection-token.ts create mode 100644 src/main/start-main-application/runnable-tokens/before-quit-of-front-end-injection-token.ts create mode 100644 src/main/start-main-application/runnable-tokens/on-load-of-application-injection-token.ts create mode 100644 src/main/start-main-application/runnables/clean-up-shell-sessions.injectable.ts create mode 100644 src/main/start-main-application/runnables/emit-close-to-event-bus.injectable.ts create mode 100644 src/main/start-main-application/runnables/emit-service-start-to-event-bus.injectable.ts create mode 100644 src/main/start-main-application/runnables/flag-renderer/flag-renderer-as-loaded.injectable.ts create mode 100644 src/main/start-main-application/runnables/flag-renderer/flag-renderer-as-not-loaded.injectable.ts create mode 100644 src/main/start-main-application/runnables/initialize-extensions.injectable.ts create mode 100644 src/main/start-main-application/runnables/kube-config-sync/start-kube-config-sync.injectable.ts create mode 100644 src/main/start-main-application/runnables/kube-config-sync/stop-kube-config-sync.injectable.ts create mode 100644 src/main/start-main-application/runnables/setup-detector-registry.injectable.ts create mode 100644 src/main/start-main-application/runnables/setup-file-protocol.injectable.ts create mode 100644 src/main/start-main-application/runnables/setup-hardware-acceleration.injectable.ts create mode 100644 src/main/start-main-application/runnables/setup-hotbar-store.injectable.ts create mode 100644 src/main/start-main-application/runnables/setup-immer.injectable.ts create mode 100644 src/main/start-main-application/runnables/setup-lens-proxy.injectable.ts create mode 100644 src/main/start-main-application/runnables/setup-mobx.injectable.ts create mode 100644 src/main/start-main-application/runnables/setup-prometheus-registry.injectable.ts create mode 100644 src/main/start-main-application/runnables/setup-proxy-env.injectable.ts create mode 100644 src/main/start-main-application/runnables/setup-reactions-in-user-store.injectable.ts create mode 100644 src/main/start-main-application/runnables/setup-runnables-for-after-root-frame-is-ready.injectable.ts create mode 100644 src/main/start-main-application/runnables/setup-sentry.injectable.ts create mode 100644 src/main/start-main-application/runnables/setup-shell.injectable.ts create mode 100644 src/main/start-main-application/runnables/setup-syncing-of-general-catalog-entities.injectable.ts create mode 100644 src/main/start-main-application/runnables/setup-syncing-of-weblinks.injectable.ts create mode 100644 src/main/start-main-application/runnables/setup-system-ca.injectable.ts create mode 100644 src/main/start-main-application/runnables/stop-cluster-manager.injectable.ts create mode 100644 src/main/start-main-application/start-main-application.injectable.ts create mode 100644 src/main/start-update-checking.injectable.ts create mode 100644 src/main/stop-services-and-exit-app.injectable.ts create mode 100644 src/main/theme/broadcast-theme-change/broadcast-theme-change.injectable.ts create mode 100644 src/main/theme/broadcast-theme-change/start-broadcasting-theme-change.injectable.ts create mode 100644 src/main/theme/broadcast-theme-change/stop-broadcasting-theme-change.injectable.ts create mode 100644 src/main/theme/operating-system-theme-state.injectable.ts create mode 100644 src/main/theme/operating-system-theme.injectable.ts create mode 100644 src/main/theme/sync-theme-from-os/start-syncing-theme-from-operating-system.injectable.ts create mode 100644 src/main/theme/sync-theme-from-os/stop-syncing-theme-from-operating-system.injectable.ts create mode 100644 src/main/tray/install-tray.injectable.ts create mode 100644 src/main/tray/tray-icon-path.injectable.ts create mode 100644 src/main/tray/tray.injectable.ts create mode 100644 src/main/tray/uninstall-tray.injectable.ts create mode 100644 src/main/utils/command-line-arguments.injectable.ts delete mode 100644 src/main/window-manager.injectable.ts delete mode 100644 src/main/window-manager.ts delete mode 100644 src/renderer/app-paths/app-paths.injectable.ts create mode 100644 src/renderer/app-paths/setup-app-paths.injectable.ts create mode 100644 src/renderer/before-frame-starts/before-frame-starts-injection-token.ts create mode 100644 src/renderer/components/cluster-manager/cluster-frame-handler.injectable.ts rename src/renderer/components/cluster-manager/{lens-views.ts => cluster-frame-handler.ts} (100%) create mode 100644 src/renderer/start-frame/start-frame.injectable.ts diff --git a/src/main/initializers/index.ts b/__mocks__/@sentry/electron/main.ts similarity index 63% rename from src/main/initializers/index.ts rename to __mocks__/@sentry/electron/main.ts index 233376a91f..cbe02cb296 100644 --- a/src/main/initializers/index.ts +++ b/__mocks__/@sentry/electron/main.ts @@ -2,5 +2,4 @@ * 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"; +export default {}; diff --git a/__mocks__/@sentry/electron/renderer.ts b/__mocks__/@sentry/electron/renderer.ts new file mode 100644 index 0000000000..cbe02cb296 --- /dev/null +++ b/__mocks__/@sentry/electron/renderer.ts @@ -0,0 +1,5 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +export default {}; diff --git a/package.json b/package.json index c82fdd9c27..a2b4408cfe 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "build:mac": "yarn run compile && electron-builder --mac --dir", "build:win": "yarn run compile && electron-builder --win --dir", "integration": "jest --runInBand --detectOpenHandles --forceExit integration", - "test:unit": "jest --watch --testPathIgnorePatterns integration", + "test:unit": "func() { jest ${1} --watch --testPathIgnorePatterns integration; }; func", "test:integration": "func() { jest ${1:-xyz} --watch --runInBand --detectOpenHandles --forceExit --modulePaths=[\"/integration/\"]; }; func", "dist": "yarn run compile && electron-builder --publish onTag", "dist:dir": "yarn run dist --dir -c.compression=store -c.mac.identity=null", @@ -204,9 +204,10 @@ "@hapi/subtext": "^7.0.3", "@kubernetes/client-node": "^0.16.3", "@material-ui/styles": "^4.11.5", - "@ogre-tools/fp": "5.2.0", - "@ogre-tools/injectable": "5.2.0", - "@ogre-tools/injectable-react": "5.2.0", + "@ogre-tools/injectable": "7.0.0", + "@ogre-tools/injectable-react": "7.0.0", + "@ogre-tools/fp": "7.0.0", + "@ogre-tools/injectable-extension-for-auto-registration": "7.0.0", "@sentry/electron": "^3.0.7", "@sentry/integrations": "^6.19.3", "@types/circular-dependency-plugin": "5.0.5", @@ -226,6 +227,7 @@ "got": "^11.8.3", "grapheme-splitter": "^1.0.4", "handlebars": "^4.7.7", + "history": "^4.10.1", "http-proxy": "^1.18.1", "immer": "^9.0.12", "joi": "^17.6.0", @@ -276,7 +278,7 @@ "ws": "^8.5.0" }, "devDependencies": { - "@async-fn/jest": "1.5.3", + "@async-fn/jest": "1.6.0", "@material-ui/core": "^4.12.3", "@material-ui/icons": "^4.11.2", "@material-ui/lab": "^4.0.0-alpha.60", diff --git a/src/behaviours/add-cluster/navigation-using-application-menu.test.tsx b/src/behaviours/add-cluster/navigation-using-application-menu.test.tsx index c95d4a563b..e982b9de1c 100644 --- a/src/behaviours/add-cluster/navigation-using-application-menu.test.tsx +++ b/src/behaviours/add-cluster/navigation-using-application-menu.test.tsx @@ -25,7 +25,7 @@ describe("add-cluster - navigation using application menu", () => { let rendered: RenderResult; beforeEach(async () => { - applicationBuilder = getApplicationBuilder().beforeSetups(({ mainDi }) => { + applicationBuilder = getApplicationBuilder().beforeApplicationStart(({ mainDi }) => { mainDi.override(isAutoUpdateEnabledInjectable, () => () => false); }); @@ -43,8 +43,8 @@ describe("add-cluster - navigation using application menu", () => { }); describe("when navigating to add cluster using application menu", () => { - beforeEach(() => { - applicationBuilder.applicationMenu.click("file.add-cluster"); + beforeEach(async () => { + await applicationBuilder.applicationMenu.click("file.add-cluster"); }); it("renders", () => { diff --git a/src/behaviours/cluster/order-of-sidebar-items.test.tsx b/src/behaviours/cluster/order-of-sidebar-items.test.tsx index 681752a28a..77bf395de7 100644 --- a/src/behaviours/cluster/order-of-sidebar-items.test.tsx +++ b/src/behaviours/cluster/order-of-sidebar-items.test.tsx @@ -19,7 +19,7 @@ describe("cluster - order of sidebar items", () => { beforeEach(() => { applicationBuilder = getApplicationBuilder().setEnvironmentToClusterFrame(); - applicationBuilder.beforeSetups(({ rendererDi }) => { + applicationBuilder.beforeApplicationStart(({ rendererDi }) => { rendererDi.register(testSidebarItemsInjectable); }); }); diff --git a/src/behaviours/cluster/sidebar-and-tab-navigation-for-core.test.tsx b/src/behaviours/cluster/sidebar-and-tab-navigation-for-core.test.tsx index 5d6c717fc9..f270b7d13e 100644 --- a/src/behaviours/cluster/sidebar-and-tab-navigation-for-core.test.tsx +++ b/src/behaviours/cluster/sidebar-and-tab-navigation-for-core.test.tsx @@ -36,7 +36,8 @@ describe("cluster - sidebar and tab navigation for core", () => { rendererDi = applicationBuilder.dis.rendererDi; applicationBuilder.setEnvironmentToClusterFrame(); - applicationBuilder.beforeSetups(({ rendererDi }) => { + + applicationBuilder.beforeApplicationStart(({ rendererDi }) => { rendererDi.override( directoryForLensLocalStorageInjectable, () => "/some-directory-for-lens-local-storage", @@ -46,7 +47,7 @@ describe("cluster - sidebar and tab navigation for core", () => { describe("given core registrations", () => { beforeEach(() => { - applicationBuilder.beforeSetups(({ rendererDi }) => { + applicationBuilder.beforeApplicationStart(({ rendererDi }) => { rendererDi.register(testRouteInjectable); rendererDi.register(testRouteComponentInjectable); rendererDi.register(testSidebarItemsInjectable); @@ -102,6 +103,7 @@ describe("cluster - sidebar and tab navigation for core", () => { }, ); }); + applicationBuilder.beforeRender(async ({ rendererDi }) => { const sidebarStorage = rendererDi.inject(sidebarStorageInjectable); @@ -326,7 +328,7 @@ const testSidebarItemsInjectable = getInjectable({ injectionToken: sidebarItemsInjectionToken, }); -const testRouteInjectable = getInjectable({ +const testRouteInjectable = getInjectable({ id: "some-route-injectable-id", instantiate: () => ({ diff --git a/src/behaviours/cluster/sidebar-and-tab-navigation-for-extensions.test.tsx b/src/behaviours/cluster/sidebar-and-tab-navigation-for-extensions.test.tsx index 97dd60ef78..eb7fd0bc05 100644 --- a/src/behaviours/cluster/sidebar-and-tab-navigation-for-extensions.test.tsx +++ b/src/behaviours/cluster/sidebar-and-tab-navigation-for-extensions.test.tsx @@ -33,7 +33,7 @@ describe("cluster - sidebar and tab navigation for extensions", () => { applicationBuilder.setEnvironmentToClusterFrame(); - applicationBuilder.beforeSetups(({ rendererDi }) => { + applicationBuilder.beforeApplicationStart(({ rendererDi }) => { rendererDi.override( directoryForLensLocalStorageInjectable, () => "/some-directory-for-lens-local-storage", diff --git a/src/behaviours/cluster/visibility-of-sidebar-items.test.tsx b/src/behaviours/cluster/visibility-of-sidebar-items.test.tsx index 1a815d2aaa..aa73936470 100644 --- a/src/behaviours/cluster/visibility-of-sidebar-items.test.tsx +++ b/src/behaviours/cluster/visibility-of-sidebar-items.test.tsx @@ -25,7 +25,7 @@ describe("cluster - visibility of sidebar items", () => { applicationBuilder.setEnvironmentToClusterFrame(); - applicationBuilder.beforeSetups(({ rendererDi }) => { + applicationBuilder.beforeApplicationStart(({ rendererDi }) => { rendererDi.register(testRouteInjectable); rendererDi.register(testRouteComponentInjectable); rendererDi.register(testSidebarItemsInjectable); diff --git a/src/behaviours/extensions/navigation-using-application-menu.test.ts b/src/behaviours/extensions/navigation-using-application-menu.test.ts index 1489d40fc8..75ffa0ebef 100644 --- a/src/behaviours/extensions/navigation-using-application-menu.test.ts +++ b/src/behaviours/extensions/navigation-using-application-menu.test.ts @@ -22,7 +22,7 @@ describe("extensions - navigation using application menu", () => { let focusWindowMock: jest.Mock; beforeEach(async () => { - applicationBuilder = getApplicationBuilder().beforeSetups(({ mainDi, rendererDi }) => { + applicationBuilder = getApplicationBuilder().beforeApplicationStart(({ mainDi, rendererDi }) => { mainDi.override(isAutoUpdateEnabledInjectable, () => () => false); rendererDi.override(extensionsStoreInjectable, () => ({}) as unknown as ExtensionsStore); rendererDi.override(fileSystemProvisionerStoreInjectable, () => ({}) as unknown as FileSystemProvisionerStore); @@ -46,8 +46,8 @@ describe("extensions - navigation using application menu", () => { }); describe("when navigating to extensions using application menu", () => { - beforeEach(() => { - applicationBuilder.applicationMenu.click("root.extensions"); + beforeEach(async () => { + await applicationBuilder.applicationMenu.click("root.extensions"); }); it("focuses the window", () => { diff --git a/src/behaviours/helm-charts/__snapshots__/navigation-to-helm-charts.test.ts.snap b/src/behaviours/helm-charts/__snapshots__/navigation-to-helm-charts.test.ts.snap new file mode 100644 index 0000000000..4f1555049d --- /dev/null +++ b/src/behaviours/helm-charts/__snapshots__/navigation-to-helm-charts.test.ts.snap @@ -0,0 +1,458 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`helm-charts - navigation to Helm charts when navigating to Helm charts renders 1`] = ` +
+
+ +
+
+ + +
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ Name +
+ + + arrow_drop_down + + +
+
+
+ Description +
+
+
+
+ Version +
+
+
+
+ App Version +
+
+
+
+ Repository +
+ + + arrow_drop_down + + +
+ +
+
+
+
+
+
+
+
+
+`; diff --git a/src/behaviours/helm-charts/navigation-to-helm-charts.test.ts b/src/behaviours/helm-charts/navigation-to-helm-charts.test.ts new file mode 100644 index 0000000000..c539db173a --- /dev/null +++ b/src/behaviours/helm-charts/navigation-to-helm-charts.test.ts @@ -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 { RenderResult } from "@testing-library/react"; +import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; +import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; + +describe("helm-charts - navigation to Helm charts", () => { + let applicationBuilder: ApplicationBuilder; + + beforeEach(() => { + applicationBuilder = getApplicationBuilder(); + }); + + describe("when navigating to Helm charts", () => { + let rendered: RenderResult; + + beforeEach(async () => { + applicationBuilder.setEnvironmentToClusterFrame(); + + rendered = await applicationBuilder.render(); + + applicationBuilder.helmCharts.navigate(); + }); + + it("renders", () => { + expect(rendered.container).toMatchSnapshot(); + }); + + it("shows page for Helm charts", () => { + const page = rendered.getByTestId("page-for-helm-charts"); + + expect(page).not.toBeNull(); + }); + }); +}); diff --git a/src/behaviours/navigating-between-routes.test.tsx b/src/behaviours/navigating-between-routes.test.tsx index 4cb55cd290..4def9b383f 100644 --- a/src/behaviours/navigating-between-routes.test.tsx +++ b/src/behaviours/navigating-between-routes.test.tsx @@ -31,7 +31,7 @@ describe("navigating between routes", () => { describe("given route without path parameters", () => { beforeEach(async () => { - applicationBuilder.beforeSetups(({ rendererDi }) => { + applicationBuilder.beforeApplicationStart(({ rendererDi }) => { rendererDi.register(testRouteWithoutPathParametersInjectable); rendererDi.register(testRouteWithoutPathParametersComponentInjectable); }); @@ -102,7 +102,7 @@ describe("navigating between routes", () => { describe("given route with optional path parameters", () => { beforeEach(async () => { - applicationBuilder.beforeSetups(({ rendererDi }) => { + applicationBuilder.beforeApplicationStart(({ rendererDi }) => { rendererDi.register(routeWithOptionalPathParametersInjectable); rendererDi.register(routeWithOptionalPathParametersComponentInjectable); }); diff --git a/src/behaviours/preferences/closing-preferences.test.tsx b/src/behaviours/preferences/closing-preferences.test.tsx index 52c88c49fd..3d3eaa89dc 100644 --- a/src/behaviours/preferences/closing-preferences.test.tsx +++ b/src/behaviours/preferences/closing-preferences.test.tsx @@ -10,8 +10,6 @@ import { getApplicationBuilder } from "../../renderer/components/test-utils/get- import currentPathInjectable from "../../renderer/routes/current-path.injectable"; import { routeInjectionToken } from "../../common/front-end-routing/route-injection-token"; import { computed } from "mobx"; -import type { UserStore } from "../../common/user-store"; -import userStoreInjectable from "../../common/user-store/user-store.injectable"; import { preferenceNavigationItemInjectionToken } from "../../renderer/components/+preferences/preferences-navigation/preference-navigation-items.injectable"; import routeIsActiveInjectable from "../../renderer/routes/route-is-active.injectable"; import { Preferences } from "../../renderer/components/+preferences"; @@ -24,7 +22,6 @@ import { createObservableHistory } from "mobx-observable-history"; import navigateToPreferenceTabInjectable from "../../renderer/components/+preferences/preferences-navigation/navigate-to-preference-tab.injectable"; import navigateToFrontPageInjectable from "../../common/front-end-routing/navigate-to-front-page.injectable"; import { navigateToRouteInjectionToken } from "../../common/front-end-routing/navigate-to-route-injection-token"; -import ipcRendererInjectable from "../../renderer/app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable"; describe("preferences - closing-preferences", () => { let applicationBuilder: ApplicationBuilder; @@ -32,23 +29,13 @@ describe("preferences - closing-preferences", () => { beforeEach(() => { applicationBuilder = getApplicationBuilder(); - applicationBuilder.beforeSetups(({ rendererDi }) => { + applicationBuilder.beforeApplicationStart(({ rendererDi }) => { rendererDi.register(testPreferencesRouteInjectable); rendererDi.register(testPreferencesRouteComponentInjectable); rendererDi.register(testFrontPageRouteInjectable); rendererDi.register(testFrontPageRouteComponentInjectable); rendererDi.register(testNavigationItemInjectable); - const userStoreStub = { - extensionRegistryUrl: { customUrl: "some-custom-url" }, - } as unknown as UserStore; - - rendererDi.override(userStoreInjectable, () => userStoreStub); - rendererDi.override(ipcRendererInjectable, () => ({ - on: jest.fn(), - invoke: jest.fn(), // TODO: replace with proper mocking via the IPC bridge - } as never)); - rendererDi.override(navigateToFrontPageInjectable, (di) => { const navigateToRoute = di.inject(navigateToRouteInjectionToken); const testFrontPage = di.inject(testFrontPageRouteInjectable); @@ -65,7 +52,7 @@ describe("preferences - closing-preferences", () => { let rendererDi: DiContainer; beforeEach(async () => { - applicationBuilder.beforeSetups(({ rendererDi }) => { + applicationBuilder.beforeApplicationStart(({ rendererDi }) => { rendererDi.override(observableHistoryInjectable, () => { const historyFake = createMemoryHistory({ initialEntries: ["/some-test-path"], @@ -138,7 +125,7 @@ describe("preferences - closing-preferences", () => { let rendererDi: DiContainer; beforeEach(async () => { - applicationBuilder.beforeSetups(({ rendererDi }) => { + applicationBuilder.beforeApplicationStart(({ rendererDi }) => { rendererDi.override(observableHistoryInjectable, () => { const historyFake = createMemoryHistory({ initialEntries: ["/preferences/app"], diff --git a/src/behaviours/preferences/navigation-to-application-preferences.test.ts b/src/behaviours/preferences/navigation-to-application-preferences.test.ts index 65189e628b..c91f091323 100644 --- a/src/behaviours/preferences/navigation-to-application-preferences.test.ts +++ b/src/behaviours/preferences/navigation-to-application-preferences.test.ts @@ -5,28 +5,13 @@ import type { RenderResult } from "@testing-library/react"; import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; -import userStoreInjectable from "../../common/user-store/user-store.injectable"; -import type { UserStore } from "../../common/user-store"; import navigateToProxyPreferencesInjectable from "../../common/front-end-routing/routes/preferences/proxy/navigate-to-proxy-preferences.injectable"; -import ipcRendererInjectable from "../../renderer/app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable"; describe("preferences - navigation to application preferences", () => { let applicationBuilder: ApplicationBuilder; beforeEach(() => { applicationBuilder = getApplicationBuilder(); - - applicationBuilder.beforeSetups(({ rendererDi }) => { - const userStoreStub = { - extensionRegistryUrl: { customUrl: "some-custom-url" }, - } as unknown as UserStore; - - rendererDi.override(userStoreInjectable, () => userStoreStub); - rendererDi.override(ipcRendererInjectable, () => ({ - on: jest.fn(), - invoke: jest.fn(), // TODO: replace with proper mocking via the IPC bridge - } as never)); - }); }); describe("given in some child page of preferences, when rendered", () => { diff --git a/src/behaviours/preferences/navigation-to-editor-preferences.test.ts b/src/behaviours/preferences/navigation-to-editor-preferences.test.ts index e1a504ba51..1b74b94eb4 100644 --- a/src/behaviours/preferences/navigation-to-editor-preferences.test.ts +++ b/src/behaviours/preferences/navigation-to-editor-preferences.test.ts @@ -5,28 +5,12 @@ import type { RenderResult } from "@testing-library/react"; import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; -import userStoreInjectable from "../../common/user-store/user-store.injectable"; -import type { UserStore } from "../../common/user-store"; -import ipcRendererInjectable from "../../renderer/app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable"; describe("preferences - navigation to editor preferences", () => { let applicationBuilder: ApplicationBuilder; beforeEach(() => { applicationBuilder = getApplicationBuilder(); - - applicationBuilder.beforeSetups(({ rendererDi }) => { - const userStoreStub = { - extensionRegistryUrl: { customUrl: "some-custom-url" }, - editorConfiguration: { minimap: {}, tabSize: 42, fontSize: 42 }, - } as unknown as UserStore; - - rendererDi.override(userStoreInjectable, () => userStoreStub); - rendererDi.override(ipcRendererInjectable, () => ({ - on: jest.fn(), - invoke: jest.fn(), // TODO: replace with proper mocking via the IPC bridge - } as never)); - }); }); describe("given in preferences, when rendered", () => { diff --git a/src/behaviours/preferences/navigation-to-extension-specific-preferences.test.tsx b/src/behaviours/preferences/navigation-to-extension-specific-preferences.test.tsx index d4eb8bec07..f44fb9bf20 100644 --- a/src/behaviours/preferences/navigation-to-extension-specific-preferences.test.tsx +++ b/src/behaviours/preferences/navigation-to-extension-specific-preferences.test.tsx @@ -5,30 +5,15 @@ import type { RenderResult } from "@testing-library/react"; import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; -import userStoreInjectable from "../../common/user-store/user-store.injectable"; -import type { UserStore } from "../../common/user-store"; import React from "react"; import type { FakeExtensionData } from "../../renderer/components/test-utils/get-renderer-extension-fake"; import { getRendererExtensionFakeFor } from "../../renderer/components/test-utils/get-renderer-extension-fake"; -import ipcRendererInjectable from "../../renderer/app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable"; describe("preferences - navigation to extension specific preferences", () => { let applicationBuilder: ApplicationBuilder; beforeEach(() => { applicationBuilder = getApplicationBuilder(); - - applicationBuilder.beforeSetups(({ rendererDi }) => { - const userStoreStub = { - extensionRegistryUrl: { customUrl: "some-custom-url" }, - } as unknown as UserStore; - - rendererDi.override(userStoreInjectable, () => userStoreStub); - rendererDi.override(ipcRendererInjectable, () => ({ - on: jest.fn(), - invoke: jest.fn(), // TODO: replace with proper mocking via the IPC bridge - } as never)); - }); }); describe("given in preferences, when rendered", () => { diff --git a/src/behaviours/preferences/navigation-to-kubernetes-preferences.test.ts b/src/behaviours/preferences/navigation-to-kubernetes-preferences.test.ts index c9dd539b9e..e404e1f489 100644 --- a/src/behaviours/preferences/navigation-to-kubernetes-preferences.test.ts +++ b/src/behaviours/preferences/navigation-to-kubernetes-preferences.test.ts @@ -5,29 +5,12 @@ import type { RenderResult } from "@testing-library/react"; import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; -import userStoreInjectable from "../../common/user-store/user-store.injectable"; -import type { UserStore } from "../../common/user-store"; -import { observable } from "mobx"; -import ipcRendererInjectable from "../../renderer/app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable"; describe("preferences - navigation to kubernetes preferences", () => { let applicationBuilder: ApplicationBuilder; beforeEach(() => { applicationBuilder = getApplicationBuilder(); - - applicationBuilder.beforeSetups(({ rendererDi }) => { - const userStoreStub = { - extensionRegistryUrl: { customUrl: "some-custom-url" }, - syncKubeconfigEntries: observable.map(), - } as unknown as UserStore; - - rendererDi.override(userStoreInjectable, () => userStoreStub); - rendererDi.override(ipcRendererInjectable, () => ({ - on: jest.fn(), - invoke: jest.fn(), // TODO: replace with proper mocking via the IPC bridge - } as never)); - }); }); describe("given in preferences, when rendered", () => { diff --git a/src/behaviours/preferences/navigation-to-proxy-preferences.test.ts b/src/behaviours/preferences/navigation-to-proxy-preferences.test.ts index 99a462ff71..3d2c8d6d2c 100644 --- a/src/behaviours/preferences/navigation-to-proxy-preferences.test.ts +++ b/src/behaviours/preferences/navigation-to-proxy-preferences.test.ts @@ -5,27 +5,12 @@ import type { RenderResult } from "@testing-library/react"; import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; -import userStoreInjectable from "../../common/user-store/user-store.injectable"; -import type { UserStore } from "../../common/user-store"; -import ipcRendererInjectable from "../../renderer/app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable"; describe("preferences - navigation to proxy preferences", () => { let applicationBuilder: ApplicationBuilder; beforeEach(() => { applicationBuilder = getApplicationBuilder(); - - applicationBuilder.beforeSetups(({ rendererDi }) => { - const userStoreStub = { - extensionRegistryUrl: { customUrl: "some-custom-url" }, - } as unknown as UserStore; - - rendererDi.override(userStoreInjectable, () => userStoreStub); - rendererDi.override(ipcRendererInjectable, () => ({ - on: jest.fn(), - invoke: jest.fn(), // TODO: replace with proper mocking via the IPC bridge - } as never)); - }); }); describe("given in preferences, when rendered", () => { diff --git a/src/behaviours/preferences/navigation-to-telemetry-preferences.test.tsx b/src/behaviours/preferences/navigation-to-telemetry-preferences.test.tsx index 2fb16a37af..f16c2d809b 100644 --- a/src/behaviours/preferences/navigation-to-telemetry-preferences.test.tsx +++ b/src/behaviours/preferences/navigation-to-telemetry-preferences.test.tsx @@ -8,29 +8,14 @@ import type { ApplicationBuilder } from "../../renderer/components/test-utils/ge import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import type { FakeExtensionData } from "../../renderer/components/test-utils/get-renderer-extension-fake"; import { getRendererExtensionFakeFor } from "../../renderer/components/test-utils/get-renderer-extension-fake"; -import type { UserStore } from "../../common/user-store"; -import userStoreInjectable from "../../common/user-store/user-store.injectable"; import navigateToTelemetryPreferencesInjectable from "../../common/front-end-routing/routes/preferences/telemetry/navigate-to-telemetry-preferences.injectable"; import sentryDnsUrlInjectable from "../../renderer/components/+preferences/sentry-dns-url.injectable"; -import ipcRendererInjectable from "../../renderer/app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable"; describe("preferences - navigation to telemetry preferences", () => { let applicationBuilder: ApplicationBuilder; beforeEach(() => { applicationBuilder = getApplicationBuilder(); - - applicationBuilder.beforeSetups(({ rendererDi }) => { - const userStoreStub = { - extensionRegistryUrl: { customUrl: "some-custom-url" }, - } as unknown as UserStore; - - rendererDi.override(userStoreInjectable, () => userStoreStub); - rendererDi.override(ipcRendererInjectable, () => ({ - on: jest.fn(), - invoke: jest.fn(), // TODO: replace with proper mocking via the IPC bridge - } as never)); - }); }); describe("given in preferences, when rendered", () => { @@ -134,7 +119,7 @@ describe("preferences - navigation to telemetry preferences", () => { let rendered: RenderResult; beforeEach(async () => { - applicationBuilder.beforeSetups(({ rendererDi }) => { + applicationBuilder.beforeApplicationStart(({ rendererDi }) => { rendererDi.override(sentryDnsUrlInjectable, () => "some-sentry-dns-url"); }); @@ -164,7 +149,7 @@ describe("preferences - navigation to telemetry preferences", () => { let rendered: RenderResult; beforeEach(async () => { - applicationBuilder.beforeSetups(({ rendererDi }) => { + applicationBuilder.beforeApplicationStart(({ rendererDi }) => { rendererDi.override(sentryDnsUrlInjectable, () => null); }); diff --git a/src/behaviours/preferences/navigation-to-terminal-preferences.test.ts b/src/behaviours/preferences/navigation-to-terminal-preferences.test.ts index f75de7665c..5d7e1e08fb 100644 --- a/src/behaviours/preferences/navigation-to-terminal-preferences.test.ts +++ b/src/behaviours/preferences/navigation-to-terminal-preferences.test.ts @@ -5,11 +5,7 @@ import type { RenderResult } from "@testing-library/react"; import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; -import userStoreInjectable from "../../common/user-store/user-store.injectable"; -import type { UserStore } from "../../common/user-store"; -import { observable } from "mobx"; import defaultShellInjectable from "../../renderer/components/+preferences/default-shell.injectable"; -import ipcRendererInjectable from "../../renderer/app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable"; describe("preferences - navigation to terminal preferences", () => { let applicationBuilder: ApplicationBuilder; @@ -17,19 +13,8 @@ describe("preferences - navigation to terminal preferences", () => { beforeEach(() => { applicationBuilder = getApplicationBuilder(); - applicationBuilder.beforeSetups(({ rendererDi }) => { - const userStoreStub = { - extensionRegistryUrl: { customUrl: "some-custom-url" }, - syncKubeconfigEntries: observable.map(), - terminalConfig: { fontSize: 42 }, - } as unknown as UserStore; - - rendererDi.override(userStoreInjectable, () => userStoreStub); + applicationBuilder.beforeApplicationStart(({ rendererDi }) => { rendererDi.override(defaultShellInjectable, () => "some-default-shell"); - rendererDi.override(ipcRendererInjectable, () => ({ - on: jest.fn(), - invoke: jest.fn(), // TODO: replace with proper mocking via the IPC bridge - } as never)); }); }); diff --git a/src/behaviours/preferences/navigation-using-application-menu.test.ts b/src/behaviours/preferences/navigation-using-application-menu.test.ts index 1478dd6cd4..e12aa85a44 100644 --- a/src/behaviours/preferences/navigation-using-application-menu.test.ts +++ b/src/behaviours/preferences/navigation-using-application-menu.test.ts @@ -6,10 +6,6 @@ import type { RenderResult } from "@testing-library/react"; import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; -import isAutoUpdateEnabledInjectable from "../../main/is-auto-update-enabled.injectable"; -import type { UserStore } from "../../common/user-store"; -import userStoreInjectable from "../../common/user-store/user-store.injectable"; -import ipcRendererInjectable from "../../renderer/app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable"; describe("preferences - navigation using application menu", () => { let applicationBuilder: ApplicationBuilder; @@ -18,20 +14,6 @@ describe("preferences - navigation using application menu", () => { beforeEach(async () => { applicationBuilder = getApplicationBuilder(); - applicationBuilder.beforeSetups(({ rendererDi, mainDi }) => { - mainDi.override(isAutoUpdateEnabledInjectable, () => () => false); - - const userStoreStub = { - extensionRegistryUrl: { customUrl: "some-custom-url" }, - } as unknown as UserStore; - - rendererDi.override(userStoreInjectable, () => userStoreStub); - rendererDi.override(ipcRendererInjectable, () => ({ - on: jest.fn(), - invoke: jest.fn(), // TODO: replace with proper mocking via the IPC bridge - } as never)); - }); - rendered = await applicationBuilder.render(); }); @@ -46,8 +28,8 @@ describe("preferences - navigation using application menu", () => { }); describe("when navigating to preferences using application menu", () => { - beforeEach(() => { - applicationBuilder.applicationMenu.click("root.preferences"); + beforeEach(async () => { + await applicationBuilder.applicationMenu.click("root.preferences"); }); it("renders", () => { diff --git a/src/behaviours/welcome/navigation-using-application-menu.test.ts b/src/behaviours/welcome/navigation-using-application-menu.test.ts index daab31c40e..e84e5b391f 100644 --- a/src/behaviours/welcome/navigation-using-application-menu.test.ts +++ b/src/behaviours/welcome/navigation-using-application-menu.test.ts @@ -13,7 +13,7 @@ describe("welcome - navigation using application menu", () => { let rendered: RenderResult; beforeEach(async () => { - applicationBuilder = getApplicationBuilder().beforeSetups(({ mainDi }) => { + applicationBuilder = getApplicationBuilder().beforeApplicationStart(({ mainDi }) => { mainDi.override(isAutoUpdateEnabledInjectable, () => () => false); }); @@ -31,8 +31,8 @@ describe("welcome - navigation using application menu", () => { }); describe("when navigating to welcome using application menu", () => { - beforeEach(() => { - applicationBuilder.applicationMenu.click("help.welcome"); + beforeEach(async () => { + await applicationBuilder.applicationMenu.click("help.welcome"); }); it("renders", () => { diff --git a/src/common/__tests__/base-store.test.ts b/src/common/__tests__/base-store.test.ts index 919854042f..e7eb952111 100644 --- a/src/common/__tests__/base-store.test.ts +++ b/src/common/__tests__/base-store.test.ts @@ -81,15 +81,13 @@ class TestStore extends BaseStore { describe("BaseStore", () => { let store: TestStore; - beforeEach(async () => { + beforeEach(() => { const mainDi = getDiForUnitTesting({ doGeneralOverrides: true }); mainDi.override(directoryForUserDataInjectable, () => "some-user-data-directory"); mainDi.permitSideEffects(getConfigurationFileModelInjectable); mainDi.permitSideEffects(appVersionInjectable); - await mainDi.runSetups(); - TestStore.resetInstance(); const mockOpts = { diff --git a/src/common/__tests__/cluster-store.test.ts b/src/common/__tests__/cluster-store.test.ts index 263cb0dd5d..4f836d5566 100644 --- a/src/common/__tests__/cluster-store.test.ts +++ b/src/common/__tests__/cluster-store.test.ts @@ -8,7 +8,7 @@ import mockFs from "mock-fs"; import path from "path"; import fse from "fs-extra"; import type { Cluster } from "../cluster/cluster"; -import { ClusterStore } from "../cluster-store/cluster-store"; +import type { ClusterStore } from "../cluster-store/cluster-store"; import { Console } from "console"; import { stdout, stderr } from "process"; import getCustomKubeConfigDirectoryInjectable from "../app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable"; @@ -21,6 +21,7 @@ import { getDiForUnitTesting } from "../../main/getDiForUnitTesting"; import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable"; import appVersionInjectable from "../get-configuration-file-model/app-version/app-version.injectable"; import assert from "assert"; +import directoryForTempInjectable from "../app-paths/directory-for-temp/directory-for-temp.injectable"; console = new Console(stdout, stderr); @@ -81,15 +82,14 @@ describe("cluster-store", () => { mockFs(); - mainDi.override(clusterStoreInjectable, (di) => ClusterStore.createInstance({ createCluster: di.inject(createClusterInjectionToken) })); mainDi.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); + mainDi.override(directoryForTempInjectable, () => "some-temp-directory"); mainDi.permitSideEffects(getConfigurationFileModelInjectable); mainDi.permitSideEffects(appVersionInjectable); + mainDi.permitSideEffects(clusterStoreInjectable); - await mainDi.runSetups(); - - createCluster = mainDi.inject(createClusterInjectionToken); + mainDi.unoverride(clusterStoreInjectable); }); afterEach(() => { @@ -104,10 +104,6 @@ describe("cluster-store", () => { getCustomKubeConfigDirectoryInjectable, ); - // TODO: Remove these by removing Singleton base-class from BaseStore - ClusterStore.getInstance(false)?.unregisterIpcListener(); - ClusterStore.resetInstance(); - const mockOpts = { "some-directory-for-user-data": { "lens-cluster-store.json": JSON.stringify({}), @@ -116,7 +112,11 @@ describe("cluster-store", () => { mockFs(mockOpts); + createCluster = mainDi.inject(createClusterInjectionToken); + clusterStore = mainDi.inject(clusterStoreInjectable); + + clusterStore.unregisterIpcListener(); }); afterEach(() => { @@ -198,8 +198,6 @@ describe("cluster-store", () => { describe("config with existing clusters", () => { beforeEach(() => { - ClusterStore.resetInstance(); - const mockOpts = { "temp-kube-config": kubeconfig, "some-directory-for-user-data": { @@ -238,6 +236,8 @@ describe("cluster-store", () => { mockFs(mockOpts); + createCluster = mainDi.inject(createClusterInjectionToken); + clusterStore = mainDi.inject(clusterStoreInjectable); }); @@ -288,8 +288,6 @@ users: token: kubeconfig-user-q4lm4:xxxyyyy `; - ClusterStore.resetInstance(); - const mockOpts = { "invalid-kube-config": invalidKubeconfig, "valid-kube-config": kubeconfig, @@ -322,6 +320,8 @@ users: mockFs(mockOpts); + createCluster = mainDi.inject(createClusterInjectionToken); + clusterStore = mainDi.inject(clusterStoreInjectable); }); @@ -338,7 +338,6 @@ users: describe("pre 3.6.0-beta.1 config with an existing cluster", () => { beforeEach(() => { - ClusterStore.resetInstance(); const mockOpts = { "some-directory-for-user-data": { "lens-cluster-store.json": JSON.stringify({ @@ -364,6 +363,8 @@ users: mockFs(mockOpts); + createCluster = mainDi.inject(createClusterInjectionToken); + clusterStore = mainDi.inject(clusterStoreInjectable); }); diff --git a/src/common/__tests__/hotbar-store.test.ts b/src/common/__tests__/hotbar-store.test.ts index 93563c97ee..cc8a4dc8fa 100644 --- a/src/common/__tests__/hotbar-store.test.ts +++ b/src/common/__tests__/hotbar-store.test.ts @@ -18,8 +18,7 @@ import hasCategoryForEntityInjectable from "../catalog/has-category-for-entity.i import catalogCatalogEntityInjectable from "../catalog-entities/general-catalog-entities/implementations/catalog-catalog-entity.injectable"; import loggerInjectable from "../logger.injectable"; import type { Logger } from "../logger"; - -console.log("I am here as reminder against mockfs (and to fix console logging)"); +import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable"; function getMockCatalogEntity(data: Partial & CatalogEntityKindData): CatalogEntity { return { @@ -101,6 +100,8 @@ describe("HotbarStore", () => { di.override(loggerInjectable, () => loggerMock); + di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); + const catalogEntityRegistry = di.inject(catalogEntityRegistryInjectable); const catalogCatalogEntity = di.inject(catalogCatalogEntityInjectable); @@ -121,12 +122,12 @@ describe("HotbarStore", () => { }); describe("given no previous data in store, running all migrations", () => { - beforeEach(async () => { + beforeEach(() => { mockFs(); - await di.runSetups(); - hotbarStore = di.inject(hotbarStoreInjectable); + + hotbarStore.load(); }); describe("load", () => { @@ -283,9 +284,9 @@ describe("HotbarStore", () => { }); describe("given data from 5.0.0-beta.3 and version being 5.0.0-beta.10", () => { - beforeEach(async () => { + beforeEach(() => { const configurationToBeMigrated = { - "some-electron-app-path-for-user-data": { + "some-directory-for-user-data": { "lens-hotbar-store.json": JSON.stringify({ __internal__: { migrations: { @@ -350,9 +351,9 @@ describe("HotbarStore", () => { di.override(appVersionInjectable, () => "5.0.0-beta.10"); - await di.runSetups(); - hotbarStore = di.inject(hotbarStoreInjectable); + + hotbarStore.load(); }); it("allows to retrieve a hotbar", () => { diff --git a/src/common/__tests__/user-store.test.ts b/src/common/__tests__/user-store.test.ts index 6f11085359..042b6363f7 100644 --- a/src/common/__tests__/user-store.test.ts +++ b/src/common/__tests__/user-store.test.ts @@ -33,10 +33,8 @@ import type { ClusterStoreModel } from "../cluster-store/cluster-store"; import { defaultThemeId } from "../vars"; import writeFileInjectable from "../fs/write-file.injectable"; import { getDiForUnitTesting } from "../../main/getDiForUnitTesting"; -import getConfigurationFileModelInjectable - from "../get-configuration-file-model/get-configuration-file-model.injectable"; -import appVersionInjectable - from "../get-configuration-file-model/app-version/app-version.injectable"; +import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable"; +import appVersionInjectable from "../get-configuration-file-model/app-version/app-version.injectable"; console = new Console(stdout, stderr); @@ -44,7 +42,7 @@ describe("user store tests", () => { let userStore: UserStore; let di: DiContainer; - beforeEach(async () => { + beforeEach(() => { di = getDiForUnitTesting({ doGeneralOverrides: true }); mockFs(); @@ -55,8 +53,6 @@ describe("user store tests", () => { di.permitSideEffects(getConfigurationFileModelInjectable); di.permitSideEffects(appVersionInjectable); - - await di.runSetups(); }); afterEach(() => { diff --git a/src/common/app-event-bus/app-event-bus.injectable.ts b/src/common/app-event-bus/app-event-bus.injectable.ts index 8ba0412ba4..31ed3dd3a1 100644 --- a/src/common/app-event-bus/app-event-bus.injectable.ts +++ b/src/common/app-event-bus/app-event-bus.injectable.ts @@ -8,6 +8,7 @@ import { appEventBus } from "./event-bus"; const appEventBusInjectable = getInjectable({ id: "app-event-bus", instantiate: () => appEventBus, + causesSideEffects: true, }); export default appEventBusInjectable; diff --git a/src/common/app-paths/app-paths-state.injectable.ts b/src/common/app-paths/app-paths-state.injectable.ts new file mode 100644 index 0000000000..5487d428b2 --- /dev/null +++ b/src/common/app-paths/app-paths-state.injectable.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import type { AppPaths } from "./app-path-injection-token"; + +const appPathsStateInjectable = getInjectable({ + id: "app-paths-state", + + instantiate: () => { + let state: AppPaths; + + return { + get: () =>{ + if (!state) { + throw new Error("Tried to get app paths before state is setupped."); + } + + return state; + }, + + set: (newState: AppPaths) => { + if (state) { + throw new Error("Tried to overwrite existing state of app paths."); + } + + state = newState; + }, + }; + }, +}); + +export default appPathsStateInjectable; diff --git a/src/common/app-paths/app-paths.injectable.ts b/src/common/app-paths/app-paths.injectable.ts new file mode 100644 index 0000000000..803f9e1380 --- /dev/null +++ b/src/common/app-paths/app-paths.injectable.ts @@ -0,0 +1,15 @@ +/** + * 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 { appPathsInjectionToken } from "./app-path-injection-token"; +import appPathsStateInjectable from "./app-paths-state.injectable"; + +const appPathsInjectable = getInjectable({ + id: "app-paths", + instantiate: (di) => di.inject(appPathsStateInjectable).get(), + injectionToken: appPathsInjectionToken, +}); + +export default appPathsInjectable; diff --git a/src/common/app-paths/app-paths.test.ts b/src/common/app-paths/app-paths.test.ts index b436426e2e..5793295e0e 100644 --- a/src/common/app-paths/app-paths.test.ts +++ b/src/common/app-paths/app-paths.test.ts @@ -2,27 +2,27 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { DiContainer } from "@ogre-tools/injectable"; import type { AppPaths } from "./app-path-injection-token"; import { appPathsInjectionToken } from "./app-path-injection-token"; import getElectronAppPathInjectable from "../../main/app-paths/get-electron-app-path/get-electron-app-path.injectable"; -import { getDisForUnitTesting } from "../../test-utils/get-dis-for-unit-testing"; import type { PathName } from "./app-path-names"; import setElectronAppPathInjectable from "../../main/app-paths/set-electron-app-path/set-electron-app-path.injectable"; import appNameInjectable from "../../main/app-paths/app-name/app-name.injectable"; import directoryForIntegrationTestingInjectable from "../../main/app-paths/directory-for-integration-testing/directory-for-integration-testing.injectable"; +import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; +import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; +import type { DiContainer } from "@ogre-tools/injectable"; describe("app-paths", () => { - let mainDi: DiContainer; + let applicationBuilder: ApplicationBuilder; let rendererDi: DiContainer; - let runSetups: () => Promise; + let mainDi: DiContainer; beforeEach(() => { - const dis = getDisForUnitTesting({ doGeneralOverrides: true }); + applicationBuilder = getApplicationBuilder(); - mainDi = dis.mainDi; - rendererDi = dis.rendererDi; - runSetups = dis.runSetups; + rendererDi = applicationBuilder.dis.rendererDi; + mainDi = applicationBuilder.dis.mainDi; const defaultAppPathsStub: AppPaths = { appData: "some-app-data", @@ -40,30 +40,32 @@ describe("app-paths", () => { recent: "some-recent", temp: "some-temp", videos: "some-videos", - userData: "some-irrelevant", + userData: "some-irrelevant-user-data", }; - mainDi.override( - getElectronAppPathInjectable, - () => - (key: PathName): string | null => - defaultAppPathsStub[key], - ); + applicationBuilder.beforeApplicationStart(({ mainDi }) => { + mainDi.override( + getElectronAppPathInjectable, + () => + (key: PathName): string | null => + defaultAppPathsStub[key], + ); - mainDi.override( - setElectronAppPathInjectable, - () => - (key: PathName, path: string): void => { - defaultAppPathsStub[key] = path; - }, - ); + mainDi.override( + setElectronAppPathInjectable, + () => + (key: PathName, path: string): void => { + defaultAppPathsStub[key] = path; + }, + ); - mainDi.override(appNameInjectable, () => "some-app-name"); + mainDi.override(appNameInjectable, () => "some-app-name"); + }); }); describe("normally", () => { beforeEach(async () => { - await runSetups(); + await applicationBuilder.render(); }); it("given in renderer, when injecting app paths, returns application specific app paths", () => { @@ -115,12 +117,14 @@ describe("app-paths", () => { describe("when running integration tests", () => { beforeEach(async () => { - mainDi.override( - directoryForIntegrationTestingInjectable, - () => "some-integration-testing-app-data", - ); + applicationBuilder.beforeApplicationStart(({ mainDi }) => { + mainDi.override( + directoryForIntegrationTestingInjectable, + () => "some-integration-testing-app-data", + ); + }); - await runSetups(); + await applicationBuilder.render(); }); it("given in renderer, when injecting path for app data, has integration specific app data path", () => { diff --git a/src/common/catalog/has-category-for-entity.injectable.ts b/src/common/catalog/has-category-for-entity.injectable.ts index 97084ec96d..cff7d720c1 100644 --- a/src/common/catalog/has-category-for-entity.injectable.ts +++ b/src/common/catalog/has-category-for-entity.injectable.ts @@ -10,6 +10,7 @@ export type HasCategoryForEntity = (data: CatalogEntityData & CatalogEntityKindD const hasCategoryForEntityInjectable = getInjectable({ id: "has-category-for-entity", + instantiate: (di): HasCategoryForEntity => { const registry = di.inject(catalogCategoryRegistryInjectable); diff --git a/src/renderer/components/cluster-manager/lens-views.injectable.ts b/src/common/cluster-frames.injectable.ts similarity index 73% rename from src/renderer/components/cluster-manager/lens-views.injectable.ts rename to src/common/cluster-frames.injectable.ts index 18c30725fb..23897012a0 100644 --- a/src/renderer/components/cluster-manager/lens-views.injectable.ts +++ b/src/common/cluster-frames.injectable.ts @@ -3,11 +3,12 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import { ClusterFrameHandler } from "./lens-views"; +import { clusterFrameMap } from "./cluster-frames"; const clusterFramesInjectable = getInjectable({ id: "cluster-frames", - instantiate: () => new ClusterFrameHandler(), + instantiate: () => clusterFrameMap, + causesSideEffects: true, }); export default clusterFramesInjectable; diff --git a/src/common/cluster-store/cluster-store.injectable.ts b/src/common/cluster-store/cluster-store.injectable.ts index a47978376a..58b540495a 100644 --- a/src/common/cluster-store/cluster-store.injectable.ts +++ b/src/common/cluster-store/cluster-store.injectable.ts @@ -9,10 +9,13 @@ import { createClusterInjectionToken } from "../cluster/create-cluster-injection const clusterStoreInjectable = getInjectable({ id: "cluster-store", - instantiate: (di) => - ClusterStore.createInstance({ + instantiate: (di) => { + ClusterStore.resetInstance(); + + return ClusterStore.createInstance({ createCluster: di.inject(createClusterInjectionToken), - }), + }); + }, causesSideEffects: true, }); diff --git a/src/common/cluster/cluster.ts b/src/common/cluster/cluster.ts index e987f4c505..1652e18ef1 100644 --- a/src/common/cluster/cluster.ts +++ b/src/common/cluster/cluster.ts @@ -13,8 +13,8 @@ import type { KubeconfigManager } from "../../main/kubeconfig-manager/kubeconfig import { loadConfigFromFile, loadConfigFromFileSync, validateKubeConfig } from "../kube-helpers"; import type { KubeApiResource, KubeResource } from "../rbac"; import { apiResourceRecord, apiResources } from "../rbac"; -import { VersionDetector } from "../../main/cluster-detectors/version-detector"; -import { DetectorRegistry } from "../../main/cluster-detectors/detector-registry"; +import type { VersionDetector } from "../../main/cluster-detectors/version-detector"; +import type { DetectorRegistry } from "../../main/cluster-detectors/detector-registry"; import plimit from "p-limit"; import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate } from "../cluster-types"; import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "../cluster-types"; @@ -29,11 +29,13 @@ import type { Logger } from "../logger"; export interface ClusterDependencies { readonly directoryForKubeConfigs: string; readonly logger: Logger; - createKubeconfigManager: (cluster: Cluster) => KubeconfigManager | undefined; - createContextHandler: (cluster: Cluster) => ClusterContextHandler | undefined; + readonly detectorRegistry: DetectorRegistry; + createKubeconfigManager: (cluster: Cluster) => KubeconfigManager; + createContextHandler: (cluster: Cluster) => ClusterContextHandler; createKubectl: (clusterVersion: string) => Kubectl; createAuthorizationReview: (config: KubeConfig) => CanI; createListNamespaces: (config: KubeConfig) => ListNamespaces; + createVersionDetector: (cluster: Cluster) => VersionDetector; } /** @@ -441,7 +443,7 @@ export class Cluster implements ClusterModel, ClusterState { @action async refreshMetadata() { this.dependencies.logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta()); - const metadata = await DetectorRegistry.getInstance().detectForCluster(this); + const metadata = await this.dependencies.detectorRegistry.detectForCluster(this); const existingMetadata = this.metadata; this.metadata = Object.assign(existingMetadata, metadata); @@ -504,7 +506,7 @@ export class Cluster implements ClusterModel, ClusterState { protected async getConnectionStatus(): Promise { try { - const versionDetector = new VersionDetector(this); + const versionDetector = this.dependencies.createVersionDetector(this); const versionData = await versionDetector.detect(); this.metadata.version = versionData.value; diff --git a/src/common/front-end-routing/verify-that-all-routes-have-route-component.test.ts b/src/common/front-end-routing/verify-that-all-routes-have-route-component.test.ts index fbeaf90408..369fb6eb8c 100644 --- a/src/common/front-end-routing/verify-that-all-routes-have-route-component.test.ts +++ b/src/common/front-end-routing/verify-that-all-routes-have-route-component.test.ts @@ -11,7 +11,7 @@ import type { ClusterStore } from "../cluster-store/cluster-store"; import { pipeline } from "@ogre-tools/fp"; describe("verify-that-all-routes-have-component", () => { - it("verify that routes have route component", async () => { + it("verify that routes have route component", () => { const rendererDi = getDiForUnitTesting({ doGeneralOverrides: true }); rendererDi.override(clusterStoreInjectable, () => ({ diff --git a/src/common/fs/ensure-dir.injectable.ts b/src/common/fs/ensure-dir.injectable.ts new file mode 100644 index 0000000000..88410ceee2 --- /dev/null +++ b/src/common/fs/ensure-dir.injectable.ts @@ -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 fsInjectable from "./fs.injectable"; + +const ensureDirInjectable = getInjectable({ + id: "ensure-dir", + + // TODO: Remove usages of ensureDir from business logic. + // TODO: Read, Write, Watch etc. operations should do this internally. + instantiate: (di) => di.inject(fsInjectable).ensureDir, + + causesSideEffects: true, +}); + +export default ensureDirInjectable; diff --git a/src/common/hotbars/store.ts b/src/common/hotbars/store.ts index 7cf7633f02..a75182b23b 100644 --- a/src/common/hotbars/store.ts +++ b/src/common/hotbars/store.ts @@ -40,8 +40,8 @@ export class HotbarStore extends BaseStore { }, migrations, }); + makeObservable(this); - this.load(); } @computed get activeHotbarId() { diff --git a/src/common/ipc/broadcast-message.injectable.ts b/src/common/ipc/broadcast-message.injectable.ts new file mode 100644 index 0000000000..9df36ac27a --- /dev/null +++ b/src/common/ipc/broadcast-message.injectable.ts @@ -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 { broadcastMessage } from "./ipc"; + +const broadcastMessageInjectable = getInjectable({ + id: "broadcast-message", + instantiate: () => broadcastMessage, + causesSideEffects: true, +}); + +export default broadcastMessageInjectable; diff --git a/src/common/k8s-api/__tests__/kube-api.test.ts b/src/common/k8s-api/__tests__/kube-api.test.ts index 05c9e22e09..9efa23bdaf 100644 --- a/src/common/k8s-api/__tests__/kube-api.test.ts +++ b/src/common/k8s-api/__tests__/kube-api.test.ts @@ -23,11 +23,9 @@ const mockFetch = fetch as FetchMock; describe("forRemoteCluster", () => { let apiManager: jest.Mocked; - beforeEach(async () => { + beforeEach(() => { const di = getDiForUnitTesting({ doGeneralOverrides: true }); - await di.runSetups(); - apiManager = new ApiManager() as jest.Mocked; di.override(apiManagerInjectable, () => apiManager); @@ -87,11 +85,9 @@ describe("KubeApi", () => { let request: KubeJsonApi; let apiManager: jest.Mocked; - beforeEach(async () => { + beforeEach(() => { const di = getDiForUnitTesting({ doGeneralOverrides: true }); - await di.runSetups(); - request = new KubeJsonApi({ serverAddress: `http://127.0.0.1:9999`, apiBase: "/api-kube", diff --git a/src/common/runnable/run-many-for.test.ts b/src/common/runnable/run-many-for.test.ts new file mode 100644 index 0000000000..193c1fd178 --- /dev/null +++ b/src/common/runnable/run-many-for.test.ts @@ -0,0 +1,267 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { AsyncFnMock } from "@async-fn/jest"; +import asyncFn from "@async-fn/jest"; +import { createContainer, getInjectable, getInjectionToken } from "@ogre-tools/injectable"; +import type { Runnable } from "./run-many-for"; +import { runManyFor } from "./run-many-for"; +import { getPromiseStatus } from "../test-utils/get-promise-status"; + +describe("runManyFor", () => { + describe("given no hierarchy, when running many", () => { + let runMock: AsyncFnMock<(...args: unknown[]) => Promise>; + let actualPromise: Promise; + + beforeEach(() => { + const rootDi = createContainer(); + + runMock = asyncFn(); + + const someInjectionTokenForRunnables = getInjectionToken({ + id: "some-injection-token", + }); + + const someInjectable = getInjectable({ + id: "some-injectable", + instantiate: () => ({ run: () => runMock("some-call") }), + injectionToken: someInjectionTokenForRunnables, + }); + + const someOtherInjectable = getInjectable({ + id: "some-other-injectable", + instantiate: () => ({ run: () => runMock("some-other-call") }), + injectionToken: someInjectionTokenForRunnables, + }); + + rootDi.register(someInjectable, someOtherInjectable); + + const runMany = runManyFor(rootDi)(someInjectionTokenForRunnables); + + actualPromise = runMany() as Promise; + }); + + it("runs all runnables at the same time", () => { + expect(runMock.mock.calls).toEqual([ + ["some-call"], + ["some-other-call"], + ]); + }); + + it("does not resolve yet", async () => { + const promiseStatus = await getPromiseStatus(actualPromise); + + expect(promiseStatus.fulfilled).toBe(false); + }); + + it("when all runnables resolve, resolves", async () => { + await Promise.all([runMock.resolve(), runMock.resolve()]); + + expect(await actualPromise).toBe(undefined); + }); + }); + + describe("given hierarchy that is three levels deep, when running many", () => { + let runMock: AsyncFnMock<(...args: unknown[]) => Promise>; + let actualPromise: Promise; + + beforeEach(() => { + const di = createContainer(); + + runMock = asyncFn(); + + const someInjectionTokenForRunnables = getInjectionToken({ + id: "some-injection-token", + }); + + const someInjectable1 = getInjectable({ + id: "some-injectable-1", + + instantiate: (di) => ({ + run: () => runMock("third-level-run"), + runAfter: di.inject(someInjectable2), + }), + + injectionToken: someInjectionTokenForRunnables, + }); + + const someInjectable2 = getInjectable({ + id: "some-injectable-2", + + instantiate: (di) => ({ + run: () => runMock("second-level-run"), + runAfter: di.inject(someInjectable3), + }), + + injectionToken: someInjectionTokenForRunnables, + }); + + const someInjectable3 = getInjectable({ + id: "some-injectable-3", + instantiate: () => ({ run: () => runMock("first-level-run") }), + injectionToken: someInjectionTokenForRunnables, + }); + + di.register(someInjectable1, someInjectable2, someInjectable3); + + const runMany = runManyFor(di)(someInjectionTokenForRunnables); + + actualPromise = runMany() as Promise; + }); + + it("runs first level runnables", () => { + expect(runMock.mock.calls).toEqual([["first-level-run"]]); + }); + + it("does not resolve yet", async () => { + const promiseStatus = await getPromiseStatus(actualPromise); + + expect(promiseStatus.fulfilled).toBe(false); + }); + + describe("when first level runnables resolve", () => { + beforeEach(async () => { + runMock.mockClear(); + + await runMock.resolveSpecific(["first-level-run"]); + }); + + it("runs second level runnables", async () => { + expect(runMock.mock.calls).toEqual([["second-level-run"]]); + }); + + it("does not resolve yet", async () => { + const promiseStatus = await getPromiseStatus(actualPromise); + + expect(promiseStatus.fulfilled).toBe(false); + }); + + describe("when second level runnables resolve", () => { + beforeEach(async () => { + runMock.mockClear(); + + await runMock.resolveSpecific(["second-level-run"]); + }); + + it("runs final third level runnables", async () => { + expect(runMock.mock.calls).toEqual([["third-level-run"]]); + }); + + it("does not resolve yet", async () => { + const promiseStatus = await getPromiseStatus(actualPromise); + + expect(promiseStatus.fulfilled).toBe(false); + }); + + describe("when final third level runnables resolve", () => { + beforeEach(async () => { + await runMock.resolveSpecific(["third-level-run"]); + }); + + it("resolves", async () => { + const promiseStatus = await getPromiseStatus(actualPromise); + + expect(promiseStatus.fulfilled).toBe(true); + }); + }); + }); + }); + }); + + it("given invalid hierarchy, when running runnables, throws", () => { + const rootDi = createContainer(); + + const runMock = asyncFn<(...args: unknown[]) => void>(); + + const someInjectionToken = getInjectionToken({ + id: "some-injection-token", + }); + + const someOtherInjectionToken = getInjectionToken({ + id: "some-other-injection-token", + }); + + const someInjectable = getInjectable({ + id: "some-runnable-1", + + instantiate: (di) => ({ + run: () => runMock("some-runnable-1"), + runAfter: di.inject(someOtherInjectable), + }), + + injectionToken: someInjectionToken, + }); + + const someOtherInjectable = getInjectable({ + id: "some-runnable-2", + + instantiate: () => ({ + run: () => runMock("some-runnable-2"), + }), + + injectionToken: someOtherInjectionToken, + }); + + rootDi.register(someInjectable, someOtherInjectable); + + const runMany = runManyFor(rootDi)( + someInjectionToken, + ); + + return expect(() => runMany()).rejects.toThrow( + "Tried to run runnable after other runnable which does not same injection token.", + ); + }); + + describe("when running many with parameter", () => { + let runMock: AsyncFnMock<(...args: unknown[]) => Promise>; + + beforeEach(() => { + const rootDi = createContainer(); + + runMock = asyncFn(); + + const someInjectionTokenForRunnablesWithParameter = getInjectionToken< + Runnable + >({ + id: "some-injection-token", + }); + + const someInjectable = getInjectable({ + id: "some-runnable-1", + + instantiate: () => ({ + run: (parameter) => runMock("run-of-some-runnable-1", parameter), + }), + + injectionToken: someInjectionTokenForRunnablesWithParameter, + }); + + const someOtherInjectable = getInjectable({ + id: "some-runnable-2", + + instantiate: () => ({ + run: (parameter) => runMock("run-of-some-runnable-2", parameter), + }), + + injectionToken: someInjectionTokenForRunnablesWithParameter, + }); + + rootDi.register(someInjectable, someOtherInjectable); + + const runMany = runManyFor(rootDi)( + someInjectionTokenForRunnablesWithParameter, + ); + + runMany("some-parameter"); + }); + + it("runs all runnables using the parameter", () => { + expect(runMock.mock.calls).toEqual([ + ["run-of-some-runnable-1", "some-parameter"], + ["run-of-some-runnable-2", "some-parameter"], + ]); + }); + }); +}); diff --git a/src/common/runnable/run-many-for.ts b/src/common/runnable/run-many-for.ts new file mode 100644 index 0000000000..f2c1a4ae56 --- /dev/null +++ b/src/common/runnable/run-many-for.ts @@ -0,0 +1,51 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { pipeline } from "@ogre-tools/fp"; +import type { + DiContainerForInjection, + InjectionToken, +} from "@ogre-tools/injectable"; +import { filter, forEach, map, tap } from "lodash/fp"; +import { throwWithIncorrectHierarchyFor } from "./throw-with-incorrect-hierarchy-for"; + +export interface Runnable { + run: Run; + runAfter?: this; +} + +type Run = (parameter: Param) => Promise | void; + +export type RunMany = ( + injectionToken: InjectionToken, void> +) => Run; + +export function runManyFor(di: DiContainerForInjection): RunMany { + return (injectionToken) => async (parameter) => { + const allRunnables = di.injectMany(injectionToken); + + const throwWithIncorrectHierarchy = throwWithIncorrectHierarchyFor(allRunnables); + + const recursedRun = async ( + runAfterRunnable: Runnable | undefined = undefined, + ) => + await pipeline( + allRunnables, + + tap(runnables => forEach(throwWithIncorrectHierarchy, runnables)), + + filter((runnable) => runnable.runAfter === runAfterRunnable), + + map(async (runnable) => { + await runnable.run(parameter); + + await recursedRun(runnable); + }), + + (promises) => Promise.all(promises), + ); + + await recursedRun(); + }; +} diff --git a/src/common/runnable/run-many-sync-for.test.ts b/src/common/runnable/run-many-sync-for.test.ts new file mode 100644 index 0000000000..215b1a3b15 --- /dev/null +++ b/src/common/runnable/run-many-sync-for.test.ts @@ -0,0 +1,197 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { createContainer, getInjectable, getInjectionToken } from "@ogre-tools/injectable"; +import type { RunnableSync } from "./run-many-sync-for"; +import { runManySyncFor } from "./run-many-sync-for"; + +describe("runManySyncFor", () => { + describe("given hierarchy, when running many", () => { + let runMock: jest.Mock; + + beforeEach(() => { + const rootDi = createContainer(); + + runMock = jest.fn(); + + const someInjectionTokenForRunnables = getInjectionToken({ + id: "some-injection-token", + }); + + const someInjectable = getInjectable({ + id: "some-injectable", + instantiate: () => ({ run: () => runMock("some-call") }), + injectionToken: someInjectionTokenForRunnables, + }); + + const someOtherInjectable = getInjectable({ + id: "some-other-injectable", + instantiate: () => ({ run: () => runMock("some-other-call") }), + injectionToken: someInjectionTokenForRunnables, + }); + + rootDi.register(someInjectable, someOtherInjectable); + + const runMany = runManySyncFor(rootDi)(someInjectionTokenForRunnables); + + runMany(); + }); + + it("runs all runnables at the same time", () => { + expect(runMock.mock.calls).toEqual([ + ["some-call"], + ["some-other-call"], + ]); + }); + }); + + describe("given hierarchy that is three levels deep, when running many", () => { + let runMock: jest.Mock<(arg: string) => void>; + + beforeEach(() => { + const di = createContainer(); + + runMock = jest.fn(); + + const someInjectionTokenForRunnables = getInjectionToken({ + id: "some-injection-token", + }); + + const someInjectable1 = getInjectable({ + id: "some-injectable-1", + + instantiate: (di) => ({ + run: () => runMock("third-level-run"), + runAfter: di.inject(someInjectable2), + }), + + injectionToken: someInjectionTokenForRunnables, + }); + + const someInjectable2 = getInjectable({ + id: "some-injectable-2", + + instantiate: (di) => ({ + run: () => runMock("second-level-run"), + runAfter: di.inject(someInjectable3), + }), + + injectionToken: someInjectionTokenForRunnables, + }); + + const someInjectable3 = getInjectable({ + id: "some-injectable-3", + instantiate: () => ({ run: () => runMock("first-level-run") }), + injectionToken: someInjectionTokenForRunnables, + }); + + di.register(someInjectable1, someInjectable2, someInjectable3); + + const runMany = runManySyncFor(di)(someInjectionTokenForRunnables); + + runMany(); + }); + + it("runs runnables in order", () => { + expect(runMock.mock.calls).toEqual([["first-level-run"], ["second-level-run"], ["third-level-run"]]); + }); + }); + + it("given invalid hierarchy, when running runnables, throws", () => { + const rootDi = createContainer(); + + const runMock = jest.fn(); + + const someInjectionToken = getInjectionToken({ + id: "some-injection-token", + }); + + const someOtherInjectionToken = getInjectionToken({ + id: "some-other-injection-token", + }); + + const someInjectable = getInjectable({ + id: "some-runnable-1", + + instantiate: (di) => ({ + run: () => runMock("some-runnable-1"), + runAfter: di.inject(someOtherInjectable), + }), + + injectionToken: someInjectionToken, + }); + + const someOtherInjectable = getInjectable({ + id: "some-runnable-2", + + instantiate: () => ({ + run: () => runMock("some-runnable-2"), + }), + + injectionToken: someOtherInjectionToken, + }); + + rootDi.register(someInjectable, someOtherInjectable); + + const runMany = runManySyncFor(rootDi)( + someInjectionToken, + ); + + return expect(() => runMany()).rejects.toThrow( + "Tried to run runnable after other runnable which does not same injection token.", + ); + }); + + describe("when running many with parameter", () => { + let runMock: jest.Mock<(arg: string, arg2: string) => void>; + + beforeEach(() => { + const rootDi = createContainer(); + + runMock = jest.fn(); + + const someInjectionTokenForRunnablesWithParameter = getInjectionToken< + RunnableSync + >({ + id: "some-injection-token", + }); + + const someInjectable = getInjectable({ + id: "some-runnable-1", + + instantiate: () => ({ + run: (parameter) => runMock("run-of-some-runnable-1", parameter), + }), + + injectionToken: someInjectionTokenForRunnablesWithParameter, + }); + + const someOtherInjectable = getInjectable({ + id: "some-runnable-2", + + instantiate: () => ({ + run: (parameter) => runMock("run-of-some-runnable-2", parameter), + }), + + injectionToken: someInjectionTokenForRunnablesWithParameter, + }); + + rootDi.register(someInjectable, someOtherInjectable); + + const runMany = runManySyncFor(rootDi)( + someInjectionTokenForRunnablesWithParameter, + ); + + runMany("some-parameter"); + }); + + it("runs all runnables using the parameter", () => { + expect(runMock.mock.calls).toEqual([ + ["run-of-some-runnable-1", "some-parameter"], + ["run-of-some-runnable-2", "some-parameter"], + ]); + }); + }); +}); + diff --git a/src/common/runnable/run-many-sync-for.ts b/src/common/runnable/run-many-sync-for.ts new file mode 100644 index 0000000000..cfe93fa4b3 --- /dev/null +++ b/src/common/runnable/run-many-sync-for.ts @@ -0,0 +1,50 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { pipeline } from "@ogre-tools/fp"; +import type { + DiContainerForInjection, + InjectionToken, +} from "@ogre-tools/injectable"; +import { filter, forEach, map, tap } from "lodash/fp"; +import type { Runnable } from "./run-many-for"; +import { throwWithIncorrectHierarchyFor } from "./throw-with-incorrect-hierarchy-for"; + +export interface RunnableSync { + run: RunSync; + runAfter?: this; +} + +type RunSync = (parameter: Param) => void; + +export type RunManySync = ( + injectionToken: InjectionToken, void> +) => RunSync; + +export function runManySyncFor(di: DiContainerForInjection): RunManySync { + return (injectionToken) => async (parameter) => { + const allRunnables = di.injectMany(injectionToken); + + const throwWithIncorrectHierarchy = throwWithIncorrectHierarchyFor(allRunnables); + + const recursedRun = ( + runAfterRunnable: RunnableSync | undefined = undefined, + ) => + pipeline( + allRunnables, + + tap(runnables => forEach(throwWithIncorrectHierarchy, runnables)), + + filter((runnable) => runnable.runAfter === runAfterRunnable), + + map((runnable) => { + runnable.run(parameter); + + recursedRun(runnable); + }), + ); + + recursedRun(); + }; +} diff --git a/src/common/runnable/throw-with-incorrect-hierarchy-for.ts b/src/common/runnable/throw-with-incorrect-hierarchy-for.ts new file mode 100644 index 0000000000..03073c4044 --- /dev/null +++ b/src/common/runnable/throw-with-incorrect-hierarchy-for.ts @@ -0,0 +1,16 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { Runnable } from "./run-many-for"; +import type { RunnableSync } from "./run-many-sync-for"; + +export const throwWithIncorrectHierarchyFor = + (allRunnables: Runnable[] | RunnableSync[]) => + (runnable: Runnable | RunnableSync) => { + if (runnable.runAfter && !allRunnables.includes(runnable.runAfter)) { + throw new Error( + "Tried to run runnable after other runnable which does not same injection token.", + ); + } + }; diff --git a/src/common/test-utils/flush-promises.ts b/src/common/test-utils/flush-promises.ts new file mode 100644 index 0000000000..55335fe445 --- /dev/null +++ b/src/common/test-utils/flush-promises.ts @@ -0,0 +1,5 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +export const flushPromises = () => new Promise(setImmediate); diff --git a/src/common/test-utils/get-promise-status.ts b/src/common/test-utils/get-promise-status.ts new file mode 100644 index 0000000000..8c171fbe54 --- /dev/null +++ b/src/common/test-utils/get-promise-status.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { flushPromises } from "./flush-promises"; + +export const getPromiseStatus = async (promise: Promise) => { + const status = { fulfilled: false }; + + promise.finally(() => { + status.fulfilled = true; + }); + + await flushPromises(); + + return status; +}; diff --git a/src/common/utils/environment-variables.injectable.ts b/src/common/utils/environment-variables.injectable.ts new file mode 100644 index 0000000000..897b349d56 --- /dev/null +++ b/src/common/utils/environment-variables.injectable.ts @@ -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"; + +const environmentVariablesInjectable = getInjectable({ + id: "environment-variables", + + instantiate: () => { + // IMPORTANT: The syntax needs to be exactly this in order to make environment variable values + // hard-coded at compile-time by Webpack. + const NODE_ENV = process.env.NODE_ENV; + const JEST_WORKER_ID = process.env.JEST_WORKER_ID; + const CICD = process.env.CICD; + + return { + // Compile-time environment variables + NODE_ENV, + JEST_WORKER_ID, + CICD, + + // Runtime environment variables + LENS_DISABLE_GPU: process.env.LENS_DISABLE_GPU, + }; + }, + + causesSideEffects: true, +}); + +export default environmentVariablesInjectable; diff --git a/src/common/utils/get-startable-stoppable.test.ts b/src/common/utils/get-startable-stoppable.test.ts new file mode 100644 index 0000000000..24b9e1862d --- /dev/null +++ b/src/common/utils/get-startable-stoppable.test.ts @@ -0,0 +1,235 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import type { AsyncFnMock } from "@async-fn/jest"; +import asyncFn from "@async-fn/jest"; +import { getStartableStoppable } from "./get-startable-stoppable"; +import { getPromiseStatus } from "../test-utils/get-promise-status"; +import { flushPromises } from "../test-utils/flush-promises"; + +describe("getStartableStoppable", () => { + let stopMock: AsyncFnMock<() => Promise>; + let startMock: AsyncFnMock<() => Promise<() => Promise>>; + let actual: { stop: () => Promise; start: () => Promise; started: boolean }; + + beforeEach(() => { + stopMock = asyncFn(); + startMock = asyncFn(); + + actual = getStartableStoppable("some-id", startMock); + }); + + it("does not start yet", () => { + expect(startMock).not.toHaveBeenCalled(); + }); + + it("does not stop yet", () => { + expect(stopMock).not.toHaveBeenCalled(); + }); + + it("when stopping before ever starting, throws", () => { + expect(actual.stop).rejects.toThrow("Tried to stop \"some-id\", but it has not started yet."); + }); + + it("is not started", () => { + expect(actual.started).toBe(false); + }); + + describe("when started", () => { + let startPromise: Promise; + + beforeEach(() => { + startPromise = actual.start(); + }); + + it("starts starting", () => { + expect(startMock).toHaveBeenCalled(); + }); + + it("starting does not resolve yet", async () => { + const promiseStatus = await getPromiseStatus(startPromise); + + expect(promiseStatus.fulfilled).toBe(false); + }); + + it("is not started yet", () => { + expect(actual.started).toBe(false); + }); + + describe("when started again before the start has finished", () => { + let error: Error; + + beforeEach(() => { + startMock.mockClear(); + + actual.start().catch((e) => { error = e; }); + }); + + it("does not start starting again", () => { + expect(startMock).not.toHaveBeenCalled(); + }); + + it("throws", () => { + expect(error.message).toBe("Tried to start \"some-id\", but it is already being started."); + }); + }); + + describe("when starting finishes", () => { + beforeEach(async () => { + await startMock.resolve(stopMock); + }); + + it("is started", () => { + expect(actual.started).toBe(true); + }); + + it("starting resolves", async () => { + const promiseStatus = await getPromiseStatus(startPromise); + + expect(promiseStatus.fulfilled).toBe(true); + }); + + it("when started again, throws", () => { + expect(actual.start).rejects.toThrow("Tried to start \"some-id\", but it has already started."); + }); + + it("does not stop yet", () => { + expect(stopMock).not.toHaveBeenCalled(); + }); + + describe("when stopped", () => { + let stopPromise: Promise; + + beforeEach(() => { + stopPromise = actual.stop(); + }); + + it("starts stopping", () => { + expect(stopMock).toHaveBeenCalled(); + }); + + it("stopping does not resolve yet", async () => { + const promiseStatus = await getPromiseStatus(stopPromise); + + expect(promiseStatus.fulfilled).toBe(false); + }); + + it("is not stopped yet", () => { + expect(actual.started).toBe(true); + }); + + describe("when stopping finishes", () => { + beforeEach(async () => { + await stopMock.resolve(); + }); + + it("is not started", () => { + expect(actual.started).toBe(false); + }); + + it("stopping resolves", async () => { + const promiseStatus = await getPromiseStatus(stopPromise); + + expect(promiseStatus.fulfilled).toBe(true); + }); + + it("when stopped again, throws", () => { + expect(actual.stop).rejects.toThrow("Tried to stop \"some-id\", but it has already stopped."); + }); + + describe("when started again", () => { + beforeEach( + () => { + startMock.mockClear(); + + actual.start(); + }); + + it("starts", () => { + expect(startMock).toHaveBeenCalled(); + }); + + it("is not started yet", () => { + expect(actual.started).toBe(false); + }); + + describe("when starting finishes", () => { + beforeEach(async () => { + await startMock.resolve(stopMock); + }); + + it("is started", () => { + expect(actual.started).toBe(true); + }); + + it("when stopped again, starts stopping again", async () => { + stopMock.mockClear(); + + actual.stop(); + + await flushPromises(); + + expect(stopMock).toHaveBeenCalled(); + }); + }); + }); + }); + }); + }); + + describe("when stopped before starting finishes", () => { + let stopPromise: Promise; + + beforeEach(() => { + stopPromise = actual.stop(); + }); + + it("does not resolve yet", async () => { + const promiseStatus = await getPromiseStatus(stopPromise); + + expect(promiseStatus.fulfilled).toBe(false); + }); + + it("is not started yet", () => { + expect(actual.started).toBe(false); + }); + + describe("when starting finishes", () => { + beforeEach(async () => { + await startMock.resolve(stopMock); + }); + + it("starts stopping", () => { + expect(stopMock).toHaveBeenCalled(); + }); + + it("is not stopped yet", () => { + expect(actual.started).toBe(true); + }); + + it("does not resolve yet", async () => { + const promiseStatus = await getPromiseStatus(stopPromise); + + expect(promiseStatus.fulfilled).toBe(false); + }); + + describe("when stopping finishes", () => { + beforeEach(async () => { + await stopMock.resolve(); + }); + + it("is stopped", () => { + expect(actual.started).toBe(false); + }); + + it("resolves", async () => { + const promiseStatus = await getPromiseStatus(stopPromise); + + expect(promiseStatus.fulfilled).toBe(true); + }); + }); + }); + }); + }); +}); diff --git a/src/common/utils/get-startable-stoppable.ts b/src/common/utils/get-startable-stoppable.ts new file mode 100644 index 0000000000..5590157ab8 --- /dev/null +++ b/src/common/utils/get-startable-stoppable.ts @@ -0,0 +1,60 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +type Stopper = () => Promise | void; +type Starter = () => Promise | Stopper; + +export const getStartableStoppable = ( + id: string, + startAndGetStopCallback: Starter, +) => { + let stop: Stopper; + let stopped = false; + let started = false; + let starting = false; + let startingPromise: Promise | Stopper; + + return { + get started() { + return started; + }, + + start: async () => { + if (starting) { + throw new Error(`Tried to start "${id}", but it is already being started.`); + } + + starting = true; + + if (started) { + throw new Error(`Tried to start "${id}", but it has already started.`); + } + + startingPromise = startAndGetStopCallback(); + stop = await startingPromise; + + stopped = false; + started = true; + starting = false; + }, + + stop: async () => { + await startingPromise; + + if (stopped) { + throw new Error(`Tried to stop "${id}", but it has already stopped.`); + } + + if (!started) { + throw new Error(`Tried to stop "${id}", but it has not started yet.`); + } + + await stop(); + + started = false; + stopped = true; + }, + }; +}; diff --git a/src/common/vars.ts b/src/common/vars.ts index 18eefe890b..efbf32ee67 100644 --- a/src/common/vars.ts +++ b/src/common/vars.ts @@ -10,21 +10,48 @@ import packageInfo from "../../package.json"; import type { ThemeId } from "../renderer/themes/store"; import { lazyInitialized } from "./utils/lazy-initialized"; +/** + * @deprecated Switch to using isMacInjectable + */ export const isMac = process.platform === "darwin"; + +/** + * @deprecated Switch to using isWindowsInjectable + */ export const isWindows = process.platform === "win32"; + +/** + * @deprecated Switch to using isLinuxInjectable + */ export const isLinux = process.platform === "linux"; + export const isDebugging = ["true", "1", "yes", "y", "on"].includes((process.env.DEBUG ?? "").toLowerCase()); export const isSnap = !!process.env.SNAP; -export const isProduction = process.env.NODE_ENV === "production"; + +/** + * @deprecated Switch to using isTestEnvInjectable + */ export const isTestEnv = !!process.env.JEST_WORKER_ID; + +/** + * @deprecated Switch to using isProductionInjectable + */ +export const isProduction = process.env.NODE_ENV === "production"; + +/** + * @deprecated Switch to using isDevelopmentInjectable + */ export const isDevelopment = !isTestEnv && !isProduction; + export const isPublishConfigured = Object.keys(packageInfo.build).includes("publish"); -export const integrationTestingArg = "--integration-testing"; -export const isIntegrationTesting = process.argv.includes(integrationTestingArg); - export const productName = packageInfo.productName; + +/** + * @deprecated Switch to using appNameInjectable + */ export const appName = `${packageInfo.productName}${isDevelopment ? "Dev" : ""}`; + export const publicPath = "/build/" as string; export const defaultThemeId: ThemeId = "lens-dark"; export const defaultFontSize = 12; @@ -101,12 +128,6 @@ export const kubectlBinaryName = getBinaryName("kubectl"); * @deprecated for being explicit side effect. */ export const kubectlBinaryPath = lazyInitialized(() => path.join(baseBinariesDir.get(), kubectlBinaryName)); -export const staticFilesDirectory = path.resolve( - !isProduction - ? process.cwd() - : process.resourcesPath, - "static", -); // Apis export const apiPrefix = "/api"; // local router apis diff --git a/src/common/vars/is-development.injectable.ts b/src/common/vars/is-development.injectable.ts index a731ae5a9f..190d754d8d 100644 --- a/src/common/vars/is-development.injectable.ts +++ b/src/common/vars/is-development.injectable.ts @@ -3,11 +3,18 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import { isDevelopment } from "../vars"; +import isProductionInjectable from "./is-production.injectable"; +import isTestEnvInjectable from "./is-test-env.injectable"; const isDevelopmentInjectable = getInjectable({ id: "is-development", - instantiate: () => isDevelopment, + + instantiate: (di) => { + const isProduction = di.inject(isProductionInjectable); + const isTestEnv = di.inject(isTestEnvInjectable); + + return !isTestEnv && !isProduction; + }, }); export default isDevelopmentInjectable; diff --git a/src/common/vars/is-integration-testing.injectable.ts b/src/common/vars/is-integration-testing.injectable.ts new file mode 100644 index 0000000000..7d6d5ce24e --- /dev/null +++ b/src/common/vars/is-integration-testing.injectable.ts @@ -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 commandLineArgumentsInjectable from "../../main/utils/command-line-arguments.injectable"; + +const isIntegrationTestingInjectable = getInjectable({ + id: "is-integration-testing", + + instantiate: (di) => { + const commandLineArguments = di.inject(commandLineArgumentsInjectable); + + return commandLineArguments.includes("--integration-testing"); + }, +}); + +export default isIntegrationTestingInjectable; diff --git a/src/common/vars/is-linux.injectable.ts b/src/common/vars/is-linux.injectable.ts index dbd3436129..d84165fad5 100644 --- a/src/common/vars/is-linux.injectable.ts +++ b/src/common/vars/is-linux.injectable.ts @@ -3,12 +3,16 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import { isLinux } from "../vars"; +import platformInjectable from "./platform.injectable"; const isLinuxInjectable = getInjectable({ id: "is-linux", - instantiate: () => isLinux, - causesSideEffects: true, + + instantiate: (di) => { + const platform = di.inject(platformInjectable); + + return platform === "linux"; + }, }); export default isLinuxInjectable; diff --git a/src/common/vars/is-mac.injectable.ts b/src/common/vars/is-mac.injectable.ts index 6a956b2426..67a6fda286 100644 --- a/src/common/vars/is-mac.injectable.ts +++ b/src/common/vars/is-mac.injectable.ts @@ -3,12 +3,16 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import { isMac } from "../vars"; +import platformInjectable from "./platform.injectable"; const isMacInjectable = getInjectable({ id: "is-mac", - instantiate: () => isMac, - causesSideEffects: true, + + instantiate: (di) => { + const platform = di.inject(platformInjectable); + + return platform === "darwin"; + }, }); export default isMacInjectable; diff --git a/src/common/vars/is-production.injectable.ts b/src/common/vars/is-production.injectable.ts new file mode 100644 index 0000000000..085b091dfa --- /dev/null +++ b/src/common/vars/is-production.injectable.ts @@ -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 environmentVariablesInjectable from "../utils/environment-variables.injectable"; + +const isProductionInjectable = getInjectable({ + id: "is-production", + + instantiate: (di) => { + const { NODE_ENV: nodeEnv } = di.inject(environmentVariablesInjectable); + + return nodeEnv === "production"; + }, +}); + +export default isProductionInjectable; diff --git a/src/common/vars/is-test-env.injectable.ts b/src/common/vars/is-test-env.injectable.ts new file mode 100644 index 0000000000..85965d0098 --- /dev/null +++ b/src/common/vars/is-test-env.injectable.ts @@ -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 environmentVariablesInjectable from "../utils/environment-variables.injectable"; + +const isTestEnvInjectable = getInjectable({ + id: "is-test-env", + + instantiate: (di) => { + const { JEST_WORKER_ID: jestWorkerId } = di.inject(environmentVariablesInjectable); + + return !!jestWorkerId; + }, +}); + +export default isTestEnvInjectable; diff --git a/src/common/vars/is-windows.injectable.ts b/src/common/vars/is-windows.injectable.ts index 4b92b78a3b..8eb78dcb58 100644 --- a/src/common/vars/is-windows.injectable.ts +++ b/src/common/vars/is-windows.injectable.ts @@ -3,12 +3,16 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import { isWindows } from "../vars"; +import platformInjectable from "./platform.injectable"; const isWindowsInjectable = getInjectable({ id: "is-windows", - instantiate: () => isWindows, - causesSideEffects: true, + + instantiate: (di) => { + const platform = di.inject(platformInjectable); + + return platform === "win32"; + }, }); export default isWindowsInjectable; diff --git a/src/common/vars/lens-resources-dir.injectable.ts b/src/common/vars/lens-resources-dir.injectable.ts new file mode 100644 index 0000000000..c454afb005 --- /dev/null +++ b/src/common/vars/lens-resources-dir.injectable.ts @@ -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 isProductionInjectable from "./is-production.injectable"; + +const lensResourcesDirInjectable = getInjectable({ + id: "lens-resources-dir", + + instantiate: (di) => { + const isProduction = di.inject(isProductionInjectable); + + return !isProduction + ? process.cwd() + : process.resourcesPath; + }, + + causesSideEffects: true, +}); + +export default lensResourcesDirInjectable; diff --git a/src/common/vars/platform.injectable.ts b/src/common/vars/platform.injectable.ts new file mode 100644 index 0000000000..11939a7f06 --- /dev/null +++ b/src/common/vars/platform.injectable.ts @@ -0,0 +1,13 @@ +/** + * 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 platformInjectable = getInjectable({ + id: "platform", + instantiate: () => process.platform, + causesSideEffects: true, +}); + +export default platformInjectable; diff --git a/src/common/vars/static-files-directory.injectable.ts b/src/common/vars/static-files-directory.injectable.ts new file mode 100644 index 0000000000..c881f12b97 --- /dev/null +++ b/src/common/vars/static-files-directory.injectable.ts @@ -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 getAbsolutePathInjectable from "../path/get-absolute-path.injectable"; +import lensResourcesDirInjectable from "./lens-resources-dir.injectable"; + +const staticFilesDirectoryInjectable = getInjectable({ + id: "static-files-directory", + + instantiate: (di) => { + const getAbsolutePath = di.inject(getAbsolutePathInjectable); + const lensResourcesDir = di.inject(lensResourcesDirInjectable); + + return getAbsolutePath(lensResourcesDir, "static"); + }, +}); + +export default staticFilesDirectoryInjectable; diff --git a/src/common/weblink-store.injectable.ts b/src/common/weblink-store.injectable.ts new file mode 100644 index 0000000000..4aca7dce2a --- /dev/null +++ b/src/common/weblink-store.injectable.ts @@ -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 { WeblinkStore } from "./weblink-store"; + +const weblinkStoreInjectable = getInjectable({ + id: "weblink-store", + + instantiate: () => { + WeblinkStore.resetInstance(); + + return WeblinkStore.createInstance(); + }, +}); + +export default weblinkStoreInjectable; diff --git a/src/extensions/__tests__/extension-loader.test.ts b/src/extensions/__tests__/extension-loader.test.ts index c9e145f8f9..ae150e4f99 100644 --- a/src/extensions/__tests__/extension-loader.test.ts +++ b/src/extensions/__tests__/extension-loader.test.ts @@ -9,8 +9,8 @@ import { stdout, stderr } from "process"; import extensionLoaderInjectable from "../extension-loader/extension-loader.injectable"; import { runInAction } from "mobx"; import updateExtensionsStateInjectable from "../extension-loader/update-extensions-state/update-extensions-state.injectable"; -import { getDisForUnitTesting } from "../../test-utils/get-dis-for-unit-testing"; import mockFs from "mock-fs"; +import { getDiForUnitTesting } from "../../main/getDiForUnitTesting"; console = new Console(stdout, stderr); @@ -109,18 +109,16 @@ describe("ExtensionLoader", () => { let extensionLoader: ExtensionLoader; let updateExtensionStateMock: jest.Mock; - beforeEach(async () => { - const dis = getDisForUnitTesting({ doGeneralOverrides: true }); + beforeEach(() => { + const di = getDiForUnitTesting({ doGeneralOverrides: true }); mockFs(); updateExtensionStateMock = jest.fn(); - dis.mainDi.override(updateExtensionsStateInjectable, () => updateExtensionStateMock); + di.override(updateExtensionsStateInjectable, () => updateExtensionStateMock); - await dis.runSetups(); - - extensionLoader = dis.mainDi.inject(extensionLoaderInjectable); + extensionLoader = di.inject(extensionLoaderInjectable); }); afterEach(() => { diff --git a/src/extensions/as-legacy-globals-for-extension-api/as-legacy-global-function-for-extension-api.ts b/src/extensions/as-legacy-globals-for-extension-api/as-legacy-global-function-for-extension-api.ts index 4ab374743c..e0b4ea3223 100644 --- a/src/extensions/as-legacy-globals-for-extension-api/as-legacy-global-function-for-extension-api.ts +++ b/src/extensions/as-legacy-globals-for-extension-api/as-legacy-global-function-for-extension-api.ts @@ -17,4 +17,4 @@ export const asLegacyGlobalFunctionForExtensionApi = (( ) as unknown as (...args: any[]) => any; return injected(...args); - }) as Inject; + }) as Inject; diff --git a/src/extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api.ts b/src/extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api.ts index 270bb6091e..81d6f94b20 100644 --- a/src/extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api.ts +++ b/src/extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api.ts @@ -43,4 +43,4 @@ export const asLegacyGlobalForExtensionApi = (( return propertyValue; }, }, - )) as Inject; + )) as Inject; diff --git a/src/extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api.ts b/src/extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api.ts index 74842daed9..4d7d9ca8f5 100644 --- a/src/extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api.ts +++ b/src/extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api.ts @@ -33,5 +33,11 @@ export const getLegacyGlobalDiForExtensionApi = () => { }; export function getEnvironmentSpecificLegacyGlobalDiForExtensionApi(environment: Environments) { - return legacyGlobalDis.get(environment); + const di = legacyGlobalDis.get(environment); + + if (!di) { + throw new Error("Tried to get DI container using legacy globals in environment which doesn't exist"); + } + + return di; } diff --git a/src/extensions/extension-discovery/extension-discovery.injectable.ts b/src/extensions/extension-discovery/extension-discovery.injectable.ts index 6833dda3a8..f7ffda8a80 100644 --- a/src/extensions/extension-discovery/extension-discovery.injectable.ts +++ b/src/extensions/extension-discovery/extension-discovery.injectable.ts @@ -12,6 +12,7 @@ import extensionInstallationStateStoreInjectable from "../extension-installation import installExtensionInjectable from "../extension-installer/install-extension/install-extension.injectable"; import extensionPackageRootDirectoryInjectable from "../extension-installer/extension-package-root-directory/extension-package-root-directory.injectable"; import installExtensionsInjectable from "../extension-installer/install-extensions/install-extensions.injectable"; +import staticFilesDirectoryInjectable from "../../common/vars/static-files-directory.injectable"; const extensionDiscoveryInjectable = getInjectable({ id: "extension-discovery", @@ -37,6 +38,8 @@ const extensionDiscoveryInjectable = getInjectable({ extensionPackageRootDirectory: di.inject( extensionPackageRootDirectoryInjectable, ), + + staticFilesDirectory: di.inject(staticFilesDirectoryInjectable), }), }); diff --git a/src/extensions/extension-discovery/extension-discovery.test.ts b/src/extensions/extension-discovery/extension-discovery.test.ts index e243fe620b..4e71f5ff7c 100644 --- a/src/extensions/extension-discovery/extension-discovery.test.ts +++ b/src/extensions/extension-discovery/extension-discovery.test.ts @@ -49,7 +49,7 @@ const mockedFse = fse as jest.Mocked; describe("ExtensionDiscovery", () => { let extensionDiscovery: ExtensionDiscovery; - beforeEach(async () => { + beforeEach(() => { const di = getDiForUnitTesting({ doGeneralOverrides: true }); di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); @@ -57,8 +57,6 @@ describe("ExtensionDiscovery", () => { mockFs(); - await di.runSetups(); - extensionDiscovery = di.inject(extensionDiscoveryInjectable); }); diff --git a/src/extensions/extension-discovery/extension-discovery.ts b/src/extensions/extension-discovery/extension-discovery.ts index 30cca5de96..3576e66362 100644 --- a/src/extensions/extension-discovery/extension-discovery.ts +++ b/src/extensions/extension-discovery/extension-discovery.ts @@ -16,7 +16,7 @@ import logger from "../../main/logger"; import type { ExtensionsStore } from "../extensions-store/extensions-store"; import type { ExtensionLoader } from "../extension-loader"; import type { LensExtensionId, LensExtensionManifest } from "../lens-extension"; -import { isProduction, staticFilesDirectory } from "../../common/vars"; +import { isProduction } from "../../common/vars"; import type { ExtensionInstallationStateStore } from "../extension-installation-state-store/extension-installation-state-store"; import type { PackageJson } from "type-fest"; import { extensionDiscoveryStateChannel } from "../../common/ipc/extension-handling"; @@ -34,6 +34,7 @@ interface Dependencies { installExtension: (name: string) => Promise; installExtensions: (packageJsonPath: string, packagesJson: PackageJson) => Promise; extensionPackageRootDirectory: string; + staticFilesDirectory: string; } export interface InstalledExtension { @@ -112,7 +113,7 @@ export class ExtensionDiscovery { } get inTreeFolderPath(): string { - return path.resolve(staticFilesDirectory, "../extensions"); + return path.resolve(this.dependencies.staticFilesDirectory, "../extensions"); } get nodeModulesPath(): string { diff --git a/src/extensions/lens-extension-set-dependencies.ts b/src/extensions/lens-extension-set-dependencies.ts index 9f30eeb8b8..42848f7859 100644 --- a/src/extensions/lens-extension-set-dependencies.ts +++ b/src/extensions/lens-extension-set-dependencies.ts @@ -11,6 +11,7 @@ import type { CatalogEntityRegistry as MainCatalogEntityRegistry } from "../main import type { CatalogEntityRegistry as RendererCatalogEntityRegistry } from "../renderer/api/catalog/entity/registry"; import type { GetExtensionPageParameters } from "../renderer/routes/get-extension-page-parameters.injectable"; import type { FileSystemProvisionerStore } from "./extension-loader/file-system-provisioner-store/file-system-provisioner-store"; +import type { NavigateForExtension } from "../main/start-main-application/lens-window/navigate-for-extension.injectable"; export interface LensExtensionDependencies { readonly fileSystemProvisionerStore: FileSystemProvisionerStore; @@ -18,6 +19,7 @@ export interface LensExtensionDependencies { export interface LensMainExtensionDependencies extends LensExtensionDependencies { readonly entityRegistry: MainCatalogEntityRegistry; + readonly navigate: NavigateForExtension; } export interface LensRendererExtensionDependencies extends LensExtensionDependencies { diff --git a/src/extensions/lens-main-extension.ts b/src/extensions/lens-main-extension.ts index 51ec8446ca..accc87989c 100644 --- a/src/extensions/lens-main-extension.ts +++ b/src/extensions/lens-main-extension.ts @@ -4,7 +4,6 @@ */ import { LensExtension, lensExtensionDependencies } from "./lens-extension"; -import { WindowManager } from "../main/window-manager"; import type { CatalogEntity } from "../common/catalog"; import type { IObservableArray } from "mobx"; import type { MenuRegistration } from "../main/menu/menu-registration"; @@ -32,7 +31,7 @@ export class LensMainExtension extends LensExtension, frameId?: number) { - return WindowManager.getInstance().navigateExtension(this.id, pageId, params, frameId); + await this[lensExtensionDependencies].navigate(this.id, pageId, params, frameId); } addCatalogSource(id: string, source: IObservableArray) { diff --git a/src/extensions/main-api/navigation.ts b/src/extensions/main-api/navigation.ts index 178f3d6d16..375d2bac74 100644 --- a/src/extensions/main-api/navigation.ts +++ b/src/extensions/main-api/navigation.ts @@ -3,8 +3,17 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { WindowManager } from "../../main/window-manager"; +import { + Environments, + getEnvironmentSpecificLegacyGlobalDiForExtensionApi, +} from "../as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; + +import navigateInjectable from "../../main/start-main-application/lens-window/navigate.injectable"; export function navigate(url: string) { - return WindowManager.getInstance().navigate(url); + const di = getEnvironmentSpecificLegacyGlobalDiForExtensionApi(Environments.main); + + const navigate = di.inject(navigateInjectable); + + return navigate(url); } diff --git a/src/main/__test__/cluster.test.ts b/src/main/__test__/cluster.test.ts index 9f63448139..0d76513a8f 100644 --- a/src/main/__test__/cluster.test.ts +++ b/src/main/__test__/cluster.test.ts @@ -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) => ({ diff --git a/src/main/__test__/context-handler.test.ts b/src/main/__test__/context-handler.test.ts index 353f1a507c..92e9d5ae25 100644 --- a/src/main/__test__/context-handler.test.ts +++ b/src/main/__test__/context-handler.test.ts @@ -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 }; diff --git a/src/main/__test__/kube-auth-proxy.test.ts b/src/main/__test__/kube-auth-proxy.test.ts index 5147a7a165..d08dadaa4e 100644 --- a/src/main/__test__/kube-auth-proxy.test.ts +++ b/src/main/__test__/kube-auth-proxy.test.ts @@ -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); diff --git a/src/main/__test__/kubeconfig-manager.test.ts b/src/main/__test__/kubeconfig-manager.test.ts index a7616c6d40..436a815686 100644 --- a/src/main/__test__/kubeconfig-manager.test.ts +++ b/src/main/__test__/kubeconfig-manager.test.ts @@ -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(), diff --git a/src/main/__test__/lens-proxy.test.ts b/src/main/__test__/lens-proxy.test.ts index a80d983dec..20af162f17 100644 --- a/src/main/__test__/lens-proxy.test.ts +++ b/src/main/__test__/lens-proxy.test.ts @@ -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", () => { diff --git a/src/main/__test__/static-file-route.test.ts b/src/main/__test__/static-file-route.test.ts index 0077bb4e65..217d1effd2 100644 --- a/src/main/__test__/static-file-route.test.ts +++ b/src/main/__test__/static-file-route.test.ts @@ -26,11 +26,9 @@ jest.mock("electron", () => ({ describe("static-file-route", () => { let handleStaticFileRoute: Route; - beforeEach(async () => { + beforeEach(() => { const di = getDiForUnitTesting({ doGeneralOverrides: true }); - await di.runSetups(); - handleStaticFileRoute = di.inject(staticFileRouteInjectable); }); diff --git a/src/main/app-paths/app-name/app-name.injectable.ts b/src/main/app-paths/app-name/app-name.injectable.ts index 4ba900d410..f4af95cf83 100644 --- a/src/main/app-paths/app-name/app-name.injectable.ts +++ b/src/main/app-paths/app-name/app-name.injectable.ts @@ -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; diff --git a/src/main/app-paths/app-paths.injectable.ts b/src/main/app-paths/app-paths.injectable.ts deleted file mode 100644 index 342c32252d..0000000000 --- a/src/main/app-paths/app-paths.injectable.ts +++ /dev/null @@ -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); -}; diff --git a/src/main/app-paths/directory-for-integration-testing/directory-for-integration-testing.injectable.ts b/src/main/app-paths/directory-for-integration-testing/directory-for-integration-testing.injectable.ts index 76779ce5a0..73647a3270 100644 --- a/src/main/app-paths/directory-for-integration-testing/directory-for-integration-testing.injectable.ts +++ b/src/main/app-paths/directory-for-integration-testing/directory-for-integration-testing.injectable.ts @@ -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; diff --git a/src/main/app-paths/get-electron-app-path/get-electron-app-path.injectable.ts b/src/main/app-paths/get-electron-app-path/get-electron-app-path.injectable.ts index b6d1dcc4e2..057c22c860 100644 --- a/src/main/app-paths/get-electron-app-path/get-electron-app-path.injectable.ts +++ b/src/main/app-paths/get-electron-app-path/get-electron-app-path.injectable.ts @@ -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({ diff --git a/src/main/app-paths/get-electron-app-path/get-electron-app-path.test.ts b/src/main/app-paths/get-electron-app-path/get-electron-app-path.test.ts index d8d86a551c..6cb937f45d 100644 --- a/src/main/app-paths/get-electron-app-path/get-electron-app-path.test.ts +++ b/src/main/app-paths/get-electron-app-path/get-electron-app-path.test.ts @@ -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; }); diff --git a/src/main/app-paths/set-electron-app-path/set-electron-app-path.injectable.ts b/src/main/app-paths/set-electron-app-path/set-electron-app-path.injectable.ts index 636063d939..73090956d3 100644 --- a/src/main/app-paths/set-electron-app-path/set-electron-app-path.injectable.ts +++ b/src/main/app-paths/set-electron-app-path/set-electron-app-path.injectable.ts @@ -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", diff --git a/src/main/app-paths/setup-app-paths.injectable.ts b/src/main/app-paths/setup-app-paths.injectable.ts new file mode 100644 index 0000000000..9a4283f063 --- /dev/null +++ b/src/main/app-paths/setup-app-paths.injectable.ts @@ -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; diff --git a/src/main/app-updater.ts b/src/main/app-updater.ts index fc7bb52648..77daf6b4b6 100644 --- a/src/main/app-updater.ts +++ b/src/main/app-updater.ts @@ -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; } diff --git a/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts b/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts index d151ba2d47..55f318697b 100644 --- a/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts +++ b/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts @@ -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(); }); diff --git a/src/main/catalog-sources/kubeconfig-sync/manager.injectable.ts b/src/main/catalog-sources/kubeconfig-sync/manager.injectable.ts index 24ba63f538..f95fa0fb17 100644 --- a/src/main/catalog-sources/kubeconfig-sync/manager.injectable.ts +++ b/src/main/catalog-sources/kubeconfig-sync/manager.injectable.ts @@ -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), }), }); diff --git a/src/main/catalog-sources/kubeconfig-sync/manager.ts b/src/main/catalog-sources/kubeconfig-sync/manager.ts index 748747fc92..af5510c3bc 100644 --- a/src/main/catalog-sources/kubeconfig-sync/manager.ts +++ b/src/main/catalog-sources/kubeconfig-sync/manager.ts @@ -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; +interface ComputeDiffDependencies { + directoryForKubeConfigs: string; + createCluster: (model: ClusterModel) => Cluster; + clusterManager: ClusterManager; +} + // exported for testing -export const computeDiff = ({ directoryForKubeConfigs, createCluster }: Pick) => (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 ({ 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, Disposer] => { +const watchFileChanges = (filePath: string, dependencies: ComputeDiffDependencies): [IComputedValue, Disposer] => { const rootSource = observable.map>(); const derivedSource = computed(() => Array.from(iter.flatMap(rootSource.values(), from => iter.map(from.values(), child => child[1])))); diff --git a/src/main/catalog-sources/sync-weblinks.injectable.ts b/src/main/catalog-sources/sync-weblinks.injectable.ts new file mode 100644 index 0000000000..836418781d --- /dev/null +++ b/src/main/catalog-sources/sync-weblinks.injectable.ts @@ -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; diff --git a/src/main/catalog-sources/weblinks.ts b/src/main/catalog-sources/weblinks.ts index 71979332db..353f5f91bb 100644 --- a/src/main/catalog-sources/weblinks.ts +++ b/src/main/catalog-sources/weblinks.ts @@ -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(); function periodicallyCheckLink(link: WebLink): Disposer { @@ -87,4 +90,4 @@ export function syncWeblinks() { }, { fireImmediately: true }); catalogEntityRegistry.addComputedSource("weblinks", computed(() => Array.from(webLinkEntities.values(), ([link]) => link))); -} +}; diff --git a/src/main/catalog-sync-to-renderer/catalog-sync-to-renderer.injectable.ts b/src/main/catalog-sync-to-renderer/catalog-sync-to-renderer.injectable.ts new file mode 100644 index 0000000000..37dfd2f7fd --- /dev/null +++ b/src/main/catalog-sync-to-renderer/catalog-sync-to-renderer.injectable.ts @@ -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; diff --git a/src/main/catalog-sync-to-renderer/start-catalog-sync.injectable.ts b/src/main/catalog-sync-to-renderer/start-catalog-sync.injectable.ts new file mode 100644 index 0000000000..71afc4d895 --- /dev/null +++ b/src/main/catalog-sync-to-renderer/start-catalog-sync.injectable.ts @@ -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; diff --git a/src/main/catalog-sync-to-renderer/stop-catalog-sync.injectable.ts b/src/main/catalog-sync-to-renderer/stop-catalog-sync.injectable.ts new file mode 100644 index 0000000000..71c5be55f1 --- /dev/null +++ b/src/main/catalog-sync-to-renderer/stop-catalog-sync.injectable.ts @@ -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; diff --git a/src/main/catalog/entity-registry.injectable.ts b/src/main/catalog/entity-registry.injectable.ts index 275d692046..003ed3d62e 100644 --- a/src/main/catalog/entity-registry.injectable.ts +++ b/src/main/catalog/entity-registry.injectable.ts @@ -8,6 +8,7 @@ import { CatalogEntityRegistry } from "./entity-registry"; const catalogEntityRegistryInjectable = getInjectable({ id: "catalog-entity-registry", + instantiate: (di) => new CatalogEntityRegistry({ hasCategoryForEntity: di.inject(hasCategoryForEntityInjectable), }), diff --git a/src/main/cluster-detectors/base-cluster-detector.ts b/src/main/cluster-detectors/base-cluster-detector.ts index f4c9fc963d..1aca321dfe 100644 --- a/src/main/cluster-detectors/base-cluster-detector.ts +++ b/src/main/cluster-detectors/base-cluster-detector.ts @@ -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; protected async k8sRequest(path: string, options: RequestPromiseOptions = {}): Promise { - return k8sRequest(this.cluster, path, options); + return this._k8sRequest(this.cluster, path, options); } } diff --git a/src/main/cluster-detectors/create-version-detector.injectable.ts b/src/main/cluster-detectors/create-version-detector.injectable.ts new file mode 100644 index 0000000000..efd59dcfc3 --- /dev/null +++ b/src/main/cluster-detectors/create-version-detector.injectable.ts @@ -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; diff --git a/src/main/cluster-detectors/detector-registry.injectable.ts b/src/main/cluster-detectors/detector-registry.injectable.ts new file mode 100644 index 0000000000..344e82003f --- /dev/null +++ b/src/main/cluster-detectors/detector-registry.injectable.ts @@ -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; diff --git a/src/main/cluster-detectors/detector-registry.ts b/src/main/cluster-detectors/detector-registry.ts index ed92ea9332..395d819850 100644 --- a/src/main/cluster-detectors/detector-registry.ts +++ b/src/main/cluster-detectors/detector-registry.ts @@ -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([], { 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(); diff --git a/src/main/cluster-manager.injectable.ts b/src/main/cluster-manager.injectable.ts new file mode 100644 index 0000000000..2b55f0e854 --- /dev/null +++ b/src/main/cluster-manager.injectable.ts @@ -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; diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts index 1933378777..787f2a1618 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster-manager.ts @@ -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 = 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(); @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:/ 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) { diff --git a/src/main/context-handler/context-handler.ts b/src/main/context-handler/context-handler.ts index 33a9049b28..cf35a0a67c 100644 --- a/src/main/context-handler/context-handler.ts +++ b/src/main/context-handler/context-handler.ts @@ -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) { diff --git a/src/main/context-handler/create-context-handler.injectable.ts b/src/main/context-handler/create-context-handler.injectable.ts index fcf3ffbf7e..8185bd7c80 100644 --- a/src/main/context-handler/create-context-handler.injectable.ts +++ b/src/main/context-handler/create-context-handler.injectable.ts @@ -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); }; }, }); diff --git a/src/main/create-cluster/create-cluster.injectable.ts b/src/main/create-cluster/create-cluster.injectable.ts index 633e7937af..cd34979925 100644 --- a/src/main/create-cluster/create-cluster.injectable.ts +++ b/src/main/create-cluster/create-cluster.injectable.ts @@ -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); diff --git a/src/main/developer-tools.ts b/src/main/developer-tools.ts deleted file mode 100644 index f3e40263f0..0000000000 --- a/src/main/developer-tools.ts +++ /dev/null @@ -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 })); - } -}; diff --git a/src/main/app-paths/get-electron-app-path/electron-app/electron-app.injectable.ts b/src/main/electron-app/electron-app.injectable.ts similarity index 100% rename from src/main/app-paths/get-electron-app-path/electron-app/electron-app.injectable.ts rename to src/main/electron-app/electron-app.injectable.ts diff --git a/src/main/electron-app/features/auto-updater.injectable.ts b/src/main/electron-app/features/auto-updater.injectable.ts new file mode 100644 index 0000000000..24bb560e1f --- /dev/null +++ b/src/main/electron-app/features/auto-updater.injectable.ts @@ -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; diff --git a/src/main/electron-app/features/disable-hardware-acceleration.injectable.ts b/src/main/electron-app/features/disable-hardware-acceleration.injectable.ts new file mode 100644 index 0000000000..1c1e291644 --- /dev/null +++ b/src/main/electron-app/features/disable-hardware-acceleration.injectable.ts @@ -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; diff --git a/src/main/electron-app/features/electron-dialog.injectable.ts b/src/main/electron-app/features/electron-dialog.injectable.ts new file mode 100644 index 0000000000..95de13f426 --- /dev/null +++ b/src/main/electron-app/features/electron-dialog.injectable.ts @@ -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; diff --git a/src/main/electron-app/features/exit-app.injectable.ts b/src/main/electron-app/features/exit-app.injectable.ts new file mode 100644 index 0000000000..0734b4c8c5 --- /dev/null +++ b/src/main/electron-app/features/exit-app.injectable.ts @@ -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; diff --git a/src/main/electron-app/features/get-command-line-switch.injectable.ts b/src/main/electron-app/features/get-command-line-switch.injectable.ts new file mode 100644 index 0000000000..271e2f367d --- /dev/null +++ b/src/main/electron-app/features/get-command-line-switch.injectable.ts @@ -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; diff --git a/src/main/electron-app/features/get-electron-theme.injectable.ts b/src/main/electron-app/features/get-electron-theme.injectable.ts new file mode 100644 index 0000000000..220c047aa6 --- /dev/null +++ b/src/main/electron-app/features/get-electron-theme.injectable.ts @@ -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; diff --git a/src/main/electron-app/features/native-theme.injectable.ts b/src/main/electron-app/features/native-theme.injectable.ts new file mode 100644 index 0000000000..a2d29d8835 --- /dev/null +++ b/src/main/electron-app/features/native-theme.injectable.ts @@ -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; diff --git a/src/main/electron-app/features/power-monitor.injectable.ts b/src/main/electron-app/features/power-monitor.injectable.ts new file mode 100644 index 0000000000..6ef08938fd --- /dev/null +++ b/src/main/electron-app/features/power-monitor.injectable.ts @@ -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; diff --git a/src/main/electron-app/features/register-file-protocol.injectable.ts b/src/main/electron-app/features/register-file-protocol.injectable.ts new file mode 100644 index 0000000000..dfc75954e3 --- /dev/null +++ b/src/main/electron-app/features/register-file-protocol.injectable.ts @@ -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; diff --git a/src/main/electron-app/features/request-single-instance-lock.injectable.ts b/src/main/electron-app/features/request-single-instance-lock.injectable.ts new file mode 100644 index 0000000000..eef6d9eaeb --- /dev/null +++ b/src/main/electron-app/features/request-single-instance-lock.injectable.ts @@ -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; diff --git a/src/main/electron-app/features/should-start-hidden.injectable.ts b/src/main/electron-app/features/should-start-hidden.injectable.ts new file mode 100644 index 0000000000..092282ac68 --- /dev/null +++ b/src/main/electron-app/features/should-start-hidden.injectable.ts @@ -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; diff --git a/src/main/electron-app/features/show-error-popup.injectable.ts b/src/main/electron-app/features/show-error-popup.injectable.ts new file mode 100644 index 0000000000..fbf31a7137 --- /dev/null +++ b/src/main/electron-app/features/show-error-popup.injectable.ts @@ -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; diff --git a/src/main/electron-app/features/show-message-popup.injectable.ts b/src/main/electron-app/features/show-message-popup.injectable.ts new file mode 100644 index 0000000000..7ad2f53c6f --- /dev/null +++ b/src/main/electron-app/features/show-message-popup.injectable.ts @@ -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; diff --git a/src/main/electron-app/features/sync-theme-from-operating-system.injectable.ts b/src/main/electron-app/features/sync-theme-from-operating-system.injectable.ts new file mode 100644 index 0000000000..fa2f8e335e --- /dev/null +++ b/src/main/electron-app/features/sync-theme-from-operating-system.injectable.ts @@ -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; diff --git a/src/main/electron-app/features/wait-for-electron-to-be-ready.injectable.ts b/src/main/electron-app/features/wait-for-electron-to-be-ready.injectable.ts new file mode 100644 index 0000000000..586e8d7ac2 --- /dev/null +++ b/src/main/electron-app/features/wait-for-electron-to-be-ready.injectable.ts @@ -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; diff --git a/src/main/electron-app/runnables/clean-up-deep-linking.injectable.ts b/src/main/electron-app/runnables/clean-up-deep-linking.injectable.ts new file mode 100644 index 0000000000..5925197db3 --- /dev/null +++ b/src/main/electron-app/runnables/clean-up-deep-linking.injectable.ts @@ -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; diff --git a/src/main/electron-app/runnables/dock-visibility/hide-dock-for-last-closed-window.injectable.ts b/src/main/electron-app/runnables/dock-visibility/hide-dock-for-last-closed-window.injectable.ts new file mode 100644 index 0000000000..de4b2b357d --- /dev/null +++ b/src/main/electron-app/runnables/dock-visibility/hide-dock-for-last-closed-window.injectable.ts @@ -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; diff --git a/src/main/electron-app/runnables/dock-visibility/show-dock-for-first-opened-window.injectable.ts b/src/main/electron-app/runnables/dock-visibility/show-dock-for-first-opened-window.injectable.ts new file mode 100644 index 0000000000..1dbafb60f5 --- /dev/null +++ b/src/main/electron-app/runnables/dock-visibility/show-dock-for-first-opened-window.injectable.ts @@ -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; diff --git a/src/main/electron-app/runnables/enforce-single-application-instance.injectable.ts b/src/main/electron-app/runnables/enforce-single-application-instance.injectable.ts new file mode 100644 index 0000000000..6908b6ac47 --- /dev/null +++ b/src/main/electron-app/runnables/enforce-single-application-instance.injectable.ts @@ -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; diff --git a/src/main/electron-app/runnables/setup-application-name.injectable.ts b/src/main/electron-app/runnables/setup-application-name.injectable.ts new file mode 100644 index 0000000000..a5f85301c0 --- /dev/null +++ b/src/main/electron-app/runnables/setup-application-name.injectable.ts @@ -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; diff --git a/src/main/electron-app/runnables/setup-deep-linking.injectable.ts b/src/main/electron-app/runnables/setup-deep-linking.injectable.ts new file mode 100644 index 0000000000..47087cd1c1 --- /dev/null +++ b/src/main/electron-app/runnables/setup-deep-linking.injectable.ts @@ -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://"))); diff --git a/src/main/electron-app/runnables/setup-developer-tools-in-development-environment.injectable.ts b/src/main/electron-app/runnables/setup-developer-tools-in-development-environment.injectable.ts new file mode 100644 index 0000000000..3438bc1428 --- /dev/null +++ b/src/main/electron-app/runnables/setup-developer-tools-in-development-environment.injectable.ts @@ -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; diff --git a/src/main/electron-app/runnables/setup-device-shutdown.injectable.ts b/src/main/electron-app/runnables/setup-device-shutdown.injectable.ts new file mode 100644 index 0000000000..b0b1ee8096 --- /dev/null +++ b/src/main/electron-app/runnables/setup-device-shutdown.injectable.ts @@ -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; diff --git a/src/main/electron-app/runnables/setup-ipc-main-handlers/setup-ipc-main-handlers.injectable.ts b/src/main/electron-app/runnables/setup-ipc-main-handlers/setup-ipc-main-handlers.injectable.ts new file mode 100644 index 0000000000..24f90f01b3 --- /dev/null +++ b/src/main/electron-app/runnables/setup-ipc-main-handlers/setup-ipc-main-handlers.injectable.ts @@ -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; diff --git a/src/main/initializers/init-ipc-main-handlers/init-ipc-main-handlers.ts b/src/main/electron-app/runnables/setup-ipc-main-handlers/setup-ipc-main-handlers.ts similarity index 69% rename from src/main/initializers/init-ipc-main-handlers/init-ipc-main-handlers.ts rename to src/main/electron-app/runnables/setup-ipc-main-handlers/setup-ipc-main-handlers.ts index 1a657b78be..9342696c77 100644 --- a/src/main/initializers/init-ipc-main-handlers/init-ipc-main-handlers.ts +++ b/src/main/electron-app/runnables/setup-ipc-main-handlers/setup-ipc-main-handlers.ts @@ -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; + clusterManager: ClusterManager; + catalogEntityRegistry: CatalogEntityRegistry; + clusterStore: ClusterStore; + operatingSystemTheme: IComputedValue; + 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(); }; diff --git a/src/main/electron-app/runnables/setup-main-window-visibility-after-activation.injectable.ts b/src/main/electron-app/runnables/setup-main-window-visibility-after-activation.injectable.ts new file mode 100644 index 0000000000..3642fa3c6d --- /dev/null +++ b/src/main/electron-app/runnables/setup-main-window-visibility-after-activation.injectable.ts @@ -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; diff --git a/src/main/electron-app/runnables/setup-runnables-after-window-is-opened.injectable.ts b/src/main/electron-app/runnables/setup-runnables-after-window-is-opened.injectable.ts new file mode 100644 index 0000000000..8e724fdf50 --- /dev/null +++ b/src/main/electron-app/runnables/setup-runnables-after-window-is-opened.injectable.ts @@ -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; diff --git a/src/main/electron-app/runnables/setup-runnables-before-closing-of-application.injectable.ts b/src/main/electron-app/runnables/setup-runnables-before-closing-of-application.injectable.ts new file mode 100644 index 0000000000..bb2c8f8970 --- /dev/null +++ b/src/main/electron-app/runnables/setup-runnables-before-closing-of-application.injectable.ts @@ -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; diff --git a/src/main/electron-app/runnables/setup-update-checking.injectable.ts b/src/main/electron-app/runnables/setup-update-checking.injectable.ts new file mode 100644 index 0000000000..918e985265 --- /dev/null +++ b/src/main/electron-app/runnables/setup-update-checking.injectable.ts @@ -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; diff --git a/src/main/exit-app.ts b/src/main/exit-app.ts deleted file mode 100644 index c9a5bc5bf1..0000000000 --- a/src/main/exit-app.ts +++ /dev/null @@ -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); -} diff --git a/src/main/extension-loader/create-extension-instance.injectable.ts b/src/main/extension-loader/create-extension-instance.injectable.ts index d91ca768cf..5617632580 100644 --- a/src/main/extension-loader/create-extension-instance.injectable.ts +++ b/src/main/extension-loader/create-extension-instance.injectable.ts @@ -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) => { diff --git a/src/main/get-metrics.injectable.ts b/src/main/get-metrics.injectable.ts new file mode 100644 index 0000000000..4d6634909c --- /dev/null +++ b/src/main/get-metrics.injectable.ts @@ -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; + +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; diff --git a/src/main/getDi.ts b/src/main/getDi.ts index d6f76122b7..9d9c60bbe8 100644 --- a/src/main/getDi.ts +++ b/src/main/getDi.ts @@ -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)$/); diff --git a/src/main/getDiForUnitTesting.ts b/src/main/getDiForUnitTesting.ts index 1c8a364357..efd065905c 100644 --- a/src/main/getDiForUnitTesting.ts +++ b/src/main/getDiForUnitTesting.ts @@ -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()); + + 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, () => () => {}); +}; diff --git a/src/main/helm/__tests__/helm-service.test.ts b/src/main/helm/__tests__/helm-service.test.ts index 42c548c4be..d9e6532e5c 100644 --- a/src/main/helm/__tests__/helm-service.test.ts +++ b/src/main/helm/__tests__/helm-service.test.ts @@ -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(); diff --git a/src/main/helm/helm-repo-manager.injectable.ts b/src/main/helm/helm-repo-manager.injectable.ts new file mode 100644 index 0000000000..9a12a3ca71 --- /dev/null +++ b/src/main/helm/helm-repo-manager.injectable.ts @@ -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 { + 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 { + 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> = {}; + + 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 { + const repos = await this.repositories(); + + return repos.find(repo => repo.name === name); + } + + private async list(helmEnv: HelmEnv): Promise { + 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 { + 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 { + 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; diff --git a/src/main/helm/helm-repo-manager.ts b/src/main/helm/helm-repo-manager.ts index b5a578982b..5726bafe16 100644 --- a/src/main/helm/helm-repo-manager.ts +++ b/src/main/helm/helm-repo-manager.ts @@ -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> & { 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 { - 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 { - 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> = {}; - - 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 { - const repos = await this.repositories(); - - return repos.find(repo => repo.name === name); - } - - private async list(helmEnv: HelmEnv): Promise { - 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 { - 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 { - logger.info(`[HELM]: removing repo ${name} (${url})`); - - return execHelm([ - "repo", - "remove", - name, - ]); - } -} +export const HelmRepoManager = asLegacyGlobalSingletonForExtensionApi(helmRepoManagerInjectable); diff --git a/src/main/index.ts b/src/main/index.ts index 03137198a3..3752c5a8c2 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -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 }; diff --git a/src/main/initializers/cluster-metadata-detectors.ts b/src/main/initializers/cluster-metadata-detectors.ts deleted file mode 100644 index 95d724bf3f..0000000000 --- a/src/main/initializers/cluster-metadata-detectors.ts +++ /dev/null @@ -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); -} diff --git a/src/main/initializers/init-ipc-main-handlers/init-ipc-main-handlers.injectable.ts b/src/main/initializers/init-ipc-main-handlers/init-ipc-main-handlers.injectable.ts deleted file mode 100644 index d92897bb50..0000000000 --- a/src/main/initializers/init-ipc-main-handlers/init-ipc-main-handlers.injectable.ts +++ /dev/null @@ -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; diff --git a/src/main/initializers/metrics-providers.ts b/src/main/initializers/metrics-providers.ts deleted file mode 100644 index b8e8208a29..0000000000 --- a/src/main/initializers/metrics-providers.ts +++ /dev/null @@ -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()); -} diff --git a/src/main/ipc/ask-user-for-file-paths.injectable.ts b/src/main/ipc/ask-user-for-file-paths.injectable.ts new file mode 100644 index 0000000000..9fc1ac1471 --- /dev/null +++ b/src/main/ipc/ask-user-for-file-paths.injectable.ts @@ -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; diff --git a/src/main/ipc/dialog.ts b/src/main/ipc/dialog.ts deleted file mode 100644 index 78dd4b69a9..0000000000 --- a/src/main/ipc/dialog.ts +++ /dev/null @@ -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 }; -} diff --git a/src/main/is-auto-update-enabled.injectable.ts b/src/main/is-auto-update-enabled.injectable.ts index 4a61318e46..c76bd27e45 100644 --- a/src/main/is-auto-update-enabled.injectable.ts +++ b/src/main/is-auto-update-enabled.injectable.ts @@ -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, }); diff --git a/src/main/k8s-request.injectable.ts b/src/main/k8s-request.injectable.ts new file mode 100644 index 0000000000..a556cf62f2 --- /dev/null +++ b/src/main/k8s-request.injectable.ts @@ -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; + +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; diff --git a/src/main/k8s-request.ts b/src/main/k8s-request.ts deleted file mode 100644 index 8531e9abb7..0000000000 --- a/src/main/k8s-request.ts +++ /dev/null @@ -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(cluster: Cluster, path: string, options: RequestPromiseOptions = {}): Promise { - 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 { - 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, - }); -} diff --git a/src/main/kubeconfig-manager/create-kubeconfig-manager.injectable.ts b/src/main/kubeconfig-manager/create-kubeconfig-manager.injectable.ts index 077cfe88b5..e1f365f098 100644 --- a/src/main/kubeconfig-manager/create-kubeconfig-manager.injectable.ts +++ b/src/main/kubeconfig-manager/create-kubeconfig-manager.injectable.ts @@ -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); diff --git a/src/main/kubeconfig-manager/kubeconfig-manager.ts b/src/main/kubeconfig-manager/kubeconfig-manager.ts index 8b9303fb2d..d7a9794a0f 100644 --- a/src/main/kubeconfig-manager/kubeconfig-manager.ts +++ b/src/main/kubeconfig-manager/kubeconfig-manager.ts @@ -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; } diff --git a/src/main/lens-proxy/lens-proxy-port.injectable.ts b/src/main/lens-proxy/lens-proxy-port.injectable.ts new file mode 100644 index 0000000000..e6826f6df0 --- /dev/null +++ b/src/main/lens-proxy/lens-proxy-port.injectable.ts @@ -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; diff --git a/src/main/lens-proxy/lens-proxy.injectable.ts b/src/main/lens-proxy/lens-proxy.injectable.ts new file mode 100644 index 0000000000..34cb327339 --- /dev/null +++ b/src/main/lens-proxy/lens-proxy.injectable.ts @@ -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; diff --git a/src/main/lens-proxy.ts b/src/main/lens-proxy/lens-proxy.ts similarity index 86% rename from src/main/lens-proxy.ts rename to src/main/lens-proxy/lens-proxy.ts index c7627c10d7..571cbea7b4 100644 --- a/src/main/lens-proxy.ts +++ b/src/main/lens-proxy/lens-proxy.ts @@ -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; -export interface LensProxyFunctions { +interface Dependencies { getClusterForRequest: GetClusterForRequest; shellApiRequest: (args: ProxyApiRequestArgs) => void | Promise; kubeApiUpgradeRequest: (args: ProxyApiRequestArgs) => void | Promise; + 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(); - 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); } } diff --git a/src/main/proxy-functions/index.ts b/src/main/lens-proxy/proxy-functions/index.ts similarity index 100% rename from src/main/proxy-functions/index.ts rename to src/main/lens-proxy/proxy-functions/index.ts diff --git a/src/main/proxy-functions/kube-api-upgrade-request.ts b/src/main/lens-proxy/proxy-functions/kube-api-upgrade-request.ts similarity index 97% rename from src/main/proxy-functions/kube-api-upgrade-request.ts rename to src/main/lens-proxy/proxy-functions/kube-api-upgrade-request.ts index c69d7349ec..ddd8e66261 100644 --- a/src/main/proxy-functions/kube-api-upgrade-request.ts +++ b/src/main/lens-proxy/proxy-functions/kube-api-upgrade-request.ts @@ -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"]); diff --git a/src/main/proxy-functions/shell-api-request/shell-api-request.injectable.ts b/src/main/lens-proxy/proxy-functions/shell-api-request/shell-api-request.injectable.ts similarity index 62% rename from src/main/proxy-functions/shell-api-request/shell-api-request.injectable.ts rename to src/main/lens-proxy/proxy-functions/shell-api-request/shell-api-request.injectable.ts index 73477d1889..b491ab5456 100644 --- a/src/main/proxy-functions/shell-api-request/shell-api-request.injectable.ts +++ b/src/main/lens-proxy/proxy-functions/shell-api-request/shell-api-request.injectable.ts @@ -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), }), }); diff --git a/src/main/proxy-functions/shell-api-request/shell-api-request.ts b/src/main/lens-proxy/proxy-functions/shell-api-request/shell-api-request.ts similarity index 74% rename from src/main/proxy-functions/shell-api-request/shell-api-request.ts rename to src/main/lens-proxy/proxy-functions/shell-api-request/shell-api-request.ts index 4455b12f5b..97b6dedd4c 100644 --- a/src/main/proxy-functions/shell-api-request/shell-api-request.ts +++ b/src/main/lens-proxy/proxy-functions/shell-api-request/shell-api-request.ts @@ -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 }; + + 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)) { diff --git a/src/main/proxy-functions/shell-api-request/shell-request-authenticator/shell-request-authenticator.injectable.ts b/src/main/lens-proxy/proxy-functions/shell-api-request/shell-request-authenticator/shell-request-authenticator.injectable.ts similarity index 100% rename from src/main/proxy-functions/shell-api-request/shell-request-authenticator/shell-request-authenticator.injectable.ts rename to src/main/lens-proxy/proxy-functions/shell-api-request/shell-request-authenticator/shell-request-authenticator.injectable.ts diff --git a/src/main/proxy-functions/shell-api-request/shell-request-authenticator/shell-request-authenticator.ts b/src/main/lens-proxy/proxy-functions/shell-api-request/shell-request-authenticator/shell-request-authenticator.ts similarity index 89% rename from src/main/proxy-functions/shell-api-request/shell-request-authenticator/shell-request-authenticator.ts rename to src/main/lens-proxy/proxy-functions/shell-api-request/shell-request-authenticator/shell-request-authenticator.ts index ab5e46eb77..fe0a8f643d 100644 --- a/src/main/proxy-functions/shell-api-request/shell-request-authenticator/shell-request-authenticator.ts +++ b/src/main/lens-proxy/proxy-functions/shell-api-request/shell-request-authenticator/shell-request-authenticator.ts @@ -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"; diff --git a/src/main/proxy-functions/types.ts b/src/main/lens-proxy/proxy-functions/types.ts similarity index 86% rename from src/main/proxy-functions/types.ts rename to src/main/lens-proxy/proxy-functions/types.ts index 01bd3cebd0..b6592ad244 100644 --- a/src/main/proxy-functions/types.ts +++ b/src/main/lens-proxy/proxy-functions/types.ts @@ -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; diff --git a/src/main/menu/application-menu-items.injectable.ts b/src/main/menu/application-menu-items.injectable.ts index b2471c6e74..417c40ac2e 100644 --- a/src/main/menu/application-menu-items.injectable.ts +++ b/src/main/menu/application-menu-items.injectable.ts @@ -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(), ); }, }, diff --git a/src/main/menu/application-menu.injectable.ts b/src/main/menu/application-menu.injectable.ts new file mode 100644 index 0000000000..633ccedbc4 --- /dev/null +++ b/src/main/menu/application-menu.injectable.ts @@ -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; diff --git a/src/main/menu/electron-menu-items.test.ts b/src/main/menu/electron-menu-items.test.ts index 7ad7dcb8e8..cc47994d82 100644 --- a/src/main/menu/electron-menu-items.test.ts +++ b/src/main/menu/electron-menu-items.test.ts @@ -16,7 +16,7 @@ describe("electron-menu-items", () => { let electronMenuItems: IComputedValue; let extensionsStub: ObservableMap; - 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); }); diff --git a/src/main/menu/menu.ts b/src/main/menu/menu.ts index 685b9a0837..fd6bc6c264 100644 --- a/src/main/menu/menu.ts +++ b/src/main/menu/menu.ts @@ -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, -) { - 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( diff --git a/src/main/menu/show-about.injectable.ts b/src/main/menu/show-about.injectable.ts new file mode 100644 index 0000000000..e79923d4c2 --- /dev/null +++ b/src/main/menu/show-about.injectable.ts @@ -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; diff --git a/src/main/menu/start-application-menu.injectable.ts b/src/main/menu/start-application-menu.injectable.ts new file mode 100644 index 0000000000..b241137cec --- /dev/null +++ b/src/main/menu/start-application-menu.injectable.ts @@ -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; diff --git a/src/main/menu/stop-application-menu.injectable.ts b/src/main/menu/stop-application-menu.injectable.ts new file mode 100644 index 0000000000..1492da32de --- /dev/null +++ b/src/main/menu/stop-application-menu.injectable.ts @@ -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; diff --git a/src/main/native-theme.ts b/src/main/native-theme.ts deleted file mode 100644 index e729245666..0000000000 --- a/src/main/native-theme.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import { nativeTheme } from "electron"; -import { broadcastMessage } from "../common/ipc"; -import { setNativeThemeChannel } from "../common/ipc/native-theme"; - -export function broadcastNativeThemeOnUpdate() { - nativeTheme.on("updated", () => { - broadcastMessage(setNativeThemeChannel, getNativeColorTheme()); - }); -} - -export function getNativeColorTheme() { - return nativeTheme.shouldUseDarkColors ? "dark" : "light"; -} diff --git a/src/main/navigate-to-route/navigate-to-route.injectable.ts b/src/main/navigate-to-route/navigate-to-route.injectable.ts index 12bf077ced..e097bf1569 100644 --- a/src/main/navigate-to-route/navigate-to-route.injectable.ts +++ b/src/main/navigate-to-route/navigate-to-route.injectable.ts @@ -13,7 +13,7 @@ const navigateToRouteInjectable = getInjectable({ instantiate: (di) => { const navigateToUrl = di.inject(navigateToUrlInjectionToken); - return (route, options) => { + return async (route, options) => { const url = buildURL(route.path, { // TODO: enhance typing params: options?.parameters as any, @@ -21,7 +21,7 @@ const navigateToRouteInjectable = getInjectable({ fragment: options?.fragment, }); - navigateToUrl(url, options); + await navigateToUrl(url, options); }; }, diff --git a/src/main/navigate-to-url/navigate-to-url.injectable.ts b/src/main/navigate-to-url/navigate-to-url.injectable.ts index 883e1387fc..fcef5680c1 100644 --- a/src/main/navigate-to-url/navigate-to-url.injectable.ts +++ b/src/main/navigate-to-url/navigate-to-url.injectable.ts @@ -3,17 +3,17 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import windowManagerInjectable from "../window-manager.injectable"; import { navigateToUrlInjectionToken } from "../../common/front-end-routing/navigate-to-url-injection-token"; +import navigateInjectable from "../start-main-application/lens-window/navigate.injectable"; const navigateToUrlInjectable = getInjectable({ id: "navigate-to-url", instantiate: (di) => { - const windowManager = di.inject(windowManagerInjectable); + const navigate = di.inject(navigateInjectable); - return (url) => { - windowManager.navigate(url); + return async (url) => { + await navigate(url); }; }, diff --git a/src/main/prometheus/prometheus-provider-registry.injectable.ts b/src/main/prometheus/prometheus-provider-registry.injectable.ts new file mode 100644 index 0000000000..bac9e1a728 --- /dev/null +++ b/src/main/prometheus/prometheus-provider-registry.injectable.ts @@ -0,0 +1,13 @@ +/** + * 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 { PrometheusProviderRegistry } from "./provider-registry"; + +const prometheusProviderRegistryInjectable = getInjectable({ + id: "prometheus-provider-registry", + instantiate: () => new PrometheusProviderRegistry(), +}); + +export default prometheusProviderRegistryInjectable; diff --git a/src/main/prometheus/provider-registry.ts b/src/main/prometheus/provider-registry.ts index e92c3c2dee..c63b22f374 100644 --- a/src/main/prometheus/provider-registry.ts +++ b/src/main/prometheus/provider-registry.ts @@ -4,7 +4,7 @@ */ import type { CoreV1Api } from "@kubernetes/client-node"; -import { isRequestError, Singleton } from "../../common/utils"; +import { isRequestError } from "../../common/utils"; export interface PrometheusService { id: string; @@ -67,7 +67,7 @@ export abstract class PrometheusProvider { } } -export class PrometheusProviderRegistry extends Singleton { +export class PrometheusProviderRegistry { public providers = new Map(); getByKind(kind: string): PrometheusProvider { diff --git a/src/main/protocol-handler/__test__/router.test.ts b/src/main/protocol-handler/__test__/router.test.ts index a7f7d34bf2..1fd6797038 100644 --- a/src/main/protocol-handler/__test__/router.test.ts +++ b/src/main/protocol-handler/__test__/router.test.ts @@ -20,6 +20,7 @@ import { LensExtension } from "../../../extensions/lens-extension"; import type { LensExtensionId } from "../../../extensions/lens-extension"; import type { ObservableMap } from "mobx"; import extensionInstancesInjectable from "../../../extensions/extension-loader/extension-instances.injectable"; +import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; jest.mock("../../../common/ipc"); @@ -46,7 +47,7 @@ describe("protocol router tests", () => { di.permitSideEffects(getConfigurationFileModelInjectable); di.permitSideEffects(appVersionInjectable); - await di.runSetups(); + di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); extensionInstances = di.inject(extensionInstancesInjectable); extensionsStore = di.inject(extensionsStoreInjectable); diff --git a/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable.ts b/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable.ts index f09a7c2b8a..630d63d82c 100644 --- a/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable.ts +++ b/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable.ts @@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable"; import extensionLoaderInjectable from "../../../extensions/extension-loader/extension-loader.injectable"; import { LensProtocolRouterMain } from "./lens-protocol-router-main"; import extensionsStoreInjectable from "../../../extensions/extensions-store/extensions-store.injectable"; +import showApplicationWindowInjectable from "../../start-main-application/lens-window/show-application-window.injectable"; const lensProtocolRouterMainInjectable = getInjectable({ id: "lens-protocol-router-main", @@ -14,6 +15,7 @@ const lensProtocolRouterMainInjectable = getInjectable({ new LensProtocolRouterMain({ extensionLoader: di.inject(extensionLoaderInjectable), extensionsStore: di.inject(extensionsStoreInjectable), + showApplicationWindow: di.inject(showApplicationWindowInjectable), }), }); diff --git a/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.ts b/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.ts index cc79840b79..56270ac6b2 100644 --- a/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.ts +++ b/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.ts @@ -12,7 +12,6 @@ import { observable, when, makeObservable } from "mobx"; import type { RouteAttempt } from "../../../common/protocol-handler"; import { ProtocolHandlerInvalid } from "../../../common/protocol-handler"; import { disposer, noop } from "../../../common/utils"; -import { WindowManager } from "../../window-manager"; import type { ExtensionLoader } from "../../../extensions/extension-loader"; import type { ExtensionsStore } from "../../../extensions/extensions-store/extensions-store"; @@ -40,11 +39,13 @@ function checkHost(url: URLParse): boolean { interface Dependencies { extensionLoader: ExtensionLoader; extensionsStore: ExtensionsStore; + showApplicationWindow: () => Promise; } export class LensProtocolRouterMain extends proto.LensProtocolRouter { private missingExtensionHandlers: FallbackHandler[] = []; + // TODO: This is used to solve out-of-place temporal dependency. Remove, and solve otherwise. @observable rendererLoaded = false; protected disposers = disposer(); @@ -73,7 +74,7 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter { throw new proto.RoutingError(proto.RoutingErrorType.INVALID_PROTOCOL, url); } - WindowManager.getInstance(false)?.ensureMainWindow().catch(noop); + this.dependencies.showApplicationWindow().catch(noop); const routeInternally = checkHost(url); logger.info(`${proto.LensProtocolRouter.LoggingPrefix}: routing ${url.toString()}`); diff --git a/src/main/protocol-handler/lens-protocol-router-main/open-deep-link-for-url/open-deep-link.injectable.ts b/src/main/protocol-handler/lens-protocol-router-main/open-deep-link-for-url/open-deep-link.injectable.ts new file mode 100644 index 0000000000..71e42b9f3c --- /dev/null +++ b/src/main/protocol-handler/lens-protocol-router-main/open-deep-link-for-url/open-deep-link.injectable.ts @@ -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 lensProtocolRouterMainInjectable from "../lens-protocol-router-main.injectable"; + +const openDeepLinkInjectable = getInjectable({ + id: "open-deep-link", + + instantiate: (di) => { + const getProtocolRouter = () => di.inject(lensProtocolRouterMainInjectable); + + return async (url: string) => { + await getProtocolRouter().route(url); + }; + }, +}); + +export default openDeepLinkInjectable; diff --git a/src/main/proxy-env.ts b/src/main/proxy-env.ts deleted file mode 100644 index e6f98554af..0000000000 --- a/src/main/proxy-env.ts +++ /dev/null @@ -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"; - -const switchValue = app.commandLine.getSwitchValue("proxy-server"); - -export function mangleProxyEnv() { - let httpsProxy = process.env.HTTPS_PROXY || process.env.HTTP_PROXY || ""; - - delete process.env.HTTPS_PROXY; - delete process.env.HTTP_PROXY; - - if (switchValue !== "") { - httpsProxy = switchValue; - } - - if (httpsProxy !== "") { - process.env.APP_HTTPS_PROXY = httpsProxy; - } -} diff --git a/src/main/router/router.test.ts b/src/main/router/router.test.ts index cc31c8de26..20b8022efc 100644 --- a/src/main/router/router.test.ts +++ b/src/main/router/router.test.ts @@ -9,37 +9,28 @@ import type { Router } from "./router"; import type { Cluster } from "../../common/cluster/cluster"; import { Request } from "mock-http"; import { getInjectable } from "@ogre-tools/injectable"; +import type { AsyncFnMock } from "@async-fn/jest"; import asyncFn from "@async-fn/jest"; import parseRequestInjectable from "./parse-request.injectable"; import { contentTypes } from "./router-content-types"; import mockFs from "mock-fs"; -import type { MockInstance } from "jest-mock"; +import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; +import type { Route } from "./route"; import type { SetRequired } from "type-fest"; -import type { Route, RouteHandler } from "./route"; - -type AsyncFnMock< - TToBeMocked extends (...args: any[]) => any, - TArguments extends Parameters = Parameters, - TResolve extends Awaited> = Awaited>, -> = MockInstance<(...args: TArguments) => Promise, TArguments> & { - resolve: (resolvedValue: TResolve) => Promise; - reject: (rejectValue?: any) => Promise; -} & ((...args: TArguments) => Promise); describe("router", () => { let router: Router; - let routeHandlerMock: AsyncFnMock>; + let routeHandlerMock: AsyncFnMock<() => any>; beforeEach(async () => { - routeHandlerMock = asyncFn() as AsyncFnMock>; + routeHandlerMock = asyncFn(); const di = getDiForUnitTesting({ doGeneralOverrides: true }); mockFs(); di.override(parseRequestInjectable, () => () => Promise.resolve({ payload: "some-payload" })); - - await di.runSetups(); + di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); const injectable = getInjectable({ id: "some-route", diff --git a/src/main/router/router.ts b/src/main/router/router.ts index 0138d14629..98ce50bac3 100644 --- a/src/main/router/router.ts +++ b/src/main/router/router.ts @@ -8,8 +8,8 @@ import type http from "http"; import { toPairs } from "lodash/fp"; import type { Cluster } from "../../common/cluster/cluster"; import { contentTypes } from "./router-content-types"; -import type { ServerIncomingMessage } from "../lens-proxy"; import type { LensApiRequest, LensApiResult, Route } from "./route"; +import type { ServerIncomingMessage } from "../lens-proxy/lens-proxy"; export interface RouterRequestOpts { req: http.IncomingMessage; diff --git a/src/main/routes/metrics/add-metrics-route.injectable.ts b/src/main/routes/metrics/add-metrics-route.injectable.ts index c78c33249c..8bd6ad2c72 100644 --- a/src/main/routes/metrics/add-metrics-route.injectable.ts +++ b/src/main/routes/metrics/add-metrics-route.injectable.ts @@ -9,16 +9,17 @@ import type { ClusterPrometheusMetadata } from "../../../common/cluster-types"; import { ClusterMetadataKey } from "../../../common/cluster-types"; import logger from "../../logger"; import type { Cluster } from "../../../common/cluster/cluster"; -import { getMetrics } from "../../k8s-request"; import type { IMetricsQuery } from "./metrics-query"; import { clusterRoute } from "../../router/route"; import { isObject } from "lodash"; import { isRequestError } from "../../../common/utils"; +import type { GetMetrics } from "../../get-metrics.injectable"; +import getMetricsInjectable from "../../get-metrics.injectable"; // This is used for backoff retry tracking. const ATTEMPTS = [false, false, false, false, true]; -async function loadMetrics(promQueries: string[], cluster: Cluster, prometheusPath: string, queryParams: Record): Promise { +const loadMetricsFor = (getMetrics: GetMetrics) => async (promQueries: string[], cluster: Cluster, prometheusPath: string, queryParams: Record): Promise => { const queries = promQueries.map(p => p.trim()); const loaders = new Map>(); @@ -47,15 +48,18 @@ async function loadMetrics(promQueries: string[], cluster: Cluster, prometheusPa } return Promise.all(queries.map(loadMetric)); -} +}; const addMetricsRouteInjectable = getRouteInjectable({ id: "add-metrics-route", - instantiate: () => clusterRoute({ + instantiate: (di) => clusterRoute({ method: "post", path: `${apiPrefix}/metrics`, })(async ({ cluster, payload, query }) => { + const getMetrics = di.inject(getMetricsInjectable); + const loadMetrics = loadMetricsFor(getMetrics); + const queryParams: IMetricsQuery = Object.fromEntries(query.entries()); const prometheusMetadata: ClusterPrometheusMetadata = {}; diff --git a/src/main/routes/metrics/get-metric-providers-route.injectable.ts b/src/main/routes/metrics/get-metric-providers-route.injectable.ts index 6284bd916e..22b2fbf11b 100644 --- a/src/main/routes/metrics/get-metric-providers-route.injectable.ts +++ b/src/main/routes/metrics/get-metric-providers-route.injectable.ts @@ -5,24 +5,27 @@ import { apiPrefix } from "../../../common/vars"; import { getRouteInjectable } from "../../router/router.injectable"; -import { PrometheusProviderRegistry } from "../../prometheus"; import { route } from "../../router/route"; +import prometheusProviderRegistryInjectable from "../../prometheus/prometheus-provider-registry.injectable"; const getMetricProvidersRouteInjectable = getRouteInjectable({ id: "get-metric-providers-route", - instantiate: () => route({ - method: "get", - path: `${apiPrefix}/metrics/providers`, - })(() => ({ - response: Array.from( - PrometheusProviderRegistry - .getInstance() - .providers - .values(), - ({ name, id, isConfigurable }) => ({ name, id, isConfigurable }), - ), - })), + instantiate: (di) => { + const prometheusProviderRegistry = di.inject(prometheusProviderRegistryInjectable); + + return route({ + method: "get", + path: `${apiPrefix}/metrics/providers`, + })(() => ({ + response: Array.from( + prometheusProviderRegistry + .providers + .values(), + ({ name, id, isConfigurable }) => ({ name, id, isConfigurable }), + ), + })); + }, }); export default getMetricProvidersRouteInjectable; diff --git a/src/main/routes/static-file-route.injectable.ts b/src/main/routes/static-file-route.injectable.ts index 479cc88e4c..86fa4b2fc5 100644 --- a/src/main/routes/static-file-route.injectable.ts +++ b/src/main/routes/static-file-route.injectable.ts @@ -6,7 +6,7 @@ import type { SupportedFileExtension } from "../router/router-content-types"; import { contentTypes } from "../router/router-content-types"; import logger from "../logger"; import { getRouteInjectable } from "../router/router.injectable"; -import { appName, publicPath, staticFilesDirectory } from "../../common/vars"; +import { appName, publicPath } from "../../common/vars"; import path from "path"; import isDevelopmentInjectable from "../../common/vars/is-development.injectable"; import httpProxy from "http-proxy"; @@ -18,24 +18,25 @@ import joinPathsInjectable from "../../common/path/join-paths.injectable"; import { webpackDevServerPort } from "../../../webpack/vars"; import type { LensApiRequest, RouteResponse } from "../router/route"; import { route } from "../router/route"; +import staticFilesDirectoryInjectable from "../../common/vars/static-files-directory.injectable"; interface ProductionDependencies { readFileBuffer: (path: string) => Promise; getAbsolutePath: GetAbsolutePath; joinPaths: JoinPaths; + staticFilesDirectory: string; } const handleStaticFileInProduction = - ({ readFileBuffer, getAbsolutePath, joinPaths }: ProductionDependencies) => + ({ readFileBuffer, getAbsolutePath, joinPaths, staticFilesDirectory }: ProductionDependencies) => async ({ params }: LensApiRequest<"/{path*}">): Promise> => { - const staticPath = getAbsolutePath(staticFilesDirectory); let filePath = params.path; for (let retryCount = 0; retryCount < 5; retryCount += 1) { - const asset = joinPaths(staticPath, filePath); + const asset = joinPaths(staticFilesDirectory, filePath); const normalizedFilePath = getAbsolutePath(asset); - if (!normalizedFilePath.startsWith(staticPath)) { + if (!normalizedFilePath.startsWith(staticFilesDirectory)) { return { statusCode: 404 }; } @@ -84,6 +85,10 @@ const staticFileRouteInjectable = getRouteInjectable({ instantiate: (di) => { const isDevelopment = di.inject(isDevelopmentInjectable); + const readFileBuffer = di.inject(readFileBufferInjectable); + const getAbsolutePath = di.inject(getAbsolutePathInjectable); + const joinPaths = di.inject(joinPathsInjectable); + const staticFilesDirectory = di.inject(staticFilesDirectoryInjectable); return route({ method: "get", @@ -94,9 +99,10 @@ const staticFileRouteInjectable = getRouteInjectable({ proxy: httpProxy.createProxy(), }) : handleStaticFileInProduction({ - readFileBuffer: di.inject(readFileBufferInjectable), - getAbsolutePath: di.inject(getAbsolutePathInjectable), - joinPaths: di.inject(joinPathsInjectable), + readFileBuffer, + getAbsolutePath, + joinPaths, + staticFilesDirectory, }), ); }, diff --git a/src/main/shell-session/shell-env-modifier/terminal-shell-env-modifiers.ts b/src/main/shell-session/shell-env-modifier/terminal-shell-env-modifiers.ts index 259a1a7c65..bf6a290052 100644 --- a/src/main/shell-session/shell-env-modifier/terminal-shell-env-modifiers.ts +++ b/src/main/shell-session/shell-env-modifier/terminal-shell-env-modifiers.ts @@ -8,13 +8,14 @@ import { computed } from "mobx"; import type { ClusterId } from "../../../common/cluster-types"; import { isDefined } from "../../../common/utils"; import type { LensMainExtension } from "../../../extensions/lens-main-extension"; -import { catalogEntityRegistry } from "../../catalog"; +import type { CatalogEntityRegistry } from "../../catalog"; interface Dependencies { extensions: IComputedValue; + catalogEntityRegistry: CatalogEntityRegistry; } -export const terminalShellEnvModify = ({ extensions }: Dependencies) => +export const terminalShellEnvModify = ({ extensions, catalogEntityRegistry }: Dependencies) => (clusterId: ClusterId, env: Record) => { const terminalShellEnvModifiers = computed(() => ( extensions.get() diff --git a/src/main/shell-session/shell-env-modifier/terminal-shell-env-modify.injectable.ts b/src/main/shell-session/shell-env-modifier/terminal-shell-env-modify.injectable.ts index c9d345c3b2..7d8cb37a1c 100644 --- a/src/main/shell-session/shell-env-modifier/terminal-shell-env-modify.injectable.ts +++ b/src/main/shell-session/shell-env-modifier/terminal-shell-env-modify.injectable.ts @@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable"; import mainExtensionsInjectable from "../../../extensions/main-extensions.injectable"; import { terminalShellEnvModify } from "./terminal-shell-env-modifiers"; +import catalogEntityRegistryInjectable from "../../catalog/entity-registry.injectable"; const terminalShellEnvModifyInjectable = getInjectable({ id: "terminal-shell-env-modify", @@ -13,6 +14,7 @@ const terminalShellEnvModifyInjectable = getInjectable({ instantiate: (di) => terminalShellEnvModify({ extensions: di.inject(mainExtensionsInjectable), + catalogEntityRegistry: di.inject(catalogEntityRegistryInjectable), }), }); diff --git a/src/main/start-main-application/lens-window/application-window/application-window-state.injectable.ts b/src/main/start-main-application/lens-window/application-window/application-window-state.injectable.ts new file mode 100644 index 0000000000..4ba8f9afd2 --- /dev/null +++ b/src/main/start-main-application/lens-window/application-window/application-window-state.injectable.ts @@ -0,0 +1,32 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import windowStateKeeper from "electron-window-state"; + +interface WindowStateConfiguration { + id: string; + defaultHeight: number; + defaultWidth: number; +} + +const applicationWindowStateInjectable = getInjectable({ + id: "application-window-state", + + instantiate: ( + di, + { id, defaultHeight, defaultWidth }: WindowStateConfiguration, + ) => + windowStateKeeper({ + defaultHeight, + defaultWidth, + file: `window-state-for-${id}.json`, + }), + + lifecycle: lifecycleEnum.keyedSingleton({ + getInstanceKey: (di, { id }: WindowStateConfiguration) => id, + }), +}); + +export default applicationWindowStateInjectable; diff --git a/src/main/start-main-application/lens-window/application-window/application-window.injectable.ts b/src/main/start-main-application/lens-window/application-window/application-window.injectable.ts new file mode 100644 index 0000000000..50c7d5abfb --- /dev/null +++ b/src/main/start-main-application/lens-window/application-window/application-window.injectable.ts @@ -0,0 +1,68 @@ +/** + * 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 { lensWindowInjectionToken } from "./lens-window-injection-token"; +import createLensWindowInjectable from "./create-lens-window.injectable"; +import lensProxyPortInjectable from "../../../lens-proxy/lens-proxy-port.injectable"; +import isMacInjectable from "../../../../common/vars/is-mac.injectable"; +import appNameInjectable from "../../../app-paths/app-name/app-name.injectable"; +import appEventBusInjectable from "../../../../common/app-event-bus/app-event-bus.injectable"; +import { delay } from "../../../../common/utils"; +import { bundledExtensionsLoaded } from "../../../../common/ipc/extension-handling"; +import ipcMainInjectable from "../../../app-paths/register-channel/ipc-main/ipc-main.injectable"; + +const applicationWindowInjectable = getInjectable({ + id: "application-window", + + instantiate: (di) => { + const createLensWindow = di.inject(createLensWindowInjectable); + const isMac = di.inject(isMacInjectable); + const applicationName = di.inject(appNameInjectable); + const appEventBus = di.inject(appEventBusInjectable); + const ipcMain = di.inject(ipcMainInjectable); + + const lensProxyPort = di.inject( + lensProxyPortInjectable, + ); + + const getContentUrl = () => `http://localhost:${lensProxyPort.get()}`; + + return createLensWindow({ + id: "only-application-window", + title: applicationName, + defaultHeight: 900, + defaultWidth: 1440, + getContentUrl, + resizable: true, + windowFrameUtilitiesAreShown: isMac, + centered: false, + + onFocus: () => { + appEventBus.emit({ name: "app", action: "focus" }); + }, + + onBlur: () => { + appEventBus.emit({ name: "app", action: "blur" }); + }, + + onDomReady: () => { + appEventBus.emit({ name: "app", action: "dom-ready" }); + }, + + beforeOpen: async () => { + const viewHasLoaded = new Promise((resolve) => { + ipcMain.once(bundledExtensionsLoaded, () => resolve()); + }); + + await viewHasLoaded; + await delay(50); // wait just a bit longer to let the first round of rendering happen + }, + }); + }, + + injectionToken: lensWindowInjectionToken, +}); + +export default applicationWindowInjectable; diff --git a/src/main/start-main-application/lens-window/application-window/create-electron-window-for.injectable.ts b/src/main/start-main-application/lens-window/application-window/create-electron-window-for.injectable.ts new file mode 100644 index 0000000000..4239692b2c --- /dev/null +++ b/src/main/start-main-application/lens-window/application-window/create-electron-window-for.injectable.ts @@ -0,0 +1,186 @@ +/** + * 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 applicationWindowStateInjectable from "./application-window-state.injectable"; +import isMacInjectable from "../../../../common/vars/is-mac.injectable"; +import { BrowserWindow } from "electron"; +import { openBrowser } from "../../../../common/utils"; +import type { SendToViewArgs } from "./lens-window-injection-token"; +import sendToChannelInElectronBrowserWindowInjectable from "./send-to-channel-in-electron-browser-window.injectable"; +import type { LensWindow } from "./create-lens-window.injectable"; + +interface ElectronWindowConfiguration { + id: string; + title: string; + defaultHeight: number; + defaultWidth: number; + getContentUrl: () => string; + resizable: boolean; + windowFrameUtilitiesAreShown: boolean; + centered: boolean; + + beforeOpen?: () => Promise; + onClose: () => void; + onFocus?: () => void; + onBlur?: () => void; + onDomReady?: () => void; +} + +const createElectronWindowFor = getInjectable({ + id: "create-electron-window-for", + + instantiate: (di) => { + const logger = di.inject(loggerInjectable); + const isMac = di.inject(isMacInjectable); + + const sendToChannelInLensWindow = di.inject( + sendToChannelInElectronBrowserWindowInjectable, + ); + + return (configuration: ElectronWindowConfiguration) => async (): Promise => { + const applicationWindowState = di.inject( + applicationWindowStateInjectable, + { + id: configuration.id, + defaultHeight: configuration.defaultHeight, + defaultWidth: configuration.defaultWidth, + }, + ); + + const { width, height, x, y } = applicationWindowState; + + const browserWindow = new BrowserWindow({ + x, + y, + width, + height, + title: configuration.title, + resizable: configuration.resizable, + center: configuration.centered, + frame: configuration.windowFrameUtilitiesAreShown, + show: false, + minWidth: 700, // accommodate 800 x 600 display minimum + minHeight: 500, // accommodate 800 x 600 display minimum + titleBarStyle: isMac ? "hiddenInset" : "hidden", + backgroundColor: "#1e2124", + + webPreferences: { + nodeIntegration: true, + nodeIntegrationInSubFrames: true, + webviewTag: true, + contextIsolation: false, + nativeWindowOpen: false, + }, + }); + + applicationWindowState.manage(browserWindow); + + browserWindow + .on("focus", () => { + configuration.onFocus?.(); + }) + + .on("blur", () => { + configuration.onBlur?.(); + }) + + .on("closed", () => { + configuration.onClose(); + applicationWindowState.unmanage(); + }) + + .webContents.on("dom-ready", () => { + configuration.onDomReady?.(); + }) + + .on("did-fail-load", (_event, code, desc) => { + logger.error( + `[CREATE-ELECTRON-WINDOW]: Failed to load window "${configuration.id}"`, + { + code, + desc, + }, + ); + }) + + .on("did-finish-load", () => { + logger.info( + `[CREATE-ELECTRON-WINDOW]: Window "${configuration.id}" loaded`, + ); + }) + + .on("will-attach-webview", (event, webPreferences, params) => { + logger.debug( + `[CREATE-ELECTRON-WINDOW]: Attaching webview to window "${configuration.id}"`, + ); + // Following is security recommendations because we allow webview tag (webviewTag: true) + // suggested by https://www.electronjs.org/docs/tutorial/security#11-verify-webview-options-before-creation + // and https://www.electronjs.org/docs/tutorial/security#10-do-not-use-allowpopups + + if (webPreferences.preload) { + logger.warn( + "[CREATE-ELECTRON-WINDOW]: Strip away preload scripts of webview", + ); + delete webPreferences.preload; + } + + // @ts-expect-error some electron version uses webPreferences.preloadURL/webPreferences.preload + if (webPreferences.preloadURL) { + logger.warn( + "[CREATE-ELECTRON-WINDOW]: Strip away preload scripts of webview", + ); + delete webPreferences.preload; + } + + if (params.allowpopups) { + logger.warn( + "[CREATE-ELECTRON-WINDOW]: We do not allow allowpopups props, stop webview from renderer", + ); + + // event.preventDefault() will destroy the guest page. + event.preventDefault(); + + return; + } + + // Always disable Node.js integration for all webviews + webPreferences.nodeIntegration = false; + }) + + .setWindowOpenHandler((details) => { + openBrowser(details.url).catch((error) => { + logger.error("[CREATE-ELECTRON-WINDOW]: failed to open browser", { + error, + }); + }); + + return { action: "deny" }; + }); + + const contentUrl = configuration.getContentUrl(); + + logger.info( + `[CREATE-ELECTRON-WINDOW]: Loading content for window "${configuration.id}" from url: ${contentUrl}...`, + ); + + await browserWindow.loadURL(contentUrl); + + await configuration.beforeOpen?.(); + + return { + show: () => browserWindow.show(), + close: () => browserWindow.close(), + + send: (args: SendToViewArgs) => + sendToChannelInLensWindow(browserWindow, args), + }; + }; + }, + + causesSideEffects: true, +}); + +export default createElectronWindowFor; diff --git a/src/main/start-main-application/lens-window/application-window/create-lens-window.injectable.ts b/src/main/start-main-application/lens-window/application-window/create-lens-window.injectable.ts new file mode 100644 index 0000000000..aafd0050a4 --- /dev/null +++ b/src/main/start-main-application/lens-window/application-window/create-lens-window.injectable.ts @@ -0,0 +1,89 @@ +/** + * 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 { SendToViewArgs } from "./lens-window-injection-token"; +import createElectronWindowForInjectable from "./create-electron-window-for.injectable"; + +export interface LensWindow { + show: () => void; + close: () => void; + send: (args: SendToViewArgs) => void; +} + +interface LensWindowConfiguration { + id: string; + title: string; + defaultHeight: number; + defaultWidth: number; + getContentUrl: () => string; + resizable: boolean; + windowFrameUtilitiesAreShown: boolean; + centered: boolean; + + beforeOpen?: () => Promise; + onFocus?: () => void; + onBlur?: () => void; + onDomReady?: () => void; +} + +const createLensWindowInjectable = getInjectable({ + id: "create-lens-window", + + instantiate: + (di) => + (configuration: LensWindowConfiguration) => { + let browserWindow: LensWindow | undefined; + + const createElectronWindow = di.inject(createElectronWindowForInjectable)( + { + id: configuration.id, + title: configuration.title, + defaultHeight: configuration.defaultHeight, + defaultWidth: configuration.defaultWidth, + getContentUrl: configuration.getContentUrl, + resizable: configuration.resizable, + windowFrameUtilitiesAreShown: configuration.windowFrameUtilitiesAreShown, + centered: configuration.centered, + onFocus: configuration.onFocus, + onBlur: configuration.onBlur, + onDomReady: configuration.onDomReady, + beforeOpen: configuration.beforeOpen, + + onClose: () => { + browserWindow = undefined; + }, + }, + ); + + return { + get visible() { + return !!browserWindow; + }, + + show: async () => { + if (!browserWindow) { + browserWindow = await createElectronWindow(); + } + + browserWindow.show(); + }, + + close: () => { + browserWindow?.close(); + browserWindow = undefined; + }, + + send: async (args: SendToViewArgs) => { + if (!browserWindow) { + browserWindow = await createElectronWindow(); + } + + return browserWindow.send(args); + }, + }; + }, +}); + +export default createLensWindowInjectable; diff --git a/src/main/start-main-application/lens-window/application-window/lens-window-injection-token.ts b/src/main/start-main-application/lens-window/application-window/lens-window-injection-token.ts new file mode 100644 index 0000000000..f7273206c9 --- /dev/null +++ b/src/main/start-main-application/lens-window/application-window/lens-window-injection-token.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectionToken } from "@ogre-tools/injectable"; +import type { ClusterFrameInfo } from "../../../../common/cluster-frames"; + +export interface SendToViewArgs { + channel: string; + frameInfo?: ClusterFrameInfo; + data?: unknown[]; +} + +export interface LensWindow { + show: () => Promise; + close: () => void; + send: (args: SendToViewArgs) => Promise; + visible: boolean; +} + +export const lensWindowInjectionToken = getInjectionToken({ + id: "lens-window", +}); diff --git a/src/main/start-main-application/lens-window/application-window/send-to-channel-in-electron-browser-window.injectable.ts b/src/main/start-main-application/lens-window/application-window/send-to-channel-in-electron-browser-window.injectable.ts new file mode 100644 index 0000000000..32422b6a94 --- /dev/null +++ b/src/main/start-main-application/lens-window/application-window/send-to-channel-in-electron-browser-window.injectable.ts @@ -0,0 +1,32 @@ +/** + * 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 { BrowserWindow } from "electron"; +import type { SendToViewArgs } from "./lens-window-injection-token"; + +const sendToChannelInElectronBrowserWindowInjectable = getInjectable({ + id: "send-to-channel-in-electron-browser-window", + + instantiate: + () => + ( + browserWindow: BrowserWindow, + { channel, frameInfo, data = [] }: SendToViewArgs, + ) => { + if (frameInfo) { + browserWindow.webContents.sendToFrame( + [frameInfo.processId, frameInfo.frameId], + channel, + ...data, + ); + } else { + browserWindow.webContents.send(channel, ...data); + } + }, + + causesSideEffects: true, +}); + +export default sendToChannelInElectronBrowserWindowInjectable; diff --git a/src/main/start-main-application/lens-window/current-cluster-frame/current-cluster-frame-cluster-id-state.injectable.ts b/src/main/start-main-application/lens-window/current-cluster-frame/current-cluster-frame-cluster-id-state.injectable.ts new file mode 100644 index 0000000000..b45447db5e --- /dev/null +++ b/src/main/start-main-application/lens-window/current-cluster-frame/current-cluster-frame-cluster-id-state.injectable.ts @@ -0,0 +1,15 @@ +/** + * 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 { observable } from "mobx"; +import type { ClusterId } from "../../../../common/cluster-types"; + +const currentClusterFrameClusterIdStateInjectable = getInjectable({ + id: "current-cluster-frame-cluster-id-state", + + instantiate: () => observable.box(), +}); + +export default currentClusterFrameClusterIdStateInjectable; diff --git a/src/main/start-main-application/lens-window/current-cluster-frame/current-cluster-frame.injectable.ts b/src/main/start-main-application/lens-window/current-cluster-frame/current-cluster-frame.injectable.ts new file mode 100644 index 0000000000..2749b23253 --- /dev/null +++ b/src/main/start-main-application/lens-window/current-cluster-frame/current-cluster-frame.injectable.ts @@ -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 { computed } from "mobx"; +import currentClusterFrameClusterIdStateInjectable from "./current-cluster-frame-cluster-id-state.injectable"; +import clusterFramesInjectable from "../../../../common/cluster-frames.injectable"; + +const currentClusterFrameInjectable = getInjectable({ + id: "current-cluster-frame", + + instantiate: (di) => { + const currentClusterFrameState = di.inject(currentClusterFrameClusterIdStateInjectable); + const clusterFrames = di.inject(clusterFramesInjectable); + + return computed(() => { + const clusterId = currentClusterFrameState.get(); + + return clusterFrames.get(clusterId); + }); + }, +}); + +export default currentClusterFrameInjectable; diff --git a/src/main/start-main-application/lens-window/current-cluster-frame/setup-listener-for-current-cluster-frame.injectable.ts b/src/main/start-main-application/lens-window/current-cluster-frame/setup-listener-for-current-cluster-frame.injectable.ts new file mode 100644 index 0000000000..fa8b36c03a --- /dev/null +++ b/src/main/start-main-application/lens-window/current-cluster-frame/setup-listener-for-current-cluster-frame.injectable.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { ipcMainOn } from "../../../../common/ipc"; +import { IpcRendererNavigationEvents } from "../../../../renderer/navigation/events"; +import type { ClusterId } from "../../../../common/cluster-types"; + +import { getInjectable } from "@ogre-tools/injectable"; +import { onLoadOfApplicationInjectionToken } from "../../runnable-tokens/on-load-of-application-injection-token"; +import currentClusterFrameClusterIdStateInjectable from "./current-cluster-frame-cluster-id-state.injectable"; + +const setupListenerForCurrentClusterFrameInjectable = getInjectable({ + id: "setup-listener-for-current-cluster-frame", + + instantiate: (di) => ({ + run: () => { + const currentClusterFrameState = di.inject(currentClusterFrameClusterIdStateInjectable); + + ipcMainOn( + IpcRendererNavigationEvents.CLUSTER_VIEW_CURRENT_ID, + (event, clusterId: ClusterId) => { + currentClusterFrameState.set(clusterId); + }, + ); + }, + }), + + causesSideEffects: true, + + injectionToken: onLoadOfApplicationInjectionToken, +}); + +export default setupListenerForCurrentClusterFrameInjectable; diff --git a/src/main/start-main-application/lens-window/hide-all-windows/close-all-windows.injectable.ts b/src/main/start-main-application/lens-window/hide-all-windows/close-all-windows.injectable.ts new file mode 100644 index 0000000000..f9bbd34935 --- /dev/null +++ b/src/main/start-main-application/lens-window/hide-all-windows/close-all-windows.injectable.ts @@ -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 { lensWindowInjectionToken } from "../application-window/lens-window-injection-token"; + +const closeAllWindowsInjectable = getInjectable({ + id: "close-all-windows", + + instantiate: (di) => () => { + const lensWindows = di.injectMany(lensWindowInjectionToken); + + lensWindows.forEach((lensWindow) => { + lensWindow.close(); + }); + }, +}); + +export default closeAllWindowsInjectable; diff --git a/src/main/start-main-application/lens-window/navigate-for-extension.injectable.ts b/src/main/start-main-application/lens-window/navigate-for-extension.injectable.ts new file mode 100644 index 0000000000..703ef15f60 --- /dev/null +++ b/src/main/start-main-application/lens-window/navigate-for-extension.injectable.ts @@ -0,0 +1,48 @@ +/** + * 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 { iter } from "../../../common/utils"; +import clusterFramesInjectable from "../../../common/cluster-frames.injectable"; +import showApplicationWindowInjectable from "./show-application-window.injectable"; +import applicationWindowInjectable from "./application-window/application-window.injectable"; + +export type NavigateForExtension = ( + extId: string, + pageId?: string, + params?: Record, + frameId?: number +) => Promise; + +const navigateForExtensionInjectable = getInjectable({ + id: "navigate-for-extension", + + instantiate: (di): NavigateForExtension => { + const applicationWindow = di.inject(applicationWindowInjectable); + const clusterFrames = di.inject(clusterFramesInjectable); + const showApplicationWindow = di.inject(showApplicationWindowInjectable); + + return async ( + extId: string, + pageId?: string, + params?: Record, + frameId?: number, + ) => { + await showApplicationWindow(); + + const frameInfo = iter.find( + clusterFrames.values(), + (frameInfo) => frameInfo.frameId === frameId, + ); + + await applicationWindow.send({ + channel: "extension:navigate", + frameInfo, + data: [extId, pageId, params], + }); + }; + }, +}); + +export default navigateForExtensionInjectable; diff --git a/src/main/start-main-application/lens-window/navigate.injectable.ts b/src/main/start-main-application/lens-window/navigate.injectable.ts new file mode 100644 index 0000000000..c7cbb10d24 --- /dev/null +++ b/src/main/start-main-application/lens-window/navigate.injectable.ts @@ -0,0 +1,41 @@ +/** + * 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 { iter } from "../../../common/utils"; +import applicationWindowInjectable from "./application-window/application-window.injectable"; +import clusterFramesInjectable from "../../../common/cluster-frames.injectable"; +import { IpcRendererNavigationEvents } from "../../../renderer/navigation/events"; +import showApplicationWindowInjectable from "./show-application-window.injectable"; + +const navigateInjectable = getInjectable({ + id: "navigate", + + instantiate: (di) => { + const applicationWindow = di.inject(applicationWindowInjectable); + const showApplicationWindow = di.inject(showApplicationWindowInjectable); + const clusterFrames = di.inject(clusterFramesInjectable); + + return async (url: string, frameId?: number) => { + await showApplicationWindow(); + + const frameInfo = iter.find( + clusterFrames.values(), + (frameInfo) => frameInfo.frameId === frameId, + ); + + const channel = frameInfo + ? IpcRendererNavigationEvents.NAVIGATE_IN_CLUSTER + : IpcRendererNavigationEvents.NAVIGATE_IN_APP; + + await applicationWindow.send({ + channel, + frameInfo, + data: [url], + }); + }; + }, +}); + +export default navigateInjectable; diff --git a/src/main/start-main-application/lens-window/reload-all-windows.injectable.ts b/src/main/start-main-application/lens-window/reload-all-windows.injectable.ts new file mode 100644 index 0000000000..fca4da5e13 --- /dev/null +++ b/src/main/start-main-application/lens-window/reload-all-windows.injectable.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { webContents } from "electron"; + +const reloadAllWindowsInjectable = getInjectable({ + id: "reload-all-windows", + + instantiate: () => () => { + webContents + .getAllWebContents() + .filter((wc) => wc.getType() === "window") + .forEach((wc) => { + wc.reload(); + wc.clearHistory(); + }); + }, + + causesSideEffects: true, +}); + +export default reloadAllWindowsInjectable; diff --git a/src/main/start-main-application/lens-window/reload-window.injectable.ts b/src/main/start-main-application/lens-window/reload-window.injectable.ts new file mode 100644 index 0000000000..99d3f22f14 --- /dev/null +++ b/src/main/start-main-application/lens-window/reload-window.injectable.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; +import type { LensWindow } from "./application-window/lens-window-injection-token"; +import { IpcRendererNavigationEvents } from "../../../renderer/navigation/events"; +import currentClusterFrameInjectable from "./current-cluster-frame/current-cluster-frame.injectable"; +import reloadAllWindowsInjectable from "./reload-all-windows.injectable"; + +const reloadWindowInjectable = getInjectable({ + id: "reload-window", + + instantiate: (di, lensWindow: LensWindow) => () => { + const currentClusterIframe = di.inject(currentClusterFrameInjectable); + const reloadAllWindows = di.inject(reloadAllWindowsInjectable); + + const frameInfo = currentClusterIframe.get(); + + if (frameInfo) { + lensWindow.send({ + channel: IpcRendererNavigationEvents.RELOAD_PAGE, + frameInfo, + }); + } else { + reloadAllWindows(); + } + }, + + lifecycle: lifecycleEnum.transient, +}); + +export default reloadWindowInjectable; diff --git a/src/main/start-main-application/lens-window/show-application-window.injectable.ts b/src/main/start-main-application/lens-window/show-application-window.injectable.ts new file mode 100644 index 0000000000..b514c41891 --- /dev/null +++ b/src/main/start-main-application/lens-window/show-application-window.injectable.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import splashWindowInjectable from "./splash-window/splash-window.injectable"; +import applicationWindowInjectable from "./application-window/application-window.injectable"; + +const showApplicationWindowInjectable = getInjectable({ + id: "show-application-window", + + instantiate: (di) => { + const applicationWindow = di.inject(applicationWindowInjectable); + + const splashWindow = di.inject( + splashWindowInjectable, + ); + + return async () => { + if (applicationWindow.visible) { + return; + } + + await splashWindow.show(); + + await applicationWindow.show(); + + splashWindow.close(); + }; + }, +}); + +export default showApplicationWindowInjectable; diff --git a/src/main/start-main-application/lens-window/splash-window/splash-window.injectable.ts b/src/main/start-main-application/lens-window/splash-window/splash-window.injectable.ts new file mode 100644 index 0000000000..bbef47f140 --- /dev/null +++ b/src/main/start-main-application/lens-window/splash-window/splash-window.injectable.ts @@ -0,0 +1,30 @@ +/** + * 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 { lensWindowInjectionToken } from "../application-window/lens-window-injection-token"; +import createLensWindowInjectable from "../application-window/create-lens-window.injectable"; + +const splashWindowInjectable = getInjectable({ + id: "splash-window", + + instantiate: (di) => { + const createLensWindow = di.inject(createLensWindowInjectable); + + return createLensWindow({ + id: "splash", + title: "Loading", + getContentUrl: () => "static://splash.html", + defaultWidth: 500, + defaultHeight: 300, + resizable: false, + windowFrameUtilitiesAreShown: false, + centered: true, + }); + }, + + injectionToken: lensWindowInjectionToken, +}); + +export default splashWindowInjectable; diff --git a/src/main/start-main-application/runnable-tokens/after-application-is-loaded-injection-token.ts b/src/main/start-main-application/runnable-tokens/after-application-is-loaded-injection-token.ts new file mode 100644 index 0000000000..f89f445ecf --- /dev/null +++ b/src/main/start-main-application/runnable-tokens/after-application-is-loaded-injection-token.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectionToken } from "@ogre-tools/injectable"; +import type { Runnable } from "../../../common/runnable/run-many-for"; + +export const afterApplicationIsLoadedInjectionToken = getInjectionToken({ + id: "after-application-is-loaded", +}); diff --git a/src/main/start-main-application/runnable-tokens/after-root-frame-is-ready-injection-token.ts b/src/main/start-main-application/runnable-tokens/after-root-frame-is-ready-injection-token.ts new file mode 100644 index 0000000000..f066c124ba --- /dev/null +++ b/src/main/start-main-application/runnable-tokens/after-root-frame-is-ready-injection-token.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectionToken } from "@ogre-tools/injectable"; +import type { Runnable } from "../../../common/runnable/run-many-for"; + +export const afterRootFrameIsReadyInjectionToken = getInjectionToken({ + id: "after-root-frame-is-ready", +}); diff --git a/src/main/start-main-application/runnable-tokens/after-window-is-opened-injection-token.ts b/src/main/start-main-application/runnable-tokens/after-window-is-opened-injection-token.ts new file mode 100644 index 0000000000..d5f33bceff --- /dev/null +++ b/src/main/start-main-application/runnable-tokens/after-window-is-opened-injection-token.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectionToken } from "@ogre-tools/injectable"; +import type { Runnable } from "../../../common/runnable/run-many-for"; + +export const afterWindowIsOpenedInjectionToken = getInjectionToken({ + id: "after-window-is-opened", +}); diff --git a/src/main/start-main-application/runnable-tokens/before-application-is-loading-injection-token.ts b/src/main/start-main-application/runnable-tokens/before-application-is-loading-injection-token.ts new file mode 100644 index 0000000000..7cda9e6aee --- /dev/null +++ b/src/main/start-main-application/runnable-tokens/before-application-is-loading-injection-token.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectionToken } from "@ogre-tools/injectable"; +import type { Runnable } from "../../../common/runnable/run-many-for"; + +export const beforeApplicationIsLoadingInjectionToken = getInjectionToken({ + id: "before-application-is-loading", +}); diff --git a/src/main/start-main-application/runnable-tokens/before-electron-is-ready-injection-token.ts b/src/main/start-main-application/runnable-tokens/before-electron-is-ready-injection-token.ts new file mode 100644 index 0000000000..08173ebef2 --- /dev/null +++ b/src/main/start-main-application/runnable-tokens/before-electron-is-ready-injection-token.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectionToken } from "@ogre-tools/injectable"; +import type { RunnableSync } from "../../../common/runnable/run-many-sync-for"; + +export const beforeElectronIsReadyInjectionToken = + getInjectionToken({ + id: "before-electron-is-ready", + }); diff --git a/src/main/start-main-application/runnable-tokens/before-quit-of-back-end-injection-token.ts b/src/main/start-main-application/runnable-tokens/before-quit-of-back-end-injection-token.ts new file mode 100644 index 0000000000..bdb1d1e1be --- /dev/null +++ b/src/main/start-main-application/runnable-tokens/before-quit-of-back-end-injection-token.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectionToken } from "@ogre-tools/injectable"; +import type { RunnableSync } from "../../../common/runnable/run-many-sync-for"; + +export const beforeQuitOfBackEndInjectionToken = + getInjectionToken({ + id: "before-quit-of-back-end", + }); diff --git a/src/main/start-main-application/runnable-tokens/before-quit-of-front-end-injection-token.ts b/src/main/start-main-application/runnable-tokens/before-quit-of-front-end-injection-token.ts new file mode 100644 index 0000000000..2327688cb3 --- /dev/null +++ b/src/main/start-main-application/runnable-tokens/before-quit-of-front-end-injection-token.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectionToken } from "@ogre-tools/injectable"; +import type { RunnableSync } from "../../../common/runnable/run-many-sync-for"; + +export const beforeQuitOfFrontEndInjectionToken = + getInjectionToken({ + id: "before-quit-of-front-end", + }); diff --git a/src/main/start-main-application/runnable-tokens/on-load-of-application-injection-token.ts b/src/main/start-main-application/runnable-tokens/on-load-of-application-injection-token.ts new file mode 100644 index 0000000000..35b7a6c0ff --- /dev/null +++ b/src/main/start-main-application/runnable-tokens/on-load-of-application-injection-token.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectionToken } from "@ogre-tools/injectable"; +import type { Runnable } from "../../../common/runnable/run-many-for"; + +export const onLoadOfApplicationInjectionToken = getInjectionToken({ + id: "on-load-of-application", +}); diff --git a/src/main/start-main-application/runnables/clean-up-shell-sessions.injectable.ts b/src/main/start-main-application/runnables/clean-up-shell-sessions.injectable.ts new file mode 100644 index 0000000000..07066535a1 --- /dev/null +++ b/src/main/start-main-application/runnables/clean-up-shell-sessions.injectable.ts @@ -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 { beforeQuitOfBackEndInjectionToken } from "../runnable-tokens/before-quit-of-back-end-injection-token"; +import { ShellSession } from "../../shell-session/shell-session"; + +const cleanUpShellSessionsInjectable = getInjectable({ + id: "clean-up-shell-sessions", + + instantiate: () => ({ + run: () => { + ShellSession.cleanup(); + }, + }), + + injectionToken: beforeQuitOfBackEndInjectionToken, +}); + +export default cleanUpShellSessionsInjectable; diff --git a/src/main/start-main-application/runnables/emit-close-to-event-bus.injectable.ts b/src/main/start-main-application/runnables/emit-close-to-event-bus.injectable.ts new file mode 100644 index 0000000000..316114f205 --- /dev/null +++ b/src/main/start-main-application/runnables/emit-close-to-event-bus.injectable.ts @@ -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 appEventBusInjectable from "../../../common/app-event-bus/app-event-bus.injectable"; +import { beforeQuitOfFrontEndInjectionToken } from "../runnable-tokens/before-quit-of-front-end-injection-token"; + +const emitCloseToEventBusInjectable = getInjectable({ + id: "emit-close-to-event-bus", + + instantiate: (di) => { + const appEventBus = di.inject(appEventBusInjectable); + + return { + run: () => { + appEventBus.emit({ name: "app", action: "close" }); + }, + }; + }, + + injectionToken: beforeQuitOfFrontEndInjectionToken, +}); + +export default emitCloseToEventBusInjectable; diff --git a/src/main/start-main-application/runnables/emit-service-start-to-event-bus.injectable.ts b/src/main/start-main-application/runnables/emit-service-start-to-event-bus.injectable.ts new file mode 100644 index 0000000000..0d3e4cf043 --- /dev/null +++ b/src/main/start-main-application/runnables/emit-service-start-to-event-bus.injectable.ts @@ -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 appEventBusInjectable from "../../../common/app-event-bus/app-event-bus.injectable"; +import { afterApplicationIsLoadedInjectionToken } from "../runnable-tokens/after-application-is-loaded-injection-token"; + +const emitServiceStartToEventBusInjectable = getInjectable({ + id: "emit-service-start-to-event-bus", + + instantiate: (di) => { + const appEventBus = di.inject(appEventBusInjectable); + + return { + run: () => { + appEventBus.emit({ name: "service", action: "start" }); + }, + }; + }, + + injectionToken: afterApplicationIsLoadedInjectionToken, +}); + +export default emitServiceStartToEventBusInjectable; diff --git a/src/main/start-main-application/runnables/flag-renderer/flag-renderer-as-loaded.injectable.ts b/src/main/start-main-application/runnables/flag-renderer/flag-renderer-as-loaded.injectable.ts new file mode 100644 index 0000000000..3c61b2a011 --- /dev/null +++ b/src/main/start-main-application/runnables/flag-renderer/flag-renderer-as-loaded.injectable.ts @@ -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 { afterRootFrameIsReadyInjectionToken } from "../../runnable-tokens/after-root-frame-is-ready-injection-token"; +import lensProtocolRouterMainInjectable from "../../../protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable"; +import { runInAction } from "mobx"; + +const flagRendererAsLoadedInjectable = getInjectable({ + id: "flag-renderer-as-loaded", + + instantiate: (di) => { + const lensProtocolRouterMain = di.inject(lensProtocolRouterMainInjectable); + + return { + run: () => { + runInAction(() => { + // Todo: remove this kludge which enables out-of-place temporal dependency. + lensProtocolRouterMain.rendererLoaded = true; + }); + }, + }; + }, + + injectionToken: afterRootFrameIsReadyInjectionToken, +}); + +export default flagRendererAsLoadedInjectable; diff --git a/src/main/start-main-application/runnables/flag-renderer/flag-renderer-as-not-loaded.injectable.ts b/src/main/start-main-application/runnables/flag-renderer/flag-renderer-as-not-loaded.injectable.ts new file mode 100644 index 0000000000..d81f7287aa --- /dev/null +++ b/src/main/start-main-application/runnables/flag-renderer/flag-renderer-as-not-loaded.injectable.ts @@ -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 { runInAction } from "mobx"; +import lensProtocolRouterMainInjectable from "../../../protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable"; +import { beforeQuitOfFrontEndInjectionToken } from "../../runnable-tokens/before-quit-of-front-end-injection-token"; + +const flagRendererAsNotLoadedInjectable = getInjectable({ + id: "stop-deep-linking", + + instantiate: (di) => { + const lensProtocolRouterMain = di.inject(lensProtocolRouterMainInjectable); + + return { + run: () => { + runInAction(() => { + // Todo: remove this kludge which enables out-of-place temporal dependency. + lensProtocolRouterMain.rendererLoaded = false; + }); + }, + }; + }, + + injectionToken: beforeQuitOfFrontEndInjectionToken, +}); + +export default flagRendererAsNotLoadedInjectable; diff --git a/src/main/start-main-application/runnables/initialize-extensions.injectable.ts b/src/main/start-main-application/runnables/initialize-extensions.injectable.ts new file mode 100644 index 0000000000..899d684abf --- /dev/null +++ b/src/main/start-main-application/runnables/initialize-extensions.injectable.ts @@ -0,0 +1,67 @@ +/** + * 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 { InstalledExtension } from "../../../extensions/extension-discovery/extension-discovery"; +import type { LensExtensionId } from "../../../extensions/lens-extension"; +import loggerInjectable from "../../../common/logger.injectable"; +import extensionDiscoveryInjectable from "../../../extensions/extension-discovery/extension-discovery.injectable"; +import extensionLoaderInjectable from "../../../extensions/extension-loader/extension-loader.injectable"; +import showErrorPopupInjectable from "../../electron-app/features/show-error-popup.injectable"; +import { onLoadOfApplicationInjectionToken } from "../runnable-tokens/on-load-of-application-injection-token"; + +const initializeExtensionsInjectable = getInjectable({ + id: "initialize-extensions", + + instantiate: (di) => { + const logger = di.inject(loggerInjectable); + const extensionDiscovery = di.inject(extensionDiscoveryInjectable); + const extensionLoader = di.inject(extensionLoaderInjectable); + const showErrorPopup = di.inject(showErrorPopupInjectable); + + return { + run: async () => { + logger.info("🧩 Initializing extensions"); + + await extensionDiscovery.init(); + + await extensionLoader.init(); + + 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: any) { + showErrorPopup( + "Lens Error", + `Could not load extensions${ + error?.message ? `: ${error.message}` : "" + }`, + ); + + console.error(error); + console.trace(); + } + }, + }; + }, + + causesSideEffects: true, + + injectionToken: onLoadOfApplicationInjectionToken, +}); + +export default initializeExtensionsInjectable; diff --git a/src/main/start-main-application/runnables/kube-config-sync/start-kube-config-sync.injectable.ts b/src/main/start-main-application/runnables/kube-config-sync/start-kube-config-sync.injectable.ts new file mode 100644 index 0000000000..d6d964c7df --- /dev/null +++ b/src/main/start-main-application/runnables/kube-config-sync/start-kube-config-sync.injectable.ts @@ -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 { afterRootFrameIsReadyInjectionToken } from "../../runnable-tokens/after-root-frame-is-ready-injection-token"; +import directoryForKubeConfigsInjectable from "../../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable"; +import ensureDirInjectable from "../../../../common/fs/ensure-dir.injectable"; +import kubeconfigSyncManagerInjectable from "../../../catalog-sources/kubeconfig-sync/manager.injectable"; + +const startKubeConfigSyncInjectable = getInjectable({ + id: "start-kubeconfig-sync", + + instantiate: (di) => { + const directoryForKubeConfigs = di.inject(directoryForKubeConfigsInjectable); + const kubeConfigSyncManager = di.inject(kubeconfigSyncManagerInjectable); + const ensureDir = di.inject(ensureDirInjectable); + + return { + run: async () => { + await ensureDir(directoryForKubeConfigs); + + kubeConfigSyncManager.startSync(); + }, + }; + }, + + injectionToken: afterRootFrameIsReadyInjectionToken, +}); + +export default startKubeConfigSyncInjectable; diff --git a/src/main/start-main-application/runnables/kube-config-sync/stop-kube-config-sync.injectable.ts b/src/main/start-main-application/runnables/kube-config-sync/stop-kube-config-sync.injectable.ts new file mode 100644 index 0000000000..2613ab77ba --- /dev/null +++ b/src/main/start-main-application/runnables/kube-config-sync/stop-kube-config-sync.injectable.ts @@ -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 { beforeQuitOfFrontEndInjectionToken } from "../../runnable-tokens/before-quit-of-front-end-injection-token"; +import kubeconfigSyncManagerInjectable from "../../../catalog-sources/kubeconfig-sync/manager.injectable"; + +const stopKubeConfigSyncInjectable = getInjectable({ + id: "stop-kube-config-sync", + + instantiate: (di) => { + const kubeConfigSyncManager = di.inject(kubeconfigSyncManagerInjectable); + + return { + run: () => { + kubeConfigSyncManager.stopSync(); + }, + }; + }, + + injectionToken: beforeQuitOfFrontEndInjectionToken, +}); + +export default stopKubeConfigSyncInjectable; diff --git a/src/main/start-main-application/runnables/setup-detector-registry.injectable.ts b/src/main/start-main-application/runnables/setup-detector-registry.injectable.ts new file mode 100644 index 0000000000..1c91e797f3 --- /dev/null +++ b/src/main/start-main-application/runnables/setup-detector-registry.injectable.ts @@ -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 { ClusterIdDetector } from "../../cluster-detectors/cluster-id-detector"; +import { LastSeenDetector } from "../../cluster-detectors/last-seen-detector"; +import { VersionDetector } from "../../cluster-detectors/version-detector"; +import { DistributionDetector } from "../../cluster-detectors/distribution-detector"; +import { NodesCountDetector } from "../../cluster-detectors/nodes-count-detector"; +import detectorRegistryInjectable from "../../cluster-detectors/detector-registry.injectable"; +import { onLoadOfApplicationInjectionToken } from "../runnable-tokens/on-load-of-application-injection-token"; + +const setupDetectorRegistryInjectable = getInjectable({ + id: "setup-detector-registry", + + instantiate: (di) => { + const detectorRegistry = di.inject(detectorRegistryInjectable); + + return { + run: () => { + detectorRegistry + .add(ClusterIdDetector) + .add(LastSeenDetector) + .add(VersionDetector) + .add(DistributionDetector) + .add(NodesCountDetector); + }, + }; + }, + + injectionToken: onLoadOfApplicationInjectionToken, +}); + +export default setupDetectorRegistryInjectable; diff --git a/src/main/start-main-application/runnables/setup-file-protocol.injectable.ts b/src/main/start-main-application/runnables/setup-file-protocol.injectable.ts new file mode 100644 index 0000000000..77e76aaf55 --- /dev/null +++ b/src/main/start-main-application/runnables/setup-file-protocol.injectable.ts @@ -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 registerFileProtocolInjectable from "../../electron-app/features/register-file-protocol.injectable"; +import staticFilesDirectoryInjectable from "../../../common/vars/static-files-directory.injectable"; +import { beforeApplicationIsLoadingInjectionToken } from "../runnable-tokens/before-application-is-loading-injection-token"; + +const setupFileProtocolInjectable = getInjectable({ + id: "setup-file-protocol", + + instantiate: (di) => { + const registerFileProtocol = di.inject(registerFileProtocolInjectable); + const staticFilesDirectory = di.inject(staticFilesDirectoryInjectable); + + return { + run: () => { + registerFileProtocol("static", staticFilesDirectory); + }, + }; + }, + + injectionToken: beforeApplicationIsLoadingInjectionToken, +}); + +export default setupFileProtocolInjectable; diff --git a/src/main/start-main-application/runnables/setup-hardware-acceleration.injectable.ts b/src/main/start-main-application/runnables/setup-hardware-acceleration.injectable.ts new file mode 100644 index 0000000000..ba1b5f2bb8 --- /dev/null +++ b/src/main/start-main-application/runnables/setup-hardware-acceleration.injectable.ts @@ -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 environmentVariablesInjectable from "../../../common/utils/environment-variables.injectable"; +import disableHardwareAccelerationInjectable from "../../electron-app/features/disable-hardware-acceleration.injectable"; +import { beforeElectronIsReadyInjectionToken } from "../runnable-tokens/before-electron-is-ready-injection-token"; + +const setupHardwareAccelerationInjectable = getInjectable({ + id: "setup-hardware-acceleration", + + instantiate: (di) => { + const { LENS_DISABLE_GPU: hardwareAccelerationShouldBeDisabled } = di.inject(environmentVariablesInjectable); + const disableHardwareAcceleration = di.inject(disableHardwareAccelerationInjectable); + + return { + run: () => { + if (hardwareAccelerationShouldBeDisabled) { + disableHardwareAcceleration(); + } + }, + }; + }, + + injectionToken: beforeElectronIsReadyInjectionToken, +}); + +export default setupHardwareAccelerationInjectable; diff --git a/src/main/start-main-application/runnables/setup-hotbar-store.injectable.ts b/src/main/start-main-application/runnables/setup-hotbar-store.injectable.ts new file mode 100644 index 0000000000..372c339e5e --- /dev/null +++ b/src/main/start-main-application/runnables/setup-hotbar-store.injectable.ts @@ -0,0 +1,26 @@ +/** + * 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 setupSyncingOfGeneralCatalogEntitiesInjectable from "./setup-syncing-of-general-catalog-entities.injectable"; +import { onLoadOfApplicationInjectionToken } from "../runnable-tokens/on-load-of-application-injection-token"; +import hotbarStoreInjectable from "../../../common/hotbars/store.injectable"; + +const setupHotbarStoreInjectable = getInjectable({ + id: "setup-hotbar-store", + + instantiate: (di) => ({ + run: () => { + const hotbarStore = di.inject(hotbarStoreInjectable); + + hotbarStore.load(); + }, + + runAfter: di.inject(setupSyncingOfGeneralCatalogEntitiesInjectable), + }), + + injectionToken: onLoadOfApplicationInjectionToken, +}); + +export default setupHotbarStoreInjectable; diff --git a/src/main/start-main-application/runnables/setup-immer.injectable.ts b/src/main/start-main-application/runnables/setup-immer.injectable.ts new file mode 100644 index 0000000000..63cd6e3f2f --- /dev/null +++ b/src/main/start-main-application/runnables/setup-immer.injectable.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import * as Immer from "immer"; +import { beforeElectronIsReadyInjectionToken } from "../runnable-tokens/before-electron-is-ready-injection-token"; + +const setupImmerInjectable = getInjectable({ + id: "setup-immer", + + instantiate: () => ({ + run: () => { + // Docs: https://immerjs.github.io/immer/ + // Required in `utils/storage-helper.ts` + Immer.setAutoFreeze(false); // allow to merge mobx observables + Immer.enableMapSet(); // allow to merge maps and sets + }, + }), + + injectionToken: beforeElectronIsReadyInjectionToken, +}); + +export default setupImmerInjectable; diff --git a/src/main/start-main-application/runnables/setup-lens-proxy.injectable.ts b/src/main/start-main-application/runnables/setup-lens-proxy.injectable.ts new file mode 100644 index 0000000000..8b90bcf5e3 --- /dev/null +++ b/src/main/start-main-application/runnables/setup-lens-proxy.injectable.ts @@ -0,0 +1,77 @@ +/** + * 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 { getAppVersion, getAppVersionFromProxyServer } from "../../../common/utils"; +import exitAppInjectable from "../../electron-app/features/exit-app.injectable"; +import lensProxyInjectable from "../../lens-proxy/lens-proxy.injectable"; +import loggerInjectable from "../../../common/logger.injectable"; +import lensProxyPortInjectable from "../../lens-proxy/lens-proxy-port.injectable"; +import isWindowsInjectable from "../../../common/vars/is-windows.injectable"; +import showErrorPopupInjectable from "../../electron-app/features/show-error-popup.injectable"; +import { beforeApplicationIsLoadingInjectionToken } from "../runnable-tokens/before-application-is-loading-injection-token"; + +const setupLensProxyInjectable = getInjectable({ + id: "setup-lens-proxy", + + instantiate: (di) => { + const lensProxy = di.inject(lensProxyInjectable); + const exitApp = di.inject(exitAppInjectable); + const logger = di.inject(loggerInjectable); + const lensProxyPort = di.inject(lensProxyPortInjectable); + const isWindows = di.inject(isWindowsInjectable); + const showErrorPopup = di.inject(showErrorPopupInjectable); + + return { + run: async () => { + try { + logger.info("🔌 Starting LensProxy"); + await lensProxy.listen(); // lensProxy.port available + } catch (error: any) { + showErrorPopup("Lens Error", `Could not start proxy: ${error?.message || "unknown error"}`); + + return exitApp(); + } + + // test proxy connection + try { + logger.info("🔎 Testing LensProxy connection ..."); + const versionFromProxy = await getAppVersionFromProxyServer( + lensProxyPort.get(), + ); + + if (getAppVersion() !== versionFromProxy) { + logger.error("Proxy server responded with invalid response"); + + return exitApp(); + } + + 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.", + ]; + + showErrorPopup("Lens Proxy Error", message.join("\n\n")); + + return exitApp(); + } + }, + }; + }, + + causesSideEffects: true, + + injectionToken: beforeApplicationIsLoadingInjectionToken, +}); + +export default setupLensProxyInjectable; diff --git a/src/main/start-main-application/runnables/setup-mobx.injectable.ts b/src/main/start-main-application/runnables/setup-mobx.injectable.ts new file mode 100644 index 0000000000..ca5a124b77 --- /dev/null +++ b/src/main/start-main-application/runnables/setup-mobx.injectable.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import * as Mobx from "mobx"; +import { getInjectable } from "@ogre-tools/injectable"; +import { beforeElectronIsReadyInjectionToken } from "../runnable-tokens/before-electron-is-ready-injection-token"; + +const setupMobxInjectable = getInjectable({ + id: "setup-mobx", + + instantiate: () => ({ + run: () => { + // Docs: https://mobx.js.org/configuration.html + Mobx.configure({ + enforceActions: "never", + + // TODO: enable later (read more: https://mobx.js.org/migrating-from-4-or-5.html) + // computedRequiresReaction: true, + // reactionRequiresObservable: true, + // observableRequiresReaction: true, + }); + }, + }), + + injectionToken: beforeElectronIsReadyInjectionToken, +}); + +export default setupMobxInjectable; diff --git a/src/main/start-main-application/runnables/setup-prometheus-registry.injectable.ts b/src/main/start-main-application/runnables/setup-prometheus-registry.injectable.ts new file mode 100644 index 0000000000..c3756bb1b1 --- /dev/null +++ b/src/main/start-main-application/runnables/setup-prometheus-registry.injectable.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { PrometheusLens } from "../../prometheus/lens"; +import { PrometheusHelm } from "../../prometheus/helm"; +import { PrometheusOperator } from "../../prometheus/operator"; +import { PrometheusStacklight } from "../../prometheus/stacklight"; +import prometheusProviderRegistryInjectable from "../../prometheus/prometheus-provider-registry.injectable"; +import { onLoadOfApplicationInjectionToken } from "../runnable-tokens/on-load-of-application-injection-token"; + +const setupPrometheusRegistryInjectable = getInjectable({ + id: "setup-prometheus-registry", + + instantiate: (di) => { + const prometheusProviderRegistry = di.inject(prometheusProviderRegistryInjectable); + + return { + run: () => { + prometheusProviderRegistry + .registerProvider(new PrometheusLens()) + .registerProvider(new PrometheusHelm()) + .registerProvider(new PrometheusOperator()) + .registerProvider(new PrometheusStacklight()); + }, + }; + }, + + injectionToken: onLoadOfApplicationInjectionToken, +}); + +export default setupPrometheusRegistryInjectable; diff --git a/src/main/start-main-application/runnables/setup-proxy-env.injectable.ts b/src/main/start-main-application/runnables/setup-proxy-env.injectable.ts new file mode 100644 index 0000000000..7c05ad8b49 --- /dev/null +++ b/src/main/start-main-application/runnables/setup-proxy-env.injectable.ts @@ -0,0 +1,43 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { beforeElectronIsReadyInjectionToken } from "../runnable-tokens/before-electron-is-ready-injection-token"; +import getCommandLineSwitchInjectable from "../../electron-app/features/get-command-line-switch.injectable"; + +const setupProxyEnvInjectable = getInjectable({ + id: "setup-proxy-env", + + instantiate: (di) => { + const getCommandLineSwitch = di.inject(getCommandLineSwitchInjectable); + + return { + run: () => { + const switchValue = getCommandLineSwitch("proxy-server"); + + let httpsProxy = + process.env.HTTPS_PROXY || process.env.HTTP_PROXY || ""; + + delete process.env.HTTPS_PROXY; + delete process.env.HTTP_PROXY; + + if (switchValue !== "") { + httpsProxy = switchValue; + } + + if (httpsProxy !== "") { + process.env.APP_HTTPS_PROXY = httpsProxy; + } + + if (getCommandLineSwitch("proxy-server") !== "") { + process.env.HTTPS_PROXY = getCommandLineSwitch("proxy-server"); + } + }, + }; + }, + + injectionToken: beforeElectronIsReadyInjectionToken, +}); + +export default setupProxyEnvInjectable; diff --git a/src/main/start-main-application/runnables/setup-reactions-in-user-store.injectable.ts b/src/main/start-main-application/runnables/setup-reactions-in-user-store.injectable.ts new file mode 100644 index 0000000000..7ab26a1506 --- /dev/null +++ b/src/main/start-main-application/runnables/setup-reactions-in-user-store.injectable.ts @@ -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 userStoreInjectable from "../../../common/user-store/user-store.injectable"; +import { onLoadOfApplicationInjectionToken } from "../runnable-tokens/on-load-of-application-injection-token"; + +const setupReactionsInUserStoreInjectable = getInjectable({ + id: "setup-reactions-in-user-store", + + instantiate: (di) => { + const userStore = di.inject(userStoreInjectable); + + return { + run: () => { + userStore.startMainReactions(); + }, + }; + }, + + injectionToken: onLoadOfApplicationInjectionToken, +}); + +export default setupReactionsInUserStoreInjectable; diff --git a/src/main/start-main-application/runnables/setup-runnables-for-after-root-frame-is-ready.injectable.ts b/src/main/start-main-application/runnables/setup-runnables-for-after-root-frame-is-ready.injectable.ts new file mode 100644 index 0000000000..82c323c501 --- /dev/null +++ b/src/main/start-main-application/runnables/setup-runnables-for-after-root-frame-is-ready.injectable.ts @@ -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 { ipcMainOn } from "../../../common/ipc"; +import { IpcRendererNavigationEvents } from "../../../renderer/navigation/events"; +import { afterRootFrameIsReadyInjectionToken } from "../runnable-tokens/after-root-frame-is-ready-injection-token"; +import { runManyFor } from "../../../common/runnable/run-many-for"; +import { onLoadOfApplicationInjectionToken } from "../runnable-tokens/on-load-of-application-injection-token"; + +const setupRunnablesForAfterRootFrameIsReadyInjectable = getInjectable({ + id: "setup-runnables-for-after-root-frame-is-ready", + + instantiate: (di) => { + const runMany = runManyFor(di); + + const runRunnablesAfterRootFrameIsReady = runMany( + afterRootFrameIsReadyInjectionToken, + ); + + return { + run: () => { + ipcMainOn(IpcRendererNavigationEvents.LOADED, async () => { + await runRunnablesAfterRootFrameIsReady(); + }); + }, + }; + }, + + // Direct usage of IPC + causesSideEffects: true, + + injectionToken: onLoadOfApplicationInjectionToken, +}); + +export default setupRunnablesForAfterRootFrameIsReadyInjectable; diff --git a/src/main/start-main-application/runnables/setup-sentry.injectable.ts b/src/main/start-main-application/runnables/setup-sentry.injectable.ts new file mode 100644 index 0000000000..e9f0587450 --- /dev/null +++ b/src/main/start-main-application/runnables/setup-sentry.injectable.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { initializeSentryReporting } from "../../../common/sentry"; +import { init } from "@sentry/electron/main"; +import { onLoadOfApplicationInjectionToken } from "../runnable-tokens/on-load-of-application-injection-token"; + +const setupSentryInjectable = getInjectable({ + id: "setup-sentry", + + instantiate: () => ({ + run: () => { + initializeSentryReporting(init); + }, + }), + + causesSideEffects: true, + + injectionToken: onLoadOfApplicationInjectionToken, +}); + +export default setupSentryInjectable; diff --git a/src/main/start-main-application/runnables/setup-shell.injectable.ts b/src/main/start-main-application/runnables/setup-shell.injectable.ts new file mode 100644 index 0000000000..0f23e0f2e8 --- /dev/null +++ b/src/main/start-main-application/runnables/setup-shell.injectable.ts @@ -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 { shellSync } from "../../shell-sync"; +import loggerInjectable from "../../../common/logger.injectable"; +import { onLoadOfApplicationInjectionToken } from "../runnable-tokens/on-load-of-application-injection-token"; + +const setupShellInjectable = getInjectable({ + id: "setup-shell", + + instantiate: (di) => { + const logger = di.inject(loggerInjectable); + + return { + run: async () => { + logger.info("🐚 Syncing shell environment"); + + await shellSync(); + }, + }; + }, + + injectionToken: onLoadOfApplicationInjectionToken, + causesSideEffects: true, +}); + +export default setupShellInjectable; diff --git a/src/main/start-main-application/runnables/setup-syncing-of-general-catalog-entities.injectable.ts b/src/main/start-main-application/runnables/setup-syncing-of-general-catalog-entities.injectable.ts new file mode 100644 index 0000000000..e44752b324 --- /dev/null +++ b/src/main/start-main-application/runnables/setup-syncing-of-general-catalog-entities.injectable.ts @@ -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 syncGeneralCatalogEntitiesInjectable from "../../catalog-sources/sync-general-catalog-entities.injectable"; +import { onLoadOfApplicationInjectionToken } from "../runnable-tokens/on-load-of-application-injection-token"; + +const setupSyncingOfGeneralCatalogEntitiesInjectable = getInjectable({ + id: "setup-syncing-of-general-catalog-entities", + + instantiate: (di) => { + const syncGeneralCatalogEntities = di.inject( + syncGeneralCatalogEntitiesInjectable, + ); + + return { + run: () => { + syncGeneralCatalogEntities(); + }, + }; + }, + + injectionToken: onLoadOfApplicationInjectionToken, +}); + +export default setupSyncingOfGeneralCatalogEntitiesInjectable; diff --git a/src/main/start-main-application/runnables/setup-syncing-of-weblinks.injectable.ts b/src/main/start-main-application/runnables/setup-syncing-of-weblinks.injectable.ts new file mode 100644 index 0000000000..0e5ada1b78 --- /dev/null +++ b/src/main/start-main-application/runnables/setup-syncing-of-weblinks.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { onLoadOfApplicationInjectionToken } from "../runnable-tokens/on-load-of-application-injection-token"; +import syncWeblinksInjectable from "../../catalog-sources/sync-weblinks.injectable"; + +const setupSyncingOfWeblinksInjectable = getInjectable({ + id: "setup-syncing-of-weblinks", + + instantiate: (di) => { + const syncWeblinks = di.inject(syncWeblinksInjectable); + + return { + run: () => { + syncWeblinks(); + }, + }; + }, + + injectionToken: onLoadOfApplicationInjectionToken, +}); + +export default setupSyncingOfWeblinksInjectable; diff --git a/src/main/start-main-application/runnables/setup-system-ca.injectable.ts b/src/main/start-main-application/runnables/setup-system-ca.injectable.ts new file mode 100644 index 0000000000..0a09ebebd4 --- /dev/null +++ b/src/main/start-main-application/runnables/setup-system-ca.injectable.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { injectSystemCAs } from "../../../common/system-ca"; +import { getInjectable } from "@ogre-tools/injectable"; +import { beforeApplicationIsLoadingInjectionToken } from "../runnable-tokens/before-application-is-loading-injection-token"; + +const setupSystemCaInjectable = getInjectable({ + id: "setup-system-ca", + + instantiate: () => ({ + run: async () => { + await injectSystemCAs(); + }, + }), + + causesSideEffects: true, + + injectionToken: beforeApplicationIsLoadingInjectionToken, +}); + +export default setupSystemCaInjectable; diff --git a/src/main/start-main-application/runnables/stop-cluster-manager.injectable.ts b/src/main/start-main-application/runnables/stop-cluster-manager.injectable.ts new file mode 100644 index 0000000000..d062270dd0 --- /dev/null +++ b/src/main/start-main-application/runnables/stop-cluster-manager.injectable.ts @@ -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 clusterManagerInjectable from "../../cluster-manager.injectable"; +import { beforeQuitOfFrontEndInjectionToken } from "../runnable-tokens/before-quit-of-front-end-injection-token"; + +const stopClusterManagerInjectable = getInjectable({ + id: "stop-cluster-manager", + + instantiate: (di) => { + const clusterManager = di.inject(clusterManagerInjectable); + + return { + run: () => { + clusterManager.stop(); + }, + }; + }, + + injectionToken: beforeQuitOfFrontEndInjectionToken, + + causesSideEffects: true, +}); + +export default stopClusterManagerInjectable; diff --git a/src/main/start-main-application/start-main-application.injectable.ts b/src/main/start-main-application/start-main-application.injectable.ts new file mode 100644 index 0000000000..c818d0308b --- /dev/null +++ b/src/main/start-main-application/start-main-application.injectable.ts @@ -0,0 +1,76 @@ +/** + * 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 { runManyFor } from "../../common/runnable/run-many-for"; +import { runManySyncFor } from "../../common/runnable/run-many-sync-for"; +import { beforeElectronIsReadyInjectionToken } from "./runnable-tokens/before-electron-is-ready-injection-token"; +import { beforeApplicationIsLoadingInjectionToken } from "./runnable-tokens/before-application-is-loading-injection-token"; +import { onLoadOfApplicationInjectionToken } from "./runnable-tokens/on-load-of-application-injection-token"; +import { afterApplicationIsLoadedInjectionToken } from "./runnable-tokens/after-application-is-loaded-injection-token"; +import splashWindowInjectable from "./lens-window/splash-window/splash-window.injectable"; + +import applicationWindowInjectable from "./lens-window/application-window/application-window.injectable"; +import shouldStartHiddenInjectable from "../electron-app/features/should-start-hidden.injectable"; +import openDeepLinkInjectable from "../protocol-handler/lens-protocol-router-main/open-deep-link-for-url/open-deep-link.injectable"; +import { pipeline } from "@ogre-tools/fp"; +import { find, map, startsWith, toLower } from "lodash/fp"; +import commandLineArgumentsInjectable from "../utils/command-line-arguments.injectable"; +import waitForElectronToBeReadyInjectable from "../electron-app/features/wait-for-electron-to-be-ready.injectable"; + +const startMainApplicationInjectable = getInjectable({ + id: "start-main-application", + + instantiate: (di) => { + const runMany = runManyFor(di); + const runManySync = runManySyncFor(di); + const waitForElectronToBeReady = di.inject(waitForElectronToBeReadyInjectable); + const applicationWindow = di.inject(applicationWindowInjectable); + const splashWindow = di.inject(splashWindowInjectable); + const shouldStartHidden = di.inject(shouldStartHiddenInjectable); + const openDeepLink = di.inject(openDeepLinkInjectable); + const commandLineArguments = di.inject(commandLineArgumentsInjectable); + + const beforeElectronIsReady = runManySync(beforeElectronIsReadyInjectionToken); + const beforeApplicationIsLoading = runMany(beforeApplicationIsLoadingInjectionToken); + const onLoadOfApplication = runMany(onLoadOfApplicationInjectionToken); + const afterApplicationIsLoaded = runMany(afterApplicationIsLoadedInjectionToken); + + return async () => { + // Stuff happening before application is ready needs to be synchronous because of + // https://github.com/electron/electron/issues/21370 + beforeElectronIsReady(); + + await waitForElectronToBeReady(); + + await beforeApplicationIsLoading(); + + if (!shouldStartHidden) { + await splashWindow.show(); + } + + await onLoadOfApplication(); + + if (!shouldStartHidden) { + const deepLinkUrl = getDeepLinkUrl(commandLineArguments); + + if (deepLinkUrl) { + await openDeepLink(deepLinkUrl); + } else { + await applicationWindow.show(); + } + + splashWindow.close(); + } + + await afterApplicationIsLoaded(); + }; + }, +}); + +const getDeepLinkUrl = (commandLineArguments: string[]) => + pipeline(commandLineArguments, map(toLower), find(startsWith("lens://"))); + +export default startMainApplicationInjectable; diff --git a/src/main/start-update-checking.injectable.ts b/src/main/start-update-checking.injectable.ts new file mode 100644 index 0000000000..4571e70df4 --- /dev/null +++ b/src/main/start-update-checking.injectable.ts @@ -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 { startUpdateChecking } from "./app-updater"; +import isAutoUpdateEnabledInjectable from "./is-auto-update-enabled.injectable"; + +const startUpdateCheckingInjectable = getInjectable({ + id: "start-update-checking", + + instantiate: (di) => startUpdateChecking({ + isAutoUpdateEnabled: di.inject(isAutoUpdateEnabledInjectable), + }), + + causesSideEffects: true, +}); + +export default startUpdateCheckingInjectable; diff --git a/src/main/stop-services-and-exit-app.injectable.ts b/src/main/stop-services-and-exit-app.injectable.ts new file mode 100644 index 0000000000..25b4324aec --- /dev/null +++ b/src/main/stop-services-and-exit-app.injectable.ts @@ -0,0 +1,32 @@ +/** + * 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 exitAppInjectable from "./electron-app/features/exit-app.injectable"; +import clusterManagerInjectable from "./cluster-manager.injectable"; +import appEventBusInjectable from "../common/app-event-bus/app-event-bus.injectable"; +import loggerInjectable from "../common/logger.injectable"; +import closeAllWindowsInjectable from "./start-main-application/lens-window/hide-all-windows/close-all-windows.injectable"; + +const stopServicesAndExitAppInjectable = getInjectable({ + id: "stop-services-and-exit-app", + + instantiate: (di) => { + const exitApp = di.inject(exitAppInjectable); + const clusterManager = di.inject(clusterManagerInjectable); + const appEventBus = di.inject(appEventBusInjectable); + const logger = di.inject(loggerInjectable); + const closeAllWindows = di.inject(closeAllWindowsInjectable); + + return () => { + appEventBus.emit({ name: "service", action: "close" }); + closeAllWindows(); + clusterManager.stop(); + logger.info("SERVICE:QUIT"); + setTimeout(exitApp, 1000); + }; + }, +}); + +export default stopServicesAndExitAppInjectable; diff --git a/src/main/theme/broadcast-theme-change/broadcast-theme-change.injectable.ts b/src/main/theme/broadcast-theme-change/broadcast-theme-change.injectable.ts new file mode 100644 index 0000000000..98be53d748 --- /dev/null +++ b/src/main/theme/broadcast-theme-change/broadcast-theme-change.injectable.ts @@ -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 { reaction } from "mobx"; +import { getStartableStoppable } from "../../../common/utils/get-startable-stoppable"; +import { setNativeThemeChannel } from "../../../common/ipc/native-theme"; +import operatingSystemThemeInjectable from "../operating-system-theme.injectable"; +import broadcastMessageInjectable from "../../../common/ipc/broadcast-message.injectable"; + +const broadcastThemeChangeInjectable = getInjectable({ + id: "broadcast-theme-change", + + instantiate: (di) => { + const currentTheme = di.inject(operatingSystemThemeInjectable); + const broadcastMessage = di.inject(broadcastMessageInjectable); + + return getStartableStoppable("broadcast-theme-change", () => + reaction(() => currentTheme.get(), (theme) => { + broadcastMessage(setNativeThemeChannel, theme); + }), + ); + }, +}); + +export default broadcastThemeChangeInjectable; diff --git a/src/main/theme/broadcast-theme-change/start-broadcasting-theme-change.injectable.ts b/src/main/theme/broadcast-theme-change/start-broadcasting-theme-change.injectable.ts new file mode 100644 index 0000000000..7794c07bd4 --- /dev/null +++ b/src/main/theme/broadcast-theme-change/start-broadcasting-theme-change.injectable.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { onLoadOfApplicationInjectionToken } from "../../start-main-application/runnable-tokens/on-load-of-application-injection-token"; +import broadcastThemeChangeInjectable from "./broadcast-theme-change.injectable"; + +const startBroadcastingThemeChangeInjectable = getInjectable({ + id: "start-broadcasting-theme-change", + + instantiate: (di) => { + const broadcastThemeChange = di.inject(broadcastThemeChangeInjectable); + + return { + run: async () => { + await broadcastThemeChange.start(); + }, + }; + }, + + injectionToken: onLoadOfApplicationInjectionToken, +}); + +export default startBroadcastingThemeChangeInjectable; diff --git a/src/main/theme/broadcast-theme-change/stop-broadcasting-theme-change.injectable.ts b/src/main/theme/broadcast-theme-change/stop-broadcasting-theme-change.injectable.ts new file mode 100644 index 0000000000..a5f922af13 --- /dev/null +++ b/src/main/theme/broadcast-theme-change/stop-broadcasting-theme-change.injectable.ts @@ -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 broadcastThemeChangeInjectable from "./broadcast-theme-change.injectable"; +import { beforeQuitOfBackEndInjectionToken } from "../../start-main-application/runnable-tokens/before-quit-of-back-end-injection-token"; + +const stopBroadcastingThemeChangeInjectable = getInjectable({ + id: "stop-broadcasting-theme-change", + + instantiate: (di) => { + const broadcastThemeChange = di.inject(broadcastThemeChangeInjectable); + + return { + run: async () => { + await broadcastThemeChange.stop(); + }, + }; + }, + + injectionToken: beforeQuitOfBackEndInjectionToken, +}); + +export default stopBroadcastingThemeChangeInjectable; diff --git a/src/main/theme/operating-system-theme-state.injectable.ts b/src/main/theme/operating-system-theme-state.injectable.ts new file mode 100644 index 0000000000..cae3905b2e --- /dev/null +++ b/src/main/theme/operating-system-theme-state.injectable.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { observable } from "mobx"; +import getElectronThemeInjectable from "../electron-app/features/get-electron-theme.injectable"; + +export type Theme = "dark" | "light"; + +const operatingSystemThemeStateInjectable = getInjectable({ + id: "operating-system-theme-state", + + instantiate: (di) => { + const getElectronTheme = di.inject(getElectronThemeInjectable); + const defaultTheme = getElectronTheme(); + + return observable.box( + defaultTheme, + ); + }, +}); + +export default operatingSystemThemeStateInjectable; diff --git a/src/main/theme/operating-system-theme.injectable.ts b/src/main/theme/operating-system-theme.injectable.ts new file mode 100644 index 0000000000..8ec5fcf445 --- /dev/null +++ b/src/main/theme/operating-system-theme.injectable.ts @@ -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 { computed } from "mobx"; +import operatingSystemThemeStateInjectable from "./operating-system-theme-state.injectable"; + +const operatingSystemThemeInjectable = getInjectable({ + id: "operating-system-theme", + + instantiate: (di) => { + const currentThemeState = di.inject(operatingSystemThemeStateInjectable); + + return computed(() => currentThemeState.get()); + }, +}); + +export default operatingSystemThemeInjectable; diff --git a/src/main/theme/sync-theme-from-os/start-syncing-theme-from-operating-system.injectable.ts b/src/main/theme/sync-theme-from-os/start-syncing-theme-from-operating-system.injectable.ts new file mode 100644 index 0000000000..9bf9c5fe49 --- /dev/null +++ b/src/main/theme/sync-theme-from-os/start-syncing-theme-from-operating-system.injectable.ts @@ -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 syncThemeFromOperatingSystemInjectable from "../../electron-app/features/sync-theme-from-operating-system.injectable"; +import { onLoadOfApplicationInjectionToken } from "../../start-main-application/runnable-tokens/on-load-of-application-injection-token"; + +const startSyncingThemeFromOperatingSystemInjectable = getInjectable({ + id: "start-syncing-theme-from-operating-system", + + instantiate: (di) => { + const syncTheme = di.inject(syncThemeFromOperatingSystemInjectable); + + return { + run: async () => { + await syncTheme.start(); + }, + }; + }, + + injectionToken: onLoadOfApplicationInjectionToken, +}); + +export default startSyncingThemeFromOperatingSystemInjectable; diff --git a/src/main/theme/sync-theme-from-os/stop-syncing-theme-from-operating-system.injectable.ts b/src/main/theme/sync-theme-from-os/stop-syncing-theme-from-operating-system.injectable.ts new file mode 100644 index 0000000000..08657281c2 --- /dev/null +++ b/src/main/theme/sync-theme-from-os/stop-syncing-theme-from-operating-system.injectable.ts @@ -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 syncThemeFromOperatingSystemInjectable from "../../electron-app/features/sync-theme-from-operating-system.injectable"; +import { beforeQuitOfBackEndInjectionToken } from "../../start-main-application/runnable-tokens/before-quit-of-back-end-injection-token"; + +const stopSyncingThemeFromOperatingSystemInjectable = getInjectable({ + id: "stop-syncing-theme-from-operating-system", + + instantiate: (di) => { + const syncTheme = di.inject(syncThemeFromOperatingSystemInjectable); + + return { + run: async () => { + await syncTheme.stop(); + }, + }; + }, + + injectionToken: beforeQuitOfBackEndInjectionToken, +}); + +export default stopSyncingThemeFromOperatingSystemInjectable; diff --git a/src/main/tray/install-tray.injectable.ts b/src/main/tray/install-tray.injectable.ts new file mode 100644 index 0000000000..716d602101 --- /dev/null +++ b/src/main/tray/install-tray.injectable.ts @@ -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 trayInjectable from "./tray.injectable"; +import { onLoadOfApplicationInjectionToken } from "../start-main-application/runnable-tokens/on-load-of-application-injection-token"; + +const installTrayInjectable = getInjectable({ + id: "install-tray", + + instantiate: (di) => { + const trayInitializer = di.inject(trayInjectable); + + return { + run: async () => { + await trayInitializer.start(); + }, + }; + }, + + injectionToken: onLoadOfApplicationInjectionToken, +}); + +export default installTrayInjectable; diff --git a/src/main/tray/tray-icon-path.injectable.ts b/src/main/tray/tray-icon-path.injectable.ts new file mode 100644 index 0000000000..1eb4d13118 --- /dev/null +++ b/src/main/tray/tray-icon-path.injectable.ts @@ -0,0 +1,26 @@ +/** + * 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 getAbsolutePathInjectable from "../../common/path/get-absolute-path.injectable"; +import staticFilesDirectoryInjectable from "../../common/vars/static-files-directory.injectable"; +import isDevelopmentInjectable from "../../common/vars/is-development.injectable"; + +const trayIconPathInjectable = getInjectable({ + id: "tray-icon-path", + + instantiate: (di) => { + const getAbsolutePath = di.inject(getAbsolutePathInjectable); + const staticFilesDirectory = di.inject(staticFilesDirectoryInjectable); + const isDevelopment = di.inject(isDevelopmentInjectable); + + return getAbsolutePath( + staticFilesDirectory, + isDevelopment ? "../build/tray" : "icons", // copied within electron-builder extras + "trayIconTemplate.png", + ); + }, +}); + +export default trayIconPathInjectable; diff --git a/src/main/tray/tray-menu-items.test.ts b/src/main/tray/tray-menu-items.test.ts index c25010967d..fb4f29f763 100644 --- a/src/main/tray/tray-menu-items.test.ts +++ b/src/main/tray/tray-menu-items.test.ts @@ -19,8 +19,6 @@ describe("tray-menu-items", () => { beforeEach(async () => { di = getDiForUnitTesting({ doGeneralOverrides: true }); - await di.runSetups(); - extensionsStub = new ObservableMap(); di.override( diff --git a/src/main/tray/tray.injectable.ts b/src/main/tray/tray.injectable.ts new file mode 100644 index 0000000000..0e61062d50 --- /dev/null +++ b/src/main/tray/tray.injectable.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { initTray } from "./tray"; +import trayMenuItemsInjectable from "./tray-menu-items.injectable"; +import navigateToPreferencesInjectable from "../../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable"; +import stopServicesAndExitAppInjectable from "../stop-services-and-exit-app.injectable"; +import { getStartableStoppable } from "../../common/utils/get-startable-stoppable"; +import isAutoUpdateEnabledInjectable from "../is-auto-update-enabled.injectable"; +import showAboutInjectable from "../menu/show-about.injectable"; +import showApplicationWindowInjectable from "../start-main-application/lens-window/show-application-window.injectable"; +import trayIconPathInjectable from "./tray-icon-path.injectable"; + +const trayInjectable = getInjectable({ + id: "tray", + + instantiate: (di) => { + const trayMenuItems = di.inject(trayMenuItemsInjectable); + const navigateToPreferences = di.inject(navigateToPreferencesInjectable); + const stopServicesAndExitApp = di.inject(stopServicesAndExitAppInjectable); + const isAutoUpdateEnabled = di.inject(isAutoUpdateEnabledInjectable); + const showApplicationWindow = di.inject(showApplicationWindowInjectable); + const showAboutPopup = di.inject(showAboutInjectable); + const trayIconPath = di.inject(trayIconPathInjectable); + + return getStartableStoppable("build-of-tray", () => + initTray( + trayMenuItems, + navigateToPreferences, + stopServicesAndExitApp, + isAutoUpdateEnabled, + showApplicationWindow, + showAboutPopup, + trayIconPath, + ), + ); + }, +}); + +export default trayInjectable; diff --git a/src/main/tray/tray.ts b/src/main/tray/tray.ts index f00d34d49c..c5d0b47ab1 100644 --- a/src/main/tray/tray.ts +++ b/src/main/tray/tray.ts @@ -7,46 +7,34 @@ import packageInfo from "../../../package.json"; import { Menu, Tray } from "electron"; import type { IComputedValue } from "mobx"; import { autorun } from "mobx"; -import { showAbout } from "../menu/menu"; -import { checkForUpdates, isAutoUpdateEnabled } from "../app-updater"; -import type { WindowManager } from "../window-manager"; +import { checkForUpdates } from "../app-updater"; import logger from "../logger"; -import { isDevelopment, isWindows, productName, staticFilesDirectory } from "../../common/vars"; -import { exitApp } from "../exit-app"; +import { isWindows, productName } from "../../common/vars"; import type { Disposer } from "../../common/utils"; import { disposer, toJS } from "../../common/utils"; import type { TrayMenuRegistration } from "./tray-menu-registration"; -import path from "path"; - const TRAY_LOG_PREFIX = "[TRAY]"; // note: instance of Tray should be saved somewhere, otherwise it disappears export let tray: Tray | null = null; -function getTrayIconPath(): string { - return path.resolve( - staticFilesDirectory, - isDevelopment ? "../build/tray" : "icons", // copied within electron-builder extras - "trayIconTemplate.png", - ); -} - export function initTray( - windowManager: WindowManager, trayMenuItems: IComputedValue, navigateToPreferences: () => void, + stopServicesAndExitApp: () => void, + isAutoUpdateEnabled: () => boolean, + showApplicationWindow: () => Promise, + showAbout: () => void, + trayIconPath: string, ): Disposer { - const icon = getTrayIconPath(); - - tray = new Tray(icon); + tray = new Tray(trayIconPath); tray.setToolTip(packageInfo.description); tray.setIgnoreDoubleClickEvents(true); if (isWindows) { tray.on("click", () => { - windowManager - .ensureMainWindow() + showApplicationWindow() .catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to open lens`, { error })); }); } @@ -54,7 +42,7 @@ export function initTray( return disposer( autorun(() => { try { - const menu = createTrayMenu(windowManager, toJS(trayMenuItems.get()), navigateToPreferences); + const menu = createTrayMenu(toJS(trayMenuItems.get()), navigateToPreferences, stopServicesAndExitApp, isAutoUpdateEnabled, showApplicationWindow, showAbout); tray?.setContextMenu(menu); } catch (error) { @@ -79,17 +67,18 @@ function getMenuItemConstructorOptions(trayItem: TrayMenuRegistration): Electron } function createTrayMenu( - windowManager: WindowManager, extensionTrayItems: TrayMenuRegistration[], navigateToPreferences: () => void, + stopServicesAndExitApp: () => void, + isAutoUpdateEnabled: () => boolean, + showApplicationWindow: () => Promise, + showAbout: () => void, ): Menu { let template: Electron.MenuItemConstructorOptions[] = [ { label: `Open ${productName}`, click() { - windowManager - .ensureMainWindow() - .catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to open lens`, { error })); + showApplicationWindow().catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to open lens`, { error })); }, }, { @@ -105,7 +94,7 @@ function createTrayMenu( label: "Check for updates", click() { checkForUpdates() - .then(() => windowManager.ensureMainWindow()); + .then(() => showApplicationWindow()); }, }); } @@ -116,7 +105,7 @@ function createTrayMenu( { label: `About ${productName}`, click() { - windowManager.ensureMainWindow() + showApplicationWindow() .then(showAbout) .catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to show Lens About view`, { error })); }, @@ -125,7 +114,7 @@ function createTrayMenu( { label: "Quit App", click() { - exitApp(); + stopServicesAndExitApp(); }, }, ])); diff --git a/src/main/tray/uninstall-tray.injectable.ts b/src/main/tray/uninstall-tray.injectable.ts new file mode 100644 index 0000000000..41b3cd676c --- /dev/null +++ b/src/main/tray/uninstall-tray.injectable.ts @@ -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 trayInjectable from "./tray.injectable"; +import { beforeQuitOfBackEndInjectionToken } from "../start-main-application/runnable-tokens/before-quit-of-back-end-injection-token"; + +const uninstallTrayInjectable = getInjectable({ + id: "uninstall-tray", + + instantiate: (di) => { + const trayInitializer = di.inject(trayInjectable); + + return { + run: async () => { + await trayInitializer.stop(); + }, + }; + }, + + injectionToken: beforeQuitOfBackEndInjectionToken, +}); + +export default uninstallTrayInjectable; diff --git a/src/main/utils/command-line-arguments.injectable.ts b/src/main/utils/command-line-arguments.injectable.ts new file mode 100644 index 0000000000..e27a3802d4 --- /dev/null +++ b/src/main/utils/command-line-arguments.injectable.ts @@ -0,0 +1,13 @@ +/** + * 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 commandLineArgumentsInjectable = getInjectable({ + id: "command-line-arguments", + instantiate: () => process.argv, + causesSideEffects: true, +}); + +export default commandLineArgumentsInjectable; diff --git a/src/main/window-manager.injectable.ts b/src/main/window-manager.injectable.ts deleted file mode 100644 index 6a1f54a660..0000000000 --- a/src/main/window-manager.injectable.ts +++ /dev/null @@ -1,20 +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 { WindowManager } from "./window-manager"; - -const windowManagerInjectable = getInjectable({ - id: "window-manager", - - instantiate: () => { - WindowManager.resetInstance(); - - return WindowManager.createInstance(); - }, - - causesSideEffects: true, -}); - -export default windowManagerInjectable; diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts deleted file mode 100644 index 3b60b2e5a1..0000000000 --- a/src/main/window-manager.ts +++ /dev/null @@ -1,287 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import type { ClusterId } from "../common/cluster-types"; -import { makeObservable, observable } from "mobx"; -import { app, BrowserWindow, dialog, ipcMain, webContents } from "electron"; -import windowStateKeeper from "electron-window-state"; -import { appEventBus } from "../common/app-event-bus/event-bus"; -import { ipcMainOn } from "../common/ipc"; -import { delay, iter, Singleton, openBrowser } from "../common/utils"; -import type { ClusterFrameInfo } from "../common/cluster-frames"; -import { clusterFrameMap } from "../common/cluster-frames"; -import { IpcRendererNavigationEvents } from "../renderer/navigation/events"; -import logger from "./logger"; -import { isMac, productName } from "../common/vars"; -import { LensProxy } from "./lens-proxy"; -import { bundledExtensionsLoaded } from "../common/ipc/extension-handling"; - -export interface SendToViewArgs { - channel: string; - frameInfo?: ClusterFrameInfo; - data?: any[]; -} - -export class WindowManager extends Singleton { - public mainContentUrl = `http://localhost:${LensProxy.getInstance().port}`; - - protected mainWindow?: BrowserWindow; - protected splashWindow?: BrowserWindow; - protected windowState?: windowStateKeeper.State; - - @observable activeClusterId?: ClusterId; - - constructor() { - super(); - makeObservable(this); - this.bindEvents(); - } - - private async initMainWindow(showSplash: boolean): Promise { - // Manage main window size and position with state persistence - this.windowState ??= windowStateKeeper({ - defaultHeight: 900, - defaultWidth: 1440, - }); - - if (!this.mainWindow) { - // show icon in dock (mac-os only) - app.dock?.show(); - - const { width, height, x, y } = this.windowState; - - this.mainWindow = new BrowserWindow({ - x, y, width, height, - title: productName, - show: false, - minWidth: 700, // accommodate 800 x 600 display minimum - minHeight: 500, // accommodate 800 x 600 display minimum - titleBarStyle: isMac ? "hiddenInset" : "hidden", - frame: isMac, - backgroundColor: "#1e2124", - webPreferences: { - nodeIntegration: true, - nodeIntegrationInSubFrames: true, - webviewTag: true, - contextIsolation: false, - nativeWindowOpen: false, - }, - }); - this.windowState.manage(this.mainWindow); - - // open external links in default browser (target=_blank, window.open) - this.mainWindow - .on("focus", () => { - appEventBus.emit({ name: "app", action: "focus" }); - }) - .on("blur", () => { - appEventBus.emit({ name: "app", action: "blur" }); - }) - .on("closed", () => { - // clean up - this.windowState?.unmanage(); - this.mainWindow = undefined; - this.splashWindow = undefined; - app.dock?.hide(); // hide icon in dock (mac-os) - }) - .webContents - .on("dom-ready", () => { - appEventBus.emit({ name: "app", action: "dom-ready" }); - }) - .on("did-fail-load", (_event, code, desc) => { - logger.error(`[WINDOW-MANAGER]: Failed to load Main window`, { code, desc }); - }) - .on("did-finish-load", () => { - logger.info("[WINDOW-MANAGER]: Main window loaded"); - }) - .on("will-attach-webview", (event, webPreferences, params) => { - logger.debug("[WINDOW-MANAGER]: Attaching webview"); - // Following is security recommendations because we allow webview tag (webviewTag: true) - // suggested by https://www.electronjs.org/docs/tutorial/security#11-verify-webview-options-before-creation - // and https://www.electronjs.org/docs/tutorial/security#10-do-not-use-allowpopups - - if (webPreferences.preload) { - logger.warn("[WINDOW-MANAGER]: Strip away preload scripts of webview"); - delete webPreferences.preload; - } - - // @ts-expect-error some electron version uses webPreferences.preloadURL/webPreferences.preload - if (webPreferences.preloadURL) { - logger.warn("[WINDOW-MANAGER]: Strip away preload scripts of webview"); - delete webPreferences.preload; - } - - if (params.allowpopups) { - logger.warn("[WINDOW-MANAGER]: We do not allow allowpopups props, stop webview from renderer"); - - // event.preventDefault() will destroy the guest page. - event.preventDefault(); - - return; - } - - // Always disable Node.js integration for all webviews - webPreferences.nodeIntegration = false; - }) - .setWindowOpenHandler((details) => { - openBrowser(details.url).catch(error => { - logger.error("[WINDOW-MANAGER]: failed to open browser", { error }); - }); - - return { action: "deny" }; - }); - } - - try { - if (showSplash) { - await this.showSplash(); - } - logger.info(`[WINDOW-MANAGER]: Loading Main window from url: ${this.mainContentUrl} ...`); - await this.mainWindow.loadURL(this.mainContentUrl); - } catch (error) { - logger.error("Loading main window failed", { error }); - dialog.showErrorBox("ERROR!", String(error)); - } - - return this.mainWindow; - } - - protected bindEvents() { - // track visible cluster from ui - ipcMainOn(IpcRendererNavigationEvents.CLUSTER_VIEW_CURRENT_ID, (event, clusterId: ClusterId) => { - this.activeClusterId = clusterId; - }); - } - - async ensureMainWindow(showSplash = true): Promise { - // This needs to be ready to hear the IPC message before the window is loaded - let viewHasLoaded = Promise.resolve(); - - if (!this.mainWindow) { - viewHasLoaded = new Promise(resolve => { - ipcMain.once(bundledExtensionsLoaded, () => resolve()); - }); - this.mainWindow = await this.initMainWindow(showSplash); - } - - try { - await viewHasLoaded; - await delay(50); // wait just a bit longer to let the first round of rendering happen - logger.info("[WINDOW-MANAGER]: Main window has reported that it has loaded"); - - this.mainWindow.show(); - this.splashWindow?.close(); - this.splashWindow = undefined; - setTimeout(() => { - appEventBus.emit({ name: "app", action: "start" }); - }, 1000); - } catch (error) { - logger.error(`Showing main window failed`, error); - dialog.showErrorBox("ERROR!", String(error)); - } - - return this.mainWindow; - } - - private sendToView(window: BrowserWindow, { channel, frameInfo, data = [] }: SendToViewArgs) { - if (frameInfo) { - window.webContents.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...data); - } else { - window.webContents.send(channel, ...data); - } - } - - async navigateExtension(extId: string, pageId?: string, params?: Record, frameId?: number) { - const window = await this.ensureMainWindow(); - const frameInfo = iter.find(clusterFrameMap.values(), frameInfo => frameInfo.frameId === frameId); - - this.sendToView(window, { - channel: "extension:navigate", - frameInfo, - data: [extId, pageId, params], - }); - } - - async navigate(url: string, frameId?: number) { - const window = await this.ensureMainWindow(); - - this.navigateSync(window, url, frameId); - } - - navigateSync(window: BrowserWindow, url: string, frameId?: number) { - const frameInfo = iter.find(clusterFrameMap.values(), frameInfo => frameInfo.frameId === frameId); - const channel = frameInfo - ? IpcRendererNavigationEvents.NAVIGATE_IN_CLUSTER - : IpcRendererNavigationEvents.NAVIGATE_IN_APP; - - this.sendToView(window, { - channel, - frameInfo, - data: [url], - }); - } - - private getActiveClusterFrameInfo() { - if (this.activeClusterId) { - return clusterFrameMap.get(this.activeClusterId); - } - - return undefined; - } - - reload() { - const frameInfo = this.getActiveClusterFrameInfo(); - - if (frameInfo && this.mainWindow) { - this.sendToView(this.mainWindow, { channel: IpcRendererNavigationEvents.RELOAD_PAGE, frameInfo }); - } else { - webContents.getAllWebContents() - .filter(wc => wc.getType() === "window") - .forEach(wc => { - wc.reload(); - wc.clearHistory(); - }); - } - } - - async showSplash() { - if (!this.splashWindow) { - this.splashWindow = new BrowserWindow({ - width: 500, - height: 300, - backgroundColor: "#1e2124", - center: true, - frame: false, - resizable: false, - show: false, - webPreferences: { - nodeIntegration: true, - contextIsolation: false, - nodeIntegrationInSubFrames: true, - nativeWindowOpen: true, - }, - }); - await this.splashWindow.loadURL("static://splash.html"); - } - this.splashWindow.show(); - } - - hide() { - if (this.mainWindow && !this.mainWindow.isDestroyed()) { - this.mainWindow.hide(); - } - - if (this.splashWindow && !this.splashWindow.isDestroyed()) { - this.splashWindow.hide(); - } - } - - destroy() { - this.mainWindow?.destroy(); - this.splashWindow?.destroy(); - this.mainWindow = undefined; - this.splashWindow = undefined; - } -} diff --git a/src/renderer/api/setup-on-api-errors.injectable.ts b/src/renderer/api/setup-on-api-errors.injectable.ts index 3ea5117594..859b333586 100644 --- a/src/renderer/api/setup-on-api-errors.injectable.ts +++ b/src/renderer/api/setup-on-api-errors.injectable.ts @@ -5,13 +5,19 @@ import { getInjectable } from "@ogre-tools/injectable"; import { apiBase } from "../../common/k8s-api"; import { onApiError } from "./on-api-error"; +import { beforeFrameStartsInjectionToken } from "../before-frame-starts/before-frame-starts-injection-token"; const setupOnApiErrorListenersInjectable = getInjectable({ id: "setup-on-api-error-listeners", - setup: () => { - apiBase?.onError.addListener(onApiError); - }, - instantiate: () => undefined, + + instantiate: () => ({ + run: () => { + apiBase?.onError.addListener(onApiError); + }, + }), + + injectionToken: beforeFrameStartsInjectionToken, + causesSideEffects: true, }); export default setupOnApiErrorListenersInjectable; diff --git a/src/renderer/app-paths/app-paths.injectable.ts b/src/renderer/app-paths/app-paths.injectable.ts deleted file mode 100644 index e6c77c1840..0000000000 --- a/src/renderer/app-paths/app-paths.injectable.ts +++ /dev/null @@ -1,28 +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 type { AppPaths } from "../../common/app-paths/app-path-injection-token"; -import { appPathsInjectionToken, appPathsIpcChannel } from "../../common/app-paths/app-path-injection-token"; -import getValueFromRegisteredChannelInjectable from "./get-value-from-registered-channel/get-value-from-registered-channel.injectable"; - -let syncAppPaths: AppPaths; - -const appPathsInjectable = getInjectable({ - id: "app-paths", - - setup: async (di) => { - const getValueFromRegisteredChannel = await di.inject( - getValueFromRegisteredChannelInjectable, - ); - - syncAppPaths = await getValueFromRegisteredChannel(appPathsIpcChannel); - }, - - instantiate: () => syncAppPaths, - - injectionToken: appPathsInjectionToken, -}); - -export default appPathsInjectable; diff --git a/src/renderer/app-paths/setup-app-paths.injectable.ts b/src/renderer/app-paths/setup-app-paths.injectable.ts new file mode 100644 index 0000000000..e6cf30f0dd --- /dev/null +++ b/src/renderer/app-paths/setup-app-paths.injectable.ts @@ -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 { appPathsIpcChannel } from "../../common/app-paths/app-path-injection-token"; +import getValueFromRegisteredChannelInjectable from "./get-value-from-registered-channel/get-value-from-registered-channel.injectable"; +import appPathsStateInjectable from "../../common/app-paths/app-paths-state.injectable"; +import { beforeFrameStartsInjectionToken } from "../before-frame-starts/before-frame-starts-injection-token"; + +const setupAppPathsInjectable = getInjectable({ + id: "setup-app-paths", + + instantiate: (di) => ({ + run: async () => { + const getValueFromRegisteredChannel = di.inject( + getValueFromRegisteredChannelInjectable, + ); + + const syncAppPaths = await getValueFromRegisteredChannel(appPathsIpcChannel); + + const appPathsState = di.inject(appPathsStateInjectable); + + appPathsState.set(syncAppPaths); + }, + }), + + injectionToken: beforeFrameStartsInjectionToken, +}); + +export default setupAppPathsInjectable; diff --git a/src/renderer/before-frame-starts/before-frame-starts-injection-token.ts b/src/renderer/before-frame-starts/before-frame-starts-injection-token.ts new file mode 100644 index 0000000000..e494508329 --- /dev/null +++ b/src/renderer/before-frame-starts/before-frame-starts-injection-token.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectionToken } from "@ogre-tools/injectable"; +import type { Runnable } from "../../common/runnable/run-many-for"; + +export const beforeFrameStartsInjectionToken = getInjectionToken({ + id: "before-frame-starts", +}); diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index 77f8cf34fe..b2fa7700b9 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -46,6 +46,8 @@ import { init } from "@sentry/electron/renderer"; import kubernetesClusterCategoryInjectable from "../common/catalog/categories/kubernetes-cluster.injectable"; import autoRegistrationInjectable from "../common/k8s-api/api-manager/auto-registration.injectable"; import assert from "assert"; +import { beforeFrameStartsInjectionToken } from "./before-frame-starts/before-frame-starts-injection-token"; +import { runManyFor } from "../common/runnable/run-many-for"; configurePackages(); // global packages registerCustomThemes(); // monaco editor themes @@ -66,7 +68,9 @@ export async function bootstrap(di: DiContainer) { initializeSentryReporting(init); } - await di.runSetups(); + const beforeFrameStarts = runManyFor(di)(beforeFrameStartsInjectionToken); + + await beforeFrameStarts(); // TODO: Consolidate import time side-effect to setup time bindEvents(); @@ -136,7 +140,7 @@ export async function bootstrap(di: DiContainer) { await clusterStore.loadInitialOnRenderer(); // HotbarStore depends on: ClusterStore - di.inject(hotbarStoreInjectable); + di.inject(hotbarStoreInjectable).load(); // ThemeStore depends on: UserStore // TODO: Remove temporal dependencies @@ -174,9 +178,11 @@ export async function bootstrap(di: DiContainer) { }); } + const history = di.inject(historyInjectable); + render( - + {DefaultProps(App)} , diff --git a/src/renderer/components/+catalog/catalog.test.tsx b/src/renderer/components/+catalog/catalog.test.tsx index e8bde796cc..1fd7e3f3f8 100644 --- a/src/renderer/components/+catalog/catalog.test.tsx +++ b/src/renderer/components/+catalog/catalog.test.tsx @@ -28,6 +28,7 @@ import appEventBusInjectable from "../../../common/app-event-bus/app-event-bus.i import { computed } from "mobx"; import ipcRendererInjectable from "../../app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable"; import { UserStore } from "../../../common/user-store"; +import broadcastMessageInjectable from "../../../common/ipc/broadcast-message.injectable"; mockWindow(); jest.mock("electron", () => ({ @@ -88,15 +89,16 @@ describe("", () => { let catalogEntityItem: MockCatalogEntity; let render: DiRender; - beforeEach(async () => { + beforeEach(() => { di = getDiForUnitTesting({ doGeneralOverrides: true }); di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); + + di.override(broadcastMessageInjectable, () => async () => {}); + di.permitSideEffects(getConfigurationFileModelInjectable); di.permitSideEffects(appVersionInjectable); - await di.runSetups(); - mockFs(); CatalogEntityDetailRegistry.createInstance(); diff --git a/src/renderer/components/+extensions/__tests__/extensions.test.tsx b/src/renderer/components/+extensions/__tests__/extensions.test.tsx index acd585ee1a..7ec77838fc 100644 --- a/src/renderer/components/+extensions/__tests__/extensions.test.tsx +++ b/src/renderer/components/+extensions/__tests__/extensions.test.tsx @@ -58,7 +58,7 @@ describe("Extensions", () => { let extensionInstallationStateStore: ExtensionInstallationStateStore; let render: DiRender; - beforeEach(async () => { + beforeEach(() => { const di = getDiForUnitTesting({ doGeneralOverrides: true }); di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); @@ -71,8 +71,6 @@ describe("Extensions", () => { "some-directory-for-user-data": {}, }); - await di.runSetups(); - render = renderFor(di); installFromInput = jest.fn(); diff --git a/src/renderer/components/+helm-charts/helm-charts.tsx b/src/renderer/components/+helm-charts/helm-charts.tsx index 4fbecc0946..23175c199b 100644 --- a/src/renderer/components/+helm-charts/helm-charts.tsx +++ b/src/renderer/components/+helm-charts/helm-charts.tsx @@ -79,6 +79,8 @@ class NonInjectedHelmCharts extends Component { render() { return ( +
+ { + const clusterContext = di.inject(clusterFrameContextInjectable); const namespaceStore = di.inject(namespaceStoreInjectable); - // TODO: Inject clusterContext directly instead of accessing dependency of a dependency - const clusterContext = namespaceStore.context; - return asyncComputed(async () => { const contextNamespaces = namespaceStore.contextNamespaces || []; diff --git a/src/renderer/components/+preferences/extension-preference-item-registrator.injectable.ts b/src/renderer/components/+preferences/extension-preference-item-registrator.injectable.ts index bad5528891..d7daebce1f 100644 --- a/src/renderer/components/+preferences/extension-preference-item-registrator.injectable.ts +++ b/src/renderer/components/+preferences/extension-preference-item-registrator.injectable.ts @@ -44,7 +44,7 @@ const extensionPreferenceItemRegistratorInjectable = getInjectable({ }), ); - injectables.forEach(di.register); + di.register(...injectables); return; }, diff --git a/src/renderer/components/+preferences/extension-telemetry-preference-item-registrator.injectable.ts b/src/renderer/components/+preferences/extension-telemetry-preference-item-registrator.injectable.ts index e4f30a32a6..ff6fa2ecbc 100644 --- a/src/renderer/components/+preferences/extension-telemetry-preference-item-registrator.injectable.ts +++ b/src/renderer/components/+preferences/extension-telemetry-preference-item-registrator.injectable.ts @@ -44,7 +44,7 @@ const extensionTelemetryPreferenceItemRegistratorInjectable = getInjectable({ }), ); - injectables.forEach(di.register); + di.register(...injectables); return; }, diff --git a/src/renderer/components/+user-management/+cluster-role-bindings/__tests__/dialog.test.tsx b/src/renderer/components/+user-management/+cluster-role-bindings/__tests__/dialog.test.tsx index 8e4beba8e8..e221c008ac 100644 --- a/src/renderer/components/+user-management/+cluster-role-bindings/__tests__/dialog.test.tsx +++ b/src/renderer/components/+user-management/+cluster-role-bindings/__tests__/dialog.test.tsx @@ -17,7 +17,7 @@ import ipcRendererInjectable from "../../../../app-paths/get-value-from-register describe("ClusterRoleBindingDialog tests", () => { let render: DiRender; - beforeEach(async () => { + beforeEach(() => { const di = getDiForUnitTesting({ doGeneralOverrides: true }); di.override(storesAndApisCanBeCreatedInjectable, () => true); @@ -26,8 +26,6 @@ describe("ClusterRoleBindingDialog tests", () => { invoke: jest.fn(), // TODO: replace with proper mocking via the IPC bridge } as never)); - await di.runSetups(); - render = renderFor(di); const store = di.inject(clusterRoleStoreInjectable); diff --git a/src/renderer/components/+user-management/+role-bindings/__tests__/dialog.test.tsx b/src/renderer/components/+user-management/+role-bindings/__tests__/dialog.test.tsx index be029ed3aa..969de55ea4 100644 --- a/src/renderer/components/+user-management/+role-bindings/__tests__/dialog.test.tsx +++ b/src/renderer/components/+user-management/+role-bindings/__tests__/dialog.test.tsx @@ -18,7 +18,7 @@ import ipcRendererInjectable from "../../../../app-paths/get-value-from-register describe("RoleBindingDialog tests", () => { let render: DiRender; - beforeEach(async () => { + beforeEach(() => { const di = getDiForUnitTesting({ doGeneralOverrides: true }); di.override(storesAndApisCanBeCreatedInjectable, () => true); @@ -28,8 +28,6 @@ describe("RoleBindingDialog tests", () => { invoke: jest.fn(), // TODO: replace with proper mocking via the IPC bridge } as never)); - await di.runSetups(); - render = renderFor(di); const store = di.inject(clusterRoleStoreInjectable); diff --git a/src/renderer/components/+welcome/__test__/welcome.test.tsx b/src/renderer/components/+welcome/__test__/welcome.test.tsx index aa86299aeb..13f338e7d4 100644 --- a/src/renderer/components/+welcome/__test__/welcome.test.tsx +++ b/src/renderer/components/+welcome/__test__/welcome.test.tsx @@ -30,7 +30,7 @@ describe("", () => { let di: DiContainer; let welcomeBannersStub: WelcomeBannerRegistration[]; - beforeEach(async () => { + beforeEach(() => { di = getDiForUnitTesting({ doGeneralOverrides: true }); render = renderFor(di); welcomeBannersStub = []; diff --git a/src/renderer/components/+workloads-pods/__tests__/pod-tolerations.test.tsx b/src/renderer/components/+workloads-pods/__tests__/pod-tolerations.test.tsx index 3dc459f23b..b0cacb8b4a 100644 --- a/src/renderer/components/+workloads-pods/__tests__/pod-tolerations.test.tsx +++ b/src/renderer/components/+workloads-pods/__tests__/pod-tolerations.test.tsx @@ -37,7 +37,7 @@ const tolerations: Toleration[] =[ describe("", () => { let render: DiRender; - beforeEach(async () => { + beforeEach(() => { const di = getDiForUnitTesting({ doGeneralOverrides: true }); di.override(directoryForLensLocalStorageInjectable, () => "some-directory-for-lens-local-storage" ); diff --git a/src/renderer/components/cluster-manager/cluster-frame-handler.injectable.ts b/src/renderer/components/cluster-manager/cluster-frame-handler.injectable.ts new file mode 100644 index 0000000000..19611f3117 --- /dev/null +++ b/src/renderer/components/cluster-manager/cluster-frame-handler.injectable.ts @@ -0,0 +1,13 @@ +/** + * 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 { ClusterFrameHandler } from "./cluster-frame-handler"; + +const clusterFrameHandlerInjectable = getInjectable({ + id: "cluster-frame-handler", + instantiate: () => new ClusterFrameHandler(), +}); + +export default clusterFrameHandlerInjectable; diff --git a/src/renderer/components/cluster-manager/lens-views.ts b/src/renderer/components/cluster-manager/cluster-frame-handler.ts similarity index 100% rename from src/renderer/components/cluster-manager/lens-views.ts rename to src/renderer/components/cluster-manager/cluster-frame-handler.ts diff --git a/src/renderer/components/cluster-manager/cluster-view.tsx b/src/renderer/components/cluster-manager/cluster-view.tsx index 15dc5ee9e1..1eba3195a9 100644 --- a/src/renderer/components/cluster-manager/cluster-view.tsx +++ b/src/renderer/components/cluster-manager/cluster-view.tsx @@ -9,7 +9,7 @@ import type { IComputedValue } from "mobx"; import { computed, makeObservable, reaction } from "mobx"; import { disposeOnUnmount, observer } from "mobx-react"; import { ClusterStatus } from "./cluster-status"; -import type { ClusterFrameHandler } from "./lens-views"; +import type { ClusterFrameHandler } from "./cluster-frame-handler"; import type { Cluster } from "../../../common/cluster/cluster"; import { ClusterStore } from "../../../common/cluster-store/cluster-store"; import { requestClusterActivation } from "../../ipc"; @@ -17,7 +17,7 @@ import { withInjectables } from "@ogre-tools/injectable-react"; import type { NavigateToCatalog } from "../../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable"; import navigateToCatalogInjectable from "../../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable"; import clusterViewRouteParametersInjectable from "./cluster-view-route-parameters.injectable"; -import clusterFramesInjectable from "./lens-views.injectable"; +import clusterFrameHandlerInjectable from "./cluster-frame-handler.injectable"; import type { CatalogEntityRegistry } from "../../api/catalog/entity/registry"; import catalogEntityRegistryInjectable from "../../api/catalog/entity/registry.injectable"; @@ -107,7 +107,7 @@ export const ClusterView = withInjectables(NonInjectedClusterView, getProps: (di) => ({ clusterId: di.inject(clusterViewRouteParametersInjectable).clusterId, navigateToCatalog: di.inject(navigateToCatalogInjectable), - clusterFrames: di.inject(clusterFramesInjectable), + clusterFrames: di.inject(clusterFrameHandlerInjectable), entityRegistry: di.inject(catalogEntityRegistryInjectable), }), }); diff --git a/src/renderer/components/delete-cluster-dialog/__tests__/delete-cluster-dialog.test.tsx b/src/renderer/components/delete-cluster-dialog/__tests__/delete-cluster-dialog.test.tsx index 1838f93116..a2a0e6a301 100644 --- a/src/renderer/components/delete-cluster-dialog/__tests__/delete-cluster-dialog.test.tsx +++ b/src/renderer/components/delete-cluster-dialog/__tests__/delete-cluster-dialog.test.tsx @@ -4,23 +4,28 @@ */ import "@testing-library/jest-dom/extend-expect"; import { KubeConfig } from "@kubernetes/client-node"; -import { fireEvent, screen } from "@testing-library/react"; +import { fireEvent } from "@testing-library/react"; +import type { RenderResult } from "@testing-library/react"; import mockFs from "mock-fs"; import React from "react"; import * as selectEvent from "react-select-event"; import type { Cluster } from "../../../../common/cluster/cluster"; import { DeleteClusterDialog } from "../view"; import type { ClusterModel } from "../../../../common/cluster-types"; -import { getDisForUnitTesting } from "../../../../test-utils/get-dis-for-unit-testing"; import { createClusterInjectionToken } from "../../../../common/cluster/create-cluster-injection-token"; import createContextHandlerInjectable from "../../../../main/context-handler/create-context-handler.injectable"; -import type { DiRender } from "../../test-utils/renderFor"; -import { renderFor } from "../../test-utils/renderFor"; import type { OpenDeleteClusterDialog } from "../open.injectable"; import openDeleteClusterDialogInjectable from "../open.injectable"; import storesAndApisCanBeCreatedInjectable from "../../../stores-apis-can-be-created.injectable"; import createKubeconfigManagerInjectable from "../../../../main/kubeconfig-manager/create-kubeconfig-manager.injectable"; -import ipcRendererInjectable from "../../../app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable"; +import type { ApplicationBuilder } from "../../test-utils/get-application-builder"; +import { getApplicationBuilder } from "../../test-utils/get-application-builder"; +import { routeInjectionToken } from "../../../../common/front-end-routing/route-injection-token"; +import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import { routeSpecificComponentInjectionToken } from "../../../routes/route-specific-component-injection-token"; +import { navigateToRouteInjectionToken } from "../../../../common/front-end-routing/navigate-to-route-injection-token"; +import hotbarStoreInjectable from "../../../../common/hotbars/store.injectable"; jest.mock("electron", () => ({ app: { @@ -38,33 +43,29 @@ jest.mock("electron", () => ({ }, })); -const kubeconfig = ` +const multiClusterConfig = ` apiVersion: v1 clusters: - cluster: server: https://localhost - name: test + name: some-current-context-cluster - cluster: server: http://localhost - name: other-cluster + name: some-non-current-context-cluster contexts: - context: - cluster: test - user: test - name: test + cluster: some-current-context-cluster + user: some-user + name: some-current-context - context: - cluster: test - user: test - name: test2 -- context: - cluster: other-cluster - user: test - name: other-context -current-context: other-context + cluster: some-non-current-context-cluster + user: some-user + name: some-non-current-context +current-context: some-current-context kind: Config preferences: {} users: -- name: test +- name: some-user user: token: kubeconfig-user-q4lm4:xxxyyyy `; @@ -74,17 +75,17 @@ apiVersion: v1 clusters: - cluster: server: http://localhost - name: other-cluster + name: some-cluster contexts: - context: - cluster: other-cluster - user: test - name: other-context -current-context: other-context + cluster: some-cluster + user: some-user + name: some-context +current-context: some-context kind: Config preferences: {} users: -- name: test +- name: some-user user: token: kubeconfig-user-q4lm4:xxxyyyy `; @@ -92,60 +93,107 @@ users: let config: KubeConfig; describe("", () => { + let applicationBuilder: ApplicationBuilder; let createCluster: (model: ClusterModel) => Cluster; let openDeleteClusterDialog: OpenDeleteClusterDialog; - let render: DiRender; beforeEach(async () => { - const { mainDi, rendererDi, runSetups } = getDisForUnitTesting({ doGeneralOverrides: true }); + applicationBuilder = getApplicationBuilder(); + + applicationBuilder.beforeApplicationStart(({ mainDi, rendererDi }) => { + mainDi.override(createContextHandlerInjectable, () => () => undefined as any); + mainDi.override(createKubeconfigManagerInjectable, () => () => undefined as any); + + rendererDi.override(hotbarStoreInjectable, () => ({})); + rendererDi.override(storesAndApisCanBeCreatedInjectable, () => true); + }); + + const { rendererDi } = applicationBuilder.dis; + + rendererDi.register(testRouteInjectable, testRouteComponentInjectable); + + applicationBuilder.beforeRender(({ rendererDi }) => { + const navigateToRoute = rendererDi.inject(navigateToRouteInjectionToken); + const testRoute = rendererDi.inject(testRouteInjectable); + + navigateToRoute(testRoute); + }); - render = renderFor(rendererDi); - mainDi.override(createContextHandlerInjectable, () => () => undefined); - mainDi.override(createKubeconfigManagerInjectable, () => () => undefined); mockFs(); - rendererDi.override(storesAndApisCanBeCreatedInjectable, () => true); - rendererDi.override(ipcRendererInjectable, () => ({ - on: jest.fn(), - invoke: jest.fn(), // TODO: replace with proper mocking via the IPC bridge - } as never)); - - await runSetups(); - - openDeleteClusterDialog = rendererDi.inject(openDeleteClusterDialogInjectable); - createCluster = mainDi.inject(createClusterInjectionToken); + applicationBuilder.beforeRender(({ rendererDi }) => { + openDeleteClusterDialog = rendererDi.inject(openDeleteClusterDialogInjectable); + createCluster = rendererDi.inject(createClusterInjectionToken); + }); }); afterEach(() => { mockFs.restore(); }); + it("shows context switcher when deleting current cluster", async () => { + const mockOpts = { + "temp-kube-config": multiClusterConfig, + }; + + mockFs(mockOpts); + + config = new KubeConfig(); + config.loadFromString(multiClusterConfig); + + applicationBuilder.beforeRender(({ rendererDi }) => { + const createCluster = rendererDi.inject(createClusterInjectionToken); + + const cluster = createCluster({ + id: "some-current-context-cluster", + contextName: "some-current-context", + preferences: { + clusterName: "some-current-context-cluster", + }, + kubeConfigPath: "./temp-kube-config", + }); + + openDeleteClusterDialog({ cluster, config }); + }); + + const rendered = await applicationBuilder.render(); + + const { getByText } = rendered; + + const menu = getByText("Select new context..."); + + expect(menu).toBeInTheDocument(); + selectEvent.openMenu(menu); + + expect(getByText("some-current-context")).toBeInTheDocument(); + expect(getByText("some-non-current-context")).toBeInTheDocument(); + }); + + describe("Kubeconfig with different clusters", () => { + let rendered: RenderResult; + beforeEach(async () => { const mockOpts = { - "temp-kube-config": kubeconfig, + "temp-kube-config": multiClusterConfig, }; mockFs(mockOpts); config = new KubeConfig(); - config.loadFromString(kubeconfig); - }); + config.loadFromString(multiClusterConfig); - afterEach(() => { - mockFs.restore(); + rendered = await applicationBuilder.render(); }); it("renders w/o errors", () => { - const { container } = render(); - - expect(container).toBeInstanceOf(HTMLElement); + expect(rendered.container).toBeInstanceOf(HTMLElement); }); - it("shows warning when deleting non-current-context cluster", async () => { + it("shows warning when deleting non-current-context cluster", () => { const cluster = createCluster({ - id: "test", - contextName: "test", + id: "some-non-current-context-cluster", + contextName: "some-non-current-context", preferences: { clusterName: "minikube", }, @@ -154,99 +202,78 @@ describe("", () => { openDeleteClusterDialog({ cluster, config }); - render(); const message = "The contents of kubeconfig file will be changed!"; - expect(await screen.findByText(message)).toBeInstanceOf(HTMLElement); + expect(rendered.getByText(message)).toBeInstanceOf(HTMLElement); }); - it("shows warning when deleting current-context cluster", async () => { + it("shows warning when deleting current-context cluster", () => { const cluster = createCluster({ - id: "other-cluster", - contextName: "other-context", + id: "some-current-context-cluster", + contextName: "some-current-context", preferences: { - clusterName: "other-cluster", + clusterName: "some-current-context-cluster", }, kubeConfigPath: "./temp-kube-config", }); openDeleteClusterDialog({ cluster, config }); - render(); - expect(await screen.findByTestId("current-context-warning")).toBeInstanceOf(HTMLElement); + expect(rendered.getByTestId("current-context-warning")).toBeInstanceOf(HTMLElement); }); - it("shows context switcher when deleting current cluster", async () => { + it("shows context switcher after checkbox click", () => { const cluster = createCluster({ - id: "other-cluster", - contextName: "other-context", + id: "some-current-context-cluster", + contextName: "some-current-context", preferences: { - clusterName: "other-cluster", + clusterName: "some-current-context-cluster", }, kubeConfigPath: "./temp-kube-config", }); openDeleteClusterDialog({ cluster, config }); - render(); - const menu = await screen.findByText("Select new context..."); - - expect(menu).toBeInTheDocument(); - selectEvent.openMenu(menu); - - expect(await screen.findByText("test")).toBeInTheDocument(); - expect(await screen.findByText("test2")).toBeInTheDocument(); - }); - - it("shows context switcher after checkbox click", async () => { - const cluster = createCluster({ - id: "some-cluster", - contextName: "test", - preferences: { - clusterName: "test", - }, - kubeConfigPath: "./temp-kube-config", - }); - - openDeleteClusterDialog({ cluster, config }); - render(); - - const link = await screen.findByTestId("context-switch"); + const { getByText, getByTestId } = rendered; + const link = getByTestId("context-switch"); expect(link).toBeInstanceOf(HTMLElement); fireEvent.click(link); - const menu = await screen.findByText("Select new context..."); + const menu = getByText("Select new context..."); expect(menu).toBeInTheDocument(); selectEvent.openMenu(menu); - expect(await screen.findByText("test")).toBeInTheDocument(); - expect(await screen.findByText("test2")).toBeInTheDocument(); + expect(getByText("some-current-context")).toBeInTheDocument(); + expect(getByText("some-non-current-context")).toBeInTheDocument(); }); - it("shows warning for internal kubeconfig cluster", async () => { + it("given cluster in internal kubeconfig, when deleting cluster outside of current context, shows warning for internal kubeconfig cluster", () => { const cluster = createCluster({ - id: "some-cluster", - contextName: "test", + id: "some-non-current-context-cluster", + contextName: "some-non-current-context", + preferences: { - clusterName: "test", + clusterName: "some-non-current-context-cluster", }, + kubeConfigPath: "./temp-kube-config", }); const spy = jest.spyOn(cluster, "isInLocalKubeconfig").mockImplementation(() => true); openDeleteClusterDialog({ cluster, config }); - render(); - expect(await screen.findByTestId("internal-kubeconfig-warning")).toBeInstanceOf(HTMLElement); + expect(rendered.getByTestId("internal-kubeconfig-warning")).toBeInstanceOf(HTMLElement); spy.mockRestore(); }); }); describe("Kubeconfig with single cluster", () => { + let rendered: RenderResult; + beforeEach(async () => { const mockOpts = { "temp-kube-config": singleClusterConfig, @@ -256,26 +283,46 @@ describe("", () => { config = new KubeConfig(); config.loadFromString(singleClusterConfig); + + rendered = await applicationBuilder.render(); }); - afterEach(() => { - mockFs.restore(); - }); - - it("shows warning if no other contexts left", async () => { + it("shows warning if no other contexts left", () => { const cluster = createCluster({ - id: "other-cluster", - contextName: "other-context", + id: "some-cluster", + contextName: "some-context", preferences: { - clusterName: "other-cluster", + clusterName: "some-cluster", }, kubeConfigPath: "./temp-kube-config", }); openDeleteClusterDialog({ cluster, config }); - render(); - expect(await screen.findByTestId("no-more-contexts-warning")).toBeInstanceOf(HTMLElement); + expect(rendered.getByTestId("no-more-contexts-warning")).toBeInstanceOf(HTMLElement); }); }); }); + +const testRouteInjectable = getInjectable({ + id: "some-test-route", + + instantiate: () => ({ + path: "/some-test-path", + clusterFrame: false, + isEnabled: computed(() => true), + }), + + injectionToken: routeInjectionToken, +}); + +const testRouteComponentInjectable = getInjectable({ + id: "some-test-component", + + instantiate: (di) => ({ + route: di.inject(testRouteInjectable), + Component: () => , + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); diff --git a/src/renderer/components/dock/__test__/dock-store.test.ts b/src/renderer/components/dock/__test__/dock-store.test.ts index ef1f575a1d..e554b4dcdc 100644 --- a/src/renderer/components/dock/__test__/dock-store.test.ts +++ b/src/renderer/components/dock/__test__/dock-store.test.ts @@ -22,7 +22,7 @@ const initialTabs: DockTab[] = [ describe("DockStore", () => { let dockStore: DockStore; - beforeEach(async () => { + beforeEach(() => { const di = getDiForUnitTesting({ doGeneralOverrides: true }); di.override(hostedClusterIdInjectable, () => "some-cluster-id"); diff --git a/src/renderer/components/dock/__test__/dock-tabs.test.tsx b/src/renderer/components/dock/__test__/dock-tabs.test.tsx index 87171042d3..df9fc08ce2 100644 --- a/src/renderer/components/dock/__test__/dock-tabs.test.tsx +++ b/src/renderer/components/dock/__test__/dock-tabs.test.tsx @@ -91,8 +91,6 @@ describe("", () => { di.permitSideEffects(getConfigurationFileModelInjectable); di.permitSideEffects(appVersionInjectable); - await di.runSetups(); - dockStore = di.inject(dockStoreInjectable); await dockStore.whenReady; diff --git a/src/renderer/components/dock/create-resource/lens-templates.injectable.ts b/src/renderer/components/dock/create-resource/lens-templates.injectable.ts index 898855a309..ce78c2286e 100644 --- a/src/renderer/components/dock/create-resource/lens-templates.injectable.ts +++ b/src/renderer/components/dock/create-resource/lens-templates.injectable.ts @@ -5,20 +5,21 @@ import { getInjectable } from "@ogre-tools/injectable"; import path from "path"; import { hasCorrectExtension } from "./has-correct-extension"; -import { staticFilesDirectory } from "../../../../common/vars"; import readFileInjectable from "../../../../common/fs/read-file.injectable"; import readDirInjectable from "../../../../common/fs/read-dir.injectable"; import type { RawTemplates } from "./create-resource-templates.injectable"; import type { GetAbsolutePath } from "../../../../common/path/get-absolute-path.injectable"; import getAbsolutePathInjectable from "../../../../common/path/get-absolute-path.injectable"; +import staticFilesDirectoryInjectable from "../../../../common/vars/static-files-directory.injectable"; interface Dependencies { readDir: (dirPath: string) => Promise; readFile: (filePath: string) => Promise; getAbsolutePath: GetAbsolutePath; + staticFilesDirectory: string; } -async function getTemplates({ readDir, readFile, getAbsolutePath }: Dependencies) { +async function getTemplates({ readDir, readFile, getAbsolutePath, staticFilesDirectory }: Dependencies) { const templatesFolder = getAbsolutePath(staticFilesDirectory, "../templates/create-resource"); /** @@ -43,6 +44,7 @@ const lensCreateResourceTemplatesInjectable = getInjectable({ readFile: di.inject(readFileInjectable), readDir: di.inject(readDirInjectable), getAbsolutePath: di.inject(getAbsolutePathInjectable), + staticFilesDirectory: di.inject(staticFilesDirectoryInjectable), }); return ["lens", templates]; diff --git a/src/renderer/components/dock/logs/__test__/log-resource-selector.test.tsx b/src/renderer/components/dock/logs/__test__/log-resource-selector.test.tsx index 8a1105a6d0..c886b195cd 100644 --- a/src/renderer/components/dock/logs/__test__/log-resource-selector.test.tsx +++ b/src/renderer/components/dock/logs/__test__/log-resource-selector.test.tsx @@ -127,7 +127,7 @@ const getFewPodsTabData = (tabId: TabId, deps: Partial", () => { let render: DiRender; - beforeEach(async () => { + beforeEach(() => { const di = getDiForUnitTesting({ doGeneralOverrides: true }); di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); @@ -142,8 +142,6 @@ describe("", () => { render = renderFor(di); - await di.runSetups(); - mockFs({ "tmp": {}, }); diff --git a/src/renderer/components/dock/logs/__test__/log-search.test.tsx b/src/renderer/components/dock/logs/__test__/log-search.test.tsx index 6af8717b20..600b58a652 100644 --- a/src/renderer/components/dock/logs/__test__/log-search.test.tsx +++ b/src/renderer/components/dock/logs/__test__/log-search.test.tsx @@ -60,12 +60,10 @@ const getOnePodViewModel = (tabId: TabId, deps: Partial { let render: DiRender; - beforeEach(async () => { + beforeEach(() => { const di = getDiForUnitTesting({ doGeneralOverrides: true }); render = renderFor(di); - - await di.runSetups(); }); it("renders w/o errors", () => { diff --git a/src/renderer/components/hotbar/__tests__/hotbar-remove-command.test.tsx b/src/renderer/components/hotbar/__tests__/hotbar-remove-command.test.tsx index a5935dd7c5..c7ce186afb 100644 --- a/src/renderer/components/hotbar/__tests__/hotbar-remove-command.test.tsx +++ b/src/renderer/components/hotbar/__tests__/hotbar-remove-command.test.tsx @@ -63,7 +63,7 @@ describe("", () => { mockFs.restore(); }); - it("renders w/o errors", async () => { + it("renders w/o errors", () => { di.override(hotbarStoreInjectable, () => ({ hotbars: [mockHotbars["1"]], getById: (id: string) => mockHotbars[id], @@ -73,14 +73,12 @@ describe("", () => { getDisplayLabel: () => "1: Default", }) as unknown as HotbarStore); - await di.runSetups(); - const { container } = render(); expect(container).toBeInstanceOf(HTMLElement); }); - it("calls remove if you click on the entry", async () => { + it("calls remove if you click on the entry", () => { const removeMock = jest.fn(); di.override(hotbarStoreInjectable, () => ({ @@ -91,8 +89,6 @@ describe("", () => { getDisplayLabel: () => "1: Default", }) as unknown as HotbarStore); - await di.runSetups(); - const { getByText } = render( <> diff --git a/src/renderer/components/item-object-list/content.tsx b/src/renderer/components/item-object-list/content.tsx index 275d7e4bfe..3529b258c2 100644 --- a/src/renderer/components/item-object-list/content.tsx +++ b/src/renderer/components/item-object-list/content.tsx @@ -24,10 +24,11 @@ import type { ThemeStore } from "../../themes/store"; import { MenuActions } from "../menu/menu-actions"; import { MenuItem } from "../menu"; import { Checkbox } from "../checkbox"; -import { UserStore } from "../../../common/user-store"; +import type { UserStore } from "../../../common/user-store"; import type { ItemListStore } from "./list-layout"; import { withInjectables } from "@ogre-tools/injectable-react"; import themeStoreInjectable from "../../themes/store.injectable"; +import userStoreInjectable from "../../../common/user-store/user-store.injectable"; import pageFiltersStoreInjectable from "./page-filters/store.injectable"; import type { OpenConfirmDialog } from "../confirm-dialog/open.injectable"; import openConfirmDialogInjectable from "../confirm-dialog/open.injectable"; @@ -69,6 +70,7 @@ export interface ItemListLayoutContentProps cellProps.id && UserStore.getInstance().toggleTableColumnVisibility(tableId, cellProps.id)) + ? (() => cellProps.id && this.props.userStore.toggleTableColumnVisibility(tableId, cellProps.id)) : undefined )} /> @@ -373,6 +375,7 @@ export const ItemListLayoutContent = withInjectables ({ ...props, themeStore: di.inject(themeStoreInjectable), + userStore: di.inject(userStoreInjectable), pageFiltersStore: di.inject(pageFiltersStoreInjectable), openConfirmDialog: di.inject(openConfirmDialogInjectable), }), diff --git a/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx b/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx index a5b13736db..4da76639b8 100644 --- a/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx +++ b/src/renderer/components/kube-object-menu/kube-object-menu.test.tsx @@ -57,7 +57,7 @@ describe("kube-object-menu", () => { let di: DiContainer; let render: DiRender; - beforeEach(async () => { + beforeEach(() => { const MenuItemComponent = () =>
  • Some menu item
  • ; const someTestExtension = new SomeTestExtension([ { diff --git a/src/renderer/components/kube-object-status-icon/kube-object-status-icon.test.tsx b/src/renderer/components/kube-object-status-icon/kube-object-status-icon.test.tsx index 8a8c179d09..eccb136222 100644 --- a/src/renderer/components/kube-object-status-icon/kube-object-status-icon.test.tsx +++ b/src/renderer/components/kube-object-status-icon/kube-object-status-icon.test.tsx @@ -18,7 +18,7 @@ describe("kube-object-status-icon", () => { let render: DiRender; let kubeObjectStatusRegistrations: KubeObjectStatusRegistration[]; - beforeEach(async () => { + beforeEach(() => { // TODO: Make mocking of date in unit tests global global.Date.now = () => new Date("2015-10-21T07:28:00Z").getTime(); @@ -35,8 +35,6 @@ describe("kube-object-status-icon", () => { di.override(rendererExtensionsInjectable, () => computed(() => [someTestExtension]), ); - - await di.runSetups(); }); it("given no statuses, when rendered, renders as empty", () => { diff --git a/src/renderer/components/layout/tab-layout.tsx b/src/renderer/components/layout/tab-layout.tsx index e9d4cb3b80..19f8db4d6d 100644 --- a/src/renderer/components/layout/tab-layout.tsx +++ b/src/renderer/components/layout/tab-layout.tsx @@ -45,7 +45,7 @@ export const TabLayout = observer(({ className, contentClass, tabs = [], childre key={url} label={title} value={url} - active={!!matchPath(currentLocation, { path: routePath, exact })} + active={!!matchPath(currentLocation, { path: routePath, exact })} /> ))} diff --git a/src/renderer/components/layout/top-bar/top-bar.test.tsx b/src/renderer/components/layout/top-bar/top-bar.test.tsx index a33573b32b..9355da74ae 100644 --- a/src/renderer/components/layout/top-bar/top-bar.test.tsx +++ b/src/renderer/components/layout/top-bar/top-bar.test.tsx @@ -76,15 +76,13 @@ describe("", () => { let di: DiContainer; let render: DiRender; - beforeEach(async () => { + beforeEach(() => { di = getDiForUnitTesting({ doGeneralOverrides: true }); mockFs(); di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); - await di.runSetups(); - render = renderFor(di); }); diff --git a/src/renderer/components/menu/menu-actions.tsx b/src/renderer/components/menu/menu-actions.tsx index 03beb5d40d..aa44d3634f 100644 --- a/src/renderer/components/menu/menu-actions.tsx +++ b/src/renderer/components/menu/menu-actions.tsx @@ -50,6 +50,7 @@ class NonInjectedMenuActions extends React.Component", () => { let di: DiContainer; let render: DiRender; - beforeEach(async () => { + beforeEach(() => { di = getDiForUnitTesting({ doGeneralOverrides: true }); render = renderFor(di); diff --git a/src/renderer/components/status-bar/status-bar.test.tsx b/src/renderer/components/status-bar/status-bar.test.tsx index dbcfa59adf..53920d45c7 100644 --- a/src/renderer/components/status-bar/status-bar.test.tsx +++ b/src/renderer/components/status-bar/status-bar.test.tsx @@ -39,15 +39,13 @@ describe("", () => { let di: DiContainer; let statusBarItems: IObservableArray; - beforeEach(async () => { + beforeEach(() => { statusBarItems = observable.array([]); di = getDiForUnitTesting({ doGeneralOverrides: true }); render = renderFor(di); di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); di.override(rendererExtensionsInjectable, () => computed(() => [new SomeTestExtension(statusBarItems)])); - - await di.runSetups(); }); it("renders w/o errors", () => { diff --git a/src/renderer/components/test-utils/get-application-builder.tsx b/src/renderer/components/test-utils/get-application-builder.tsx index eda2653db3..178874ed76 100644 --- a/src/renderer/components/test-utils/get-application-builder.tsx +++ b/src/renderer/components/test-utils/get-application-builder.tsx @@ -9,7 +9,6 @@ import { extensionRegistratorInjectionToken } from "../../../extensions/extensio import type { IObservableArray } from "mobx"; import { computed, observable, runInAction } from "mobx"; import { renderFor } from "./renderFor"; -import observableHistoryInjectable from "../../navigation/observable-history.injectable"; import React from "react"; import { Router } from "react-router"; import { Observer } from "mobx-react"; @@ -18,7 +17,6 @@ import allowedResourcesInjectable from "../../../common/cluster-store/allowed-re import type { RenderResult } from "@testing-library/react"; import { fireEvent } from "@testing-library/react"; import type { KubeResource } from "../../../common/rbac"; -import directoryForLensLocalStorageInjectable from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable"; import { Sidebar } from "../layout/sidebar"; import { getDisForUnitTesting } from "../../../test-utils/get-dis-for-unit-testing"; import type { DiContainer } from "@ogre-tools/injectable"; @@ -34,6 +32,18 @@ import type { MenuItemOpts } from "../../../main/menu/application-menu-items.inj import applicationMenuItemsInjectable from "../../../main/menu/application-menu-items.injectable"; import type { MenuItem, MenuItemConstructorOptions } from "electron"; import storesAndApisCanBeCreatedInjectable from "../../stores-apis-can-be-created.injectable"; +import navigateToHelmChartsInjectable from "../../../common/front-end-routing/routes/cluster/helm/charts/navigate-to-helm-charts.injectable"; +import hostedClusterInjectable from "../../../common/cluster-store/hosted-cluster.injectable"; +import { ClusterFrameContext } from "../../cluster-frame-context/cluster-frame-context"; +import type { Cluster } from "../../../common/cluster/cluster"; +import { KubeObjectStore } from "../../../common/k8s-api/kube-object.store"; +import clusterFrameContextInjectable from "../../cluster-frame-context/cluster-frame-context.injectable"; +import startMainApplicationInjectable from "../../../main/start-main-application/start-main-application.injectable"; +import startFrameInjectable from "../../start-frame/start-frame.injectable"; +import { flushPromises } from "../../../common/test-utils/flush-promises"; +import type { NamespaceStore } from "../+namespaces/store"; +import namespaceStoreInjectable from "../+namespaces/store.injectable"; +import historyInjectable from "../../navigation/history.injectable"; type Callback = (dis: DiContainers) => void | Promise; @@ -42,12 +52,12 @@ export interface ApplicationBuilder { setEnvironmentToClusterFrame: () => ApplicationBuilder; addExtensions: (...extensions: LensRendererExtension[]) => Promise; allowKubeResource: (resourceName: KubeResource) => ApplicationBuilder; - beforeSetups: (callback: Callback) => ApplicationBuilder; + beforeApplicationStart: (callback: Callback) => ApplicationBuilder; beforeRender: (callback: Callback) => ApplicationBuilder; render: () => Promise; applicationMenu: { - click: (path: string) => void; + click: (path: string) => Promise; }; preferences: { @@ -57,6 +67,10 @@ export interface ApplicationBuilder { click: (id: string) => void; }; }; + + helmCharts: { + navigate: () => void; + }; } interface DiContainers { @@ -70,13 +84,14 @@ interface Environment { } export const getApplicationBuilder = () => { - const { rendererDi, mainDi, runSetups } = getDisForUnitTesting({ + const { rendererDi, mainDi } = getDisForUnitTesting({ doGeneralOverrides: true, }); const dis = { rendererDi, mainDi }; const clusterStoreStub = { + provideInitialFromMain: () => {}, getById: (): null => null, } as unknown as ClusterStore; @@ -84,7 +99,7 @@ export const getApplicationBuilder = () => { rendererDi.override(storesAndApisCanBeCreatedInjectable, () => true); mainDi.override(clusterStoreInjectable, () => clusterStoreStub); - const beforeSetupsCallbacks: Callback[] = []; + const beforeApplicationStartCallbacks: Callback[] = []; const beforeRenderCallbacks: Callback[] = []; const extensionsState = observable.array(); @@ -130,7 +145,7 @@ export const getApplicationBuilder = () => { dis, applicationMenu: { - click: (path: string) => { + click: async (path: string) => { const applicationMenuItems = mainDi.inject( applicationMenuItemsInjectable, ); @@ -160,6 +175,8 @@ export const getApplicationBuilder = () => { undefined, {}, ); + + await flushPromises(); }, }, @@ -199,6 +216,14 @@ export const getApplicationBuilder = () => { }, }, + helmCharts: { + navigate: () => { + const navigateToHelmCharts = rendererDi.inject(navigateToHelmChartsInjectable); + + navigateToHelmCharts(); + }, + }, + setEnvironmentToClusterFrame: () => { environment = environments.clusterFrame; @@ -208,11 +233,29 @@ export const getApplicationBuilder = () => { computed(() => new Set([...allowedResourcesState])), ); - rendererDi.override( - directoryForLensLocalStorageInjectable, - () => "/irrelevant", + const clusterStub = { + accessibleNamespaces: [], + } as unknown as Cluster; + + const namespaceStoreStub = { + contextNamespaces: [], + } as unknown as NamespaceStore; + + const clusterFrameContextFake = new ClusterFrameContext( + clusterStub, + + { + namespaceStore: namespaceStoreStub, + }, ); + rendererDi.override(namespaceStoreInjectable, () => namespaceStoreStub); + rendererDi.override(hostedClusterInjectable, () => clusterStub); + rendererDi.override(clusterFrameContextInjectable, () => clusterFrameContextFake); + + // Todo: get rid of global state. + KubeObjectStore.defaultContext.set(clusterFrameContextFake); + return builder; }, @@ -254,8 +297,8 @@ export const getApplicationBuilder = () => { return builder; }, - beforeSetups(callback: (dis: DiContainers) => void) { - beforeSetupsCallbacks.push(callback); + beforeApplicationStart(callback: (dis: DiContainers) => void) { + beforeApplicationStartCallbacks.push(callback); return builder; }, @@ -267,14 +310,20 @@ export const getApplicationBuilder = () => { }, async render() { - for (const callback of beforeSetupsCallbacks) { + for (const callback of beforeApplicationStartCallbacks) { await callback(dis); } - await runSetups(); + const startMainApplication = mainDi.inject(startMainApplicationInjectable); + + await startMainApplication(); + + const startFrame = rendererDi.inject(startFrameInjectable); + + await startFrame(); const render = renderFor(rendererDi); - const history = rendererDi.inject(observableHistoryInjectable); + const history = rendererDi.inject(historyInjectable); const currentRouteComponent = rendererDi.inject(currentRouteComponentInjectable); for (const callback of beforeRenderCallbacks) { diff --git a/src/renderer/create-cluster/create-cluster.injectable.ts b/src/renderer/create-cluster/create-cluster.injectable.ts index e9a46d6c72..688662a965 100644 --- a/src/renderer/create-cluster/create-cluster.injectable.ts +++ b/src/renderer/create-cluster/create-cluster.injectable.ts @@ -15,12 +15,17 @@ const createClusterInjectable = getInjectable({ instantiate: (di) => { const dependencies: ClusterDependencies = { directoryForKubeConfigs: di.inject(directoryForKubeConfigsInjectable), - createKubeconfigManager: () => undefined, + logger: di.inject(loggerInjectable), + + // TODO: Dismantle wrong abstraction + // Note: "as never" to get around strictness in unnatural scenario + createKubeconfigManager: () => undefined as never, createKubectl: () => { throw new Error("Tried to access back-end feature in front-end.");}, - createContextHandler: () => undefined, + createContextHandler: () => undefined as never, createAuthorizationReview: () => { throw new Error("Tried to access back-end feature in front-end."); }, createListNamespaces: () => { throw new Error("Tried to access back-end feature in front-end."); }, - logger: di.inject(loggerInjectable), + detectorRegistry: undefined as never, + createVersionDetector: () => { throw new Error("Tried to access back-end feature in front-end."); }, }; return (model) => new Cluster(dependencies, model); diff --git a/src/renderer/getDi.tsx b/src/renderer/getDi.tsx index 8735eabc6d..540ba3ae3d 100644 --- a/src/renderer/getDi.tsx +++ b/src/renderer/getDi.tsx @@ -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( - getRequireContextForRendererCode, - getRequireContextForCommonExtensionCode, - getRequireContextForCommonCode, - ); + const di = createContainer(); + + autoRegister({ + di, + requireContexts: [ + require.context("./", true, /\.injectable\.(ts|tsx)$/), + require.context("../common", true, /\.injectable\.(ts|tsx)$/), + require.context("../extensions", true, /\.injectable\.(ts|tsx)$/), + ], + }); setLegacyGlobalDiForExtensionApi(di, Environments.renderer); return di; }; - -const getRequireContextForRendererCode = () => - require.context("./", true, /\.injectable\.(ts|tsx)$/); - -const getRequireContextForCommonCode = () => - require.context("../common", true, /\.injectable\.(ts|tsx)$/); - -const getRequireContextForCommonExtensionCode = () => - require.context("../extensions", true, /\.injectable\.(ts|tsx)$/); diff --git a/src/renderer/getDiForUnitTesting.tsx b/src/renderer/getDiForUnitTesting.tsx index 353eedf0ee..22ac104e88 100644 --- a/src/renderer/getDiForUnitTesting.tsx +++ b/src/renderer/getDiForUnitTesting.tsx @@ -4,7 +4,7 @@ */ import glob from "glob"; -import { memoize } from "lodash/fp"; +import { memoize, noop } from "lodash/fp"; import { createContainer } from "@ogre-tools/injectable"; import { Environments, setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; import getValueFromRegisteredChannelInjectable from "./app-paths/get-value-from-registered-channel/get-value-from-registered-channel.injectable"; @@ -34,12 +34,21 @@ import terminalSpawningPoolInjectable from "./components/dock/terminal/terminal- import hostedClusterIdInjectable from "../common/cluster-store/hosted-cluster-id.injectable"; import type { GetDiForUnitTestingOptions } from "../test-utils/get-dis-for-unit-testing"; import historyInjectable from "./navigation/history.injectable"; -import { noop } from "./utils"; +import { ApiManager } from "../common/k8s-api/api-manager"; +import lensResourcesDirInjectable from "../common/vars/lens-resources-dir.injectable"; +import broadcastMessageInjectable from "../common/ipc/broadcast-message.injectable"; +import apiManagerInjectable from "../common/k8s-api/api-manager/manager.injectable"; +import ipcRendererInjectable + from "./app-paths/get-value-from-registered-channel/ipc-renderer/ipc-renderer.injectable"; +import type { IpcRenderer } from "electron"; +import setupOnApiErrorListenersInjectable from "./api/setup-on-api-errors.injectable"; +import { observable } from "mobx"; export const getDiForUnitTesting = (opts: GetDiForUnitTestingOptions = {}) => { const { doGeneralOverrides = false, } = opts; + const di = createContainer(); setLegacyGlobalDiForExtensionApi(di, Environments.renderer); @@ -68,6 +77,17 @@ export const getDiForUnitTesting = (opts: GetDiForUnitTestingOptions = {}) => { di.override(historyInjectable, () => createMemoryHistory()); + di.override(lensResourcesDirInjectable, () => "/irrelevant"); + + di.override(ipcRendererInjectable, () => ({ + invoke: () => {}, + on: () => {}, + }) as unknown as IpcRenderer); + + di.override(broadcastMessageInjectable, () => () => { + throw new Error("Tried to broadcast message over IPC without explicit override."); + }); + // eslint-disable-next-line unused-imports/no-unused-vars-ts di.override(extensionsStoreInjectable, () => ({ isEnabled: ({ id, isBundled }) => false }) as ExtensionsStore); @@ -77,7 +97,22 @@ export const getDiForUnitTesting = (opts: GetDiForUnitTestingOptions = {}) => { // eslint-disable-next-line unused-imports/no-unused-vars-ts di.override(clusterStoreInjectable, () => ({ getById: (id): Cluster => ({}) as Cluster }) as ClusterStore); - di.override(userStoreInjectable, () => ({}) as UserStore); + + di.override(setupOnApiErrorListenersInjectable, () => ({ run: () => {} })); + + di.override( + userStoreInjectable, + () => + ({ + isTableColumnHidden: () => false, + extensionRegistryUrl: { customUrl: "some-custom-url" }, + syncKubeconfigEntries: observable.map(), + terminalConfig: { fontSize: 42 }, + editorConfiguration: { minimap: {}, tabSize: 42, fontSize: 42 }, + } as unknown as UserStore), + ); + + di.override(apiManagerInjectable, () => new ApiManager()); di.override(getValueFromRegisteredChannelInjectable, () => () => Promise.resolve(undefined as never)); di.override(registerIpcChannelListenerInjectable, () => () => undefined); diff --git a/src/renderer/ipc-channel-listeners/register-ipc-channel-listeners.injectable.ts b/src/renderer/ipc-channel-listeners/register-ipc-channel-listeners.injectable.ts index 9ce36a787b..bf9568b71c 100644 --- a/src/renderer/ipc-channel-listeners/register-ipc-channel-listeners.injectable.ts +++ b/src/renderer/ipc-channel-listeners/register-ipc-channel-listeners.injectable.ts @@ -3,25 +3,26 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import { noop } from "lodash/fp"; import { ipcChannelListenerInjectionToken } from "./ipc-channel-listener-injection-token"; -import registerIpcChannelListenerInjectable - from "../app-paths/get-value-from-registered-channel/register-ipc-channel-listener.injectable"; +import registerIpcChannelListenerInjectable from "../app-paths/get-value-from-registered-channel/register-ipc-channel-listener.injectable"; +import { beforeFrameStartsInjectionToken } from "../before-frame-starts/before-frame-starts-injection-token"; const registerIpcChannelListenersInjectable = getInjectable({ id: "register-ipc-channel-listeners", - setup: async di => { - const registerIpcChannelListener = await di.inject(registerIpcChannelListenerInjectable); + instantiate: di => ({ + run: async () => { + const registerIpcChannelListener = di.inject(registerIpcChannelListenerInjectable); - const listeners = await di.injectMany(ipcChannelListenerInjectionToken); + const listeners = di.injectMany(ipcChannelListenerInjectionToken); - listeners.forEach(listener => { - registerIpcChannelListener(listener); - }); - }, + listeners.forEach(listener => { + registerIpcChannelListener(listener); + }); + }, + }), - instantiate: () => noop, + injectionToken: beforeFrameStartsInjectionToken, }); export default registerIpcChannelListenersInjectable; diff --git a/src/renderer/navigation/history.injectable.ts b/src/renderer/navigation/history.injectable.ts index fee7022952..63f725dc28 100644 --- a/src/renderer/navigation/history.injectable.ts +++ b/src/renderer/navigation/history.injectable.ts @@ -4,10 +4,11 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import { createBrowserHistory } from "history"; +import type { History } from "history"; const historyInjectable = getInjectable({ id: "history", - instantiate: () => createBrowserHistory(), + instantiate: (): History => createBrowserHistory(), }); export default historyInjectable; diff --git a/src/renderer/navigation/observable-history.injectable.ts b/src/renderer/navigation/observable-history.injectable.ts index d3fd5bffba..81d9df2dd1 100644 --- a/src/renderer/navigation/observable-history.injectable.ts +++ b/src/renderer/navigation/observable-history.injectable.ts @@ -10,6 +10,7 @@ import historyInjectable from "./history.injectable"; const observableHistoryInjectable = getInjectable({ id: "observable-history", + instantiate: (di) => { const history = di.inject(historyInjectable); const logger = di.inject(loggerInjectable); diff --git a/src/renderer/routes/extension-route-registrator.injectable.tsx b/src/renderer/routes/extension-route-registrator.injectable.tsx index 5b9e847ec4..e3ce5cbd6d 100644 --- a/src/renderer/routes/extension-route-registrator.injectable.tsx +++ b/src/renderer/routes/extension-route-registrator.injectable.tsx @@ -2,7 +2,7 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { DiContainerForInstantiate } from "@ogre-tools/injectable"; +import type { DiContainerForInjection } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable"; import type { LensRendererExtension } from "../../extensions/lens-renderer-extension"; @@ -35,7 +35,7 @@ const extensionRouteRegistratorInjectable = getInjectable({ ...extension.clusterPages.map(toRouteInjectable(true)), ].flat(); - routeInjectables.forEach(di.register as never); + di.register(...routeInjectables); }; }, @@ -46,7 +46,7 @@ export default extensionRouteRegistratorInjectable; const toRouteInjectableFor = ( - di: DiContainerForInstantiate, + di: DiContainerForInjection, extension: LensRendererExtension, extensionInstallationCount: number, ) => diff --git a/src/renderer/routes/navigate-to-url.injectable.ts b/src/renderer/routes/navigate-to-url.injectable.ts index 71f39ed8b4..832080674c 100644 --- a/src/renderer/routes/navigate-to-url.injectable.ts +++ b/src/renderer/routes/navigate-to-url.injectable.ts @@ -7,14 +7,15 @@ import observableHistoryInjectable from "../navigation/observable-history.inject import { runInAction } from "mobx"; import type { NavigateToUrl } from "../../common/front-end-routing/navigate-to-url-injection-token"; import { navigateToUrlInjectionToken } from "../../common/front-end-routing/navigate-to-url-injection-token"; -import { broadcastMessage } from "../../common/ipc"; import { IpcRendererNavigationEvents } from "../navigation/events"; +import broadcastMessageInjectable from "../../common/ipc/broadcast-message.injectable"; const navigateToUrlInjectable = getInjectable({ id: "navigate-to-url", instantiate: (di): NavigateToUrl => { const observableHistory = di.inject(observableHistoryInjectable); + const broadcastMessage = di.inject(broadcastMessageInjectable); return (url, options = {}): void => { if (options.forceRootFrame) { diff --git a/src/renderer/search-store/search-store.test.ts b/src/renderer/search-store/search-store.test.ts index f3fc9a4289..742a6210cb 100644 --- a/src/renderer/search-store/search-store.test.ts +++ b/src/renderer/search-store/search-store.test.ts @@ -28,13 +28,11 @@ const logs = [ describe("search store tests", () => { let searchStore: SearchStore; - beforeEach(async () => { + beforeEach(() => { const di = getDiForUnitTesting({ doGeneralOverrides: true }); di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data"); - await di.runSetups(); - searchStore = di.inject(searchStoreInjectable); }); diff --git a/src/renderer/start-frame/start-frame.injectable.ts b/src/renderer/start-frame/start-frame.injectable.ts new file mode 100644 index 0000000000..fc38cc7d5e --- /dev/null +++ b/src/renderer/start-frame/start-frame.injectable.ts @@ -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 { runManyFor } from "../../common/runnable/run-many-for"; +import { beforeFrameStartsInjectionToken } from "../before-frame-starts/before-frame-starts-injection-token"; + +const startFrameInjectable = getInjectable({ + id: "start-frame", + + // TODO: Consolidate contents of bootstrap.tsx here + instantiate: (di) => async () => { + const beforeFrameStarts = runManyFor(di)(beforeFrameStartsInjectionToken); + + await beforeFrameStarts(); + }, +}); + +export default startFrameInjectable; diff --git a/src/test-utils/get-dis-for-unit-testing.ts b/src/test-utils/get-dis-for-unit-testing.ts index 9141350180..2f7d59d036 100644 --- a/src/test-utils/get-dis-for-unit-testing.ts +++ b/src/test-utils/get-dis-for-unit-testing.ts @@ -19,6 +19,5 @@ export const getDisForUnitTesting = (opts?: GetDiForUnitTestingOptions) => { return { rendererDi, mainDi, - runSetups: () => Promise.all([rendererDi.runSetups(), mainDi.runSetups()]), }; }; diff --git a/src/test-utils/override-ipc-bridge.ts b/src/test-utils/override-ipc-bridge.ts index 8a142ac321..a914cc66b6 100644 --- a/src/test-utils/override-ipc-bridge.ts +++ b/src/test-utils/override-ipc-bridge.ts @@ -8,9 +8,10 @@ import getValueFromRegisteredChannelInjectable from "../renderer/app-paths/get-v import registerChannelInjectable from "../main/app-paths/register-channel/register-channel.injectable"; import asyncFn from "@async-fn/jest"; import registerIpcChannelListenerInjectable from "../renderer/app-paths/get-value-from-registered-channel/register-ipc-channel-listener.injectable"; -import windowManagerInjectable from "../main/window-manager.injectable"; -import type { SendToViewArgs, WindowManager } from "../main/window-manager"; -import { appNavigationIpcChannel } from "../common/front-end-routing/navigation-ipc-channel"; +import type { SendToViewArgs } from "../main/start-main-application/lens-window/application-window/lens-window-injection-token"; +import sendToChannelInElectronBrowserWindowInjectable from "../main/start-main-application/lens-window/application-window/send-to-channel-in-electron-browser-window.injectable"; +import { isEmpty } from "lodash/fp"; + export const overrideIpcBridge = ({ rendererDi, @@ -68,7 +69,10 @@ export const overrideIpcBridge = ({ mainIpcRegistrations.set(channel, callback); }); - const rendererIpcFakeHandles = new Map void)[]>(); + const rendererIpcFakeHandles = new Map< + string, + ((...args: any[]) => void)[] + >(); rendererDi.override( registerIpcChannelListenerInjectable, @@ -81,20 +85,20 @@ export const overrideIpcBridge = ({ ); mainDi.override( - windowManagerInjectable, - () => { - const sendToView = ({ channel: channelName, data }: SendToViewArgs) => { - const handles = rendererIpcFakeHandles.get(channelName); + sendToChannelInElectronBrowserWindowInjectable, + () => + (browserWindow, { channel: channelName, data = [] }: SendToViewArgs) => { + const handles = rendererIpcFakeHandles.get(channelName) || []; - handles?.forEach(handle => handle(...data ?? [])); - }; - const navigate: WindowManager["navigate"] = async (url) => { - sendToView({ channel: appNavigationIpcChannel.name, data: [url] }); - }; + if (isEmpty(handles)) { + throw new Error( + `Tried to send message to channel "${channelName}" but there where no listeners. Current channels with listeners: "${[ + ...rendererIpcFakeHandles.keys(), + ].join('", "')}"`, + ); + } - return { - navigate, - } as WindowManager; - }, + handles.forEach((handle) => handle(...data)); + }, ); }; diff --git a/yarn.lock b/yarn.lock index 6436946d51..21beb4bb50 100644 --- a/yarn.lock +++ b/yarn.lock @@ -26,10 +26,10 @@ static-eval "2.0.2" underscore "1.7.0" -"@async-fn/jest@1.5.3": - version "1.5.3" - resolved "https://registry.yarnpkg.com/@async-fn/jest/-/jest-1.5.3.tgz#42be6c0e8ba5ccd737e006ca600e7e319fe2a591" - integrity sha512-iQ9gAPZFW5U6TNcFS99ffwYYsB9LNecTnvG73BaDc/zAD0qOWctY1imEACC1pLymmm/xaf/OUq9I9QenfkatTA== +"@async-fn/jest@1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@async-fn/jest/-/jest-1.6.0.tgz#48980e6f07c4d0d72b468b8b57a1b3be8473a746" + integrity sha512-Jm4kf9qQSzcOZIyiI13C4EM4euSLORA8O4JTOWwy7SwaUr8lhVOn0nVbNLx9jnP35JTYeLsLZHfAyZLhYDIl2g== "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.8.3": version "7.16.7" @@ -989,28 +989,37 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@ogre-tools/fp@5.2.0", "@ogre-tools/fp@^5.2.0": - version "5.2.0" - resolved "https://registry.yarnpkg.com/@ogre-tools/fp/-/fp-5.2.0.tgz#75d46f5c4304242663c5a9c26544583c107a4755" - integrity sha512-Tm8RfJUmY+Xq8R4XA+B100PI4K0MTpsMq1Li16N3MENFpNP2eGmp1cLR8YhxahMtPPuzKScvlaIybnP6pX/c6Q== +"@ogre-tools/fp@7.0.0", "@ogre-tools/fp@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@ogre-tools/fp/-/fp-7.0.0.tgz#55cd32cc2fcf0505fa0d3ebfd45eb0a9bbb9554c" + integrity sha512-vmd+Ctr9pTSulWWiYaJT4Ca2vkq1MVQvwZ42hJq+LK/tgC4vVMf6G13EMHwhRlhPyrro1/5NeN2kf6SlhgrVOg== dependencies: lodash "^4.17.21" -"@ogre-tools/injectable-react@5.2.0": - version "5.2.0" - resolved "https://registry.yarnpkg.com/@ogre-tools/injectable-react/-/injectable-react-5.2.0.tgz#ec55cd88d3b7e778952d77e6fcb4771f315430d2" - integrity sha512-V1aMIKGIXrhZe6OwhcZ5xm4PLfE9F21KXOXhhz7dXcECWKh0+tHUJaG3MaMPzJqloRSE9SNbMz5nCOv172xRvg== +"@ogre-tools/injectable-extension-for-auto-registration@7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@ogre-tools/injectable-extension-for-auto-registration/-/injectable-extension-for-auto-registration-7.0.0.tgz#2144417dd3b3c10afe232661e11d7d0e292a3e6b" + integrity sha512-11RVfzMIBIS/29EUnrhRl/WhwWTYi2cJCAEyh2NnPBV7m51wS40kE/ZjDNn9s37Mfsxc8wNX0aUKwMphXFosVw== dependencies: - "@ogre-tools/fp" "^5.2.0" - "@ogre-tools/injectable" "^5.2.0" + "@ogre-tools/fp" "^7.0.0" + "@ogre-tools/injectable" "^7.0.0" lodash "^4.17.21" -"@ogre-tools/injectable@5.2.0", "@ogre-tools/injectable@^5.2.0": - version "5.2.0" - resolved "https://registry.yarnpkg.com/@ogre-tools/injectable/-/injectable-5.2.0.tgz#40c1e3eedc6103d985b6daa124ef6d3eaff0c82c" - integrity sha512-pREfT/51AAWrqBerFc/UCvFN+Xa3U2sgkC0KzHGP5kogjuI1UecWTTChoiO01yoqgfVffqfBoe9uOFs/TzSogQ== +"@ogre-tools/injectable-react@7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@ogre-tools/injectable-react/-/injectable-react-7.0.0.tgz#09c45fbf9a904673a6a0b450cb9a6a7c1110ab14" + integrity sha512-L/NlM4nzIg0FOfH/5T3wYBsPUxALkpt+HaZSOcgzCM9fJnaSgYv/FwdAqshg+nA3KngPDYIMkAUdS+j3uq1yEA== dependencies: - "@ogre-tools/fp" "^5.2.0" + "@ogre-tools/fp" "^7.0.0" + "@ogre-tools/injectable" "^7.0.0" + lodash "^4.17.21" + +"@ogre-tools/injectable@7.0.0", "@ogre-tools/injectable@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@ogre-tools/injectable/-/injectable-7.0.0.tgz#25e6168ba3781f6b562bfbb06d1d54c976e9c94d" + integrity sha512-yfdNUU/q7Oy+bXnQN1lRD2wzt81xqGpf37C4iWfarZ+5GF4sjGi9w4Wuxe1s24rl2rpHZtcQ0j9bEEX51ifQHA== + dependencies: + "@ogre-tools/fp" "^7.0.0" lodash "^4.17.21" "@panva/asn1.js@^1.0.0":