Technical requirements for behavioural unit tests (#5084)

* Implement a lot of technical requirements for behavioural unit tests

Note: the crux of this was to make routing env-agnostic, and not based on URLs as magic strings, but instead something type-enforced.

Note: extension-based routes comply to same exact interface by "late-registering" their routes when installed. Routes are just injectables.

Note: another chunk of global shared state is no more.

Note: a lot of explicit side effects have been cornered to injectables.

Note: a lot of stuff has become reactive as part if this.

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

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

* Make a directory commonly available

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

* Require id for <Select /> to prevent non-deterministic renders

This was caused by global state in a 3rd party lib: "react-select".

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

* Specify id for all <Select /> to satisfy previous commit

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

* Prevent explicit side effect in component by using existing dependency instead

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

* Extract instantiation of "conf" as injectables for causing side effects

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

* Introduce a legacy-helper to make gradual refactoring of inheritors of Singleton easier

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

* Make legacy unit tests for hotbar green and more simple by using the new legacy helper

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

* Temporarily kludge all unit tests green with a disclaimer about allowing side-effects

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

* Remove kludge in previous commit by explicitly permitting specific side effects where old unit tests require it

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

* Prevent old unit test with side effects from accessing file system

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

* Migrate to actual typing for di.permitSideEffects

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

* Prevent unit tests from failing because of non-standard method of HTML-element not present in js-dom

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

* Adapt integration tests to recent changes

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

* Fix code style

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

* Fix artifact from bad rebase

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

* Add a deprecation from a review comment

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

* Remove change that is not required

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

* Remove redundant comment

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

* Fix code style

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

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

* Remove redundant file

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

* Fix bad merge

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

* Improve variable name

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

* Tweak logger interface to be more descriptive

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

* Make injecting legacy singleton always provide new instance

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

* Remove conditional typing when not needed

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

* Improve naming of variable

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

* Remove unnecessary code style changes

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

* Remove flag for causing side effects from too broad scope

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

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

* Override side-effects in unit test using injectable instead of monkey patching

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

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

* Flag some side-effects and add general overrides

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

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

* Fix unit tests in CI by removing explicit side-effect

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

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

* Remove explicit side-effect from getting default shell

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

* Introduce abstraction for getting absolute paths

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

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

* Switch to using abstraction for getting absolute path to control explicit side effect

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

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

* Introduce abstraction for joining paths

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

* Switch to using abstraction for joining paths to control explicit side effect

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

* Fix fake implementation for join paths

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

* Fix test after removing explicit side effect

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

* Remove explicit side effects from kubeconfig-syncs

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

* Fix arguments after removing explicit side effect

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

* Make registrators not async for not being needed anymore

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

* Make generalCatalogEntities non-observable, as there is no requirement

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

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

* Remove redundant code

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

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

* Simplify logic for registering general catalog entity sources

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

* Add TODO

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

* Replace function for getting application menu items with reactive solution

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

* Fix typo in interface name

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

* Remove global shared state usages of hot bar store

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

* Remove redundant enum

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

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
Iku-tursoandJanne Savolainen authored and GitHub committed 2022-03-31 16:57:05 +03:00
1 parent ab2cd27a27
commit a277cfcf02
583 files changed
+13073 -6556

No files matched your search

+6 -1
View File
@@ -52,6 +52,8 @@ import createKubeAuthProxyInjectable from "../kube-auth-proxy/create-kube-auth-p
import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token";
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";
console = new Console(stdout, stderr);
@@ -96,6 +98,9 @@ describe("kube auth proxy tests", () => {
di.override(spawnInjectable, () => mockSpawn);
di.permitSideEffects(getConfigurationFileModelInjectable);
di.permitSideEffects(appVersionInjectable);
mockFs(mockMinikubeConfig);
await di.runSetups();
@@ -134,7 +139,7 @@ describe("kube auth proxy tests", () => {
beforeEach(async () => {
mockedCP = mock<ChildProcess>();
listeners = new EventEmitter();
jest.spyOn(Kubectl.prototype, "checkBinary").mockReturnValueOnce(Promise.resolve(true));
jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValueOnce(Promise.resolve(false));
mockedCP.on.mockImplementation((event: string, listener: (message: any, sendHandle: any) => void): ChildProcess => {
+3 -2
View File
@@ -16,9 +16,9 @@ import registerChannelInjectable from "./register-channel/register-channel.injec
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 path from "path";
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",
@@ -55,10 +55,11 @@ 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", path.join(appDataPath, appName));
setElectronAppPath("userData", joinPaths(appDataPath, appName));
};
// Todo: this kludge is here only until we have a proper place to setup integration testing.
@@ -7,6 +7,8 @@ import getElectronAppPathInjectable from "./get-electron-app-path.injectable";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import type { App } from "electron";
import registerChannelInjectable from "../register-channel/register-channel.injectable";
import joinPathsInjectable from "../../../common/path/join-paths.injectable";
import { joinPathsFake } from "../../../common/test-utils/join-paths-fake";
describe("get-electron-app-path", () => {
let getElectronAppPath: (name: string) => string | null;
@@ -31,6 +33,7 @@ describe("get-electron-app-path", () => {
di.override(electronAppInjectable, () => appStub);
di.override(registerChannelInjectable, () => () => undefined);
di.override(joinPathsInjectable, () => joinPathsFake);
await di.runSetups();
@@ -16,6 +16,10 @@ 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";
jest.mock("electron", () => ({
app: {
@@ -48,6 +52,13 @@ describe("kubeconfig-sync.source tests", () => {
createCluster: di.inject(createClusterInjectionToken),
});
di.override(clusterStoreInjectable, () =>
ClusterStore.createInstance({ createCluster: () => null }),
);
di.permitSideEffects(getConfigurationFileModelInjectable);
di.permitSideEffects(appVersionInjectable);
di.inject(clusterStoreInjectable);
ClusterManager.createInstance();
-76
View File
@@ -1,76 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { observable } from "mobx";
import { GeneralEntity } from "../../common/catalog-entities/general";
import { catalogURL, preferencesURL, welcomeURL } from "../../common/routes";
import { catalogEntityRegistry } from "../catalog";
export const catalogEntity = new GeneralEntity({
metadata: {
uid: "catalog-entity",
name: "Catalog",
source: "app",
labels: {},
},
spec: {
path: catalogURL(),
icon: {
material: "view_list",
background: "#3d90ce",
},
},
status: {
phase: "active",
},
});
const preferencesEntity = new GeneralEntity({
metadata: {
uid: "preferences-entity",
name: "Preferences",
source: "app",
labels: {},
},
spec: {
path: preferencesURL(),
icon: {
material: "settings",
background: "#3d90ce",
},
},
status: {
phase: "active",
},
});
const welcomePageEntity = new GeneralEntity({
metadata: {
uid: "welcome-page-entity",
name: "Welcome Page",
source: "app",
labels: {},
},
spec: {
path: welcomeURL(),
icon: {
material: "meeting_room",
background: "#3d90ce",
},
},
status: {
phase: "active",
},
});
const generalEntities = observable([
catalogEntity,
preferencesEntity,
welcomePageEntity,
]);
export function syncGeneralEntities() {
catalogEntityRegistry.addObservableSource("lens:general", generalEntities);
}
-1
View File
@@ -4,4 +4,3 @@
*/
export { syncWeblinks } from "./weblinks";
export { syncGeneralEntities } from "./general";
@@ -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 catalogEntityRegistryInjectable from "../catalog/catalog-entity-registry.injectable";
import { generalCatalogEntityInjectionToken } from "../../common/catalog-entities/general-catalog-entities/general-catalog-entity-injection-token";
import { computed } from "mobx";
const syncGeneralCatalogEntitiesInjectable = getInjectable({
id: "sync-general-catalog-entities",
instantiate: (di) => {
const generalCatalogEntities = di.injectMany(generalCatalogEntityInjectionToken);
const catalogEntityRegistry = di.inject(catalogEntityRegistryInjectable);
// TODO: This shouldn't be reactive at all but catalogEntityRegistry accepts only reactive sources
const reactiveGeneralCatalogEntities = computed(() => generalCatalogEntities);
return () => {
catalogEntityRegistry.addComputedSource(
"lens:general",
reactiveGeneralCatalogEntities,
);
};
},
});
export default syncGeneralCatalogEntitiesInjectable;
@@ -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 { catalogEntityRegistry } from "./catalog-entity-registry";
const catalogEntityRegistryInjectable = getInjectable({
id: "catalog-entity-registry",
instantiate: () => catalogEntityRegistry,
causesSideEffects: true,
});
export default catalogEntityRegistryInjectable;
+2 -2
View File
@@ -4,7 +4,7 @@
*/
import { createContainer } from "@ogre-tools/injectable";
import { setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
import { Environments, setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
export const getDi = () => {
const di = createContainer(
@@ -13,7 +13,7 @@ export const getDi = () => {
getRequireContextForCommonCode,
);
setLegacyGlobalDiForExtensionApi(di);
setLegacyGlobalDiForExtensionApi(di, Environments.main);
return di;
};
+49 -5
View File
@@ -4,10 +4,10 @@
*/
import glob from "glob";
import { memoize, kebabCase } from "lodash/fp";
import { kebabCase, memoize, noop } from "lodash/fp";
import { createContainer } from "@ogre-tools/injectable";
import { setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
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";
@@ -16,14 +16,32 @@ import writeJsonFileInjectable from "../common/fs/write-json-file.injectable";
import readJsonFileInjectable from "../common/fs/read-json-file.injectable";
import readFileInjectable from "../common/fs/read-file.injectable";
import directoryForBundledBinariesInjectable from "../common/app-paths/directory-for-bundled-binaries/directory-for-bundled-binaries.injectable";
import loggerInjectable from "../common/logger.injectable";
import spawnInjectable from "./child-process/spawn.injectable";
import extensionsStoreInjectable from "../extensions/extensions-store/extensions-store.injectable";
import type { ExtensionsStore } from "../extensions/extensions-store/extensions-store";
import fileSystemProvisionerStoreInjectable from "../extensions/extension-loader/create-extension-instance/file-system-provisioner-store/file-system-provisioner-store.injectable";
import type { FileSystemProvisionerStore } from "../extensions/extension-loader/create-extension-instance/file-system-provisioner-store/file-system-provisioner-store";
import clusterStoreInjectable from "../common/cluster-store/cluster-store.injectable";
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 type { UserStore } from "../common/user-store";
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 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/hotbar-store.injectable";
export const getDiForUnitTesting = (
{ doGeneralOverrides } = { doGeneralOverrides: false },
) => {
const di = createContainer();
setLegacyGlobalDiForExtensionApi(di);
setLegacyGlobalDiForExtensionApi(di, Environments.main);
for (const filePath of getInjectableFilePaths()) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
@@ -38,6 +56,24 @@ export const getDiForUnitTesting = (
di.preventSideEffects();
if (doGeneralOverrides) {
di.override(isMacInjectable, () => true);
di.override(isWindowsInjectable, () => false);
di.override(isLinuxInjectable, () => false);
di.override(getAbsolutePathInjectable, () => getAbsolutePathFake);
di.override(joinPathsInjectable, () => joinPathsFake);
// eslint-disable-next-line unused-imports/no-unused-vars-ts
di.override(extensionsStoreInjectable, () => ({ isEnabled: ({ id, isBundled }) => false }) as ExtensionsStore);
di.override(hotbarStoreInjectable, () => ({}));
di.override(fileSystemProvisionerStoreInjectable, () => ({}) as FileSystemProvisionerStore);
// 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(
getElectronAppPathInjectable,
() => (name: string) => `some-electron-app-path-for-${kebabCase(name)}`,
@@ -47,8 +83,9 @@ export const getDiForUnitTesting = (
di.override(appNameInjectable, () => "some-electron-app-name");
di.override(registerChannelInjectable, () => () => undefined);
di.override(directoryForBundledBinariesInjectable, () => "some-bin-directory");
di.override(spawnInjectable, () => () => {
return {
di.override(spawnInjectable, () => () => {
return {
stderr: { on: jest.fn(), removeAllListeners: jest.fn() },
stdout: { on: jest.fn(), removeAllListeners: jest.fn() },
on: jest.fn(),
@@ -66,6 +103,13 @@ export const getDiForUnitTesting = (
di.override(readFileInjectable, () => () => {
throw new Error("Tried to read file from file system without specifying explicit override.");
});
di.override(loggerInjectable, () => ({
warn: noop,
debug: noop,
error: (message: string, ...args: any) => console.error(message, ...args),
info: noop,
}));
}
return di;
+18 -11
View File
@@ -30,11 +30,10 @@ import { IpcRendererNavigationEvents } from "../renderer/navigation/events";
import { startCatalogSyncToRenderer } from "./catalog-pusher";
import { catalogEntityRegistry } from "./catalog";
import { HelmRepoManager } from "./helm/helm-repo-manager";
import { syncGeneralEntities, syncWeblinks } from "./catalog-sources";
import { syncWeblinks } from "./catalog-sources";
import configurePackages from "../common/configure-packages";
import { PrometheusProviderRegistry } from "./prometheus";
import * as initializers from "./initializers";
import { HotbarStore } from "../common/hotbar-store";
import { WeblinkStore } from "../common/weblink-store";
import { SentryInit } from "../common/sentry";
import { ensureDir } from "fs-extra";
@@ -48,7 +47,6 @@ import lensProtocolRouterMainInjectable from "./protocol-handler/lens-protocol-r
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 electronMenuItemsInjectable from "./menu/electron-menu-items.injectable";
import directoryForKubeConfigsInjectable from "../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import kubeconfigSyncManagerInjectable from "./catalog-sources/kubeconfig-sync-manager/kubeconfig-sync-manager.injectable";
import clusterStoreInjectable from "../common/cluster-store/cluster-store.injectable";
@@ -57,6 +55,11 @@ import shellApiRequestInjectable from "./proxy-functions/shell-api-request/shell
import userStoreInjectable from "../common/user-store/user-store.injectable";
import trayMenuItemsInjectable from "./tray/tray-menu-items.injectable";
import { broadcastNativeThemeOnUpdate } from "./native-theme";
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/hotbar-store.injectable";
import applicationMenuItemsInjectable from "./menu/application-menu-items.injectable";
const di = getDi();
@@ -66,7 +69,7 @@ app.on("ready", async () => {
await di.runSetups();
injectSystemCAs();
const onCloseCleanup = disposer();
const onQuitCleanup = disposer();
@@ -184,7 +187,7 @@ app.on("ready", async () => {
lensProtocolRouterMain.route(rawUrl);
});
logger.debug("[APP-MAIN] waiting for 'ready' and other messages");
logger.debug("[APP-MAIN] waiting for 'ready' and other messages");
const directoryForExes = di.inject(directoryForExesInjectable);
@@ -204,7 +207,9 @@ app.on("ready", async () => {
* store has migrations that will remove items that previous migrations add
* if this is not present
*/
syncGeneralEntities();
const syncGeneralCatalogEntities = di.inject(syncGeneralCatalogEntitiesInjectable);
syncGeneralCatalogEntities();
logger.info("💾 Loading stores");
@@ -218,7 +223,7 @@ app.on("ready", async () => {
clusterStore.provideInitialFromMain();
// HotbarStore depends on: ClusterStore
HotbarStore.createInstance();
di.inject(hotbarStoreInjectable);
WeblinkStore.createInstance();
@@ -291,13 +296,15 @@ app.on("ready", async () => {
const startHidden = process.argv.includes("--hidden") || (isMac && app.getLoginItemSettings().wasOpenedAsHidden);
logger.info("🖥️ Starting WindowManager");
const windowManager = WindowManager.createInstance();
const menuItems = di.inject(electronMenuItemsInjectable);
const windowManager = di.inject(windowManagerInjectable);
const applicationMenuItems = di.inject(applicationMenuItemsInjectable);
const trayMenuItems = di.inject(trayMenuItemsInjectable);
const navigateToPreferences = di.inject(navigateToPreferencesInjectable);
onQuitCleanup.push(
initMenu(windowManager, menuItems),
initTray(windowManager, trayMenuItems),
initMenu(applicationMenuItems),
initTray(windowManager, trayMenuItems, navigateToPreferences),
() => ShellSession.cleanup(),
);
@@ -3,17 +3,18 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import electronMenuItemsInjectable from "../../menu/electron-menu-items.injectable";
import directoryForLensLocalStorageInjectable
from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.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({
electronMenuItems: di.inject(electronMenuItemsInjectable),
applicationMenuItems: di.inject(applicationMenuItemsInjectable),
directoryForLensLocalStorage: di.inject(directoryForLensLocalStorageInjectable),
getAbsolutePath: di.inject(getAbsolutePathInjectable),
}),
});
@@ -14,25 +14,24 @@ import { catalogEntityRegistry } from "../../catalog";
import { pushCatalogToRenderer } from "../../catalog-pusher";
import { ClusterManager } from "../../cluster-manager";
import { ResourceApplier } from "../../resource-applier";
import { WindowManager } from "../../window-manager";
import path from "path";
import { remove } from "fs-extra";
import { getAppMenu } from "../../menu/menu";
import type { MenuRegistration } from "../../menu/menu-registration";
import type { IComputedValue } from "mobx";
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";
interface Dependencies {
electronMenuItems: IComputedValue<MenuRegistration[]>;
directoryForLensLocalStorage: string;
getAbsolutePath: GetAbsolutePath;
applicationMenuItems: IComputedValue<MenuItemOpts[]>;
}
export const initIpcMainHandlers = ({ electronMenuItems, directoryForLensLocalStorage }: Dependencies) => () => {
export const initIpcMainHandlers = ({ applicationMenuItems, directoryForLensLocalStorage, getAbsolutePath }: Dependencies) => () => {
ipcMainHandle(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => {
return ClusterStore.getInstance()
.getById(clusterId)
@@ -88,7 +87,7 @@ export const initIpcMainHandlers = ({ electronMenuItems, directoryForLensLocalSt
try {
// remove the local storage file
const localStorageFilePath = path.resolve(directoryForLensLocalStorage, `${cluster.id}.json`);
const localStorageFilePath = getAbsolutePath(directoryForLensLocalStorage, `${cluster.id}.json`);
await remove(localStorageFilePath);
} catch {
@@ -151,7 +150,9 @@ export const initIpcMainHandlers = ({ electronMenuItems, directoryForLensLocalSt
ipcMainHandle(broadcastMainChannel, (event, channel, ...args) => broadcastMessage(channel, ...args));
ipcMainOn(windowOpenAppMenuAsContextMenuChannel, async (event) => {
const menu = Menu.buildFromTemplate(getAppMenu(WindowManager.getInstance(), electronMenuItems.get()));
const appMenu = applicationMenuItems.get();
const menu = Menu.buildFromTemplate(appMenu);
menu.popup({
...BrowserWindow.fromWebContents(event.sender),
@@ -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 { isAutoUpdateEnabled } from "./app-updater";
const isAutoUpdateEnabledInjectable = getInjectable({
id: "is-auto-update-enabled",
instantiate: () => isAutoUpdateEnabled,
causesSideEffects: true,
});
export default isAutoUpdateEnabledInjectable;
@@ -4,7 +4,7 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import { Kubectl } from "./kubectl";
import directoryForKubectlBinariesInjectable from "./directory-for-kubectl-binaries/directory-for-kubectl-binaries.injectable";
import directoryForKubectlBinariesInjectable from "../../common/app-paths/directory-for-kubectl-binaries/directory-for-kubectl-binaries.injectable";
import userStoreInjectable from "../../common/user-store/user-store.injectable";
const createKubectlInjectable = getInjectable({
@@ -1,16 +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 directoryForBinariesInjectable from "../../../common/app-paths/directory-for-binaries/directory-for-binaries.injectable";
import path from "path";
const directoryForKubectlBinariesInjectable = getInjectable({
id: "directory-for-kubectl-binaries",
instantiate: (di) =>
path.join(di.inject(directoryForBinariesInjectable), "kubectl"),
});
export default directoryForKubectlBinariesInjectable;
@@ -0,0 +1,337 @@
/**
* 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 { 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 {
BrowserWindow,
MenuItem,
MenuItemConstructorOptions,
webContents,
} from "electron";
import loggerInjectable from "../../common/logger.injectable";
import appNameInjectable from "../app-paths/app-name/app-name.injectable";
import electronMenuItemsInjectable from "./electron-menu-items.injectable";
import isAutoUpdateEnabledInjectable from "../is-auto-update-enabled.injectable";
import navigateToPreferencesInjectable from "../../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable";
import navigateToExtensionsInjectable from "../../common/front-end-routing/routes/extensions/navigate-to-extensions.injectable";
import navigateToCatalogInjectable from "../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable";
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 isMacInjectable from "../../common/vars/is-mac.injectable";
import { computed } from "mobx";
function ignoreIf(check: boolean, menuItems: MenuItemConstructorOptions[]) {
return check ? [] : menuItems;
}
export interface MenuItemOpts extends MenuItemConstructorOptions {
submenu?: MenuItemConstructorOptions[];
}
const applicationMenuItemsInjectable = getInjectable({
id: "application-menu-items",
instantiate: (di) => {
const logger = di.inject(loggerInjectable);
const appName = di.inject(appNameInjectable);
const isMac = di.inject(isMacInjectable);
const isAutoUpdateEnabled = di.inject(isAutoUpdateEnabledInjectable);
const electronMenuItems = di.inject(electronMenuItemsInjectable);
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}`);
const macAppMenu: MenuItemOpts = {
label: appName,
id: "root",
submenu: [
{
label: `About ${productName}`,
id: "about",
click(menuItem: MenuItem, browserWindow: BrowserWindow) {
showAbout(browserWindow);
},
},
...ignoreIf(autoUpdateDisabled, [
{
label: "Check for updates",
click() {
checkForUpdates().then(() => windowManager.ensureMainWindow());
},
},
]),
{ type: "separator" },
{
label: "Preferences",
accelerator: "CmdOrCtrl+,",
id: "preferences",
click() {
navigateToPreferences();
},
},
{
label: "Extensions",
accelerator: "CmdOrCtrl+Shift+E",
id: "extensions",
click() {
navigateToExtensions();
},
},
{ type: "separator" },
{ role: "services" },
{ type: "separator" },
{ role: "hide" },
{ role: "hideOthers" },
{ role: "unhide" },
{ type: "separator" },
{
label: "Quit",
accelerator: "Cmd+Q",
id: "quit",
click() {
exitApp();
},
},
],
};
const fileMenu: MenuItemOpts = {
label: "File",
id: "file",
submenu: [
{
label: "Add Cluster",
accelerator: "CmdOrCtrl+Shift+A",
id: "add-cluster",
click() {
navigateToAddCluster();
},
},
...ignoreIf(isMac, [
{ type: "separator" },
{
label: "Preferences",
id: "preferences",
accelerator: "Ctrl+,",
click() {
navigateToPreferences();
},
},
{
label: "Extensions",
accelerator: "Ctrl+Shift+E",
click() {
navigateToExtensions();
},
},
]),
{ type: "separator" },
...(isMac
? ([
{
role: "close",
label: "Close Window",
accelerator: "Shift+Cmd+W",
},
] as MenuItemConstructorOptions[])
: []),
...ignoreIf(isMac, [
{
label: "Exit",
accelerator: "Alt+F4",
id: "quit",
click() {
exitApp();
},
},
]),
],
};
const editMenu: MenuItemOpts = {
label: "Edit",
id: "edit",
submenu: [
{ role: "undo" },
{ role: "redo" },
{ type: "separator" },
{ role: "cut" },
{ role: "copy" },
{ role: "paste" },
{ role: "delete" },
{ type: "separator" },
{ role: "selectAll" },
],
};
const viewMenu: MenuItemOpts = {
label: "View",
id: "view",
submenu: [
{
label: "Catalog",
accelerator: "Shift+CmdOrCtrl+C",
id: "catalog",
click() {
navigateToCatalog();
},
},
{
label: "Command Palette...",
accelerator: "Shift+CmdOrCtrl+P",
id: "command-palette",
click(_m, _b, event) {
/**
* Don't broadcast unless it was triggered by menu iteration so that
* there aren't double events in renderer
*
* NOTE: this `?` is required because of a bug in playwright. https://github.com/microsoft/playwright/issues/10554
*/
if (!event?.triggeredByAccelerator) {
broadcastMessage("command-palette:open");
}
},
},
{ type: "separator" },
{
label: "Back",
accelerator: "CmdOrCtrl+[",
id: "go-back",
click() {
webContents
.getAllWebContents()
.filter((wc) => wc.getType() === "window")
.forEach((wc) => wc.goBack());
},
},
{
label: "Forward",
accelerator: "CmdOrCtrl+]",
id: "go-forward",
click() {
webContents
.getAllWebContents()
.filter((wc) => wc.getType() === "window")
.forEach((wc) => wc.goForward());
},
},
{
label: "Reload",
accelerator: "CmdOrCtrl+R",
id: "reload",
click() {
windowManager.reload();
},
},
{ role: "toggleDevTools" },
{ type: "separator" },
{ role: "resetZoom" },
{ role: "zoomIn" },
{ role: "zoomOut" },
{ type: "separator" },
{ role: "togglefullscreen" },
],
};
const helpMenu: MenuItemOpts = {
role: "help",
id: "help",
submenu: [
{
label: "Welcome",
id: "welcome",
click() {
navigateToWelcome();
},
},
{
label: "Documentation",
id: "documentation",
click: async () => {
openBrowser(docsUrl).catch((error) => {
logger.error("[MENU]: failed to open browser", { error });
});
},
},
{
label: "Support",
id: "support",
click: async () => {
openBrowser(supportUrl).catch((error) => {
logger.error("[MENU]: failed to open browser", { error });
});
},
},
...ignoreIf(isMac, [
{
label: `About ${productName}`,
id: "about",
click(menuItem: MenuItem, browserWindow: BrowserWindow) {
showAbout(browserWindow);
},
},
...ignoreIf(autoUpdateDisabled, [
{
label: "Check for updates",
click() {
checkForUpdates().then(() =>
windowManager.ensureMainWindow(),
);
},
},
]),
]),
],
};
// Prepare menu items order
const appMenu = new Map([
["mac", macAppMenu],
["file", fileMenu],
["edit", editMenu],
["view", viewMenu],
["help", helpMenu],
]);
// Modify menu from extensions-api
for (const menuItem of electronMenuItems.get()) {
if (!appMenu.has(menuItem.parentId)) {
logger.error(
`[MENU]: cannot register menu item for parentId=${menuItem.parentId}, parent item doesn't exist`,
{ menuItem },
);
continue;
}
appMenu.get(menuItem.parentId).submenu.push(menuItem);
}
if (!isMac) {
appMenu.delete("mac");
}
return [...appMenu.values()];
});
},
});
export default applicationMenuItemsInjectable;
+7 -297
View File
@@ -2,30 +2,18 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { app, BrowserWindow, dialog, Menu, MenuItem, MenuItemConstructorOptions, webContents } from "electron";
import { app, BrowserWindow, dialog, Menu } from "electron";
import { autorun, IComputedValue } from "mobx";
import type { WindowManager } from "../window-manager";
import { appName, isMac, isWindows, docsUrl, supportUrl, productName } from "../../common/vars";
import logger from "../logger";
import { exitApp } from "../exit-app";
import { broadcastMessage } from "../../common/ipc";
import { openBrowser } from "../../common/utils";
import { appName, isWindows, productName } from "../../common/vars";
import packageJson from "../../../package.json";
import { preferencesURL, extensionsURL, addClusterURL, catalogURL, welcomeURL } from "../../common/routes";
import { checkForUpdates, isAutoUpdateEnabled } from "../app-updater";
import type { MenuRegistration } from "./menu-registration";
import type { MenuItemOpts } from "./application-menu-items.injectable";
export type MenuTopId = "mac" | "file" | "edit" | "view" | "help";
interface MenuItemsOpts extends MenuItemConstructorOptions {
submenu?: MenuItemConstructorOptions[];
}
export function initMenu(
windowManager: WindowManager,
electronMenuItems: IComputedValue<MenuRegistration[]>,
applicationMenuItems: IComputedValue<MenuItemOpts[]>,
) {
return autorun(() => buildMenu(windowManager, electronMenuItems.get()), {
return autorun(() => buildMenu(applicationMenuItems.get()), {
delay: 100,
});
}
@@ -48,288 +36,10 @@ export function showAbout(browserWindow: BrowserWindow) {
});
}
export function getAppMenu(
windowManager: WindowManager,
electronMenuItems: MenuRegistration[],
) {
function ignoreIf(check: boolean, menuItems: MenuItemConstructorOptions[]) {
return check ? [] : menuItems;
}
async function navigate(url: string) {
logger.info(`[MENU]: navigating to ${url}`);
await windowManager.navigate(url);
}
const autoUpdateDisabled = !isAutoUpdateEnabled();
logger.info(`[MENU]: autoUpdateDisabled=${autoUpdateDisabled}`);
const macAppMenu: MenuItemsOpts = {
label: app.getName(),
id: "root",
submenu: [
{
label: `About ${productName}`,
id: "about",
click(menuItem: MenuItem, browserWindow: BrowserWindow) {
showAbout(browserWindow);
},
},
...ignoreIf(autoUpdateDisabled, [{
label: "Check for updates",
click() {
checkForUpdates()
.then(() => windowManager.ensureMainWindow());
},
}]),
{ type: "separator" },
{
label: "Preferences",
accelerator: "CmdOrCtrl+,",
id: "preferences",
click() {
navigate(preferencesURL());
},
},
{
label: "Extensions",
accelerator: "CmdOrCtrl+Shift+E",
id: "extensions",
click() {
navigate(extensionsURL());
},
},
{ type: "separator" },
{ role: "services" },
{ type: "separator" },
{ role: "hide" },
{ role: "hideOthers" },
{ role: "unhide" },
{ type: "separator" },
{
label: "Quit",
accelerator: "Cmd+Q",
id: "quit",
click() {
exitApp();
},
},
],
};
const fileMenu: MenuItemsOpts = {
label: "File",
id: "file",
submenu: [
{
label: "Add Cluster",
accelerator: "CmdOrCtrl+Shift+A",
id: "add-cluster",
click() {
navigate(addClusterURL());
},
},
...ignoreIf(isMac, [
{ type: "separator" },
{
label: "Preferences",
id: "preferences",
accelerator: "Ctrl+,",
click() {
navigate(preferencesURL());
},
},
{
label: "Extensions",
accelerator: "Ctrl+Shift+E",
click() {
navigate(extensionsURL());
},
},
]),
{ type: "separator" },
...(isMac ? [
{
role: "close",
label: "Close Window",
accelerator: "Shift+Cmd+W",
},
] as MenuItemConstructorOptions[] : []),
...ignoreIf(isMac, [
{
label: "Exit",
accelerator: "Alt+F4",
id: "quit",
click() {
exitApp();
},
},
]),
],
};
const editMenu: MenuItemsOpts = {
label: "Edit",
id: "edit",
submenu: [
{ role: "undo" },
{ role: "redo" },
{ type: "separator" },
{ role: "cut" },
{ role: "copy" },
{ role: "paste" },
{ role: "delete" },
{ type: "separator" },
{ role: "selectAll" },
],
};
const viewMenu: MenuItemsOpts = {
label: "View",
id: "view",
submenu: [
{
label: "Catalog",
accelerator: "Shift+CmdOrCtrl+C",
id: "catalog",
click() {
navigate(catalogURL());
},
},
{
label: "Command Palette...",
accelerator: "Shift+CmdOrCtrl+P",
id: "command-palette",
click(_m, _b, event) {
/**
* Don't broadcast unless it was triggered by menu iteration so that
* there aren't double events in renderer
*
* NOTE: this `?` is required because of a bug in playwright. https://github.com/microsoft/playwright/issues/10554
*/
if (!event?.triggeredByAccelerator) {
broadcastMessage("command-palette:open");
}
},
},
{ type: "separator" },
{
label: "Back",
accelerator: "CmdOrCtrl+[",
id: "go-back",
click() {
webContents.getAllWebContents().filter(wc => wc.getType() === "window").forEach(wc => wc.goBack());
},
},
{
label: "Forward",
accelerator: "CmdOrCtrl+]",
id: "go-forward",
click() {
webContents.getAllWebContents().filter(wc => wc.getType() === "window").forEach(wc => wc.goForward());
},
},
{
label: "Reload",
accelerator: "CmdOrCtrl+R",
id: "reload",
click() {
windowManager.reload();
},
},
{ role: "toggleDevTools" },
{ type: "separator" },
{ role: "resetZoom" },
{ role: "zoomIn" },
{ role: "zoomOut" },
{ type: "separator" },
{ role: "togglefullscreen" },
],
};
const helpMenu: MenuItemsOpts = {
role: "help",
id: "help",
submenu: [
{
label: "Welcome",
id: "welcome",
click() {
navigate(welcomeURL());
},
},
{
label: "Documentation",
id: "documentation",
click: async () => {
openBrowser(docsUrl).catch(error => {
logger.error("[MENU]: failed to open browser", { error });
});
},
},
{
label: "Support",
id: "support",
click: async () => {
openBrowser(supportUrl).catch(error => {
logger.error("[MENU]: failed to open browser", { error });
});
},
},
...ignoreIf(isMac, [
{
label: `About ${productName}`,
id: "about",
click(menuItem: MenuItem, browserWindow: BrowserWindow) {
showAbout(browserWindow);
},
},
...ignoreIf(autoUpdateDisabled, [{
label: "Check for updates",
click() {
checkForUpdates()
.then(() => windowManager.ensureMainWindow());
},
}]),
]),
],
};
// Prepare menu items order
const appMenu = new Map([
["mac", macAppMenu],
["file", fileMenu],
["edit", editMenu],
["view", viewMenu],
["help", helpMenu],
]);
// Modify menu from extensions-api
for (const menuItem of electronMenuItems) {
if (!appMenu.has(menuItem.parentId)) {
logger.error(
`[MENU]: cannot register menu item for parentId=${menuItem.parentId}, parent item doesn't exist`,
{ menuItem },
);
continue;
}
appMenu.get(menuItem.parentId).submenu.push(menuItem);
}
if (!isMac) {
appMenu.delete("mac");
}
return [...appMenu.values()];
}
export function buildMenu(
windowManager: WindowManager,
electronMenuItems: MenuRegistration[],
applicationMenuItems: MenuItemOpts[],
) {
Menu.setApplicationMenu(
Menu.buildFromTemplate(getAppMenu(windowManager, electronMenuItems)),
Menu.buildFromTemplate(applicationMenuItems),
);
}
@@ -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 { navigateToUrlInjectionToken } from "../../common/front-end-routing/navigate-to-url-injection-token";
import { navigateToRouteInjectionToken } from "../../common/front-end-routing/navigate-to-route-injection-token";
import { buildURL } from "../../common/utils/buildUrl";
const navigateToRouteInjectable = getInjectable({
id: "navigate-to-route",
instantiate: (di) => {
const navigateToUrl = di.inject(navigateToUrlInjectionToken);
return (route, options) => {
const url = buildURL(route.path, {
// TODO: enhance typing
params: options?.parameters as any,
query: options?.query,
fragment: options?.fragment,
});
navigateToUrl(url, options);
};
},
injectionToken: navigateToRouteInjectionToken,
});
export default navigateToRouteInjectable;
@@ -0,0 +1,23 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import windowManagerInjectable from "../window-manager.injectable";
import { navigateToUrlInjectionToken } from "../../common/front-end-routing/navigate-to-url-injection-token";
const navigateToUrlInjectable = getInjectable({
id: "navigate-to-url",
instantiate: (di) => {
const windowManager = di.inject(windowManagerInjectable);
return (url) => {
windowManager.navigateSync(url);
};
},
injectionToken: navigateToUrlInjectionToken,
});
export default navigateToUrlInjectable;
@@ -13,12 +13,11 @@ import { ExtensionsStore } from "../../../extensions/extensions-store/extensions
import type { LensProtocolRouterMain } from "../lens-protocol-router-main/lens-protocol-router-main";
import mockFs from "mock-fs";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import extensionLoaderInjectable
from "../../../extensions/extension-loader/extension-loader.injectable";
import lensProtocolRouterMainInjectable
from "../lens-protocol-router-main/lens-protocol-router-main.injectable";
import extensionsStoreInjectable
from "../../../extensions/extensions-store/extensions-store.injectable";
import extensionLoaderInjectable from "../../../extensions/extension-loader/extension-loader.injectable";
import lensProtocolRouterMainInjectable from "../lens-protocol-router-main/lens-protocol-router-main.injectable";
import extensionsStoreInjectable from "../../../extensions/extensions-store/extensions-store.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";
jest.mock("../../../common/ipc");
@@ -42,11 +41,16 @@ describe("protocol router tests", () => {
"tmp": {},
});
di.override(extensionsStoreInjectable, () => ExtensionsStore.createInstance());
di.permitSideEffects(getConfigurationFileModelInjectable);
di.permitSideEffects(appVersionInjectable);
await di.runSetups();
extensionLoader = di.inject(extensionLoaderInjectable);
extensionsStore = di.inject(extensionsStoreInjectable);
extensionsStore = di.inject(extensionsStoreInjectable);
lpr = di.inject(lensProtocolRouterMainInjectable);
@@ -56,9 +60,6 @@ describe("protocol router tests", () => {
afterEach(() => {
jest.clearAllMocks();
// TODO: Remove Singleton from BaseStore to achieve independent unit testing
ExtensionsStore.resetInstance();
mockFs.restore();
});
-2
View File
@@ -7,7 +7,6 @@ import Call from "@hapi/call";
import type http from "http";
import type httpProxy from "http-proxy";
import { toPairs } from "lodash/fp";
import path from "path";
import type { Cluster } from "../../common/cluster/cluster";
import type { LensApiResultContentType } from "./router-content-types";
import { contentTypes } from "./router-content-types";
@@ -51,7 +50,6 @@ interface Dependencies {
export class Router {
protected router = new Call.Router();
protected static rootPath = path.resolve(__static);
constructor(routes: Route<unknown>[], private dependencies: Dependencies) {
routes.forEach(route => {
@@ -9,23 +9,28 @@ import logger from "../logger";
import { routeInjectionToken } from "../router/router.injectable";
import { appName, publicPath } from "../../common/vars";
import path from "path";
import readFileInjectable from "../../common/fs/read-file.injectable";
import isDevelopmentInjectable from "../../common/vars/is-development.injectable";
import httpProxy from "http-proxy";
import readFileBufferInjectable from "../../common/fs/read-file-buffer.injectable";
import getAbsolutePathInjectable, { GetAbsolutePath } from "../../common/path/get-absolute-path.injectable";
import type { JoinPaths } from "../../common/path/join-paths.injectable";
import joinPathsInjectable from "../../common/path/join-paths.injectable";
interface ProductionDependencies {
readFile: (path: string) => Promise<Buffer>;
readFileBuffer: (path: string) => Promise<Buffer>;
getAbsolutePath: GetAbsolutePath;
joinPaths: JoinPaths;
}
const handleStaticFileInProduction =
({ readFile }: ProductionDependencies) =>
({ readFileBuffer, getAbsolutePath, joinPaths }: ProductionDependencies) =>
async ({ params }: LensApiRequest) => {
const staticPath = path.resolve(__static);
const staticPath = getAbsolutePath(__static);
let filePath = params.path;
for (let retryCount = 0; retryCount < 5; retryCount += 1) {
const asset = path.join(staticPath, filePath);
const normalizedFilePath = path.resolve(asset);
const asset = joinPaths(staticPath, filePath);
const normalizedFilePath = getAbsolutePath(asset);
if (!normalizedFilePath.startsWith(staticPath)) {
return { statusCode: 404 };
@@ -38,7 +43,7 @@ const handleStaticFileInProduction =
const contentType = contentTypes[fileExtension] || contentTypes.txt;
return { response: await readFile(asset), contentType };
return { response: await readFileBuffer(asset), contentType };
} catch (err) {
if (retryCount > 5) {
logger.error("handleStaticFile:", err.toString());
@@ -78,13 +83,16 @@ const staticFileRouteInjectable = getInjectable({
instantiate: (di): Route<Buffer> => {
const isDevelopment = di.inject(isDevelopmentInjectable);
const readFileBuffer = di.inject(readFileBufferInjectable);
const getAbsolutePath = di.inject(getAbsolutePathInjectable);
const joinPaths = di.inject(joinPathsInjectable);
return {
method: "get",
path: `/{path*}`,
handler: isDevelopment
? handleStaticFileInDevelopment({ proxy: httpProxy.createProxy() })
: handleStaticFileInProduction({ readFile: di.inject(readFileInjectable) }),
: handleStaticFileInProduction({ readFileBuffer, getAbsolutePath, joinPaths }),
};
},
+5 -5
View File
@@ -13,10 +13,10 @@ import type { WindowManager } from "../window-manager";
import logger from "../logger";
import { isDevelopment, isWindows, productName } from "../../common/vars";
import { exitApp } from "../exit-app";
import { preferencesURL } from "../../common/routes";
import { toJS } from "../../common/utils";
import type { TrayMenuRegistration } from "./tray-menu-registration";
const TRAY_LOG_PREFIX = "[TRAY]";
// note: instance of Tray should be saved somewhere, otherwise it disappears
@@ -33,6 +33,7 @@ export function getTrayIcon(): string {
export function initTray(
windowManager: WindowManager,
trayMenuItems: IComputedValue<TrayMenuRegistration[]>,
navigateToPreferences: () => void,
) {
const icon = getTrayIcon();
@@ -51,7 +52,7 @@ export function initTray(
const disposers = [
autorun(() => {
try {
const menu = createTrayMenu(windowManager, toJS(trayMenuItems.get()));
const menu = createTrayMenu(windowManager, toJS(trayMenuItems.get()), navigateToPreferences);
tray.setContextMenu(menu);
} catch (error) {
@@ -80,6 +81,7 @@ function getMenuItemConstructorOptions(trayItem: TrayMenuRegistration): Electron
function createTrayMenu(
windowManager: WindowManager,
extensionTrayItems: TrayMenuRegistration[],
navigateToPreferences: () => void,
): Menu {
let template: Electron.MenuItemConstructorOptions[] = [
{
@@ -93,9 +95,7 @@ function createTrayMenu(
{
label: "Preferences",
click() {
windowManager
.navigate(preferencesURL())
.catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to navigate to Preferences`, { error }));
navigateToPreferences();
},
},
];
+20
View File
@@ -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 { WindowManager } from "./window-manager";
const windowManagerInjectable = getInjectable({
id: "window-manager",
instantiate: () => {
WindowManager.resetInstance();
return WindowManager.createInstance();
},
causesSideEffects: true,
});
export default windowManagerInjectable;
+5 -1
View File
@@ -187,7 +187,7 @@ export class WindowManager extends Singleton {
return this.mainWindow;
}
private sendToView({ channel, frameInfo, data = [] }: SendToViewArgs) {
sendToView({ channel, frameInfo, data = [] }: SendToViewArgs) {
if (frameInfo) {
this.mainWindow.webContents.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...data);
} else {
@@ -210,6 +210,10 @@ export class WindowManager extends Singleton {
async navigate(url: string, frameId?: number) {
await this.ensureMainWindow();
this.navigateSync(url, frameId);
}
navigateSync(url: string, frameId?: number) {
const frameInfo = iter.find(clusterFrameMap.values(), frameInfo => frameInfo.frameId === frameId);
const channel = frameInfo
? IpcRendererNavigationEvents.NAVIGATE_IN_CLUSTER