`;
+exports[`extension special characters in page registrations renders 1`] = `
+
+
+
+`;
exports[`extension special characters in page registrations when navigating to route with ID having special characters renders 1`] = `
Some page
+
`;
diff --git a/src/behaviours/__snapshots__/navigate-to-extension-page.test.tsx.snap b/src/behaviours/__snapshots__/navigate-to-extension-page.test.tsx.snap
index edab04b903..c96763fe6e 100644
--- a/src/behaviours/__snapshots__/navigate-to-extension-page.test.tsx.snap
+++ b/src/behaviours/__snapshots__/navigate-to-extension-page.test.tsx.snap
@@ -1,12 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`navigate to extension page renders 1`] = ``;
+exports[`navigate to extension page renders 1`] = `
+
+
+
+`;
exports[`navigate to extension page when extension navigates to child route renders 1`] = `
Child page
+
`;
@@ -31,6 +40,9 @@ exports[`navigate to extension page when extension navigates to route with param
Some button
+
`;
@@ -55,6 +67,9 @@ exports[`navigate to extension page when extension navigates to route without pa
Some button
+
`;
@@ -79,5 +94,8 @@ exports[`navigate to extension page when extension navigates to route without pa
Some button
+
`;
diff --git a/src/behaviours/__snapshots__/navigating-between-routes.test.tsx.snap b/src/behaviours/__snapshots__/navigating-between-routes.test.tsx.snap
index 90ff615b2b..10e9eb2d39 100644
--- a/src/behaviours/__snapshots__/navigating-between-routes.test.tsx.snap
+++ b/src/behaviours/__snapshots__/navigating-between-routes.test.tsx.snap
@@ -8,6 +8,9 @@ exports[`navigating between routes given route with optional path parameters whe
"someOtherParameter": "some-other-value"
}
+
`;
@@ -16,5 +19,8 @@ exports[`navigating between routes given route without path parameters when navi
Some component
+
`;
diff --git a/src/behaviours/add-cluster/__snapshots__/navigation-using-application-menu.test.tsx.snap b/src/behaviours/add-cluster/__snapshots__/navigation-using-application-menu.test.tsx.snap
index d19612eac3..0fd00133aa 100644
--- a/src/behaviours/add-cluster/__snapshots__/navigation-using-application-menu.test.tsx.snap
+++ b/src/behaviours/add-cluster/__snapshots__/navigation-using-application-menu.test.tsx.snap
@@ -1,6 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`add-cluster - navigation using application menu renders 1`] = ``;
+exports[`add-cluster - navigation using application menu renders 1`] = `
+
+
+
+`;
exports[`add-cluster - navigation using application menu when navigating to add cluster using application menu renders 1`] = `
@@ -85,5 +91,8 @@ exports[`add-cluster - navigation using application menu when navigating to add
+
`;
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 e982b9de1c..bb68918c1a 100644
--- a/src/behaviours/add-cluster/navigation-using-application-menu.test.tsx
+++ b/src/behaviours/add-cluster/navigation-using-application-menu.test.tsx
@@ -6,16 +6,17 @@
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 React from "react";
// TODO: Make components free of side effects by making them deterministic
jest.mock("../../renderer/components/tooltip/tooltip", () => ({
Tooltip: () => null,
}));
+
jest.mock("../../renderer/components/tooltip/withTooltip", () => ({
withTooltip: (Target: any) => ({ tooltip, tooltipOverrideDisabled, ...props }: any) => ,
}));
+
jest.mock("../../renderer/components/monaco-editor/monaco-editor", () => ({
MonacoEditor: () => null,
}));
@@ -25,9 +26,7 @@ describe("add-cluster - navigation using application menu", () => {
let rendered: RenderResult;
beforeEach(async () => {
- applicationBuilder = getApplicationBuilder().beforeApplicationStart(({ mainDi }) => {
- mainDi.override(isAutoUpdateEnabledInjectable, () => () => false);
- });
+ applicationBuilder = getApplicationBuilder();
rendered = await applicationBuilder.render();
});
diff --git a/src/behaviours/application-update/__snapshots__/installing-update-using-tray.test.ts.snap b/src/behaviours/application-update/__snapshots__/installing-update-using-tray.test.ts.snap
new file mode 100644
index 0000000000..32e6cb1cb1
--- /dev/null
+++ b/src/behaviours/application-update/__snapshots__/installing-update-using-tray.test.ts.snap
@@ -0,0 +1,536 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`installing update using tray when started renders 1`] = `
+
+
+
+
+
+`;
+
+exports[`installing update using tray when started when user checks for updates using tray renders 1`] = `
+
+
+
+
+
+
+
+ info_outline
+
+
+
+
+ Checking for updates...
+
+
+
+
+ close
+
+
+
+
+
+
+
+`;
+
+exports[`installing update using tray when started when user checks for updates using tray when new update is discovered renders 1`] = `
+
+
+
+
+
+
+
+ info_outline
+
+
+
+
+ Checking for updates...
+
+
+
+
+ close
+
+
+
+
+
+
+
+
+ info_outline
+
+
+
+
+ Download for version some-version started...
+
+
+
+
+ close
+
+
+
+
+
+
+
+`;
+
+exports[`installing update using tray when started when user checks for updates using tray when new update is discovered when download fails renders 1`] = `
+
+
+
+
+
+
+
+ info_outline
+
+
+
+
+ Checking for updates...
+
+
+
+
+ close
+
+
+
+
+
+
+
+
+ info_outline
+
+
+
+
+ Download for version some-version started...
+
+
+
+
+ close
+
+
+
+
+
+
+
+
+ info_outline
+
+
+
+
+ Download of update failed
+
+
+
+
+ close
+
+
+
+
+
+
+
+`;
+
+exports[`installing update using tray when started when user checks for updates using tray when new update is discovered when download succeeds renders 1`] = `
+
+
+
+
+
+
+
+ info_outline
+
+
+
+
+ Checking for updates...
+
+
+
+
+ close
+
+
+
+
+
+
+
+
+ info_outline
+
+
+
+
+ Download for version some-version started...
+
+
+
+
+ close
+
+
+
+
+
+
+
+
+ info_outline
+
+
+
+
+
+
+ Update Available
+
+
+ Version some-version of Lens IDE is available and ready to be installed. Would you like to update now?
+
+Lens should restart automatically, if it doesn't please restart manually. Installed extensions might require updating.
+
+
+
+
+
+
+
+
+
+
+ close
+
+
+
+
+
+
+
+`;
+
+exports[`installing update using tray when started when user checks for updates using tray when no new update is discovered renders 1`] = `
+
+
+
+
+
+
+
+ info_outline
+
+
+
+
+ Checking for updates...
+
+
+
+
+ close
+
+
+
+
+
+
+
+
+ info_outline
+
+
+
+
+ No new updates available
+
+
+
+
+ close
+
+
+
+
+
+
+
+`;
diff --git a/src/behaviours/application-update/__snapshots__/installing-update.test.ts.snap b/src/behaviours/application-update/__snapshots__/installing-update.test.ts.snap
new file mode 100644
index 0000000000..7025289254
--- /dev/null
+++ b/src/behaviours/application-update/__snapshots__/installing-update.test.ts.snap
@@ -0,0 +1,81 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`installing update when started renders 1`] = `
+
+
+
+
+
+`;
+
+exports[`installing update when started when user checks for updates renders 1`] = `
+
+
+
+
+
+`;
+
+exports[`installing update when started when user checks for updates when new update is discovered renders 1`] = `
+
+
+
+
+
+`;
+
+exports[`installing update when started when user checks for updates when new update is discovered when download fails renders 1`] = `
+
+
+
+
+
+`;
+
+exports[`installing update when started when user checks for updates when new update is discovered when download succeeds renders 1`] = `
+
+
+
+
+
+`;
+
+exports[`installing update when started when user checks for updates when new update is discovered when download succeeds when user answers not to install the update renders 1`] = `
+
+
+
+
+
+`;
+
+exports[`installing update when started when user checks for updates when new update is discovered when download succeeds when user answers to install the update renders 1`] = `
+
+
+
+
+
+`;
+
+exports[`installing update when started when user checks for updates when no new update is discovered renders 1`] = `
+
+
+
+
+
+`;
diff --git a/src/behaviours/application-update/__snapshots__/periodical-checking-of-updates.test.ts.snap b/src/behaviours/application-update/__snapshots__/periodical-checking-of-updates.test.ts.snap
new file mode 100644
index 0000000000..84fa35ae04
--- /dev/null
+++ b/src/behaviours/application-update/__snapshots__/periodical-checking-of-updates.test.ts.snap
@@ -0,0 +1,11 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`periodical checking of updates given updater is enabled and configuration exists, when started renders 1`] = `
+
+
+
+
+
+`;
diff --git a/src/behaviours/application-update/__snapshots__/selection-of-update-stability.test.ts.snap b/src/behaviours/application-update/__snapshots__/selection-of-update-stability.test.ts.snap
new file mode 100644
index 0000000000..dc96c447b0
--- /dev/null
+++ b/src/behaviours/application-update/__snapshots__/selection-of-update-stability.test.ts.snap
@@ -0,0 +1,11 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`selection of update stability when started renders 1`] = `
+
+
+
+
+
+`;
diff --git a/src/behaviours/application-update/downgrading-version-update.test.ts b/src/behaviours/application-update/downgrading-version-update.test.ts
new file mode 100644
index 0000000000..e8e5635fb3
--- /dev/null
+++ b/src/behaviours/application-update/downgrading-version-update.test.ts
@@ -0,0 +1,87 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
+import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
+import electronUpdaterIsActiveInjectable from "../../main/electron-app/features/electron-updater-is-active.injectable";
+import publishIsConfiguredInjectable from "../../main/application-update/publish-is-configured.injectable";
+import type { AsyncFnMock } from "@async-fn/jest";
+import asyncFn from "@async-fn/jest";
+import type { CheckForPlatformUpdates } from "../../main/application-update/check-for-platform-updates/check-for-platform-updates.injectable";
+import checkForPlatformUpdatesInjectable from "../../main/application-update/check-for-platform-updates/check-for-platform-updates.injectable";
+import processCheckingForUpdatesInjectable from "../../main/application-update/check-for-updates/process-checking-for-updates.injectable";
+import selectedUpdateChannelInjectable from "../../common/application-update/selected-update-channel/selected-update-channel.injectable";
+import type { DiContainer } from "@ogre-tools/injectable";
+import appVersionInjectable from "../../common/get-configuration-file-model/app-version/app-version.injectable";
+import { updateChannels } from "../../common/application-update/update-channels";
+
+describe("downgrading version update", () => {
+ let applicationBuilder: ApplicationBuilder;
+ let checkForPlatformUpdatesMock: AsyncFnMock;
+ let mainDi: DiContainer;
+
+ beforeEach(() => {
+ jest.useFakeTimers();
+
+ applicationBuilder = getApplicationBuilder();
+
+ applicationBuilder.beforeApplicationStart(({ mainDi }) => {
+ checkForPlatformUpdatesMock = asyncFn();
+
+ mainDi.override(
+ checkForPlatformUpdatesInjectable,
+ () => checkForPlatformUpdatesMock,
+ );
+
+ mainDi.override(electronUpdaterIsActiveInjectable, () => true);
+ mainDi.override(publishIsConfiguredInjectable, () => true);
+ });
+
+ mainDi = applicationBuilder.dis.mainDi;
+ });
+
+ [
+ {
+ updateChannel: updateChannels.latest,
+ appVersion: "4.0.0-beta",
+ downgradeIsAllowed: true,
+ },
+ {
+ updateChannel: updateChannels.beta,
+ appVersion: "4.0.0-beta",
+ downgradeIsAllowed: false,
+ },
+ {
+ updateChannel: updateChannels.beta,
+ appVersion: "4.0.0-beta.1",
+ downgradeIsAllowed: false,
+ },
+ {
+ updateChannel: updateChannels.alpha,
+ appVersion: "4.0.0-beta",
+ downgradeIsAllowed: true,
+ },
+ {
+ updateChannel: updateChannels.alpha,
+ appVersion: "4.0.0-alpha",
+ downgradeIsAllowed: false,
+ },
+ ].forEach(({ appVersion, updateChannel, downgradeIsAllowed }) => {
+ it(`given application version "${appVersion}" and update channel "${updateChannel.id}", when checking for updates, can${downgradeIsAllowed ? "": "not"} downgrade`, async () => {
+ mainDi.override(appVersionInjectable, () => appVersion);
+
+ await applicationBuilder.render();
+
+ const selectedUpdateChannel = mainDi.inject(selectedUpdateChannelInjectable);
+
+ selectedUpdateChannel.setValue(updateChannel.id);
+
+ const processCheckingForUpdates = mainDi.inject(processCheckingForUpdatesInjectable);
+
+ processCheckingForUpdates();
+
+ expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(expect.any(Object), { allowDowngrade: downgradeIsAllowed });
+ });
+ });
+});
diff --git a/src/behaviours/application-update/installing-update-using-tray.test.ts b/src/behaviours/application-update/installing-update-using-tray.test.ts
new file mode 100644
index 0000000000..f6570bb8fa
--- /dev/null
+++ b/src/behaviours/application-update/installing-update-using-tray.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 { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
+import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
+import type { RenderResult } from "@testing-library/react";
+import electronUpdaterIsActiveInjectable from "../../main/electron-app/features/electron-updater-is-active.injectable";
+import publishIsConfiguredInjectable from "../../main/application-update/publish-is-configured.injectable";
+import type { CheckForPlatformUpdates } from "../../main/application-update/check-for-platform-updates/check-for-platform-updates.injectable";
+import checkForPlatformUpdatesInjectable from "../../main/application-update/check-for-platform-updates/check-for-platform-updates.injectable";
+import type { AsyncFnMock } from "@async-fn/jest";
+import asyncFn from "@async-fn/jest";
+import type { DownloadPlatformUpdate } from "../../main/application-update/download-platform-update/download-platform-update.injectable";
+import downloadPlatformUpdateInjectable from "../../main/application-update/download-platform-update/download-platform-update.injectable";
+import showApplicationWindowInjectable from "../../main/start-main-application/lens-window/show-application-window.injectable";
+import progressOfUpdateDownloadInjectable from "../../common/application-update/progress-of-update-download/progress-of-update-download.injectable";
+
+describe("installing update using tray", () => {
+ let applicationBuilder: ApplicationBuilder;
+ let checkForPlatformUpdatesMock: AsyncFnMock;
+ let downloadPlatformUpdateMock: AsyncFnMock;
+ let showApplicationWindowMock: jest.Mock;
+
+ beforeEach(() => {
+ applicationBuilder = getApplicationBuilder();
+
+ applicationBuilder.beforeApplicationStart(({ mainDi }) => {
+ checkForPlatformUpdatesMock = asyncFn();
+ downloadPlatformUpdateMock = asyncFn();
+ showApplicationWindowMock = jest.fn();
+
+ mainDi.override(showApplicationWindowInjectable, () => showApplicationWindowMock);
+
+ mainDi.override(
+ checkForPlatformUpdatesInjectable,
+ () => checkForPlatformUpdatesMock,
+ );
+
+ mainDi.override(
+ downloadPlatformUpdateInjectable,
+ () => downloadPlatformUpdateMock,
+ );
+
+ mainDi.override(electronUpdaterIsActiveInjectable, () => true);
+ mainDi.override(publishIsConfiguredInjectable, () => true);
+ });
+ });
+
+ describe("when started", () => {
+ let rendered: RenderResult;
+
+ beforeEach(async () => {
+ rendered = await applicationBuilder.render();
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("user cannot install update yet", () => {
+ expect(applicationBuilder.tray.get("install-update")).toBeUndefined();
+ });
+
+ describe("when user checks for updates using tray", () => {
+ let processCheckingForUpdatesPromise: Promise;
+
+ beforeEach(async () => {
+ processCheckingForUpdatesPromise =
+ applicationBuilder.tray.click("check-for-updates");
+ });
+
+ it("does not show application window yet", () => {
+ expect(showApplicationWindowMock).not.toHaveBeenCalled();
+ });
+
+ it("user cannot check for updates again", () => {
+ expect(
+ applicationBuilder.tray.get("check-for-updates")?.enabled.get(),
+ ).toBe(false);
+ });
+
+ it("name of tray item for checking updates indicates that checking is happening", () => {
+ expect(
+ applicationBuilder.tray.get("check-for-updates")?.label?.get(),
+ ).toBe("Checking for updates...");
+ });
+
+ it("user cannot install update yet", () => {
+ expect(applicationBuilder.tray.get("install-update")).toBeUndefined();
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ describe("when no new update is discovered", () => {
+ beforeEach(async () => {
+ await checkForPlatformUpdatesMock.resolve({
+ updateWasDiscovered: false,
+ });
+
+ await processCheckingForUpdatesPromise;
+ });
+
+ it("shows application window", () => {
+ expect(showApplicationWindowMock).toHaveBeenCalled();
+ });
+
+ it("user cannot install update", () => {
+ expect(applicationBuilder.tray.get("install-update")).toBeUndefined();
+ });
+
+ it("user can check for updates again", () => {
+ expect(
+ applicationBuilder.tray.get("check-for-updates")?.enabled.get(),
+ ).toBe(true);
+ });
+
+ it("name of tray item for checking updates no longer indicates that checking is happening", () => {
+ expect(
+ applicationBuilder.tray.get("check-for-updates")?.label?.get(),
+ ).toBe("Check for updates");
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+ });
+
+ describe("when new update is discovered", () => {
+ beforeEach(async () => {
+ await checkForPlatformUpdatesMock.resolve({
+ updateWasDiscovered: true,
+ version: "some-version",
+ });
+
+ await processCheckingForUpdatesPromise;
+ });
+
+ it("shows application window", () => {
+ expect(showApplicationWindowMock).toHaveBeenCalled();
+ });
+
+ it("user cannot check for updates again yet", () => {
+ expect(
+ applicationBuilder.tray.get("check-for-updates")?.enabled.get(),
+ ).toBe(false);
+ });
+
+ it("name of tray item for checking updates indicates that downloading is happening", () => {
+ expect(
+ applicationBuilder.tray.get("check-for-updates")?.label?.get(),
+ ).toBe("Downloading update some-version (0%)...");
+ });
+
+ it("when download progresses with decimals, percentage increases as integers", () => {
+ const progressOfUpdateDownload = applicationBuilder.dis.mainDi.inject(
+ progressOfUpdateDownloadInjectable,
+ );
+
+ progressOfUpdateDownload.set({ percentage: 42.424242 });
+
+ expect(
+ applicationBuilder.tray.get("check-for-updates")?.label?.get(),
+ ).toBe("Downloading update some-version (42%)...");
+ });
+
+ it("user still cannot install update", () => {
+ expect(applicationBuilder.tray.get("install-update")).toBeUndefined();
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ describe("when download fails", () => {
+ beforeEach(async () => {
+ await downloadPlatformUpdateMock.resolve({ downloadWasSuccessful: false });
+ });
+
+ it("user cannot install update", () => {
+ expect(
+ applicationBuilder.tray.get("install-update"),
+ ).toBeUndefined();
+ });
+
+ it("user can check for updates again", () => {
+ expect(
+ applicationBuilder.tray.get("check-for-updates")?.enabled.get(),
+ ).toBe(true);
+ });
+
+ it("name of tray item for checking updates no longer indicates that downloading is happening", () => {
+ expect(
+ applicationBuilder.tray.get("check-for-updates")?.label?.get(),
+ ).toBe("Check for updates");
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+ });
+
+ describe("when download succeeds", () => {
+ beforeEach(async () => {
+ await downloadPlatformUpdateMock.resolve({ downloadWasSuccessful: true });
+ });
+
+ it("user can install update", () => {
+ expect(
+ applicationBuilder.tray.get("install-update")?.label?.get(),
+ ).toBe("Install update some-version");
+ });
+
+ it("user can check for updates again", () => {
+ expect(
+ applicationBuilder.tray.get("check-for-updates")?.enabled.get(),
+ ).toBe(true);
+ });
+
+ it("name of tray item for checking updates no longer indicates that downloading is happening", () => {
+ expect(
+ applicationBuilder.tray.get("check-for-updates")?.label?.get(),
+ ).toBe("Check for updates");
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/src/behaviours/application-update/installing-update.test.ts b/src/behaviours/application-update/installing-update.test.ts
new file mode 100644
index 0000000000..3fec5f6d27
--- /dev/null
+++ b/src/behaviours/application-update/installing-update.test.ts
@@ -0,0 +1,225 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
+import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
+import quitAndInstallUpdateInjectable from "../../main/electron-app/features/quit-and-install-update.injectable";
+import type { RenderResult } from "@testing-library/react";
+import electronUpdaterIsActiveInjectable from "../../main/electron-app/features/electron-updater-is-active.injectable";
+import publishIsConfiguredInjectable from "../../main/application-update/publish-is-configured.injectable";
+import type { CheckForPlatformUpdates } from "../../main/application-update/check-for-platform-updates/check-for-platform-updates.injectable";
+import checkForPlatformUpdatesInjectable from "../../main/application-update/check-for-platform-updates/check-for-platform-updates.injectable";
+import type { AsyncFnMock } from "@async-fn/jest";
+import asyncFn from "@async-fn/jest";
+import type { DownloadPlatformUpdate } from "../../main/application-update/download-platform-update/download-platform-update.injectable";
+import downloadPlatformUpdateInjectable from "../../main/application-update/download-platform-update/download-platform-update.injectable";
+import setUpdateOnQuitInjectable from "../../main/electron-app/features/set-update-on-quit.injectable";
+import type { AskBoolean } from "../../main/ask-boolean/ask-boolean.injectable";
+import askBooleanInjectable from "../../main/ask-boolean/ask-boolean.injectable";
+import showInfoNotificationInjectable from "../../renderer/components/notifications/show-info-notification.injectable";
+import processCheckingForUpdatesInjectable from "../../main/application-update/check-for-updates/process-checking-for-updates.injectable";
+
+describe("installing update", () => {
+ let applicationBuilder: ApplicationBuilder;
+ let quitAndInstallUpdateMock: jest.Mock;
+ let checkForPlatformUpdatesMock: AsyncFnMock;
+ let downloadPlatformUpdateMock: AsyncFnMock;
+ let setUpdateOnQuitMock: jest.Mock;
+ let showInfoNotificationMock: jest.Mock;
+ let askBooleanMock: AsyncFnMock;
+
+ beforeEach(() => {
+ applicationBuilder = getApplicationBuilder();
+
+ applicationBuilder.beforeApplicationStart(({ mainDi, rendererDi }) => {
+ quitAndInstallUpdateMock = jest.fn();
+ checkForPlatformUpdatesMock = asyncFn();
+ downloadPlatformUpdateMock = asyncFn();
+ setUpdateOnQuitMock = jest.fn();
+ showInfoNotificationMock = jest.fn(() => () => {});
+ askBooleanMock = asyncFn();
+
+ rendererDi.override(showInfoNotificationInjectable, () => showInfoNotificationMock);
+
+ mainDi.override(askBooleanInjectable, () => askBooleanMock);
+ mainDi.override(setUpdateOnQuitInjectable, () => setUpdateOnQuitMock);
+
+ mainDi.override(
+ checkForPlatformUpdatesInjectable,
+ () => checkForPlatformUpdatesMock,
+ );
+
+ mainDi.override(
+ downloadPlatformUpdateInjectable,
+ () => downloadPlatformUpdateMock,
+ );
+
+ mainDi.override(
+ quitAndInstallUpdateInjectable,
+ () => quitAndInstallUpdateMock,
+ );
+
+ mainDi.override(electronUpdaterIsActiveInjectable, () => true);
+ mainDi.override(publishIsConfiguredInjectable, () => true);
+ });
+ });
+
+ describe("when started", () => {
+ let rendered: RenderResult;
+ let processCheckingForUpdates: () => Promise;
+
+ beforeEach(async () => {
+ rendered = await applicationBuilder.render();
+
+ processCheckingForUpdates = applicationBuilder.dis.mainDi.inject(processCheckingForUpdatesInjectable);
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ describe("when user checks for updates", () => {
+ let processCheckingForUpdatesPromise: Promise;
+
+ beforeEach(async () => {
+ processCheckingForUpdatesPromise = processCheckingForUpdates();
+ });
+
+ it("checks for updates", () => {
+ expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(
+ expect.any(Object),
+ { allowDowngrade: true },
+ );
+ });
+
+ it("notifies the user that checking for updates is happening", () => {
+ expect(showInfoNotificationMock).toHaveBeenCalledWith("Checking for updates...");
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ describe("when no new update is discovered", () => {
+ beforeEach(async () => {
+ showInfoNotificationMock.mockClear();
+
+ await checkForPlatformUpdatesMock.resolve({
+ updateWasDiscovered: false,
+ });
+
+ await processCheckingForUpdatesPromise;
+ });
+
+ it("notifies the user", () => {
+ expect(showInfoNotificationMock).toHaveBeenCalledWith("No new updates available");
+ });
+
+ it("does not start downloading update", () => {
+ expect(downloadPlatformUpdateMock).not.toHaveBeenCalled();
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+ });
+
+ describe("when new update is discovered", () => {
+ beforeEach(async () => {
+ await checkForPlatformUpdatesMock.resolve({
+ updateWasDiscovered: true,
+ version: "some-version",
+ });
+
+ await processCheckingForUpdatesPromise;
+ });
+
+ it("starts downloading the update", () => {
+ expect(downloadPlatformUpdateMock).toHaveBeenCalled();
+ });
+
+ it("notifies the user that download is happening", () => {
+ expect(showInfoNotificationMock).toHaveBeenCalledWith("Download for version some-version started...");
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ describe("when download fails", () => {
+ beforeEach(async () => {
+ await downloadPlatformUpdateMock.resolve({ downloadWasSuccessful: false });
+ });
+
+ it("does not quit and install update yet", () => {
+ expect(quitAndInstallUpdateMock).not.toHaveBeenCalled();
+ });
+
+ it("notifies the user about failed download", () => {
+ expect(showInfoNotificationMock).toHaveBeenCalledWith("Download of update failed");
+ });
+
+ it("does not ask user to install update", () => {
+ expect(askBooleanMock).not.toHaveBeenCalled();
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+ });
+
+ describe("when download succeeds", () => {
+ beforeEach(async () => {
+ await downloadPlatformUpdateMock.resolve({ downloadWasSuccessful: true });
+ });
+
+ it("does not quit and install update yet", () => {
+ expect(quitAndInstallUpdateMock).not.toHaveBeenCalled();
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("asks user to install update immediately", () => {
+ expect(askBooleanMock).toHaveBeenCalledWith({
+ title: "Update Available",
+ question:
+ "Version some-version of Lens IDE is available and ready to be installed. Would you like to update now?\n\n" +
+ "Lens should restart automatically, if it doesn't please restart manually. Installed extensions might require updating.",
+ });
+ });
+
+ describe("when user answers to install the update", () => {
+ beforeEach(async () => {
+ await askBooleanMock.resolve(true);
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("quits application and installs the update", () => {
+ expect(quitAndInstallUpdateMock).toHaveBeenCalled();
+ });
+ });
+
+ describe("when user answers not to install the update", () => {
+ beforeEach(async () => {
+ await askBooleanMock.resolve(false);
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("does not quit application and install the update", () => {
+ expect(quitAndInstallUpdateMock).not.toHaveBeenCalled();
+ });
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/src/behaviours/application-update/periodical-checking-of-updates.test.ts b/src/behaviours/application-update/periodical-checking-of-updates.test.ts
new file mode 100644
index 0000000000..e81c002e34
--- /dev/null
+++ b/src/behaviours/application-update/periodical-checking-of-updates.test.ts
@@ -0,0 +1,117 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
+import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
+import type { RenderResult } from "@testing-library/react";
+import electronUpdaterIsActiveInjectable from "../../main/electron-app/features/electron-updater-is-active.injectable";
+import publishIsConfiguredInjectable from "../../main/application-update/publish-is-configured.injectable";
+import type { AsyncFnMock } from "@async-fn/jest";
+import asyncFn from "@async-fn/jest";
+import processCheckingForUpdatesInjectable from "../../main/application-update/check-for-updates/process-checking-for-updates.injectable";
+import periodicalCheckForUpdatesInjectable from "../../main/application-update/periodical-check-for-updates/periodical-check-for-updates.injectable";
+
+const ENOUGH_TIME = 1000 * 60 * 60 * 2;
+
+describe("periodical checking of updates", () => {
+ let applicationBuilder: ApplicationBuilder;
+ let processCheckingForUpdatesMock: AsyncFnMock<() => Promise>;
+
+ beforeEach(() => {
+ jest.useFakeTimers();
+
+ applicationBuilder = getApplicationBuilder();
+
+ applicationBuilder.beforeApplicationStart(({ mainDi }) => {
+ mainDi.unoverride(periodicalCheckForUpdatesInjectable);
+ mainDi.permitSideEffects(periodicalCheckForUpdatesInjectable);
+
+ processCheckingForUpdatesMock = asyncFn();
+
+ mainDi.override(
+ processCheckingForUpdatesInjectable,
+ () => processCheckingForUpdatesMock,
+ );
+ });
+ });
+
+ describe("given updater is enabled and configuration exists, when started", () => {
+ let rendered: RenderResult;
+
+ beforeEach(async () => {
+ applicationBuilder.beforeApplicationStart(({ mainDi }) => {
+ mainDi.override(electronUpdaterIsActiveInjectable, () => true);
+ mainDi.override(publishIsConfiguredInjectable, () => true);
+ });
+
+ rendered = await applicationBuilder.render();
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("checks for updates", () => {
+ expect(processCheckingForUpdatesMock).toHaveBeenCalled();
+ });
+
+ it("when just not enough time passes, does not check for updates again automatically yet", () => {
+ processCheckingForUpdatesMock.mockClear();
+
+ jest.advanceTimersByTime(ENOUGH_TIME - 1);
+
+ expect(processCheckingForUpdatesMock).not.toHaveBeenCalled();
+ });
+
+ it("when just enough time passes, checks for updates again automatically", () => {
+ processCheckingForUpdatesMock.mockClear();
+
+ jest.advanceTimersByTime(ENOUGH_TIME);
+
+ expect(processCheckingForUpdatesMock).toHaveBeenCalled();
+ });
+ });
+
+ describe("given updater is enabled but no configuration exist, when started", () => {
+ beforeEach(async () => {
+ applicationBuilder.beforeApplicationStart(({ mainDi }) => {
+ mainDi.override(electronUpdaterIsActiveInjectable, () => true);
+ mainDi.override(publishIsConfiguredInjectable, () => false);
+ });
+
+ await applicationBuilder.render();
+ });
+
+ it("does not check for updates", () => {
+ expect(processCheckingForUpdatesMock).not.toHaveBeenCalled();
+ });
+
+ it("when time passes, never checks for updates", () => {
+ jest.runOnlyPendingTimers();
+
+ expect(processCheckingForUpdatesMock).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("given updater is not enabled but and configuration exist, when started", () => {
+ beforeEach(async () => {
+ applicationBuilder.beforeApplicationStart(({ mainDi }) => {
+ mainDi.override(electronUpdaterIsActiveInjectable, () => false);
+ mainDi.override(publishIsConfiguredInjectable, () => true);
+ });
+
+ await applicationBuilder.render();
+ });
+
+ it("does not check for updates", () => {
+ expect(processCheckingForUpdatesMock).not.toHaveBeenCalled();
+ });
+
+ it("when time passes, never checks for updates", () => {
+ jest.runOnlyPendingTimers();
+
+ expect(processCheckingForUpdatesMock).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/src/behaviours/application-update/selection-of-update-stability.test.ts b/src/behaviours/application-update/selection-of-update-stability.test.ts
new file mode 100644
index 0000000000..1792fcd484
--- /dev/null
+++ b/src/behaviours/application-update/selection-of-update-stability.test.ts
@@ -0,0 +1,331 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
+import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
+import quitAndInstallUpdateInjectable from "../../main/electron-app/features/quit-and-install-update.injectable";
+import type { RenderResult } from "@testing-library/react";
+import electronUpdaterIsActiveInjectable from "../../main/electron-app/features/electron-updater-is-active.injectable";
+import publishIsConfiguredInjectable from "../../main/application-update/publish-is-configured.injectable";
+import type { CheckForPlatformUpdates } from "../../main/application-update/check-for-platform-updates/check-for-platform-updates.injectable";
+import checkForPlatformUpdatesInjectable from "../../main/application-update/check-for-platform-updates/check-for-platform-updates.injectable";
+import type { AsyncFnMock } from "@async-fn/jest";
+import asyncFn from "@async-fn/jest";
+import type { UpdateChannel, UpdateChannelId } from "../../common/application-update/update-channels";
+import { updateChannels } from "../../common/application-update/update-channels";
+import type { DownloadPlatformUpdate } from "../../main/application-update/download-platform-update/download-platform-update.injectable";
+import downloadPlatformUpdateInjectable from "../../main/application-update/download-platform-update/download-platform-update.injectable";
+import selectedUpdateChannelInjectable from "../../common/application-update/selected-update-channel/selected-update-channel.injectable";
+import type { IComputedValue } from "mobx";
+import setUpdateOnQuitInjectable from "../../main/electron-app/features/set-update-on-quit.injectable";
+import type { AskBoolean } from "../../main/ask-boolean/ask-boolean.injectable";
+import askBooleanInjectable from "../../main/ask-boolean/ask-boolean.injectable";
+import showInfoNotificationInjectable from "../../renderer/components/notifications/show-info-notification.injectable";
+import processCheckingForUpdatesInjectable from "../../main/application-update/check-for-updates/process-checking-for-updates.injectable";
+import appVersionInjectable from "../../common/get-configuration-file-model/app-version/app-version.injectable";
+
+describe("selection of update stability", () => {
+ let applicationBuilder: ApplicationBuilder;
+ let quitAndInstallUpdateMock: jest.Mock;
+ let checkForPlatformUpdatesMock: AsyncFnMock;
+ let downloadPlatformUpdateMock: AsyncFnMock;
+ let setUpdateOnQuitMock: jest.Mock;
+ let showInfoNotificationMock: jest.Mock;
+ let askBooleanMock: AsyncFnMock;
+
+ beforeEach(() => {
+ applicationBuilder = getApplicationBuilder();
+
+ applicationBuilder.beforeApplicationStart(({ mainDi, rendererDi }) => {
+ quitAndInstallUpdateMock = jest.fn();
+ checkForPlatformUpdatesMock = asyncFn();
+ downloadPlatformUpdateMock = asyncFn();
+ setUpdateOnQuitMock = jest.fn();
+ showInfoNotificationMock = jest.fn(() => () => {});
+ askBooleanMock = asyncFn();
+
+ rendererDi.override(showInfoNotificationInjectable, () => showInfoNotificationMock);
+
+ mainDi.override(askBooleanInjectable, () => askBooleanMock);
+ mainDi.override(setUpdateOnQuitInjectable, () => setUpdateOnQuitMock);
+
+ mainDi.override(
+ checkForPlatformUpdatesInjectable,
+ () => checkForPlatformUpdatesMock,
+ );
+
+ mainDi.override(
+ downloadPlatformUpdateInjectable,
+ () => downloadPlatformUpdateMock,
+ );
+
+ mainDi.override(
+ quitAndInstallUpdateInjectable,
+ () => quitAndInstallUpdateMock,
+ );
+
+ mainDi.override(electronUpdaterIsActiveInjectable, () => true);
+ mainDi.override(publishIsConfiguredInjectable, () => true);
+ });
+ });
+
+ describe("when started", () => {
+ let rendered: RenderResult;
+ let processCheckingForUpdates: () => Promise;
+
+ beforeEach(async () => {
+ rendered = await applicationBuilder.render();
+
+ processCheckingForUpdates = applicationBuilder.dis.mainDi.inject(processCheckingForUpdatesInjectable);
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ describe('given update channel "alpha" is selected, when checking for updates', () => {
+ let selectedUpdateChannel: {
+ value: IComputedValue;
+ setValue: (channelId: UpdateChannelId) => void;
+ };
+
+ beforeEach(() => {
+ selectedUpdateChannel = applicationBuilder.dis.mainDi.inject(
+ selectedUpdateChannelInjectable,
+ );
+
+ selectedUpdateChannel.setValue(updateChannels.alpha.id);
+
+ processCheckingForUpdates();
+ });
+
+ it('checks updates from update channel "alpha"', () => {
+ expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(
+ updateChannels.alpha,
+ { allowDowngrade: true },
+ );
+ });
+
+ it("when update is discovered, does not check update from other update channels", async () => {
+ checkForPlatformUpdatesMock.mockClear();
+
+ await checkForPlatformUpdatesMock.resolve({
+ updateWasDiscovered: true,
+ version: "some-version",
+ });
+
+ expect(checkForPlatformUpdatesMock).not.toHaveBeenCalled();
+ });
+
+ describe("when no update is discovered", () => {
+ beforeEach(async () => {
+ checkForPlatformUpdatesMock.mockClear();
+
+ await checkForPlatformUpdatesMock.resolve({
+ updateWasDiscovered: false,
+ });
+ });
+
+ it('checks updates from update channel "beta"', () => {
+ expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(
+ updateChannels.beta,
+ { allowDowngrade: true },
+ );
+ });
+
+ it("when update is discovered, does not check update from other update channels", async () => {
+ checkForPlatformUpdatesMock.mockClear();
+
+ await checkForPlatformUpdatesMock.resolve({
+ updateWasDiscovered: true,
+ version: "some-version",
+ });
+
+ expect(checkForPlatformUpdatesMock).not.toHaveBeenCalled();
+ });
+
+ describe("when no update is discovered again", () => {
+ beforeEach(async () => {
+ checkForPlatformUpdatesMock.mockClear();
+
+ await checkForPlatformUpdatesMock.resolve({
+ updateWasDiscovered: false,
+ });
+ });
+
+ it('finally checks updates from update channel "latest"', () => {
+ expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(
+ updateChannels.latest,
+ { allowDowngrade: true },
+ );
+ });
+
+ it("when update is discovered, does not check update from other update channels", async () => {
+ checkForPlatformUpdatesMock.mockClear();
+
+ await checkForPlatformUpdatesMock.resolve({
+ updateWasDiscovered: true,
+ version: "some-version",
+ });
+
+ expect(checkForPlatformUpdatesMock).not.toHaveBeenCalled();
+ });
+ });
+ });
+ });
+
+ describe('given update channel "beta" is selected', () => {
+ let selectedUpdateChannel: {
+ value: IComputedValue;
+ setValue: (channelId: UpdateChannelId) => void;
+ };
+
+ beforeEach(() => {
+ selectedUpdateChannel = applicationBuilder.dis.mainDi.inject(
+ selectedUpdateChannelInjectable,
+ );
+
+ selectedUpdateChannel.setValue(updateChannels.beta.id);
+ });
+
+ describe("when checking for updates", () => {
+ beforeEach(() => {
+ processCheckingForUpdates();
+ });
+
+ describe('when update from "beta" channel is discovered', () => {
+ beforeEach(async () => {
+ await checkForPlatformUpdatesMock.resolve({
+ updateWasDiscovered: true,
+ version: "some-beta-version",
+ });
+ });
+
+ describe("when update is downloaded", () => {
+ beforeEach(async () => {
+ await downloadPlatformUpdateMock.resolve({ downloadWasSuccessful: true });
+ });
+
+ it("when user would close the application, installs the update", () => {
+ expect(setUpdateOnQuitMock).toHaveBeenLastCalledWith(true);
+ });
+
+ it('given user changes update channel to "latest", when user would close the application, does not install the update for not being stable enough', () => {
+ selectedUpdateChannel.setValue(updateChannels.latest.id);
+
+ expect(setUpdateOnQuitMock).toHaveBeenLastCalledWith(false);
+ });
+
+ it('given user changes update channel to "alpha", when user would close the application, installs the update for being stable enough', () => {
+ selectedUpdateChannel.setValue(updateChannels.alpha.id);
+
+ expect(setUpdateOnQuitMock).toHaveBeenLastCalledWith(false);
+ });
+ });
+ });
+ });
+ });
+ });
+
+ it("given valid update channel selection is stored, when checking for updates, checks for updates from the update channel", async () => {
+ applicationBuilder.beforeApplicationStart(({ mainDi }) => {
+ // TODO: Switch to more natural way of setting initial value
+ // TODO: UserStore is currently responsible for getting and setting initial value
+ const selectedUpdateChannel = mainDi.inject(selectedUpdateChannelInjectable);
+
+ selectedUpdateChannel.setValue(updateChannels.beta.id);
+ });
+
+ await applicationBuilder.render();
+
+ const processCheckingForUpdates = applicationBuilder.dis.mainDi.inject(processCheckingForUpdatesInjectable);
+
+ processCheckingForUpdates();
+
+ expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(updateChannels.beta, expect.any(Object));
+ });
+
+ it("given invalid update channel selection is stored, when checking for updates, checks for updates from the update channel", async () => {
+ applicationBuilder.beforeApplicationStart(({ mainDi }) => {
+ // TODO: Switch to more natural way of setting initial value
+ // TODO: UserStore is currently responsible for getting and setting initial value
+ const selectedUpdateChannel = mainDi.inject(selectedUpdateChannelInjectable);
+
+ selectedUpdateChannel.setValue("something-invalid" as UpdateChannelId);
+ });
+
+ await applicationBuilder.render();
+
+ const processCheckingForUpdates = applicationBuilder.dis.mainDi.inject(processCheckingForUpdatesInjectable);
+
+ processCheckingForUpdates();
+
+ expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(updateChannels.latest, expect.any(Object));
+ });
+
+ it('given no update channel selection is stored and currently using stable release, when user checks for updates, checks for updates from "latest" update channel by default', async () => {
+ applicationBuilder.beforeApplicationStart(({ mainDi }) => {
+ mainDi.override(appVersionInjectable, () => "1.0.0");
+ });
+
+ await applicationBuilder.render();
+
+ const processCheckingForUpdates = applicationBuilder.dis.mainDi.inject(processCheckingForUpdatesInjectable);
+
+ processCheckingForUpdates();
+
+ expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(
+ updateChannels.latest,
+ { allowDowngrade: true },
+ );
+ });
+
+ it('given no update channel selection is stored and currently using alpha release, when checking for updates, checks for updates from "alpha" channel', async () => {
+ applicationBuilder.beforeApplicationStart(({ mainDi }) => {
+ mainDi.override(appVersionInjectable, () => "1.0.0-alpha");
+ });
+
+ await applicationBuilder.render();
+
+ const processCheckingForUpdates = applicationBuilder.dis.mainDi.inject(processCheckingForUpdatesInjectable);
+
+ processCheckingForUpdates();
+
+ expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(updateChannels.alpha, expect.any(Object));
+ });
+
+ it('given no update channel selection is stored and currently using beta release, when checking for updates, checks for updates from "beta" channel', async () => {
+ applicationBuilder.beforeApplicationStart(({ mainDi }) => {
+ mainDi.override(appVersionInjectable, () => "1.0.0-beta");
+ });
+
+ await applicationBuilder.render();
+
+ const processCheckingForUpdates = applicationBuilder.dis.mainDi.inject(processCheckingForUpdatesInjectable);
+
+ processCheckingForUpdates();
+
+ expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(updateChannels.beta, expect.any(Object));
+ });
+
+ it("given update channel selection is stored and currently using prerelease, when checking for updates, checks for updates from stored channel", async () => {
+ applicationBuilder.beforeApplicationStart(({ mainDi }) => {
+ mainDi.override(appVersionInjectable, () => "1.0.0-alpha");
+
+ // TODO: Switch to more natural way of setting initial value
+ // TODO: UserStore is currently responsible for getting and setting initial value
+ const selectedUpdateChannel = mainDi.inject(selectedUpdateChannelInjectable);
+
+ selectedUpdateChannel.setValue(updateChannels.beta.id);
+ });
+
+ await applicationBuilder.render();
+
+ const processCheckingForUpdates = applicationBuilder.dis.mainDi.inject(processCheckingForUpdatesInjectable);
+
+ processCheckingForUpdates();
+
+ expect(checkForPlatformUpdatesMock).toHaveBeenCalledWith(updateChannels.beta, expect.any(Object));
+ });
+});
diff --git a/src/behaviours/cluster/__snapshots__/order-of-sidebar-items.test.tsx.snap b/src/behaviours/cluster/__snapshots__/order-of-sidebar-items.test.tsx.snap
index 092337ec82..9af01f0969 100644
--- a/src/behaviours/cluster/__snapshots__/order-of-sidebar-items.test.tsx.snap
+++ b/src/behaviours/cluster/__snapshots__/order-of-sidebar-items.test.tsx.snap
@@ -328,6 +328,9 @@ exports[`cluster - order of sidebar items when rendered renders 1`] = `
+
`;
@@ -723,5 +726,8 @@ exports[`cluster - order of sidebar items when rendered when parent is expanded
+
`;
diff --git a/src/behaviours/cluster/__snapshots__/sidebar-and-tab-navigation-for-core.test.tsx.snap b/src/behaviours/cluster/__snapshots__/sidebar-and-tab-navigation-for-core.test.tsx.snap
index ad1f7a8d4c..06d1a1e210 100644
--- a/src/behaviours/cluster/__snapshots__/sidebar-and-tab-navigation-for-core.test.tsx.snap
+++ b/src/behaviours/cluster/__snapshots__/sidebar-and-tab-navigation-for-core.test.tsx.snap
@@ -293,6 +293,9 @@ exports[`cluster - sidebar and tab navigation for core given core registrations
+
`;
@@ -589,6 +592,9 @@ exports[`cluster - sidebar and tab navigation for core given core registrations
+
`;
@@ -909,6 +915,9 @@ exports[`cluster - sidebar and tab navigation for core given core registrations
+
`;
@@ -1234,6 +1243,9 @@ exports[`cluster - sidebar and tab navigation for core given core registrations
+
`;
@@ -1534,6 +1546,9 @@ exports[`cluster - sidebar and tab navigation for core given core registrations
+
`;
@@ -1854,6 +1869,9 @@ exports[`cluster - sidebar and tab navigation for core given core registrations
+
`;
@@ -2150,5 +2168,8 @@ exports[`cluster - sidebar and tab navigation for core given core registrations
+
`;
diff --git a/src/behaviours/cluster/__snapshots__/sidebar-and-tab-navigation-for-extensions.test.tsx.snap b/src/behaviours/cluster/__snapshots__/sidebar-and-tab-navigation-for-extensions.test.tsx.snap
index be9c321cd5..19cb615cce 100644
--- a/src/behaviours/cluster/__snapshots__/sidebar-and-tab-navigation-for-extensions.test.tsx.snap
+++ b/src/behaviours/cluster/__snapshots__/sidebar-and-tab-navigation-for-extensions.test.tsx.snap
@@ -293,6 +293,9 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
+
`;
@@ -589,6 +592,9 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
+
`;
@@ -929,6 +935,9 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
+
`;
@@ -1313,6 +1322,9 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
+
`;
@@ -1697,6 +1709,9 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
+
`;
@@ -2036,6 +2051,9 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
+
`;
@@ -2376,6 +2394,9 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
+
`;
@@ -2672,5 +2693,8 @@ exports[`cluster - sidebar and tab navigation for extensions given extension wit
+
`;
diff --git a/src/behaviours/cluster/__snapshots__/visibility-of-sidebar-items.test.tsx.snap b/src/behaviours/cluster/__snapshots__/visibility-of-sidebar-items.test.tsx.snap
index 4f28c6ecef..cc44f56496 100644
--- a/src/behaviours/cluster/__snapshots__/visibility-of-sidebar-items.test.tsx.snap
+++ b/src/behaviours/cluster/__snapshots__/visibility-of-sidebar-items.test.tsx.snap
@@ -261,6 +261,9 @@ exports[`cluster - visibility of sidebar items given kube resource for route is
+
`;
@@ -573,5 +576,8 @@ exports[`cluster - visibility of sidebar items given kube resource for route is
+
`;
diff --git a/src/behaviours/extensions/__snapshots__/navigation-using-application-menu.test.ts.snap b/src/behaviours/extensions/__snapshots__/navigation-using-application-menu.test.ts.snap
index 3a1b1309dd..c14ccb6160 100644
--- a/src/behaviours/extensions/__snapshots__/navigation-using-application-menu.test.ts.snap
+++ b/src/behaviours/extensions/__snapshots__/navigation-using-application-menu.test.ts.snap
@@ -1,6 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`extensions - navigation using application menu renders 1`] = ``;
+exports[`extensions - navigation using application menu renders 1`] = `
+
+
+
+`;
exports[`extensions - navigation using application menu when navigating to extensions using application menu renders 1`] = `
@@ -118,5 +124,8 @@ exports[`extensions - navigation using application menu when navigating to exten
+
`;
diff --git a/src/behaviours/extensions/navigation-using-application-menu.test.ts b/src/behaviours/extensions/navigation-using-application-menu.test.ts
index 75ffa0ebef..5d05ec31c2 100644
--- a/src/behaviours/extensions/navigation-using-application-menu.test.ts
+++ b/src/behaviours/extensions/navigation-using-application-menu.test.ts
@@ -6,12 +6,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 isAutoUpdateEnabledInjectable from "../../main/is-auto-update-enabled.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/file-system-provisioner-store/file-system-provisioner-store.injectable";
-import type { FileSystemProvisionerStore } from "../../extensions/extension-loader/file-system-provisioner-store/file-system-provisioner-store";
-import focusWindowInjectable from "../../renderer/ipc-channel-listeners/focus-window.injectable";
+import focusWindowInjectable from "../../renderer/navigation/focus-window.injectable";
// TODO: Make components free of side effects by making them deterministic
jest.mock("../../renderer/components/input/input");
@@ -22,11 +17,7 @@ describe("extensions - navigation using application menu", () => {
let focusWindowMock: jest.Mock;
beforeEach(async () => {
- applicationBuilder = getApplicationBuilder().beforeApplicationStart(({ mainDi, rendererDi }) => {
- mainDi.override(isAutoUpdateEnabledInjectable, () => () => false);
- rendererDi.override(extensionsStoreInjectable, () => ({}) as unknown as ExtensionsStore);
- rendererDi.override(fileSystemProvisionerStoreInjectable, () => ({}) as unknown as FileSystemProvisionerStore);
-
+ applicationBuilder = getApplicationBuilder().beforeApplicationStart(({ rendererDi }) => {
focusWindowMock = jest.fn();
rendererDi.override(focusWindowInjectable, () => focusWindowMock);
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
index 4f1555049d..e323205008 100644
--- 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
@@ -454,5 +454,8 @@ exports[`helm-charts - navigation to Helm charts when navigating to Helm charts
+
`;
diff --git a/src/behaviours/preferences/__snapshots__/closing-preferences.test.tsx.snap b/src/behaviours/preferences/__snapshots__/closing-preferences.test.tsx.snap
index cb4973203d..5cdee87f39 100644
--- a/src/behaviours/preferences/__snapshots__/closing-preferences.test.tsx.snap
+++ b/src/behaviours/preferences/__snapshots__/closing-preferences.test.tsx.snap
@@ -356,13 +356,12 @@ exports[`preferences - closing-preferences given accessing preferences directly
class="Select__control css-1s2u09g-control"
>
+
`;
@@ -1054,13 +1064,12 @@ exports[`preferences - closing-preferences given already in a page and then navi
class="Select__control css-1s2u09g-control"
>
- Select...
+ Stable
+
`;
@@ -1377,6 +1388,9 @@ exports[`preferences - closing-preferences given already in a page and then navi
+
`;
@@ -1519,6 +1533,9 @@ exports[`preferences - closing-preferences given already in a page and then navi
+
`;
@@ -1661,5 +1678,8 @@ exports[`preferences - closing-preferences given already in a page and then navi
+
`;
diff --git a/src/behaviours/preferences/__snapshots__/navigation-to-application-preferences.test.ts.snap b/src/behaviours/preferences/__snapshots__/navigation-to-application-preferences.test.ts.snap
index f67337e80c..7cd89769f5 100644
--- a/src/behaviours/preferences/__snapshots__/navigation-to-application-preferences.test.ts.snap
+++ b/src/behaviours/preferences/__snapshots__/navigation-to-application-preferences.test.ts.snap
@@ -199,6 +199,9 @@ exports[`preferences - navigation to application preferences given in some child
+
`;
@@ -546,13 +549,12 @@ exports[`preferences - navigation to application preferences given in some child
class="Select__control css-1s2u09g-control"
>
- Select...
+ Stable
+
`;
diff --git a/src/behaviours/preferences/__snapshots__/navigation-to-editor-preferences.test.ts.snap b/src/behaviours/preferences/__snapshots__/navigation-to-editor-preferences.test.ts.snap
index 4e92ac4f95..9f99faceb3 100644
--- a/src/behaviours/preferences/__snapshots__/navigation-to-editor-preferences.test.ts.snap
+++ b/src/behaviours/preferences/__snapshots__/navigation-to-editor-preferences.test.ts.snap
@@ -344,13 +344,12 @@ exports[`preferences - navigation to editor preferences given in preferences, wh
class="Select__control css-1s2u09g-control"
>
- Select...
+ Stable
+
`;
@@ -935,5 +936,8 @@ exports[`preferences - navigation to editor preferences given in preferences, wh
+
`;
diff --git a/src/behaviours/preferences/__snapshots__/navigation-to-extension-specific-preferences.test.tsx.snap b/src/behaviours/preferences/__snapshots__/navigation-to-extension-specific-preferences.test.tsx.snap
index d3f42e6d63..ec00c0e498 100644
--- a/src/behaviours/preferences/__snapshots__/navigation-to-extension-specific-preferences.test.tsx.snap
+++ b/src/behaviours/preferences/__snapshots__/navigation-to-extension-specific-preferences.test.tsx.snap
@@ -344,13 +344,12 @@ exports[`preferences - navigation to extension specific preferences given in pre
class="Select__control css-1s2u09g-control"
>
- Select...
+ Stable
+
`;
@@ -884,13 +885,12 @@ exports[`preferences - navigation to extension specific preferences given in pre
class="Select__control css-1s2u09g-control"
>
- Select...
+ Stable
+
`;
@@ -1239,5 +1241,8 @@ exports[`preferences - navigation to extension specific preferences given in pre
+
`;
diff --git a/src/behaviours/preferences/__snapshots__/navigation-to-kubernetes-preferences.test.ts.snap b/src/behaviours/preferences/__snapshots__/navigation-to-kubernetes-preferences.test.ts.snap
index 2e9b7722aa..8ab873fac4 100644
--- a/src/behaviours/preferences/__snapshots__/navigation-to-kubernetes-preferences.test.ts.snap
+++ b/src/behaviours/preferences/__snapshots__/navigation-to-kubernetes-preferences.test.ts.snap
@@ -344,13 +344,12 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
class="Select__control css-1s2u09g-control"
>
- Select...
+ Stable
+
`;
@@ -836,7 +837,7 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
class="flex gaps"
>
+
+
+
+
+
- The repositories have not been added yet
-
+ class="Spinner singleColor center"
+ />
@@ -969,5 +983,8 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
+
`;
diff --git a/src/behaviours/preferences/__snapshots__/navigation-to-proxy-preferences.test.ts.snap b/src/behaviours/preferences/__snapshots__/navigation-to-proxy-preferences.test.ts.snap
index 8c6507ef0a..4710bb1957 100644
--- a/src/behaviours/preferences/__snapshots__/navigation-to-proxy-preferences.test.ts.snap
+++ b/src/behaviours/preferences/__snapshots__/navigation-to-proxy-preferences.test.ts.snap
@@ -344,13 +344,12 @@ exports[`preferences - navigation to proxy preferences given in preferences, whe
class="Select__control css-1s2u09g-control"
>
- Select...
+ Stable
+
`;
@@ -727,5 +728,8 @@ exports[`preferences - navigation to proxy preferences given in preferences, whe
+
`;
diff --git a/src/behaviours/preferences/__snapshots__/navigation-to-telemetry-preferences.test.tsx.snap b/src/behaviours/preferences/__snapshots__/navigation-to-telemetry-preferences.test.tsx.snap
index 80f5b61bb1..68901d7a4d 100644
--- a/src/behaviours/preferences/__snapshots__/navigation-to-telemetry-preferences.test.tsx.snap
+++ b/src/behaviours/preferences/__snapshots__/navigation-to-telemetry-preferences.test.tsx.snap
@@ -185,6 +185,9 @@ exports[`preferences - navigation to telemetry preferences given URL for Sentry
+
`;
@@ -532,13 +535,12 @@ exports[`preferences - navigation to telemetry preferences given in preferences,
class="Select__control css-1s2u09g-control"
>
- Select...
+ Stable
+
`;
@@ -1072,13 +1076,12 @@ exports[`preferences - navigation to telemetry preferences given in preferences,
class="Select__control css-1s2u09g-control"
>
- Select...
+ Stable
+
`;
@@ -1429,6 +1434,9 @@ exports[`preferences - navigation to telemetry preferences given in preferences,
+
`;
@@ -1568,5 +1576,8 @@ exports[`preferences - navigation to telemetry preferences given no URL for Sent
+
`;
diff --git a/src/behaviours/preferences/__snapshots__/navigation-to-terminal-preferences.test.ts.snap b/src/behaviours/preferences/__snapshots__/navigation-to-terminal-preferences.test.ts.snap
index 5e5934c3bb..729309888e 100644
--- a/src/behaviours/preferences/__snapshots__/navigation-to-terminal-preferences.test.ts.snap
+++ b/src/behaviours/preferences/__snapshots__/navigation-to-terminal-preferences.test.ts.snap
@@ -344,13 +344,12 @@ exports[`preferences - navigation to terminal preferences given in preferences,
class="Select__control css-1s2u09g-control"
>
- Select...
+ Stable
+
`;
@@ -845,5 +846,8 @@ exports[`preferences - navigation to terminal preferences given in preferences,
+
`;
diff --git a/src/behaviours/preferences/__snapshots__/navigation-using-application-menu.test.ts.snap b/src/behaviours/preferences/__snapshots__/navigation-using-application-menu.test.ts.snap
index 141279f4e4..367ac869ba 100644
--- a/src/behaviours/preferences/__snapshots__/navigation-using-application-menu.test.ts.snap
+++ b/src/behaviours/preferences/__snapshots__/navigation-using-application-menu.test.ts.snap
@@ -1,6 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`preferences - navigation using application menu renders 1`] = ``;
+exports[`preferences - navigation using application menu renders 1`] = `
+
+
+
+`;
exports[`preferences - navigation using application menu when navigating to preferences using application menu renders 1`] = `
@@ -346,13 +352,12 @@ exports[`preferences - navigation using application menu when navigating to pref
class="Select__control css-1s2u09g-control"
>
+
+`;
+
+exports[`show-about-using-tray when navigating using tray renders 1`] = `
+
+
+
+
+
+
+
+
+ Application
+
+
+
+ Theme
+
+
+
+
+
+
+
+
+ Select...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Extension Install Registry
+
+
+
+
+
+
+
+
+ Select...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This setting is to change the registry URL for installing extensions by name.
+ If you are unable to access the default registry (https://registry.npmjs.org) you can change it in your
+
+ .npmrc
+
+ file or in the input below.
+
+
+
+
+
+
+
+
+
+ Start-up
+
+
+
+
+
+
+
+ Update Channel
+
+
+
+
+
+
+
+
+ Stable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Locale Timezone
+
+
+
+
+
+
+
+
+ Select...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ close
+
+
+
+
+ ESC
+
+
+
+
+
+
+
+
+
+`;
diff --git a/src/behaviours/preferences/navigation-to-terminal-preferences.test.ts b/src/behaviours/preferences/navigation-to-terminal-preferences.test.ts
index 5d7e1e08fb..73eff39006 100644
--- a/src/behaviours/preferences/navigation-to-terminal-preferences.test.ts
+++ b/src/behaviours/preferences/navigation-to-terminal-preferences.test.ts
@@ -5,17 +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 defaultShellInjectable from "../../renderer/components/+preferences/default-shell.injectable";
describe("preferences - navigation to terminal preferences", () => {
let applicationBuilder: ApplicationBuilder;
beforeEach(() => {
applicationBuilder = getApplicationBuilder();
-
- applicationBuilder.beforeApplicationStart(({ rendererDi }) => {
- rendererDi.override(defaultShellInjectable, () => "some-default-shell");
- });
});
describe("given in preferences, when rendered", () => {
diff --git a/src/behaviours/preferences/navigation-using-tray.test.ts b/src/behaviours/preferences/navigation-using-tray.test.ts
new file mode 100644
index 0000000000..065cc54f4b
--- /dev/null
+++ b/src/behaviours/preferences/navigation-using-tray.test.ts
@@ -0,0 +1,44 @@
+/**
+ * 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("show-about-using-tray", () => {
+ let applicationBuilder: ApplicationBuilder;
+ let rendered: RenderResult;
+
+ beforeEach(async () => {
+ applicationBuilder = getApplicationBuilder();
+
+ rendered = await applicationBuilder.render();
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("does not show application preferences page yet", () => {
+ const actual = rendered.queryByTestId("application-preferences-page");
+
+ expect(actual).toBeNull();
+ });
+
+ describe("when navigating using tray", () => {
+ beforeEach(async () => {
+ await applicationBuilder.tray.click("open-preferences");
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("shows application preferences page", () => {
+ const actual = rendered.getByTestId("application-preferences-page");
+
+ expect(actual).not.toBeNull();
+ });
+ });
+});
diff --git a/src/behaviours/welcome/__snapshots__/navigation-using-application-menu.test.ts.snap b/src/behaviours/welcome/__snapshots__/navigation-using-application-menu.test.ts.snap
index d59f7d040a..05eee498bf 100644
--- a/src/behaviours/welcome/__snapshots__/navigation-using-application-menu.test.ts.snap
+++ b/src/behaviours/welcome/__snapshots__/navigation-using-application-menu.test.ts.snap
@@ -1,6 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`welcome - navigation using application menu renders 1`] = ``;
+exports[`welcome - navigation using application menu renders 1`] = `
+
+
+
+`;
exports[`welcome - navigation using application menu when navigating to welcome using application menu renders 1`] = `
@@ -87,5 +93,8 @@ exports[`welcome - navigation using application menu when navigating to welcome
+
`;
diff --git a/src/behaviours/welcome/navigation-using-application-menu.test.ts b/src/behaviours/welcome/navigation-using-application-menu.test.ts
index e84e5b391f..a9f09c783c 100644
--- a/src/behaviours/welcome/navigation-using-application-menu.test.ts
+++ b/src/behaviours/welcome/navigation-using-application-menu.test.ts
@@ -6,16 +6,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 isAutoUpdateEnabledInjectable from "../../main/is-auto-update-enabled.injectable";
describe("welcome - navigation using application menu", () => {
let applicationBuilder: ApplicationBuilder;
let rendered: RenderResult;
beforeEach(async () => {
- applicationBuilder = getApplicationBuilder().beforeApplicationStart(({ mainDi }) => {
- mainDi.override(isAutoUpdateEnabledInjectable, () => () => false);
- });
+ applicationBuilder = getApplicationBuilder();
rendered = await applicationBuilder.render();
});
diff --git a/src/common/__tests__/cluster-store.test.ts b/src/common/__tests__/cluster-store.test.ts
index 82d7b97638..ca52f6f1d2 100644
--- a/src/common/__tests__/cluster-store.test.ts
+++ b/src/common/__tests__/cluster-store.test.ts
@@ -369,6 +369,8 @@ users:
mockFs(mockOpts);
+ mainDi.override(appVersionInjectable, () => "3.6.0");
+
createCluster = mainDi.inject(createClusterInjectionToken);
clusterStore = mainDi.inject(clusterStoreInjectable);
diff --git a/src/common/__tests__/user-store.test.ts b/src/common/__tests__/user-store.test.ts
index 042b6363f7..238fc5bce8 100644
--- a/src/common/__tests__/user-store.test.ts
+++ b/src/common/__tests__/user-store.test.ts
@@ -21,7 +21,7 @@ jest.mock("electron", () => ({
},
}));
-import { UserStore } from "../user-store";
+import type { UserStore } from "../user-store";
import { Console } from "console";
import { SemVer } from "semver";
import electron from "electron";
@@ -49,14 +49,15 @@ describe("user store tests", () => {
di.override(writeFileInjectable, () => () => Promise.resolve());
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
- di.override(userStoreInjectable, () => UserStore.createInstance());
-
di.permitSideEffects(getConfigurationFileModelInjectable);
+
di.permitSideEffects(appVersionInjectable);
+ di.permitSideEffects(userStoreInjectable);
+
+ di.unoverride(userStoreInjectable);
});
afterEach(() => {
- UserStore.resetInstance();
mockFs.restore();
});
@@ -126,6 +127,8 @@ describe("user store tests", () => {
},
});
+ di.override(appVersionInjectable, () => "10.0.0");
+
userStore = di.inject(userStoreInjectable);
});
diff --git a/src/common/app-paths/app-path-injection-token.ts b/src/common/app-paths/app-path-injection-token.ts
index 3b03e44daf..e29bcdbebf 100644
--- a/src/common/app-paths/app-path-injection-token.ts
+++ b/src/common/app-paths/app-path-injection-token.ts
@@ -4,12 +4,9 @@
*/
import { getInjectionToken } from "@ogre-tools/injectable";
import type { PathName } from "./app-path-names";
-import { createChannel } from "../ipc-channel/create-channel/create-channel";
export type AppPaths = Record;
export const appPathsInjectionToken = getInjectionToken({ id: "app-paths-token" });
-export const appPathsIpcChannel = createChannel("app-paths");
-
diff --git a/src/common/app-paths/app-paths-channel.injectable.ts b/src/common/app-paths/app-paths-channel.injectable.ts
new file mode 100644
index 0000000000..99fc738b41
--- /dev/null
+++ b/src/common/app-paths/app-paths-channel.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 type { AppPaths } from "./app-path-injection-token";
+import type { RequestChannel } from "../utils/channel/request-channel-injection-token";
+import { messageChannelInjectionToken } from "../utils/channel/message-channel-injection-token";
+
+export type AppPathsChannel = RequestChannel;
+
+const appPathsChannelInjectable = getInjectable({
+ id: "app-paths-channel",
+
+ instantiate: (): AppPathsChannel => ({
+ id: "app-paths",
+ }),
+
+ injectionToken: messageChannelInjectionToken,
+});
+
+export default appPathsChannelInjectable;
diff --git a/src/common/application-update/application-update-status-channel.injectable.ts b/src/common/application-update/application-update-status-channel.injectable.ts
new file mode 100644
index 0000000000..1365fd19af
--- /dev/null
+++ b/src/common/application-update/application-update-status-channel.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 type { MessageChannel } from "../utils/channel/message-channel-injection-token";
+import { messageChannelInjectionToken } from "../utils/channel/message-channel-injection-token";
+
+export type ApplicationUpdateStatusEventId =
+ | "checking-for-updates"
+ | "no-updates-available"
+ | "download-for-update-started"
+ | "download-for-update-failed";
+
+// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
+export type ApplicationUpdateStatusChannelMessage = { eventId: ApplicationUpdateStatusEventId; version?: string };
+export type ApplicationUpdateStatusChannel = MessageChannel;
+
+const applicationUpdateStatusChannelInjectable = getInjectable({
+ id: "application-update-status-channel",
+
+ instantiate: (): ApplicationUpdateStatusChannel => ({
+ id: "application-update-status-channel",
+ }),
+
+ injectionToken: messageChannelInjectionToken,
+});
+
+export default applicationUpdateStatusChannelInjectable;
diff --git a/src/common/application-update/discovered-update-version/discovered-update-version.injectable.ts b/src/common/application-update/discovered-update-version/discovered-update-version.injectable.ts
new file mode 100644
index 0000000000..60557de211
--- /dev/null
+++ b/src/common/application-update/discovered-update-version/discovered-update-version.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 createSyncBoxInjectable from "../../utils/sync-box/create-sync-box.injectable";
+import type { UpdateChannel } from "../update-channels";
+import { syncBoxInjectionToken } from "../../utils/sync-box/sync-box-injection-token";
+
+const discoveredUpdateVersionInjectable = getInjectable({
+ id: "discovered-update-version",
+
+ instantiate: (di) => {
+ const createSyncBox = di.inject(createSyncBoxInjectable);
+
+ return createSyncBox<
+ | { version: string; updateChannel: UpdateChannel }
+ | null
+ >(
+ "discovered-update-version",
+ null,
+ );
+ },
+
+ injectionToken: syncBoxInjectionToken,
+});
+
+export default discoveredUpdateVersionInjectable;
diff --git a/src/common/application-update/progress-of-update-download/progress-of-update-download.injectable.ts b/src/common/application-update/progress-of-update-download/progress-of-update-download.injectable.ts
new file mode 100644
index 0000000000..26ecd1d618
--- /dev/null
+++ b/src/common/application-update/progress-of-update-download/progress-of-update-download.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 createSyncBoxInjectable from "../../utils/sync-box/create-sync-box.injectable";
+import { syncBoxInjectionToken } from "../../utils/sync-box/sync-box-injection-token";
+
+export interface ProgressOfDownload {
+ percentage: number;
+}
+
+const progressOfUpdateDownloadInjectable = getInjectable({
+ id: "progress-of-update-download-state",
+
+ instantiate: (di) => {
+ const createSyncBox = di.inject(createSyncBoxInjectable);
+
+ return createSyncBox("progress-of-update-download", { percentage: 0 });
+ },
+
+ injectionToken: syncBoxInjectionToken,
+});
+
+export default progressOfUpdateDownloadInjectable;
diff --git a/src/common/application-update/selected-update-channel/default-update-channel.injectable.ts b/src/common/application-update/selected-update-channel/default-update-channel.injectable.ts
new file mode 100644
index 0000000000..3d9101b672
--- /dev/null
+++ b/src/common/application-update/selected-update-channel/default-update-channel.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 { SemVer } from "semver";
+import appVersionInjectable from "../../get-configuration-file-model/app-version/app-version.injectable";
+import type { UpdateChannelId } from "../update-channels";
+import { updateChannels } from "../update-channels";
+
+const defaultUpdateChannelInjectable = getInjectable({
+ id: "default-update-channel",
+
+ instantiate: (di) => {
+ const appVersion = di.inject(appVersionInjectable);
+
+ const currentReleaseChannel = new SemVer(appVersion).prerelease[0]?.toString() as UpdateChannelId;
+
+ if (currentReleaseChannel && updateChannels[currentReleaseChannel]) {
+ return updateChannels[currentReleaseChannel];
+ }
+
+ return updateChannels.latest;
+ },
+});
+
+export default defaultUpdateChannelInjectable;
diff --git a/src/common/application-update/selected-update-channel/selected-update-channel.injectable.ts b/src/common/application-update/selected-update-channel/selected-update-channel.injectable.ts
new file mode 100644
index 0000000000..ceb47aee5e
--- /dev/null
+++ b/src/common/application-update/selected-update-channel/selected-update-channel.injectable.ts
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getInjectable } from "@ogre-tools/injectable";
+import type { IComputedValue } from "mobx";
+import { action, computed, observable } from "mobx";
+import type { UpdateChannel, UpdateChannelId } from "../update-channels";
+import { updateChannels } from "../update-channels";
+import defaultUpdateChannelInjectable from "./default-update-channel.injectable";
+
+export interface SelectedUpdateChannel {
+ value: IComputedValue;
+ setValue: (channelId?: UpdateChannelId) => void;
+}
+
+const selectedUpdateChannelInjectable = getInjectable({
+ id: "selected-update-channel",
+
+ instantiate: (di): SelectedUpdateChannel => {
+ const defaultUpdateChannel = di.inject(defaultUpdateChannelInjectable);
+ const state = observable.box(defaultUpdateChannel);
+
+ return {
+ value: computed(() => state.get()),
+
+ setValue: action((channelId) => {
+ const targetUpdateChannel =
+ channelId && updateChannels[channelId]
+ ? updateChannels[channelId]
+ : defaultUpdateChannel;
+
+ state.set(targetUpdateChannel);
+ }),
+ };
+ },
+});
+
+export default selectedUpdateChannelInjectable;
diff --git a/src/common/application-update/update-channels.ts b/src/common/application-update/update-channels.ts
new file mode 100644
index 0000000000..c5f7b4b8c1
--- /dev/null
+++ b/src/common/application-update/update-channels.ts
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+
+export type UpdateChannelId = "alpha" | "beta" | "latest";
+
+const latestChannel: UpdateChannel = {
+ id: "latest",
+ label: "Stable",
+ moreStableUpdateChannel: null,
+};
+
+const betaChannel: UpdateChannel = {
+ id: "beta",
+ label: "Beta",
+ moreStableUpdateChannel: latestChannel,
+};
+
+const alphaChannel: UpdateChannel = {
+ id: "alpha",
+ label: "Alpha",
+ moreStableUpdateChannel: betaChannel,
+};
+
+export const updateChannels: Record = {
+ latest: latestChannel,
+ beta: betaChannel,
+ alpha: alphaChannel,
+};
+
+export interface UpdateChannel {
+ readonly id: UpdateChannelId;
+ readonly label: string;
+ readonly moreStableUpdateChannel: UpdateChannel | null;
+}
diff --git a/src/common/application-update/update-is-being-downloaded/update-is-being-downloaded.injectable.ts b/src/common/application-update/update-is-being-downloaded/update-is-being-downloaded.injectable.ts
new file mode 100644
index 0000000000..e1701d7952
--- /dev/null
+++ b/src/common/application-update/update-is-being-downloaded/update-is-being-downloaded.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 createSyncBoxInjectable from "../../utils/sync-box/create-sync-box.injectable";
+import { syncBoxInjectionToken } from "../../utils/sync-box/sync-box-injection-token";
+
+const updateIsBeingDownloadedInjectable = getInjectable({
+ id: "update-is-being-downloaded",
+
+ instantiate: (di) => {
+ const createSyncBox = di.inject(createSyncBoxInjectable);
+
+ return createSyncBox("update-is-being-downloaded", false);
+ },
+
+ injectionToken: syncBoxInjectionToken,
+});
+
+export default updateIsBeingDownloadedInjectable;
diff --git a/src/common/application-update/updates-are-being-discovered/updates-are-being-discovered.injectable.ts b/src/common/application-update/updates-are-being-discovered/updates-are-being-discovered.injectable.ts
new file mode 100644
index 0000000000..21f1c14bec
--- /dev/null
+++ b/src/common/application-update/updates-are-being-discovered/updates-are-being-discovered.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 createSyncBoxInjectable from "../../utils/sync-box/create-sync-box.injectable";
+import { syncBoxInjectionToken } from "../../utils/sync-box/sync-box-injection-token";
+
+const updatesAreBeingDiscoveredInjectable = getInjectable({
+ id: "updates-are-being-discovered",
+
+ instantiate: (di) => {
+ const createSyncBox = di.inject(createSyncBoxInjectable);
+
+ return createSyncBox("updates-are-being-discovered", false);
+ },
+
+ injectionToken: syncBoxInjectionToken,
+});
+
+export default updatesAreBeingDiscoveredInjectable;
diff --git a/src/common/ask-boolean/ask-boolean-answer-channel.injectable.ts b/src/common/ask-boolean/ask-boolean-answer-channel.injectable.ts
new file mode 100644
index 0000000000..9901c04e30
--- /dev/null
+++ b/src/common/ask-boolean/ask-boolean-answer-channel.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 type { MessageChannel } from "../utils/channel/message-channel-injection-token";
+import { messageChannelInjectionToken } from "../utils/channel/message-channel-injection-token";
+
+export type AskBooleanAnswerChannel = MessageChannel<{ id: string; value: boolean }>;
+
+const askBooleanAnswerChannelInjectable = getInjectable({
+ id: "ask-boolean-answer-channel",
+
+ instantiate: (): AskBooleanAnswerChannel => ({
+ id: "ask-boolean-answer",
+ }),
+
+ injectionToken: messageChannelInjectionToken,
+});
+
+export default askBooleanAnswerChannelInjectable;
diff --git a/src/common/ask-boolean/ask-boolean-question-channel.injectable.ts b/src/common/ask-boolean/ask-boolean-question-channel.injectable.ts
new file mode 100644
index 0000000000..664337158f
--- /dev/null
+++ b/src/common/ask-boolean/ask-boolean-question-channel.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 { getInjectable } from "@ogre-tools/injectable";
+import type { MessageChannel } from "../utils/channel/message-channel-injection-token";
+import { messageChannelInjectionToken } from "../utils/channel/message-channel-injection-token";
+
+// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
+export type AskBooleanQuestionParameters = { id: string; title: string; question: string };
+export type AskBooleanQuestionChannel = MessageChannel;
+
+const askBooleanQuestionChannelInjectable = getInjectable({
+ id: "ask-boolean-question-channel",
+
+ instantiate: (): AskBooleanQuestionChannel => ({
+ id: "ask-boolean-question",
+ }),
+
+ injectionToken: messageChannelInjectionToken,
+});
+
+export default askBooleanQuestionChannelInjectable;
diff --git a/src/common/front-end-routing/app-navigation-channel.injectable.ts b/src/common/front-end-routing/app-navigation-channel.injectable.ts
new file mode 100644
index 0000000000..869fbfdecd
--- /dev/null
+++ b/src/common/front-end-routing/app-navigation-channel.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 { IpcRendererNavigationEvents } from "../../renderer/navigation/events";
+import type { MessageChannel } from "../utils/channel/message-channel-injection-token";
+import { messageChannelInjectionToken } from "../utils/channel/message-channel-injection-token";
+
+export type AppNavigationChannel = MessageChannel;
+
+const appNavigationChannelInjectable = getInjectable({
+ id: "app-navigation-channel",
+
+ instantiate: (): AppNavigationChannel => ({
+ id: IpcRendererNavigationEvents.NAVIGATE_IN_APP,
+ }),
+
+ injectionToken: messageChannelInjectionToken,
+});
+
+export default appNavigationChannelInjectable;
diff --git a/src/common/front-end-routing/cluster-frame-navigation-channel.injectable.ts b/src/common/front-end-routing/cluster-frame-navigation-channel.injectable.ts
new file mode 100644
index 0000000000..596bd6d351
--- /dev/null
+++ b/src/common/front-end-routing/cluster-frame-navigation-channel.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 { IpcRendererNavigationEvents } from "../../renderer/navigation/events";
+import type { MessageChannel } from "../utils/channel/message-channel-injection-token";
+import { messageChannelInjectionToken } from "../utils/channel/message-channel-injection-token";
+
+export type ClusterFrameNavigationChannel = MessageChannel;
+
+const clusterFrameNavigationChannelInjectable = getInjectable({
+ id: "cluster-frame-navigation-channel",
+
+ instantiate: (): ClusterFrameNavigationChannel => ({
+ id: IpcRendererNavigationEvents.NAVIGATE_IN_CLUSTER,
+ }),
+
+ injectionToken: messageChannelInjectionToken,
+});
+
+export default clusterFrameNavigationChannelInjectable;
diff --git a/src/common/front-end-routing/navigation-ipc-channel.ts b/src/common/front-end-routing/navigation-ipc-channel.ts
deleted file mode 100644
index 6094664f81..0000000000
--- a/src/common/front-end-routing/navigation-ipc-channel.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-/**
- * Copyright (c) OpenLens Authors. All rights reserved.
- * Licensed under MIT License. See LICENSE in root directory for more information.
- */
-import { createChannel } from "../ipc-channel/create-channel/create-channel";
-import { IpcRendererNavigationEvents } from "../../renderer/navigation/events";
-
-export const appNavigationIpcChannel = createChannel(IpcRendererNavigationEvents.NAVIGATE_IN_APP);
-export const clusterFrameNavigationIpcChannel = createChannel(IpcRendererNavigationEvents.NAVIGATE_IN_CLUSTER);
diff --git a/src/common/get-configuration-file-model/app-version/app-version.injectable.ts b/src/common/get-configuration-file-model/app-version/app-version.injectable.ts
index 0fe3142332..5fdfd30eba 100644
--- a/src/common/get-configuration-file-model/app-version/app-version.injectable.ts
+++ b/src/common/get-configuration-file-model/app-version/app-version.injectable.ts
@@ -3,12 +3,11 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
-import packageInfo from "../../../../package.json";
+import packageJsonInjectable from "../../vars/package-json.injectable";
const appVersionInjectable = getInjectable({
id: "app-version",
- instantiate: () => packageInfo.version,
- causesSideEffects: true,
+ instantiate: (di) => di.inject(packageJsonInjectable).version,
});
export default appVersionInjectable;
diff --git a/src/common/ipc-channel/create-channel/create-channel.ts b/src/common/ipc-channel/create-channel/create-channel.ts
deleted file mode 100644
index 6b9fe1b0d9..0000000000
--- a/src/common/ipc-channel/create-channel/create-channel.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-/**
- * Copyright (c) OpenLens Authors. All rights reserved.
- * Licensed under MIT License. See LICENSE in root directory for more information.
- */
-import type { Channel } from "../channel";
-
-export const createChannel = (name: string): Channel => ({
- name,
- _template: null as never,
-});
diff --git a/src/common/ipc/index.ts b/src/common/ipc/index.ts
index 60ae46438e..bb60ce4f6c 100644
--- a/src/common/ipc/index.ts
+++ b/src/common/ipc/index.ts
@@ -5,5 +5,4 @@
export * from "./ipc";
export * from "./invalid-kubeconfig";
-export * from "./update-available";
export * from "./type-enforced-ipc";
diff --git a/src/common/ipc/update-available.ts b/src/common/ipc/update-available.ts
deleted file mode 100644
index ed5b18b13d..0000000000
--- a/src/common/ipc/update-available.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-/**
- * Copyright (c) OpenLens Authors. All rights reserved.
- * Licensed under MIT License. See LICENSE in root directory for more information.
- */
-
-import type { UpdateInfo } from "electron-updater";
-
-export const UpdateAvailableChannel = "update-available";
-export const AutoUpdateChecking = "auto-update:checking";
-export const AutoUpdateNoUpdateAvailable = "auto-update:no-update";
-export const AutoUpdateLogPrefix = "[UPDATE-CHECKER]";
-
-export type UpdateAvailableFromMain = [backChannel: string, updateInfo: UpdateInfo];
-
-export function areArgsUpdateAvailableFromMain(args: unknown[]): args is UpdateAvailableFromMain {
- if (args.length !== 2) {
- return false;
- }
-
- if (typeof args[0] !== "string") {
- return false;
- }
-
- if (typeof args[1] !== "object" || args[1] === null) {
- // TODO: improve this checking
- return false;
- }
-
- return true;
-}
-
-export type BackchannelArg = {
- doUpdate: false;
-} | {
- doUpdate: true;
- now: boolean;
-};
-
-export type UpdateAvailableToBackchannel = [updateDecision: BackchannelArg];
-
-export function areArgsUpdateAvailableToBackchannel(args: unknown[]): args is UpdateAvailableToBackchannel {
- if (args.length !== 1) {
- return false;
- }
-
- if (typeof args[0] !== "object" || args[0] === null) {
- // TODO: improve this checking
- return false;
- }
-
- return true;
-}
diff --git a/src/common/root-frame-rendered-channel/root-frame-rendered-channel.injectable.ts b/src/common/root-frame-rendered-channel/root-frame-rendered-channel.injectable.ts
new file mode 100644
index 0000000000..a7787c6cc4
--- /dev/null
+++ b/src/common/root-frame-rendered-channel/root-frame-rendered-channel.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 type { MessageChannel } from "../utils/channel/message-channel-injection-token";
+import { messageChannelInjectionToken } from "../utils/channel/message-channel-injection-token";
+
+export type RootFrameRenderedChannel = MessageChannel;
+
+const rootFrameRenderedChannelInjectable = getInjectable({
+ id: "root-frame-rendered-channel",
+
+ instantiate: (): RootFrameRenderedChannel => ({
+ id: "root-frame-rendered",
+ }),
+
+ injectionToken: messageChannelInjectionToken,
+});
+
+export default rootFrameRenderedChannelInjectable;
diff --git a/src/common/user-store/preferences-helpers.ts b/src/common/user-store/preferences-helpers.ts
index 7bb3aee41b..ed3fb7c249 100644
--- a/src/common/user-store/preferences-helpers.ts
+++ b/src/common/user-store/preferences-helpers.ts
@@ -6,14 +6,11 @@
import moment from "moment-timezone";
import path from "path";
import os from "os";
-import { getAppVersion } from "../utils";
import type { editor } from "monaco-editor";
import merge from "lodash/merge";
-import { SemVer } from "semver";
import { defaultThemeId, defaultEditorFontFamily, defaultFontSize, defaultTerminalFontFamily } from "../vars";
import type { ObservableMap } from "mobx";
import { observable } from "mobx";
-import { readonly } from "../utils/readonly";
export interface KubeconfigSyncEntry extends KubeconfigSyncValue {
filePath: string;
@@ -296,38 +293,6 @@ const terminalConfig: PreferenceDescription = {
},
};
-export interface UpdateChannelInfo {
- label: string;
-}
-
-export const updateChannels = readonly(new Map([
- ["latest", {
- label: "Stable",
- }],
- ["beta", {
- label: "Beta",
- }],
- ["alpha", {
- label: "Alpha",
- }],
-]));
-export const defaultUpdateChannel = new SemVer(getAppVersion()).prerelease[0]?.toString() || "latest";
-
-const updateChannel: PreferenceDescription = {
- fromStore(val) {
- return !val || !updateChannels.has(val)
- ? defaultUpdateChannel
- : val;
- },
- toStore(val) {
- if (!updateChannels.has(val) || val === defaultUpdateChannel) {
- return undefined;
- }
-
- return val;
- },
-};
-
export type ExtensionRegistryLocation = "default" | "npmrc" | "custom";
export type ExtensionRegistry = {
@@ -365,7 +330,7 @@ export type UserStoreFlatModel = {
export type UserPreferencesModel = {
[field in keyof typeof DESCRIPTORS]: PreferencesModelType;
-};
+} & { updateChannel: string };
export const DESCRIPTORS = {
httpsProxy,
@@ -385,6 +350,5 @@ export const DESCRIPTORS = {
editorConfiguration,
terminalCopyOnSelect,
terminalConfig,
- updateChannel,
extensionRegistryUrl,
};
diff --git a/src/common/user-store/user-store.injectable.ts b/src/common/user-store/user-store.injectable.ts
index cd44cc60e5..3b4aba0b56 100644
--- a/src/common/user-store/user-store.injectable.ts
+++ b/src/common/user-store/user-store.injectable.ts
@@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import { ipcMain } from "electron";
import userStoreFileNameMigrationInjectable from "./file-name-migration.injectable";
import { UserStore } from "./user-store";
+import selectedUpdateChannelInjectable from "../application-update/selected-update-channel/selected-update-channel.injectable";
const userStoreInjectable = getInjectable({
id: "user-store",
@@ -17,7 +18,9 @@ const userStoreInjectable = getInjectable({
di.inject(userStoreFileNameMigrationInjectable);
}
- return UserStore.createInstance();
+ return UserStore.createInstance({
+ selectedUpdateChannel: di.inject(selectedUpdateChannelInjectable),
+ });
},
causesSideEffects: true,
diff --git a/src/common/user-store/user-store.ts b/src/common/user-store/user-store.ts
index 3cfd551fd7..b806732735 100644
--- a/src/common/user-store/user-store.ts
+++ b/src/common/user-store/user-store.ts
@@ -4,7 +4,7 @@
*/
import { app } from "electron";
-import semver, { SemVer } from "semver";
+import semver from "semver";
import { action, computed, observable, reaction, makeObservable, isObservableArray, isObservableSet, isObservableMap } from "mobx";
import { BaseStore } from "../base-store";
import migrations from "../../migrations/user-store";
@@ -15,15 +15,22 @@ import { getOrInsertSet, toggle, toJS, object } from "../../renderer/utils";
import { DESCRIPTORS } from "./preferences-helpers";
import type { UserPreferencesModel, StoreType } from "./preferences-helpers";
import logger from "../../main/logger";
+import type { SelectedUpdateChannel } from "../application-update/selected-update-channel/selected-update-channel.injectable";
+import type { UpdateChannelId } from "../application-update/update-channels";
export interface UserStoreModel {
lastSeenAppVersion: string;
preferences: UserPreferencesModel;
}
+interface Dependencies {
+ selectedUpdateChannel: SelectedUpdateChannel;
+}
+
export class UserStore extends BaseStore /* implements UserStoreFlatModel (when strict null is enabled) */ {
readonly displayName = "UserStore";
- constructor() {
+
+ constructor(private readonly dependencies: Dependencies) {
super({
configName: "lens-user-store",
migrations,
@@ -63,7 +70,6 @@ export class UserStore extends BaseStore /* implements UserStore
@observable kubectlBinariesPath!: StoreType;
@observable terminalCopyOnSelect!: StoreType;
@observable terminalConfig!: StoreType;
- @observable updateChannel!: StoreType;
@observable extensionRegistryUrl!: StoreType;
/**
@@ -100,10 +106,6 @@ export class UserStore extends BaseStore /* implements UserStore
return this.shell || process.env.SHELL || process.env.PTYSHELL;
}
- @computed get isAllowedToDowngrade() {
- return new SemVer(getAppVersion()).prerelease[0] !== this.updateChannel;
- }
-
startMainReactions() {
// open at system start-up
reaction(() => this.openAtLogin, openAtLogin => {
@@ -175,6 +177,11 @@ export class UserStore extends BaseStore /* implements UserStore
this[key] = newVal;
}
}
+
+ // TODO: Switch to action-based saving instead saving stores by reaction
+ if (preferences?.updateChannel) {
+ this.dependencies.selectedUpdateChannel.setValue(preferences?.updateChannel as UpdateChannelId);
+ }
}
toJSON(): UserStoreModel {
@@ -185,7 +192,12 @@ export class UserStore extends BaseStore /* implements UserStore
return toJS({
lastSeenAppVersion: this.lastSeenAppVersion,
- preferences,
+
+ preferences: {
+ ...preferences,
+
+ updateChannel: this.dependencies.selectedUpdateChannel.value.get().id,
+ },
});
}
}
diff --git a/src/common/utils/channel/channel-injection-token.ts b/src/common/utils/channel/channel-injection-token.ts
new file mode 100644
index 0000000000..6006290f89
--- /dev/null
+++ b/src/common/utils/channel/channel-injection-token.ts
@@ -0,0 +1,12 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+
+
+export interface Channel {
+ id: string;
+ _messageTemplate?: MessageTemplate;
+ _returnTemplate?: ReturnTemplate;
+}
+
diff --git a/src/common/utils/channel/channel.test.ts b/src/common/utils/channel/channel.test.ts
new file mode 100644
index 0000000000..f2748104d7
--- /dev/null
+++ b/src/common/utils/channel/channel.test.ts
@@ -0,0 +1,273 @@
+/**
+ * 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 { getInjectable } from "@ogre-tools/injectable";
+import type { LensWindow } from "../../../main/start-main-application/lens-window/application-window/lens-window-injection-token";
+import { lensWindowInjectionToken } from "../../../main/start-main-application/lens-window/application-window/lens-window-injection-token";
+import type { MessageToChannel } from "./message-to-channel-injection-token";
+import { messageToChannelInjectionToken } from "./message-to-channel-injection-token";
+import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
+import createLensWindowInjectable from "../../../main/start-main-application/lens-window/application-window/create-lens-window.injectable";
+import closeAllWindowsInjectable from "../../../main/start-main-application/lens-window/hide-all-windows/close-all-windows.injectable";
+import { messageChannelListenerInjectionToken } from "./message-channel-listener-injection-token";
+import type { MessageChannel } from "./message-channel-injection-token";
+import type { RequestFromChannel } from "./request-from-channel-injection-token";
+import { requestFromChannelInjectionToken } from "./request-from-channel-injection-token";
+import type { RequestChannel } from "./request-channel-injection-token";
+import { requestChannelListenerInjectionToken } from "./request-channel-listener-injection-token";
+import type { AsyncFnMock } from "@async-fn/jest";
+import asyncFn from "@async-fn/jest";
+import { getPromiseStatus } from "../../test-utils/get-promise-status";
+
+type TestMessageChannel = MessageChannel;
+type TestRequestChannel = RequestChannel;
+
+describe("channel", () => {
+ describe("messaging from main to renderer, given listener for channel in a window and application has started", () => {
+ let testMessageChannel: TestMessageChannel;
+ let messageListenerInWindowMock: jest.Mock;
+ let mainDi: DiContainer;
+ let messageToChannel: MessageToChannel;
+
+ beforeEach(async () => {
+ const applicationBuilder = getApplicationBuilder();
+
+ mainDi = applicationBuilder.dis.mainDi;
+ const rendererDi = applicationBuilder.dis.rendererDi;
+
+ messageListenerInWindowMock = jest.fn();
+
+ const testChannelListenerInTestWindowInjectable = getInjectable({
+ id: "test-channel-listener-in-test-window",
+
+ instantiate: (di) => ({
+ channel: di.inject(testMessageChannelInjectable),
+
+ handler: messageListenerInWindowMock,
+ }),
+
+ injectionToken: messageChannelListenerInjectionToken,
+ });
+
+ rendererDi.register(testChannelListenerInTestWindowInjectable);
+
+ // Notice how test channel has presence in both DIs, being from common
+ mainDi.register(testMessageChannelInjectable);
+ rendererDi.register(testMessageChannelInjectable);
+
+ testMessageChannel = mainDi.inject(testMessageChannelInjectable);
+
+ messageToChannel = mainDi.inject(
+ messageToChannelInjectionToken,
+ );
+
+ await applicationBuilder.render();
+
+ const closeAllWindows = mainDi.inject(closeAllWindowsInjectable);
+
+ closeAllWindows();
+ });
+
+ describe("given window is shown", () => {
+ let someWindowFake: LensWindow;
+
+ beforeEach(async () => {
+ someWindowFake = createTestWindow(mainDi, "some-window");
+
+ await someWindowFake.show();
+ });
+
+ it("when sending message, triggers listener in window", () => {
+ messageToChannel(testMessageChannel, "some-message");
+
+ expect(messageListenerInWindowMock).toHaveBeenCalledWith("some-message");
+ });
+
+ it("given window is hidden, when sending message, does not trigger listener in window", () => {
+ someWindowFake.close();
+
+ messageToChannel(testMessageChannel, "some-message");
+
+ expect(messageListenerInWindowMock).not.toHaveBeenCalled();
+ });
+ });
+
+ it("given multiple shown windows, when sending message, triggers listeners in all windows", async () => {
+ const someWindowFake = createTestWindow(mainDi, "some-window");
+ const someOtherWindowFake = createTestWindow(mainDi, "some-other-window");
+
+ await someWindowFake.show();
+ await someOtherWindowFake.show();
+
+ messageToChannel(testMessageChannel, "some-message");
+
+ expect(messageListenerInWindowMock.mock.calls).toEqual([
+ ["some-message"],
+ ["some-message"],
+ ]);
+ });
+ });
+
+ describe("messaging from renderer to main, given listener for channel in a main and application has started", () => {
+ let testMessageChannel: TestMessageChannel;
+ let messageListenerInMainMock: jest.Mock;
+ let rendererDi: DiContainer;
+ let mainDi: DiContainer;
+ let messageToChannel: MessageToChannel;
+
+ beforeEach(async () => {
+ const applicationBuilder = getApplicationBuilder();
+
+ mainDi = applicationBuilder.dis.mainDi;
+ rendererDi = applicationBuilder.dis.rendererDi;
+
+ messageListenerInMainMock = jest.fn();
+
+ const testChannelListenerInMainInjectable = getInjectable({
+ id: "test-channel-listener-in-main",
+
+ instantiate: (di) => ({
+ channel: di.inject(testMessageChannelInjectable),
+
+ handler: messageListenerInMainMock,
+ }),
+
+ injectionToken: messageChannelListenerInjectionToken,
+ });
+
+ mainDi.register(testChannelListenerInMainInjectable);
+
+ // Notice how test channel has presence in both DIs, being from common
+ mainDi.register(testMessageChannelInjectable);
+ rendererDi.register(testMessageChannelInjectable);
+
+ testMessageChannel = rendererDi.inject(testMessageChannelInjectable);
+
+ messageToChannel = rendererDi.inject(
+ messageToChannelInjectionToken,
+ );
+
+ await applicationBuilder.render();
+ });
+
+ it("when sending message, triggers listener in main", () => {
+ messageToChannel(testMessageChannel, "some-message");
+
+ expect(messageListenerInMainMock).toHaveBeenCalledWith("some-message");
+ });
+ });
+
+ describe("requesting from main in renderer, given listener for channel in a main and application has started", () => {
+ let testRequestChannel: TestRequestChannel;
+ let requestListenerInMainMock: AsyncFnMock<(arg: string) => string>;
+ let rendererDi: DiContainer;
+ let mainDi: DiContainer;
+ let requestFromChannel: RequestFromChannel;
+
+ beforeEach(async () => {
+ const applicationBuilder = getApplicationBuilder();
+
+ mainDi = applicationBuilder.dis.mainDi;
+ rendererDi = applicationBuilder.dis.rendererDi;
+
+ requestListenerInMainMock = asyncFn();
+
+ const testChannelListenerInMainInjectable = getInjectable({
+ id: "test-channel-listener-in-main",
+
+ instantiate: (di) => ({
+ channel: di.inject(testRequestChannelInjectable),
+
+ handler: requestListenerInMainMock,
+ }),
+
+ injectionToken: requestChannelListenerInjectionToken,
+ });
+
+ mainDi.register(testChannelListenerInMainInjectable);
+
+ // Notice how test channel has presence in both DIs, being from common
+ mainDi.register(testRequestChannelInjectable);
+ rendererDi.register(testRequestChannelInjectable);
+
+ testRequestChannel = rendererDi.inject(testRequestChannelInjectable);
+
+ requestFromChannel = rendererDi.inject(
+ requestFromChannelInjectionToken,
+ );
+
+ await applicationBuilder.render();
+ });
+
+ describe("when requesting from channel", () => {
+ let actualPromise: Promise;
+
+ beforeEach(() => {
+ actualPromise = requestFromChannel(testRequestChannel, "some-request");
+ });
+
+ it("triggers listener in main", () => {
+ expect(requestListenerInMainMock).toHaveBeenCalledWith("some-request");
+ });
+
+ it("does not resolve yet", async () => {
+ const promiseStatus = await getPromiseStatus(actualPromise);
+
+ expect(promiseStatus.fulfilled).toBe(false);
+ });
+
+ it("when main resolves with response, resolves with response", async () => {
+ await requestListenerInMainMock.resolve("some-response");
+
+ const actual = await actualPromise;
+
+ expect(actual).toBe("some-response");
+ });
+ });
+ });
+});
+
+const testMessageChannelInjectable = getInjectable({
+ id: "some-message-test-channel",
+
+ instantiate: (): TestMessageChannel => ({
+ id: "some-message-channel-id",
+ }),
+});
+
+const testRequestChannelInjectable = getInjectable({
+ id: "some-request-test-channel",
+
+ instantiate: (): TestRequestChannel => ({
+ id: "some-request-channel-id",
+ }),
+});
+
+const createTestWindow = (di: DiContainer, id: string) => {
+ const testWindowInjectable = getInjectable({
+ id,
+
+ instantiate: (di) => {
+ const createLensWindow = di.inject(createLensWindowInjectable);
+
+ return createLensWindow({
+ id,
+ title: "Some test window",
+ defaultHeight: 42,
+ defaultWidth: 42,
+ getContentSource: () => ({ url: "some-content-url" }),
+ resizable: true,
+ windowFrameUtilitiesAreShown: false,
+ centered: false,
+ });
+ },
+
+ injectionToken: lensWindowInjectionToken,
+ });
+
+ di.register(testWindowInjectable);
+
+ return di.inject(testWindowInjectable);
+};
diff --git a/src/common/utils/channel/enlist-message-channel-listener-injection-token.ts b/src/common/utils/channel/enlist-message-channel-listener-injection-token.ts
new file mode 100644
index 0000000000..fa6983e130
--- /dev/null
+++ b/src/common/utils/channel/enlist-message-channel-listener-injection-token.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 { getInjectionToken } from "@ogre-tools/injectable";
+import type { MessageChannel } from "./message-channel-injection-token";
+import type { MessageChannelListener } from "./message-channel-listener-injection-token";
+
+export type EnlistMessageChannelListener = <
+ TChannel extends MessageChannel,
+>(listener: MessageChannelListener) => () => void;
+
+export const enlistMessageChannelListenerInjectionToken =
+ getInjectionToken({
+ id: "enlist-message-channel-listener",
+ });
diff --git a/src/common/utils/channel/enlist-request-channel-listener-injection-token.ts b/src/common/utils/channel/enlist-request-channel-listener-injection-token.ts
new file mode 100644
index 0000000000..f87082c466
--- /dev/null
+++ b/src/common/utils/channel/enlist-request-channel-listener-injection-token.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 { getInjectionToken } from "@ogre-tools/injectable";
+import type { RequestChannel } from "./request-channel-injection-token";
+import type { RequestChannelListener } from "./request-channel-listener-injection-token";
+
+export type EnlistRequestChannelListener = <
+ TChannel extends RequestChannel,
+>(listener: RequestChannelListener) => () => void;
+
+export const enlistRequestChannelListenerInjectionToken =
+ getInjectionToken({
+ id: "enlist-request-channel-listener",
+ });
diff --git a/src/common/utils/channel/listening-of-channels.injectable.ts b/src/common/utils/channel/listening-of-channels.injectable.ts
new file mode 100644
index 0000000000..30fee42fb9
--- /dev/null
+++ b/src/common/utils/channel/listening-of-channels.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 { getStartableStoppable } from "../get-startable-stoppable";
+import { disposer } from "../index";
+import { messageChannelListenerInjectionToken } from "./message-channel-listener-injection-token";
+import { requestChannelListenerInjectionToken } from "./request-channel-listener-injection-token";
+import { enlistMessageChannelListenerInjectionToken } from "./enlist-message-channel-listener-injection-token";
+import { enlistRequestChannelListenerInjectionToken } from "./enlist-request-channel-listener-injection-token";
+
+const listeningOfChannelsInjectable = getInjectable({
+ id: "listening-of-channels",
+
+ instantiate: (di) => {
+ const enlistMessageChannelListener = di.inject(enlistMessageChannelListenerInjectionToken);
+ const enlistRequestChannelListener = di.inject(enlistRequestChannelListenerInjectionToken);
+ const messageChannelListeners = di.injectMany(messageChannelListenerInjectionToken);
+ const requestChannelListeners = di.injectMany(requestChannelListenerInjectionToken);
+
+ return getStartableStoppable("listening-of-channels", () => {
+ const messageChannelDisposers = messageChannelListeners.map(enlistMessageChannelListener);
+ const requestChannelDisposers = requestChannelListeners.map(enlistRequestChannelListener);
+
+ return disposer(...messageChannelDisposers, ...requestChannelDisposers);
+ });
+ },
+});
+
+
+export default listeningOfChannelsInjectable;
diff --git a/src/common/utils/channel/message-channel-injection-token.ts b/src/common/utils/channel/message-channel-injection-token.ts
new file mode 100644
index 0000000000..3141acedf3
--- /dev/null
+++ b/src/common/utils/channel/message-channel-injection-token.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 { getInjectionToken } from "@ogre-tools/injectable";
+import type { JsonValue } from "type-fest";
+
+export interface MessageChannel {
+ id: string;
+ _messageSignature?: Message;
+}
+
+export const messageChannelInjectionToken = getInjectionToken>({
+ id: "message-channel",
+});
diff --git a/src/common/utils/channel/message-channel-listener-injection-token.ts b/src/common/utils/channel/message-channel-listener-injection-token.ts
new file mode 100644
index 0000000000..8879e19013
--- /dev/null
+++ b/src/common/utils/channel/message-channel-listener-injection-token.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 { getInjectionToken } from "@ogre-tools/injectable";
+import type { SetRequired } from "type-fest";
+import type { MessageChannel } from "./message-channel-injection-token";
+
+export interface MessageChannelListener> {
+ channel: TChannel;
+ handler: (value: SetRequired["_messageSignature"]) => void;
+}
+
+export const messageChannelListenerInjectionToken = getInjectionToken>>(
+ {
+ id: "message-channel-listener",
+ },
+);
diff --git a/src/common/utils/channel/message-to-channel-injection-token.ts b/src/common/utils/channel/message-to-channel-injection-token.ts
new file mode 100644
index 0000000000..8c5f03b9ee
--- /dev/null
+++ b/src/common/utils/channel/message-to-channel-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 { SetRequired } from "type-fest";
+import type { MessageChannel } from "./message-channel-injection-token";
+
+export interface MessageToChannel {
+ , TMessage extends void>(
+ channel: TChannel,
+ ): void;
+
+ >(
+ channel: TChannel,
+ message: SetRequired["_messageSignature"],
+ ): void;
+}
+
+export const messageToChannelInjectionToken =
+ getInjectionToken({
+ id: "message-to-message-channel",
+ });
diff --git a/src/common/utils/channel/request-channel-injection-token.ts b/src/common/utils/channel/request-channel-injection-token.ts
new file mode 100644
index 0000000000..67044db878
--- /dev/null
+++ b/src/common/utils/channel/request-channel-injection-token.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 { getInjectionToken } from "@ogre-tools/injectable";
+import type { JsonValue } from "type-fest";
+
+export interface RequestChannel<
+ Request extends JsonValue | void = void,
+ Response extends JsonValue | void = void,
+> {
+ id: string;
+ _requestSignature?: Request;
+ _responseSignature?: Response;
+}
+
+export const requestChannelInjectionToken = getInjectionToken>({
+ id: "request-channel",
+});
diff --git a/src/common/utils/channel/request-channel-listener-injection-token.ts b/src/common/utils/channel/request-channel-listener-injection-token.ts
new file mode 100644
index 0000000000..690b96d9dc
--- /dev/null
+++ b/src/common/utils/channel/request-channel-listener-injection-token.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 { getInjectionToken } from "@ogre-tools/injectable";
+import type { SetRequired } from "type-fest";
+import type { RequestChannel } from "./request-channel-injection-token";
+
+export interface RequestChannelListener> {
+ channel: TChannel;
+
+ handler: (
+ request: SetRequired["_requestSignature"]
+ ) =>
+ | SetRequired["_responseSignature"]
+ | Promise<
+ SetRequired["_responseSignature"]
+ >;
+}
+
+export const requestChannelListenerInjectionToken = getInjectionToken>>(
+ {
+ id: "request-channel-listener",
+ },
+);
diff --git a/src/common/utils/channel/request-from-channel-injection-token.ts b/src/common/utils/channel/request-from-channel-injection-token.ts
new file mode 100644
index 0000000000..5f4492543f
--- /dev/null
+++ b/src/common/utils/channel/request-from-channel-injection-token.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 { getInjectionToken } from "@ogre-tools/injectable";
+import type { SetRequired } from "type-fest";
+import type { RequestChannel } from "./request-channel-injection-token";
+
+export type RequestFromChannel = <
+ TChannel extends RequestChannel,
+>(
+ channel: TChannel,
+ ...request: TChannel["_requestSignature"] extends void
+ ? []
+ : [TChannel["_requestSignature"]]
+) => Promise["_responseSignature"]>;
+
+export const requestFromChannelInjectionToken =
+ getInjectionToken({
+ id: "request-from-request-channel",
+ });
diff --git a/src/common/utils/get-random-id.injectable.ts b/src/common/utils/get-random-id.injectable.ts
new file mode 100644
index 0000000000..3b96c50633
--- /dev/null
+++ b/src/common/utils/get-random-id.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 { v4 as getRandomId } from "uuid";
+
+const getRandomIdInjectable = getInjectable({
+ id: "get-random-id",
+ instantiate: () => getRandomId,
+ causesSideEffects: true,
+});
+
+export default getRandomIdInjectable;
diff --git a/src/common/utils/is-promise/is-promise.test.ts b/src/common/utils/is-promise/is-promise.test.ts
new file mode 100644
index 0000000000..565f272ed6
--- /dev/null
+++ b/src/common/utils/is-promise/is-promise.test.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 { isPromise } from "./is-promise";
+
+describe("isPromise", () => {
+ it("given promise, returns true", () => {
+ const actual = isPromise(new Promise(() => {}));
+
+ expect(actual).toBe(true);
+ });
+
+ it("given non-promise, returns false", () => {
+ const actual = isPromise({});
+
+ expect(actual).toBe(false);
+ });
+
+ it("given thenable, returns false", () => {
+ const actual = isPromise({ then: () => {} });
+
+ expect(actual).toBe(false);
+ });
+
+ it("given nothing, returns false", () => {
+ const actual = isPromise(undefined);
+
+ expect(actual).toBe(false);
+ });
+});
diff --git a/src/common/ipc-channel/channel.ts b/src/common/utils/is-promise/is-promise.ts
similarity index 56%
rename from src/common/ipc-channel/channel.ts
rename to src/common/utils/is-promise/is-promise.ts
index 2153134fff..6261f569cd 100644
--- a/src/common/ipc-channel/channel.ts
+++ b/src/common/utils/is-promise/is-promise.ts
@@ -2,7 +2,6 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
-export interface Channel {
- name: string;
- _template: TInstance;
+export function isPromise(reference: any): reference is Promise {
+ return reference?.constructor === Promise;
}
diff --git a/src/common/utils/sync-box/create-sync-box.injectable.ts b/src/common/utils/sync-box/create-sync-box.injectable.ts
new file mode 100644
index 0000000000..2cf3de6a69
--- /dev/null
+++ b/src/common/utils/sync-box/create-sync-box.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 { computed } from "mobx";
+import syncBoxChannelInjectable from "./sync-box-channel.injectable";
+import { messageToChannelInjectionToken } from "../channel/message-to-channel-injection-token";
+import syncBoxStateInjectable from "./sync-box-state.injectable";
+import type { SyncBox } from "./sync-box-injection-token";
+
+const createSyncBoxInjectable = getInjectable({
+ id: "create-sync-box",
+
+ instantiate: (di) => {
+ const syncBoxChannel = di.inject(syncBoxChannelInjectable);
+ const messageToChannel = di.inject(messageToChannelInjectionToken);
+ const getSyncBoxState = (id: string) => di.inject(syncBoxStateInjectable, id);
+
+ return (id: string, initialValue: TData): SyncBox => {
+ const state = getSyncBoxState(id);
+
+ state.set(initialValue);
+
+ return {
+ id,
+
+ value: computed(() => state.get()),
+
+ set: (value) => {
+ state.set(value);
+
+ messageToChannel(syncBoxChannel, { id, value });
+ },
+ };
+ };
+ },
+});
+
+export default createSyncBoxInjectable;
+
diff --git a/src/common/utils/sync-box/sync-box-channel-listener.injectable.ts b/src/common/utils/sync-box/sync-box-channel-listener.injectable.ts
new file mode 100644
index 0000000000..b603c85997
--- /dev/null
+++ b/src/common/utils/sync-box/sync-box-channel-listener.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 type { SyncBoxChannel } from "./sync-box-channel.injectable";
+import syncBoxChannelInjectable from "./sync-box-channel.injectable";
+import syncBoxStateInjectable from "./sync-box-state.injectable";
+import type { MessageChannelListener } from "../channel/message-channel-listener-injection-token";
+import { messageChannelListenerInjectionToken } from "../channel/message-channel-listener-injection-token";
+
+const syncBoxChannelListenerInjectable = getInjectable({
+ id: "sync-box-channel-listener",
+
+ instantiate: (di): MessageChannelListener => {
+ const getSyncBoxState = (id: string) => di.inject(syncBoxStateInjectable, id);
+ const channel = di.inject(syncBoxChannelInjectable);
+
+ return {
+ channel,
+
+ handler: ({ id, value }) => {
+ const target = getSyncBoxState(id);
+
+ if (target) {
+ target.set(value);
+ }
+ },
+ };
+ },
+
+ injectionToken: messageChannelListenerInjectionToken,
+});
+
+export default syncBoxChannelListenerInjectable;
diff --git a/src/common/utils/sync-box/sync-box-channel.injectable.ts b/src/common/utils/sync-box/sync-box-channel.injectable.ts
new file mode 100644
index 0000000000..9389a99867
--- /dev/null
+++ b/src/common/utils/sync-box/sync-box-channel.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 type { MessageChannel } from "../channel/message-channel-injection-token";
+import { messageChannelInjectionToken } from "../channel/message-channel-injection-token";
+
+export type SyncBoxChannel = MessageChannel<{ id: string; value: any }>;
+
+const syncBoxChannelInjectable = getInjectable({
+ id: "sync-box-channel",
+
+ instantiate: (): SyncBoxChannel => ({
+ id: "sync-box-channel",
+ }),
+
+ injectionToken: messageChannelInjectionToken,
+});
+
+export default syncBoxChannelInjectable;
diff --git a/src/common/utils/sync-box/sync-box-initial-value-channel.injectable.ts b/src/common/utils/sync-box/sync-box-initial-value-channel.injectable.ts
new file mode 100644
index 0000000000..89374c3565
--- /dev/null
+++ b/src/common/utils/sync-box/sync-box-initial-value-channel.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 type { RequestChannel } from "../channel/request-channel-injection-token";
+import { requestChannelInjectionToken } from "../channel/request-channel-injection-token";
+
+export type SyncBoxInitialValueChannel = RequestChannel<
+ void,
+ { id: string; value: any }[]
+>;
+
+const syncBoxInitialValueChannelInjectable = getInjectable({
+ id: "sync-box-initial-value-channel",
+
+ instantiate: (): SyncBoxInitialValueChannel => ({
+ id: "sync-box-initial-value-channel",
+ }),
+
+ injectionToken: requestChannelInjectionToken,
+});
+
+export default syncBoxInitialValueChannelInjectable;
diff --git a/src/common/utils/sync-box/sync-box-injection-token.ts b/src/common/utils/sync-box/sync-box-injection-token.ts
new file mode 100644
index 0000000000..d35c7d5367
--- /dev/null
+++ b/src/common/utils/sync-box/sync-box-injection-token.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 { getInjectionToken } from "@ogre-tools/injectable";
+import type { IComputedValue } from "mobx";
+import type { JsonValue } from "type-fest";
+
+export interface SyncBox {
+ id: string;
+ value: IComputedValue;
+ set: (value: TValue) => void;
+}
+
+export const syncBoxInjectionToken = getInjectionToken>({
+ id: "sync-box",
+});
diff --git a/src/common/utils/sync-box/sync-box-state.injectable.ts b/src/common/utils/sync-box/sync-box-state.injectable.ts
new file mode 100644
index 0000000000..e695833da4
--- /dev/null
+++ b/src/common/utils/sync-box/sync-box-state.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, lifecycleEnum } from "@ogre-tools/injectable";
+import { observable } from "mobx";
+
+const syncBoxStateInjectable = getInjectable({
+ id: "sync-box-state",
+
+ instantiate: () => observable.box(),
+
+ lifecycle: lifecycleEnum.keyedSingleton({
+ getInstanceKey: (di, id: string) => id,
+ }),
+});
+
+export default syncBoxStateInjectable;
diff --git a/src/common/utils/sync-box/sync-box.test.ts b/src/common/utils/sync-box/sync-box.test.ts
new file mode 100644
index 0000000000..2dccbd87a5
--- /dev/null
+++ b/src/common/utils/sync-box/sync-box.test.ts
@@ -0,0 +1,179 @@
+/**
+ * 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 { observe, runInAction } from "mobx";
+import type { ApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
+import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
+import createSyncBoxInjectable from "./create-sync-box.injectable";
+import { flushPromises } from "../../test-utils/flush-promises";
+import type { SyncBox } from "./sync-box-injection-token";
+
+describe("sync-box", () => {
+ let applicationBuilder: ApplicationBuilder;
+
+ beforeEach(() => {
+ applicationBuilder = getApplicationBuilder();
+
+ applicationBuilder.dis.mainDi.register(someInjectable);
+ applicationBuilder.dis.rendererDi.register(someInjectable);
+ });
+
+ // TODO: Separate starting for main application and starting of window in application builder
+ xdescribe("given application is started, when value is set in main", () => {
+ let valueInMain: string;
+ let syncBoxInMain: SyncBox;
+
+ beforeEach(async () => {
+ syncBoxInMain = applicationBuilder.dis.mainDi.inject(someInjectable);
+
+ // await applicationBuilder.start();
+
+ observe(syncBoxInMain.value, ({ newValue }) => {
+ valueInMain = newValue as string;
+ }, true);
+
+ runInAction(() => {
+ syncBoxInMain.set("some-value-from-main");
+ });
+ });
+
+ it("knows value in main", () => {
+ expect(valueInMain).toBe("some-value-from-main");
+ });
+
+ describe("when window starts", () => {
+ let valueInRenderer: string;
+ let syncBoxInRenderer: SyncBox;
+
+ beforeEach(() => {
+ // applicationBuilder.renderWindow()
+
+ syncBoxInRenderer = applicationBuilder.dis.rendererDi.inject(someInjectable);
+
+ observe(syncBoxInRenderer.value, ({ newValue }) => {
+ valueInRenderer = newValue as string;
+ }, true);
+ });
+
+ it("does not have the initial value yet", () => {
+ expect(valueInRenderer).toBe(undefined);
+ });
+
+ describe("when getting initial value resolves", () => {
+ beforeEach(async () => {
+ await flushPromises();
+ });
+
+ it("has value in renderer", () => {
+ expect(valueInRenderer).toBe("some-value-from-main");
+ });
+
+ describe("when value is set from renderer", () => {
+ beforeEach(() => {
+ runInAction(() => {
+ syncBoxInRenderer.set("some-value-from-renderer");
+ });
+ });
+
+ it("has value in main", () => {
+ expect(valueInMain).toBe("some-value-from-renderer");
+ });
+
+ it("has value in renderer", () => {
+ expect(valueInRenderer).toBe("some-value-from-renderer");
+ });
+ });
+ });
+
+ describe("when value is set from renderer before getting initial value from main resolves", () => {
+ beforeEach(() => {
+ runInAction(() => {
+ syncBoxInRenderer.set("some-value-from-renderer");
+ });
+ });
+
+ it("has value in main", () => {
+ expect(valueInMain).toBe("some-value-from-renderer");
+ });
+
+ it("has value in renderer", () => {
+ expect(valueInRenderer).toBe("some-value-from-renderer");
+ });
+ });
+ });
+ });
+
+ describe("when application starts with a window", () => {
+ let valueInRenderer: string;
+ let valueInMain: string;
+ let syncBoxInMain: SyncBox;
+ let syncBoxInRenderer: SyncBox;
+
+ beforeEach(async () => {
+ syncBoxInMain = applicationBuilder.dis.mainDi.inject(someInjectable);
+ syncBoxInRenderer = applicationBuilder.dis.rendererDi.inject(someInjectable);
+
+ await applicationBuilder.render();
+
+ observe(syncBoxInRenderer.value, ({ newValue }) => {
+ valueInRenderer = newValue as string;
+ }, true);
+
+ observe(syncBoxInMain.value, ({ newValue }) => {
+ valueInMain = newValue as string;
+ }, true);
+ });
+
+ it("knows initial value in main", () => {
+ expect(valueInMain).toBe("some-initial-value");
+ });
+
+ it("knows initial value in renderer", () => {
+ expect(valueInRenderer).toBe("some-initial-value");
+ });
+
+ describe("when value is set from main", () => {
+ beforeEach(() => {
+ runInAction(() => {
+ syncBoxInMain.set("some-value-from-main");
+ });
+ });
+
+ it("has value in main", () => {
+ expect(valueInMain).toBe("some-value-from-main");
+ });
+
+ it("has value in renderer", () => {
+ expect(valueInRenderer).toBe("some-value-from-main");
+ });
+
+ describe("when value is set from renderer", () => {
+ beforeEach(() => {
+ runInAction(() => {
+ syncBoxInRenderer.set("some-value-from-renderer");
+ });
+ });
+
+ it("has value in main", () => {
+ expect(valueInMain).toBe("some-value-from-renderer");
+ });
+
+ it("has value in renderer", () => {
+ expect(valueInRenderer).toBe("some-value-from-renderer");
+ });
+ });
+ });
+ });
+});
+
+const someInjectable = getInjectable({
+ id: "some-injectable",
+
+ instantiate: (di) => {
+ const createSyncBox = di.inject(createSyncBoxInjectable);
+
+ return createSyncBox("some-sync-box", "some-initial-value");
+ },
+});
diff --git a/src/common/utils/tentative-parse-json.ts b/src/common/utils/tentative-parse-json.ts
new file mode 100644
index 0000000000..a0cb089a74
--- /dev/null
+++ b/src/common/utils/tentative-parse-json.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 { pipeline } from "@ogre-tools/fp";
+import { defaultTo } from "lodash/fp";
+import { withErrorSuppression } from "./with-error-suppression/with-error-suppression";
+
+export const tentativeParseJson = (toBeParsed: any) => pipeline(
+ toBeParsed,
+ withErrorSuppression(JSON.parse),
+ defaultTo(toBeParsed),
+);
+
+
diff --git a/src/common/utils/tentative-stringify-json.ts b/src/common/utils/tentative-stringify-json.ts
new file mode 100644
index 0000000000..dc7206be7c
--- /dev/null
+++ b/src/common/utils/tentative-stringify-json.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 { pipeline } from "@ogre-tools/fp";
+import { defaultTo } from "lodash/fp";
+import { withErrorSuppression } from "./with-error-suppression/with-error-suppression";
+
+export const tentativeStringifyJson = (toBeParsed: any) => pipeline(
+ toBeParsed,
+ withErrorSuppression(JSON.stringify),
+ defaultTo(toBeParsed),
+);
+
+
diff --git a/src/common/utils/with-error-logging/with-error-logging.injectable.ts b/src/common/utils/with-error-logging/with-error-logging.injectable.ts
new file mode 100644
index 0000000000..12b48c6204
--- /dev/null
+++ b/src/common/utils/with-error-logging/with-error-logging.injectable.ts
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getInjectable } from "@ogre-tools/injectable";
+import loggerInjectable from "../../logger.injectable";
+import { isPromise } from "../is-promise/is-promise";
+
+export type WithErrorLoggingFor = (
+ getErrorMessage: (error: unknown) => string
+) => any>(
+ toBeDecorated: T
+) => (...args: Parameters) => ReturnType;
+
+const withErrorLoggingInjectable = getInjectable({
+ id: "with-error-logging",
+
+ instantiate: (di): WithErrorLoggingFor => {
+ const logger = di.inject(loggerInjectable);
+
+ return (getErrorMessage) =>
+ (toBeDecorated) =>
+ (...args) => {
+ try {
+ const returnValue = toBeDecorated(...args);
+
+ if (isPromise(returnValue)) {
+ returnValue.catch((e) => {
+ const errorMessage = getErrorMessage(e);
+
+ logger.error(errorMessage, e);
+ });
+ }
+
+ return returnValue;
+ } catch (e) {
+ const errorMessage = getErrorMessage(e);
+
+ logger.error(errorMessage, e);
+
+ throw e;
+ }
+ };
+ },
+});
+
+export default withErrorLoggingInjectable;
diff --git a/src/common/utils/with-error-logging/with-error-logging.test.ts b/src/common/utils/with-error-logging/with-error-logging.test.ts
new file mode 100644
index 0000000000..533374d9ad
--- /dev/null
+++ b/src/common/utils/with-error-logging/with-error-logging.test.ts
@@ -0,0 +1,243 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+
+import { getDiForUnitTesting } from "../../../main/getDiForUnitTesting";
+import loggerInjectable from "../../logger.injectable";
+import type { Logger } from "../../logger";
+import withErrorLoggingInjectable from "./with-error-logging.injectable";
+import { pipeline } from "@ogre-tools/fp";
+import type { AsyncFnMock } from "@async-fn/jest";
+import asyncFn from "@async-fn/jest";
+import { getPromiseStatus } from "../../test-utils/get-promise-status";
+
+describe("with-error-logging", () => {
+ describe("given decorated sync function", () => {
+ let loggerStub: Logger;
+ let toBeDecorated: jest.Mock;
+ let decorated: (a: string, b: string) => number | undefined;
+
+ beforeEach(() => {
+ const di = getDiForUnitTesting();
+
+ loggerStub = {
+ error: jest.fn(),
+ } as unknown as Logger;
+
+ di.override(loggerInjectable, () => loggerStub);
+
+ const withErrorLoggingFor = di.inject(withErrorLoggingInjectable);
+
+ toBeDecorated = jest.fn();
+
+ decorated = pipeline(
+ toBeDecorated,
+ withErrorLoggingFor((error: any) => `some-error-message-for-${error.message}`),
+ );
+ });
+
+ describe("when function does not throw and returns value", () => {
+ let returnValue: number | undefined;
+
+ beforeEach(() => {
+ // eslint-disable-next-line unused-imports/no-unused-vars-ts
+ toBeDecorated.mockImplementation((_, __) => 42);
+
+ returnValue = decorated("some-parameter", "some-other-parameter");
+ });
+
+ it("passes arguments to decorated function", () => {
+ expect(toBeDecorated).toHaveBeenCalledWith("some-parameter", "some-other-parameter");
+ });
+
+ it("does not log error", () => {
+ expect(loggerStub.error).not.toHaveBeenCalled();
+ });
+
+ it("returns the value", () => {
+ expect(returnValue).toBe(42);
+ });
+ });
+
+ describe("when function does not throw and returns no value", () => {
+ let returnValue: number | undefined;
+
+ beforeEach(() => {
+ // eslint-disable-next-line unused-imports/no-unused-vars-ts
+ toBeDecorated.mockImplementation((_, __) => undefined);
+
+ returnValue = decorated("some-parameter", "some-other-parameter");
+ });
+
+ it("passes arguments to decorated function", () => {
+ expect(toBeDecorated).toHaveBeenCalledWith("some-parameter", "some-other-parameter");
+ });
+
+ it("does not log error", () => {
+ expect(loggerStub.error).not.toHaveBeenCalled();
+ });
+
+ it("returns nothing", () => {
+ expect(returnValue).toBeUndefined();
+ });
+ });
+
+ describe("when function throws", () => {
+ let error: Error;
+
+ beforeEach(() => {
+ // eslint-disable-next-line unused-imports/no-unused-vars-ts
+ toBeDecorated.mockImplementation((_, __) => {
+ throw new Error("some-error");
+ });
+
+ try {
+ decorated("some-parameter", "some-other-parameter");
+ } catch (e: any) {
+ error = e;
+ }
+ });
+
+ it("passes arguments to decorated function", () => {
+ expect(toBeDecorated).toHaveBeenCalledWith("some-parameter", "some-other-parameter");
+ });
+
+ it("logs the error", () => {
+ expect(loggerStub.error).toHaveBeenCalledWith("some-error-message-for-some-error", error);
+ });
+
+ it("throws", () => {
+ expect(error.message).toBe("some-error");
+ });
+ });
+ });
+
+ describe("given decorated async function", () => {
+ let loggerStub: Logger;
+ let decorated: (a: string, b: string) => Promise;
+ let toBeDecorated: AsyncFnMock;
+
+ beforeEach(() => {
+ const di = getDiForUnitTesting();
+
+ loggerStub = {
+ error: jest.fn(),
+ } as unknown as Logger;
+
+ di.override(loggerInjectable, () => loggerStub);
+
+ const withErrorLoggingFor = di.inject(withErrorLoggingInjectable);
+
+ toBeDecorated = asyncFn();
+
+ decorated = pipeline(
+ toBeDecorated,
+
+ withErrorLoggingFor(
+ (error: any) =>
+ `some-error-message-for-${error.message || error.someProperty}`,
+ ),
+ );
+ });
+
+ describe("when called", () => {
+ let returnValuePromise: Promise;
+
+ beforeEach(() => {
+ returnValuePromise = decorated("some-parameter", "some-other-parameter");
+ });
+
+ it("passes arguments to decorated function", () => {
+ expect(toBeDecorated).toHaveBeenCalledWith("some-parameter", "some-other-parameter");
+ });
+
+ it("does not log error yet", () => {
+ expect(loggerStub.error).not.toHaveBeenCalled();
+ });
+
+ it("does not resolve yet", async () => {
+ const promiseStatus = await getPromiseStatus(returnValuePromise);
+
+ expect(promiseStatus.fulfilled).toBe(false);
+ });
+
+ describe("when call rejects with error instance", () => {
+ let error: Error;
+
+ beforeEach(async () => {
+ try {
+ await toBeDecorated.reject(new Error("some-error"));
+ await returnValuePromise;
+ } catch (e) {
+ error = e as Error;
+ }
+ });
+
+ it("logs the error", () => {
+ expect(loggerStub.error).toHaveBeenCalledWith("some-error-message-for-some-error", error);
+ });
+
+ it("rejects", () => {
+ return expect(() => returnValuePromise).rejects.toThrow("some-error");
+ });
+ });
+
+ describe("when call rejects with something else than error instance", () => {
+ let error: unknown;
+
+ beforeEach(async () => {
+ try {
+ await toBeDecorated.reject({ someProperty: "some-rejection" });
+ await returnValuePromise;
+ } catch (e) {
+ error = e;
+ }
+ });
+
+ it("logs the rejection", () => {
+ expect(loggerStub.error).toHaveBeenCalledWith(
+ "some-error-message-for-some-rejection",
+ error,
+ );
+ });
+
+ it("rejects", () => {
+ return expect(() => returnValuePromise).rejects.toEqual({ someProperty: "some-rejection" });
+ });
+ });
+
+ describe("when call resolves with value", () => {
+ beforeEach(async () => {
+ await toBeDecorated.resolve(42);
+ });
+
+ it("does not log error", () => {
+ expect(loggerStub.error).not.toHaveBeenCalled();
+ });
+
+ it("resolves with the value", async () => {
+ const returnValue = await returnValuePromise;
+
+ expect(returnValue).toBe(42);
+ });
+ });
+
+ describe("when call resolves without value", () => {
+ beforeEach(async () => {
+ await toBeDecorated.resolve(undefined);
+ });
+
+ it("does not log error", () => {
+ expect(loggerStub.error).not.toHaveBeenCalled();
+ });
+
+ it("resolves without value", async () => {
+ const returnValue = await returnValuePromise;
+
+ expect(returnValue).toBeUndefined();
+ });
+ });
+ });
+ });
+});
diff --git a/src/common/utils/with-error-suppression/with-error-suppression.test.ts b/src/common/utils/with-error-suppression/with-error-suppression.test.ts
new file mode 100644
index 0000000000..db4909fd55
--- /dev/null
+++ b/src/common/utils/with-error-suppression/with-error-suppression.test.ts
@@ -0,0 +1,104 @@
+/**
+ * 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 { getPromiseStatus } from "../../test-utils/get-promise-status";
+import { withErrorSuppression } from "./with-error-suppression";
+
+describe("with-error-suppression", () => {
+ describe("given decorated sync function", () => {
+ let toBeDecorated: jest.Mock;
+ let decorated: (a: string, b: string) => void;
+
+ beforeEach(() => {
+ toBeDecorated = jest.fn();
+
+ decorated = withErrorSuppression(toBeDecorated);
+ });
+
+ describe("when function does not throw", () => {
+ let returnValue: void;
+
+ beforeEach(() => {
+ returnValue = decorated("some-parameter", "some-other-parameter");
+ });
+
+ it("passes arguments to decorated function", () => {
+ expect(toBeDecorated).toHaveBeenCalledWith("some-parameter", "some-other-parameter");
+ });
+
+ it("returns nothing", () => {
+ expect(returnValue).toBeUndefined();
+ });
+ });
+
+ describe("when function throws", () => {
+ let returnValue: void;
+
+ beforeEach(() => {
+ // eslint-disable-next-line unused-imports/no-unused-vars-ts
+ toBeDecorated.mockImplementation((_, __) => {
+ throw new Error("some-error");
+ });
+
+ returnValue = decorated("some-parameter", "some-other-parameter");
+ });
+
+ it("passes arguments to decorated function", () => {
+ expect(toBeDecorated).toHaveBeenCalledWith("some-parameter", "some-other-parameter");
+ });
+
+ it("returns nothing", () => {
+ expect(returnValue).toBeUndefined();
+ });
+ });
+ });
+
+ describe("given decorated async function", () => {
+ let decorated: (a: string, b: string) => Promise | Promise;
+ let toBeDecorated: AsyncFnMock<(a: string, b: string) => number>;
+
+ beforeEach(() => {
+ toBeDecorated = asyncFn();
+
+ decorated = withErrorSuppression(toBeDecorated);
+ });
+
+ describe("when called", () => {
+ let returnValuePromise: Promise | Promise;
+
+ beforeEach(() => {
+ returnValuePromise = decorated("some-parameter", "some-other-parameter");
+ });
+
+ it("passes arguments to decorated function", () => {
+ expect(toBeDecorated).toHaveBeenCalledWith("some-parameter", "some-other-parameter");
+ });
+
+ it("does not resolve yet", async () => {
+ const promiseStatus = await getPromiseStatus(returnValuePromise);
+
+ expect(promiseStatus.fulfilled).toBe(false);
+ });
+
+ it("when call rejects, resolves with nothing", async () => {
+ await toBeDecorated.reject(new Error("some-error"));
+
+ const returnValue = await returnValuePromise;
+
+ expect(returnValue).toBeUndefined();
+ });
+
+ it("when call resolves, resolves with the value", async () => {
+ await toBeDecorated.resolve(42);
+
+ const returnValue = await returnValuePromise;
+
+ expect(returnValue).toBe(42);
+ });
+ });
+ });
+});
diff --git a/src/common/utils/with-error-suppression/with-error-suppression.ts b/src/common/utils/with-error-suppression/with-error-suppression.ts
new file mode 100644
index 0000000000..657ed13c16
--- /dev/null
+++ b/src/common/utils/with-error-suppression/with-error-suppression.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 { noop } from "lodash/fp";
+
+export function withErrorSuppression Promise>(toBeDecorated: TDecorated): (...args: Parameters) => ReturnType | Promise;
+export function withErrorSuppression any>(toBeDecorated: TDecorated): (...args: Parameters) => ReturnType | void;
+
+export function withErrorSuppression(toBeDecorated: any) {
+ return (...args: any[]) => {
+ try {
+ const returnValue = toBeDecorated(...args);
+
+ if (isPromise(returnValue)) {
+ return returnValue.catch(noop);
+ }
+
+ return returnValue;
+ } catch (e) {
+ return undefined;
+ }
+ };
+}
+
+function isPromise(reference: any): reference is Promise {
+ return !!reference?.then;
+}
diff --git a/src/common/utils/with-orphan-promise/with-orphan-promise.injectable.ts b/src/common/utils/with-orphan-promise/with-orphan-promise.injectable.ts
new file mode 100644
index 0000000000..42e6cb9a61
--- /dev/null
+++ b/src/common/utils/with-orphan-promise/with-orphan-promise.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 withErrorLoggingInjectable from "../with-error-logging/with-error-logging.injectable";
+import { withErrorSuppression } from "../with-error-suppression/with-error-suppression";
+import { pipeline } from "@ogre-tools/fp";
+
+const withOrphanPromiseInjectable = getInjectable({
+ id: "with-orphan-promise",
+
+ instantiate: (di) => {
+ const withErrorLoggingFor = di.inject(withErrorLoggingInjectable);
+
+ return Promise>(toBeDecorated: T) =>
+ (...args: Parameters): void => {
+ const decorated = pipeline(
+ toBeDecorated,
+ withErrorLoggingFor(() => "Orphan promise rejection encountered"),
+ withErrorSuppression,
+ );
+
+ decorated(...args);
+ };
+ },
+});
+
+export default withOrphanPromiseInjectable;
diff --git a/src/common/utils/with-orphan-promise/with-orphan-promise.test.ts b/src/common/utils/with-orphan-promise/with-orphan-promise.test.ts
new file mode 100644
index 0000000000..cea88b2352
--- /dev/null
+++ b/src/common/utils/with-orphan-promise/with-orphan-promise.test.ts
@@ -0,0 +1,59 @@
+/**
+ * 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 { getDiForUnitTesting } from "../../../main/getDiForUnitTesting";
+import loggerInjectable from "../../logger.injectable";
+import type { Logger } from "../../logger";
+import withOrphanPromiseInjectable from "./with-orphan-promise.injectable";
+
+describe("with orphan promise, when called", () => {
+ let toBeDecorated: AsyncFnMock<(arg1: string, arg2: string) => Promise>;
+ let actual: void;
+ let loggerStub: Logger;
+
+ beforeEach(() => {
+ const di = getDiForUnitTesting({ doGeneralOverrides: true });
+
+ loggerStub = { error: jest.fn() } as unknown as Logger;
+
+ di.override(loggerInjectable, () => loggerStub);
+
+ const withOrphanPromise = di.inject(withOrphanPromiseInjectable);
+
+ toBeDecorated = asyncFn();
+
+ const decorated = withOrphanPromise(toBeDecorated);
+
+ actual = decorated("some-argument", "some-other-argument");
+ });
+
+ it("calls decorated with arguments", () => {
+ expect(toBeDecorated).toHaveBeenCalledWith("some-argument", "some-other-argument");
+ });
+
+ it("given promise returned by decorated has not been fulfilled yet, already returns nothing", () => {
+ expect(actual).toBeUndefined();
+ });
+
+ it("when decorated function resolves, nothing happens", async () => {
+ await toBeDecorated.resolve("irrelevant");
+ // Note: there is no expect, test is here only for documentation.
+ });
+
+ describe("when decorated function rejects", () => {
+ beforeEach(async () => {
+ await toBeDecorated.reject("some-error");
+ });
+
+ it("logs the rejection", () => {
+ expect(loggerStub.error).toHaveBeenCalledWith("Orphan promise rejection encountered", "some-error");
+ });
+
+ it("nothing else happens", () => {
+ // Note: there is no expect, test is here only for documentation.
+ });
+ });
+});
diff --git a/src/common/vars.ts b/src/common/vars.ts
index 1d47eac9d8..e11e49a7a2 100644
--- a/src/common/vars.ts
+++ b/src/common/vars.ts
@@ -43,8 +43,6 @@ export const isProduction = process.env.NODE_ENV === "production";
*/
export const isDevelopment = !isTestEnv && !isProduction;
-export const isPublishConfigured = Object.keys(packageInfo.build).includes("publish");
-
export const productName = packageInfo.productName;
/**
diff --git a/src/common/vars/package-json.injectable.ts b/src/common/vars/package-json.injectable.ts
new file mode 100644
index 0000000000..fa132be518
--- /dev/null
+++ b/src/common/vars/package-json.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 packageJson from "../../../package.json";
+
+const packageJsonInjectable = getInjectable({
+ id: "package-json",
+ instantiate: () => packageJson,
+ causesSideEffects: true,
+});
+
+export default packageJsonInjectable;
diff --git a/src/main/__test__/kube-auth-proxy.test.ts b/src/main/__test__/kube-auth-proxy.test.ts
index 298c8f351b..16cebb813a 100644
--- a/src/main/__test__/kube-auth-proxy.test.ts
+++ b/src/main/__test__/kube-auth-proxy.test.ts
@@ -46,7 +46,6 @@ import { mock } from "jest-mock-extended";
import { waitUntilUsed } from "tcp-port-used";
import type { Readable } from "stream";
import { EventEmitter } from "stream";
-import { UserStore } from "../../common/user-store";
import { Console } from "console";
import { stdout, stderr } from "process";
import mockFs from "mock-fs";
@@ -120,12 +119,9 @@ describe("kube auth proxy tests", () => {
createCluster = di.inject(createClusterInjectionToken);
createKubeAuthProxy = di.inject(createKubeAuthProxyInjectable);
-
- UserStore.createInstance();
});
afterEach(() => {
- UserStore.resetInstance();
mockFs.restore();
});
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 f4af95cf83..0a1db468d8 100644
--- a/src/main/app-paths/app-name/app-name.injectable.ts
+++ b/src/main/app-paths/app-name/app-name.injectable.ts
@@ -3,16 +3,17 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
-import packageInfo from "../../../../package.json";
import isDevelopmentInjectable from "../../../common/vars/is-development.injectable";
+import productNameInjectable from "./product-name.injectable";
const appNameInjectable = getInjectable({
id: "app-name",
instantiate: (di) => {
const isDevelopment = di.inject(isDevelopmentInjectable);
+ const productName = di.inject(productNameInjectable);
- return `${packageInfo.productName}${isDevelopment ? "Dev" : ""}`;
+ return `${productName}${isDevelopment ? "Dev" : ""}`;
},
causesSideEffects: true,
diff --git a/src/main/app-paths/app-name/product-name.injectable.ts b/src/main/app-paths/app-name/product-name.injectable.ts
new file mode 100644
index 0000000000..8c5c53bfba
--- /dev/null
+++ b/src/main/app-paths/app-name/product-name.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 packageInfo from "../../../../package.json";
+
+const productNameInjectable = getInjectable({
+ id: "product-name",
+ instantiate: () => packageInfo.productName,
+ causesSideEffects: true,
+});
+
+export default productNameInjectable;
diff --git a/src/main/app-paths/app-paths-request-channel-listener.injectable.ts b/src/main/app-paths/app-paths-request-channel-listener.injectable.ts
new file mode 100644
index 0000000000..3bd0c95bf7
--- /dev/null
+++ b/src/main/app-paths/app-paths-request-channel-listener.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 type { RequestChannelListener } from "../../common/utils/channel/request-channel-listener-injection-token";
+import { requestChannelListenerInjectionToken } from "../../common/utils/channel/request-channel-listener-injection-token";
+import type { AppPathsChannel } from "../../common/app-paths/app-paths-channel.injectable";
+import appPathsChannelInjectable from "../../common/app-paths/app-paths-channel.injectable";
+import appPathsInjectable from "../../common/app-paths/app-paths.injectable";
+
+const appPathsRequestChannelListenerInjectable = getInjectable({
+ id: "app-paths-request-channel-listener",
+
+ instantiate: (di): RequestChannelListener => {
+ const channel = di.inject(appPathsChannelInjectable);
+ const appPaths = di.inject(appPathsInjectable);
+
+ return {
+ channel,
+ handler: () => appPaths,
+ };
+ },
+ injectionToken: requestChannelListenerInjectionToken,
+});
+
+export default appPathsRequestChannelListenerInjectable;
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 6cb937f45d..8e28c806d7 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
@@ -6,7 +6,6 @@ import electronAppInjectable from "../../electron-app/electron-app.injectable";
import getElectronAppPathInjectable from "./get-electron-app-path.injectable";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import type { App } from "electron";
-import registerChannelInjectable from "../register-channel/register-channel.injectable";
import joinPathsInjectable from "../../../common/path/join-paths.injectable";
import { joinPathsFake } from "../../../common/test-utils/join-paths-fake";
@@ -32,7 +31,6 @@ describe("get-electron-app-path", () => {
} as App;
di.override(electronAppInjectable, () => appStub);
- di.override(registerChannelInjectable, () => () => undefined);
di.override(joinPathsInjectable, () => joinPathsFake);
getElectronAppPath = di.inject(getElectronAppPathInjectable) as (name: string) => string;
diff --git a/src/main/app-paths/register-channel/register-channel.injectable.ts b/src/main/app-paths/register-channel/register-channel.injectable.ts
deleted file mode 100644
index d0b517cf25..0000000000
--- a/src/main/app-paths/register-channel/register-channel.injectable.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-/**
- * Copyright (c) OpenLens Authors. All rights reserved.
- * Licensed under MIT License. See LICENSE in root directory for more information.
- */
-import { getInjectable } from "@ogre-tools/injectable";
-import ipcMainInjectable from "./ipc-main/ipc-main.injectable";
-import { registerChannel } from "./register-channel";
-
-const registerChannelInjectable = getInjectable({
- id: "register-channel",
-
- instantiate: (di) => registerChannel({
- ipcMain: di.inject(ipcMainInjectable),
- }),
-});
-
-export default registerChannelInjectable;
diff --git a/src/main/app-paths/register-channel/register-channel.ts b/src/main/app-paths/register-channel/register-channel.ts
deleted file mode 100644
index 73f3e13243..0000000000
--- a/src/main/app-paths/register-channel/register-channel.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 type { IpcMain } from "electron";
-import type { Channel } from "../../../common/ipc-channel/channel";
-
-interface Dependencies {
- ipcMain: IpcMain;
-}
-
-export const registerChannel =
- ({ ipcMain }: Dependencies) =>
- , TInstance>(
- channel: TChannel,
- getValue: () => TInstance,
- ) =>
- ipcMain.handle(channel.name, getValue);
diff --git a/src/main/app-paths/setup-app-paths.injectable.ts b/src/main/app-paths/setup-app-paths.injectable.ts
index 9a4283f063..816c58db8b 100644
--- a/src/main/app-paths/setup-app-paths.injectable.ts
+++ b/src/main/app-paths/setup-app-paths.injectable.ts
@@ -12,8 +12,6 @@ import appPathsStateInjectable from "../../common/app-paths/app-paths-state.inje
import { pathNames } from "../../common/app-paths/app-path-names";
import { fromPairs, map } from "lodash/fp";
import { pipeline } from "@ogre-tools/fp";
-import { appPathsIpcChannel } from "../../common/app-paths/app-path-injection-token";
-import registerChannelInjectable from "./register-channel/register-channel.injectable";
import joinPathsInjectable from "../../common/path/join-paths.injectable";
import { beforeElectronIsReadyInjectionToken } from "../start-main-application/runnable-tokens/before-electron-is-ready-injection-token";
@@ -25,7 +23,6 @@ const setupAppPathsInjectable = getInjectable({
const appName = di.inject(appNameInjectable);
const getAppPath = di.inject(getElectronAppPathInjectable);
const appPathsState = di.inject(appPathsStateInjectable);
- const registerChannel = di.inject(registerChannelInjectable);
const directoryForIntegrationTesting = di.inject(directoryForIntegrationTestingInjectable);
const joinPaths = di.inject(joinPathsInjectable);
@@ -46,8 +43,6 @@ const setupAppPathsInjectable = getInjectable({
) as AppPaths;
appPathsState.set(appPaths);
-
- registerChannel(appPathsIpcChannel, () => appPaths);
},
};
},
diff --git a/src/main/app-updater.ts b/src/main/app-updater.ts
deleted file mode 100644
index 77daf6b4b6..0000000000
--- a/src/main/app-updater.ts
+++ /dev/null
@@ -1,133 +0,0 @@
-/**
- * Copyright (c) OpenLens Authors. All rights reserved.
- * Licensed under MIT License. See LICENSE in root directory for more information.
- */
-
-import type { UpdateInfo } from "electron-updater";
-import { autoUpdater } from "electron-updater";
-import logger from "./logger";
-import { isPublishConfigured, isTestEnv } from "../common/vars";
-import { delay } from "../common/utils";
-import type { UpdateAvailableToBackchannel } from "../common/ipc";
-import { areArgsUpdateAvailableToBackchannel, AutoUpdateChecking, AutoUpdateLogPrefix, AutoUpdateNoUpdateAvailable, broadcastMessage, onceCorrect, UpdateAvailableChannel } from "../common/ipc";
-import { once } from "lodash";
-import { ipcMain } from "electron";
-import { nextUpdateChannel } from "./utils/update-channel";
-import { UserStore } from "../common/user-store";
-
-let installVersion: undefined | string;
-
-export function isAutoUpdateEnabled() {
- return autoUpdater.isUpdaterActive() && isPublishConfigured;
-}
-
-function handleAutoUpdateBackChannel(event: Electron.IpcMainEvent, ...[arg]: UpdateAvailableToBackchannel) {
- if (arg.doUpdate) {
- if (arg.now) {
- logger.info(`${AutoUpdateLogPrefix}: User chose to update now`);
- autoUpdater.quitAndInstall(true, true);
- } else {
- logger.info(`${AutoUpdateLogPrefix}: User chose to update on quit`);
- }
- } else {
- logger.info(`${AutoUpdateLogPrefix}: User chose not to update, will update on quit anyway`);
- }
-}
-
-autoUpdater.logger = {
- info: message => logger.info(`[AUTO-UPDATE]: electron-updater: %s`, message),
- warn: message => logger.warn(`[AUTO-UPDATE]: electron-updater: %s`, message),
- error: message => logger.error(`[AUTO-UPDATE]: electron-updater: %s`, message),
- debug: message => logger.debug(`[AUTO-UPDATE]: electron-updater: %s`, message),
-};
-
-interface Dependencies {
- isAutoUpdateEnabled: () => boolean;
-}
-
-/**
- * starts the automatic update checking
- * @param interval milliseconds between interval to check on, defaults to 2h
- */
-export const startUpdateChecking = ({ isAutoUpdateEnabled } : Dependencies) => once(function (interval = 1000 * 60 * 60 * 2): void {
- if (!isAutoUpdateEnabled() || isTestEnv) {
- return;
- }
-
- const userStore = UserStore.getInstance();
-
- autoUpdater.autoDownload = false;
- autoUpdater.autoInstallOnAppQuit = true;
- autoUpdater.channel = userStore.updateChannel;
- autoUpdater.allowDowngrade = userStore.isAllowedToDowngrade;
-
- autoUpdater
- .on("update-available", (info: UpdateInfo) => {
- if (installVersion === info.version) {
- // same version, don't broadcast
- return;
- }
-
- installVersion = info.version;
-
- autoUpdater.downloadUpdate()
- .catch(error => logger.error(`${AutoUpdateLogPrefix}: failed to download update`, { error: String(error) }));
- })
- .on("update-downloaded", (info: UpdateInfo) => {
- try {
- const backchannel = `auto-update:${info.version}`;
-
- ipcMain.removeAllListeners(backchannel); // only one handler should be present
-
- // make sure that the handler is in place before broadcasting (prevent race-condition)
- onceCorrect({
- source: ipcMain,
- channel: backchannel,
- listener: handleAutoUpdateBackChannel,
- verifier: areArgsUpdateAvailableToBackchannel,
- });
- logger.info(`${AutoUpdateLogPrefix}: broadcasting update available`, { backchannel, version: info.version });
- broadcastMessage(UpdateAvailableChannel, backchannel, info);
- } catch (error) {
- logger.error(`${AutoUpdateLogPrefix}: broadcasting failed`, { error });
- installVersion = undefined;
- }
- })
- .on("update-not-available", () => {
- const nextChannel = nextUpdateChannel(userStore.updateChannel, autoUpdater.channel);
-
- logger.info(`${AutoUpdateLogPrefix}: update not available from ${autoUpdater.channel}, will check ${nextChannel} channel next`);
-
- if (nextChannel !== autoUpdater.channel) {
- autoUpdater.channel = nextChannel;
- autoUpdater.checkForUpdates()
- .catch(error => logger.error(`${AutoUpdateLogPrefix}: failed with an error`, error));
- } else {
- broadcastMessage(AutoUpdateNoUpdateAvailable);
- }
- });
-
- async function helper() {
- while (true) {
- await checkForUpdates();
- await delay(interval);
- }
- }
-
- helper();
-});
-
-export async function checkForUpdates(): Promise {
- const userStore = UserStore.getInstance();
-
- try {
- logger.info(`📡 Checking for app updates`);
-
- autoUpdater.channel = userStore.updateChannel;
- autoUpdater.allowDowngrade = userStore.isAllowedToDowngrade;
- broadcastMessage(AutoUpdateChecking);
- await autoUpdater.checkForUpdates();
- } catch (error) {
- logger.error(`${AutoUpdateLogPrefix}: failed with an error`, error);
- }
-}
diff --git a/src/main/application-update/check-for-platform-updates/check-for-platform-updates.injectable.ts b/src/main/application-update/check-for-platform-updates/check-for-platform-updates.injectable.ts
new file mode 100644
index 0000000000..286999c484
--- /dev/null
+++ b/src/main/application-update/check-for-platform-updates/check-for-platform-updates.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 electronUpdaterInjectable from "../../electron-app/features/electron-updater.injectable";
+import type { UpdateChannel } from "../../../common/application-update/update-channels";
+import loggerInjectable from "../../../common/logger.injectable";
+import type { UpdateCheckResult } from "electron-updater";
+
+export type CheckForUpdatesResult = {
+ updateWasDiscovered: false;
+} | {
+ updateWasDiscovered: true;
+ version: string;
+};
+
+export type CheckForPlatformUpdates = (updateChannel: UpdateChannel, opts: { allowDowngrade: boolean }) => Promise;
+
+const checkForPlatformUpdatesInjectable = getInjectable({
+ id: "check-for-platform-updates",
+
+ instantiate: (di): CheckForPlatformUpdates => {
+ const electronUpdater = di.inject(electronUpdaterInjectable);
+ const logger = di.inject(loggerInjectable);
+
+ return async (updateChannel, { allowDowngrade }) => {
+ electronUpdater.channel = updateChannel.id;
+ electronUpdater.autoDownload = false;
+ electronUpdater.allowDowngrade = allowDowngrade;
+
+ let result: UpdateCheckResult;
+
+ try {
+ result = await electronUpdater.checkForUpdates();
+ } catch (error) {
+ logger.error("[UPDATE-APP/CHECK-FOR-UPDATES]", error);
+
+ return {
+ updateWasDiscovered: false,
+ };
+ }
+
+ const { updateInfo, cancellationToken } = result;
+
+ if (!cancellationToken) {
+ return {
+ updateWasDiscovered: false,
+ };
+ }
+
+ return {
+ updateWasDiscovered: true,
+ version: updateInfo.version,
+ };
+ };
+ },
+});
+
+export default checkForPlatformUpdatesInjectable;
diff --git a/src/main/application-update/check-for-platform-updates/check-for-platform-updates.test.ts b/src/main/application-update/check-for-platform-updates/check-for-platform-updates.test.ts
new file mode 100644
index 0000000000..b826a1a5a7
--- /dev/null
+++ b/src/main/application-update/check-for-platform-updates/check-for-platform-updates.test.ts
@@ -0,0 +1,128 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getDiForUnitTesting } from "../../getDiForUnitTesting";
+import electronUpdaterInjectable from "../../electron-app/features/electron-updater.injectable";
+import type { AsyncFnMock } from "@async-fn/jest";
+import asyncFn from "@async-fn/jest";
+import type { AppUpdater, UpdateCheckResult } from "electron-updater";
+import type { CheckForPlatformUpdates } from "./check-for-platform-updates.injectable";
+import checkForPlatformUpdatesInjectable from "./check-for-platform-updates.injectable";
+import type { UpdateChannel, UpdateChannelId } from "../../../common/application-update/update-channels";
+import { getPromiseStatus } from "../../../common/test-utils/get-promise-status";
+import loggerInjectable from "../../../common/logger.injectable";
+import type { Logger } from "../../../common/logger";
+
+describe("check-for-platform-updates", () => {
+ let checkForPlatformUpdates: CheckForPlatformUpdates;
+ let electronUpdaterFake: AppUpdater;
+ let checkForUpdatesMock: AsyncFnMock<() => UpdateCheckResult>;
+ let logErrorMock: jest.Mock;
+
+ beforeEach(() => {
+ const di = getDiForUnitTesting();
+
+ checkForUpdatesMock = asyncFn();
+
+ electronUpdaterFake = {
+ channel: undefined,
+ autoDownload: undefined,
+ allowDowngrade: undefined,
+
+ checkForUpdates: checkForUpdatesMock,
+ } as unknown as AppUpdater;
+
+ di.override(electronUpdaterInjectable, () => electronUpdaterFake);
+
+ logErrorMock = jest.fn();
+
+ di.override(loggerInjectable, () => ({ error: logErrorMock }) as unknown as Logger);
+
+ checkForPlatformUpdates = di.inject(checkForPlatformUpdatesInjectable);
+ });
+
+ describe("when called", () => {
+ let actualPromise: Promise;
+
+ beforeEach(() => {
+ const testUpdateChannel: UpdateChannel = {
+ id: "some-update-channel" as UpdateChannelId,
+ label: "Some update channel",
+ moreStableUpdateChannel: null,
+ };
+
+ actualPromise = checkForPlatformUpdates(testUpdateChannel, { allowDowngrade: true });
+ });
+
+ it("sets update channel", () => {
+ expect(electronUpdaterFake.channel).toBe("some-update-channel");
+ });
+
+ it("sets flag for allowing downgrade", () => {
+ expect(electronUpdaterFake.allowDowngrade).toBe(true);
+ });
+
+ it("disables auto downloading for being controlled", () => {
+ expect(electronUpdaterFake.autoDownload).toBe(false);
+ });
+
+ it("checks for updates", () => {
+ expect(checkForUpdatesMock).toHaveBeenCalled();
+ });
+
+ it("does not resolve yet", async () => {
+ const promiseStatus = await getPromiseStatus(actualPromise);
+
+ expect(promiseStatus.fulfilled).toBe(false);
+ });
+
+ it("when checking for updates resolves with update, resolves with the discovered update", async () => {
+ await checkForUpdatesMock.resolve({
+ updateInfo: {
+ version: "some-version",
+ },
+
+ cancellationToken: "some-cancellation-token",
+ } as unknown as UpdateCheckResult);
+
+ const actual = await actualPromise;
+
+ expect(actual).toEqual({ updateWasDiscovered: true, version: "some-version" });
+ });
+
+ it("when checking for updates resolves without update, resolves with update not being discovered", async () => {
+ await checkForUpdatesMock.resolve({
+ updateInfo: {
+ version: "some-version-that-matches-to-current-installed-version",
+ },
+
+ cancellationToken: null,
+ } as unknown as UpdateCheckResult);
+
+ const actual = await actualPromise;
+
+ expect(actual).toEqual({ updateWasDiscovered: false });
+ });
+
+ describe("when checking for updates rejects", () => {
+ let errorStub: Error;
+
+ beforeEach(() => {
+ errorStub = new Error("Some error");
+
+ checkForUpdatesMock.reject(errorStub);
+ });
+
+ it("logs errors", () => {
+ expect(logErrorMock).toHaveBeenCalledWith("[UPDATE-APP/CHECK-FOR-UPDATES]", errorStub);
+ });
+
+ it("resolves with update not being discovered", async () => {
+ const actual = await actualPromise;
+
+ expect(actual).toEqual({ updateWasDiscovered: false });
+ });
+ });
+ });
+});
diff --git a/src/main/application-update/check-for-updates-tray-item.injectable.ts b/src/main/application-update/check-for-updates-tray-item.injectable.ts
new file mode 100644
index 0000000000..5ff4be731a
--- /dev/null
+++ b/src/main/application-update/check-for-updates-tray-item.injectable.ts
@@ -0,0 +1,79 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getInjectable } from "@ogre-tools/injectable";
+import { computed } from "mobx";
+import updatingIsEnabledInjectable from "./updating-is-enabled.injectable";
+import { trayMenuItemInjectionToken } from "../tray/tray-menu-item/tray-menu-item-injection-token";
+import showApplicationWindowInjectable from "../start-main-application/lens-window/show-application-window.injectable";
+import discoveredUpdateVersionInjectable from "../../common/application-update/discovered-update-version/discovered-update-version.injectable";
+import updateIsBeingDownloadedInjectable from "../../common/application-update/update-is-being-downloaded/update-is-being-downloaded.injectable";
+import updatesAreBeingDiscoveredInjectable from "../../common/application-update/updates-are-being-discovered/updates-are-being-discovered.injectable";
+import progressOfUpdateDownloadInjectable from "../../common/application-update/progress-of-update-download/progress-of-update-download.injectable";
+import assert from "assert";
+import processCheckingForUpdatesInjectable from "./check-for-updates/process-checking-for-updates.injectable";
+import { withErrorSuppression } from "../../common/utils/with-error-suppression/with-error-suppression";
+import { pipeline } from "@ogre-tools/fp";
+import withErrorLoggingInjectable from "../../common/utils/with-error-logging/with-error-logging.injectable";
+
+const checkForUpdatesTrayItemInjectable = getInjectable({
+ id: "check-for-updates-tray-item",
+
+ instantiate: (di) => {
+ const showApplicationWindow = di.inject(showApplicationWindowInjectable);
+ const updatingIsEnabled = di.inject(updatingIsEnabledInjectable);
+ const progressOfUpdateDownload = di.inject(progressOfUpdateDownloadInjectable);
+ const discoveredVersionState = di.inject(discoveredUpdateVersionInjectable);
+ const downloadingUpdateState = di.inject(updateIsBeingDownloadedInjectable);
+ const checkingForUpdatesState = di.inject(updatesAreBeingDiscoveredInjectable);
+ const processCheckingForUpdates = di.inject(processCheckingForUpdatesInjectable);
+ const withErrorLoggingFor = di.inject(withErrorLoggingInjectable);
+
+ return {
+ id: "check-for-updates",
+ parentId: null,
+ orderNumber: 30,
+
+ label: computed(() => {
+ if (downloadingUpdateState.value.get()) {
+ const discoveredVersion = discoveredVersionState.value.get();
+
+ assert(discoveredVersion);
+
+ const roundedPercentage = Math.round(progressOfUpdateDownload.value.get().percentage);
+
+ return `Downloading update ${discoveredVersion.version} (${roundedPercentage}%)...`;
+ }
+
+ if (checkingForUpdatesState.value.get()) {
+ return "Checking for updates...";
+ }
+
+ return "Check for updates";
+ }),
+
+ enabled: computed(() => !checkingForUpdatesState.value.get() && !downloadingUpdateState.value.get()),
+
+ visible: computed(() => updatingIsEnabled),
+
+ click: pipeline(
+ async () => {
+ await processCheckingForUpdates();
+
+ await showApplicationWindow();
+ },
+
+ withErrorLoggingFor(() => "[TRAY]: Checking for updates failed."),
+
+ // TODO: Find out how to improve typing so that instead of
+ // x => withErrorSuppression(x) there could only be withErrorSuppression
+ (x) => withErrorSuppression(x),
+ ),
+ };
+ },
+
+ injectionToken: trayMenuItemInjectionToken,
+});
+
+export default checkForUpdatesTrayItemInjectable;
diff --git a/src/main/application-update/check-for-updates/broadcast-change-in-updating-status.injectable.ts b/src/main/application-update/check-for-updates/broadcast-change-in-updating-status.injectable.ts
new file mode 100644
index 0000000000..7e9257e966
--- /dev/null
+++ b/src/main/application-update/check-for-updates/broadcast-change-in-updating-status.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 { getInjectable } from "@ogre-tools/injectable";
+import type { ApplicationUpdateStatusChannelMessage } from "../../../common/application-update/application-update-status-channel.injectable";
+import { messageToChannelInjectionToken } from "../../../common/utils/channel/message-to-channel-injection-token";
+import applicationUpdateStatusChannelInjectable from "../../../common/application-update/application-update-status-channel.injectable";
+
+const broadcastChangeInUpdatingStatusInjectable = getInjectable({
+ id: "broadcast-change-in-updating-status",
+
+ instantiate: (di) => {
+ const messageToChannel = di.inject(messageToChannelInjectionToken);
+ const applicationUpdateStatusChannel = di.inject(applicationUpdateStatusChannelInjectable);
+
+ return (data: ApplicationUpdateStatusChannelMessage) => {
+ messageToChannel(applicationUpdateStatusChannel, data);
+ };
+ },
+});
+
+export default broadcastChangeInUpdatingStatusInjectable;
diff --git a/src/main/application-update/check-for-updates/check-for-updates-starting-from-channel.injectable.ts b/src/main/application-update/check-for-updates/check-for-updates-starting-from-channel.injectable.ts
new file mode 100644
index 0000000000..caf695ff80
--- /dev/null
+++ b/src/main/application-update/check-for-updates/check-for-updates-starting-from-channel.injectable.ts
@@ -0,0 +1,54 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getInjectable } from "@ogre-tools/injectable";
+import type { UpdateChannel } from "../../../common/application-update/update-channels";
+import checkForPlatformUpdatesInjectable from "../check-for-platform-updates/check-for-platform-updates.injectable";
+import updateCanBeDowngradedInjectable from "./update-can-be-downgraded.injectable";
+
+export type CheckForUpdatesFromChannelResult = {
+ updateWasDiscovered: false;
+} | {
+ updateWasDiscovered: true;
+ version: string;
+ actualUpdateChannel: UpdateChannel;
+};
+
+const checkForUpdatesStartingFromChannelInjectable = getInjectable({
+ id: "check-for-updates-starting-from-channel",
+
+ instantiate: (di) => {
+ const checkForPlatformUpdates = di.inject(
+ checkForPlatformUpdatesInjectable,
+ );
+
+ const updateCanBeDowngraded = di.inject(updateCanBeDowngradedInjectable);
+
+ const _recursiveCheck = async (
+ updateChannel: UpdateChannel,
+ ): Promise => {
+ const result = await checkForPlatformUpdates(updateChannel, {
+ allowDowngrade: updateCanBeDowngraded.get(),
+ });
+
+ if (result.updateWasDiscovered) {
+ return {
+ updateWasDiscovered: true,
+ version: result.version,
+ actualUpdateChannel: updateChannel,
+ };
+ }
+
+ if (updateChannel.moreStableUpdateChannel) {
+ return await _recursiveCheck(updateChannel.moreStableUpdateChannel);
+ }
+
+ return { updateWasDiscovered: false };
+ };
+
+ return _recursiveCheck;
+ },
+});
+
+export default checkForUpdatesStartingFromChannelInjectable;
diff --git a/src/main/application-update/check-for-updates/process-checking-for-updates.injectable.ts b/src/main/application-update/check-for-updates/process-checking-for-updates.injectable.ts
new file mode 100644
index 0000000000..2688d00d4a
--- /dev/null
+++ b/src/main/application-update/check-for-updates/process-checking-for-updates.injectable.ts
@@ -0,0 +1,93 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getInjectable } from "@ogre-tools/injectable";
+import selectedUpdateChannelInjectable from "../../../common/application-update/selected-update-channel/selected-update-channel.injectable";
+import updatesAreBeingDiscoveredInjectable from "../../../common/application-update/updates-are-being-discovered/updates-are-being-discovered.injectable";
+import discoveredUpdateVersionInjectable from "../../../common/application-update/discovered-update-version/discovered-update-version.injectable";
+import { runInAction } from "mobx";
+import askBooleanInjectable from "../../ask-boolean/ask-boolean.injectable";
+import quitAndInstallUpdateInjectable from "../../electron-app/features/quit-and-install-update.injectable";
+import downloadUpdateInjectable from "../download-update/download-update.injectable";
+import broadcastChangeInUpdatingStatusInjectable from "./broadcast-change-in-updating-status.injectable";
+import checkForUpdatesStartingFromChannelInjectable from "./check-for-updates-starting-from-channel.injectable";
+import withOrphanPromiseInjectable from "../../../common/utils/with-orphan-promise/with-orphan-promise.injectable";
+
+const processCheckingForUpdatesInjectable = getInjectable({
+ id: "process-checking-for-updates",
+
+ instantiate: (di) => {
+ const askBoolean = di.inject(askBooleanInjectable);
+ const quitAndInstallUpdate = di.inject(quitAndInstallUpdateInjectable);
+ const downloadUpdate = di.inject(downloadUpdateInjectable);
+ const selectedUpdateChannel = di.inject(selectedUpdateChannelInjectable);
+ const broadcastChangeInUpdatingStatus = di.inject(broadcastChangeInUpdatingStatusInjectable);
+ const checkingForUpdatesState = di.inject(updatesAreBeingDiscoveredInjectable);
+ const discoveredVersionState = di.inject(discoveredUpdateVersionInjectable);
+ const checkForUpdatesStartingFromChannel = di.inject(checkForUpdatesStartingFromChannelInjectable);
+ const withOrphanPromise = di.inject(withOrphanPromiseInjectable);
+
+ return async () => {
+ broadcastChangeInUpdatingStatus({ eventId: "checking-for-updates" });
+
+ runInAction(() => {
+ checkingForUpdatesState.set(true);
+ });
+
+ const result = await checkForUpdatesStartingFromChannel(selectedUpdateChannel.value.get());
+
+ if (!result.updateWasDiscovered) {
+ broadcastChangeInUpdatingStatus({ eventId: "no-updates-available" });
+
+ runInAction(() => {
+ discoveredVersionState.set(null);
+ checkingForUpdatesState.set(false);
+ });
+
+ return;
+ }
+
+ const { version, actualUpdateChannel } = result;
+
+ broadcastChangeInUpdatingStatus({
+ eventId: "download-for-update-started",
+ version,
+ });
+
+ runInAction(() => {
+ discoveredVersionState.set({
+ version,
+ updateChannel: actualUpdateChannel,
+ });
+
+ checkingForUpdatesState.set(false);
+ });
+
+ withOrphanPromise(async () => {
+ const { downloadWasSuccessful } = await downloadUpdate();
+
+ if (!downloadWasSuccessful) {
+ broadcastChangeInUpdatingStatus({
+ eventId: "download-for-update-failed",
+ });
+
+ return;
+ }
+
+ const userWantsToInstallUpdate = await askBoolean({
+ title: "Update Available",
+
+ question: `Version ${version} of Lens IDE is available and ready to be installed. Would you like to update now?\n\n` +
+ `Lens should restart automatically, if it doesn't please restart manually. Installed extensions might require updating.`,
+ });
+
+ if (userWantsToInstallUpdate) {
+ quitAndInstallUpdate();
+ }
+ })();
+ };
+ },
+});
+
+export default processCheckingForUpdatesInjectable;
diff --git a/src/main/application-update/check-for-updates/update-can-be-downgraded.injectable.ts b/src/main/application-update/check-for-updates/update-can-be-downgraded.injectable.ts
new file mode 100644
index 0000000000..72ea5c4023
--- /dev/null
+++ b/src/main/application-update/check-for-updates/update-can-be-downgraded.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 { computed } from "mobx";
+import selectedUpdateChannelInjectable from "../../../common/application-update/selected-update-channel/selected-update-channel.injectable";
+import appVersionInjectable from "../../../common/get-configuration-file-model/app-version/app-version.injectable";
+import { SemVer } from "semver";
+
+const updateCanBeDowngradedInjectable = getInjectable({
+ id: "update-can-be-downgraded",
+
+ instantiate: (di) => {
+ const selectedUpdateChannel = di.inject(selectedUpdateChannelInjectable);
+ const appVersion = di.inject(appVersionInjectable);
+
+ return computed(() => {
+ const semVer = new SemVer(appVersion);
+
+ return (
+ semVer.prerelease[0] !==
+ selectedUpdateChannel.value.get().id
+ );
+ });
+ },
+});
+
+export default updateCanBeDowngradedInjectable;
diff --git a/src/main/application-update/download-platform-update/download-platform-update.injectable.ts b/src/main/application-update/download-platform-update/download-platform-update.injectable.ts
new file mode 100644
index 0000000000..374efd2caf
--- /dev/null
+++ b/src/main/application-update/download-platform-update/download-platform-update.injectable.ts
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getInjectable } from "@ogre-tools/injectable";
+import electronUpdaterInjectable from "../../electron-app/features/electron-updater.injectable";
+import loggerInjectable from "../../../common/logger.injectable";
+import type { ProgressInfo } from "electron-updater";
+import type { ProgressOfDownload } from "../../../common/application-update/progress-of-update-download/progress-of-update-download.injectable";
+
+export type DownloadPlatformUpdate = (
+ onDownloadProgress: (arg: ProgressOfDownload) => void
+) => Promise<{ downloadWasSuccessful: boolean }>;
+
+const downloadPlatformUpdateInjectable = getInjectable({
+ id: "download-platform-update",
+
+ instantiate: (di): DownloadPlatformUpdate => {
+ const electronUpdater = di.inject(electronUpdaterInjectable);
+ const logger = di.inject(loggerInjectable);
+
+ return async (onDownloadProgress) => {
+ onDownloadProgress({ percentage: 0 });
+
+ const updateDownloadProgress = ({ percent: percentage }: ProgressInfo) =>
+ onDownloadProgress({ percentage });
+
+ electronUpdater.on("download-progress", updateDownloadProgress);
+
+ try {
+ await electronUpdater.downloadUpdate();
+ } catch(error) {
+ logger.error("[UPDATE-APP/DOWNLOAD]", error);
+
+ return { downloadWasSuccessful: false };
+ } finally {
+ electronUpdater.off("download-progress", updateDownloadProgress);
+ }
+
+ return { downloadWasSuccessful: true };
+ };
+ },
+});
+
+export default downloadPlatformUpdateInjectable;
diff --git a/src/main/application-update/download-platform-update/download-platform-update.test.ts b/src/main/application-update/download-platform-update/download-platform-update.test.ts
new file mode 100644
index 0000000000..04507be980
--- /dev/null
+++ b/src/main/application-update/download-platform-update/download-platform-update.test.ts
@@ -0,0 +1,155 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getDiForUnitTesting } from "../../getDiForUnitTesting";
+import electronUpdaterInjectable from "../../electron-app/features/electron-updater.injectable";
+import type { DownloadPlatformUpdate } from "./download-platform-update.injectable";
+import downloadPlatformUpdateInjectable from "./download-platform-update.injectable";
+import type { AppUpdater } from "electron-updater";
+import type { AsyncFnMock } from "@async-fn/jest";
+import asyncFn from "@async-fn/jest";
+import { getPromiseStatus } from "../../../common/test-utils/get-promise-status";
+import type { DiContainer } from "@ogre-tools/injectable";
+import loggerInjectable from "../../../common/logger.injectable";
+import type { Logger } from "../../../common/logger";
+
+describe("download-platform-update", () => {
+ let downloadPlatformUpdate: DownloadPlatformUpdate;
+ let downloadUpdateMock: AsyncFnMock<() => void>;
+ let electronUpdaterFake: AppUpdater;
+ let electronUpdaterOnMock: jest.Mock;
+ let electronUpdaterOffMock: jest.Mock;
+ let di: DiContainer;
+ let logErrorMock: jest.Mock;
+
+ beforeEach(() => {
+ di = getDiForUnitTesting();
+
+ downloadUpdateMock = asyncFn();
+ electronUpdaterOnMock = jest.fn();
+ electronUpdaterOffMock = jest.fn();
+
+ electronUpdaterFake = {
+ channel: undefined,
+ autoDownload: undefined,
+
+ on: electronUpdaterOnMock,
+ off: electronUpdaterOffMock,
+
+ downloadUpdate: downloadUpdateMock,
+ } as unknown as AppUpdater;
+
+ di.override(electronUpdaterInjectable, () => electronUpdaterFake);
+
+ logErrorMock = jest.fn();
+ di.override(loggerInjectable, () => ({ error: logErrorMock }) as unknown as Logger);
+
+ downloadPlatformUpdate = di.inject(downloadPlatformUpdateInjectable);
+ });
+
+ describe("when called", () => {
+ let actualPromise: Promise<{ downloadWasSuccessful: boolean }>;
+ let onDownloadProgressMock: jest.Mock;
+
+ beforeEach(() => {
+ onDownloadProgressMock = jest.fn();
+
+ actualPromise = downloadPlatformUpdate(onDownloadProgressMock);
+ });
+
+ it("calls for downloading of update", () => {
+ expect(downloadUpdateMock).toHaveBeenCalled();
+ });
+
+ it("does not resolve yet", async () => {
+ const promiseStatus = await getPromiseStatus(actualPromise);
+
+ expect(promiseStatus.fulfilled).toBe(false);
+ });
+
+ it("starts progress of download from 0", () => {
+ expect(onDownloadProgressMock).toHaveBeenCalledWith({ percentage: 0 });
+ });
+
+ describe("when downloading progresses", () => {
+ beforeEach(() => {
+ onDownloadProgressMock.mockClear();
+
+ const [, callback] = electronUpdaterOnMock.mock.calls.find(
+ ([event]) => event === "download-progress",
+ );
+
+ callback({
+ percent: 42,
+ total: 0,
+ delta: 0,
+ transferred: 0,
+ bytesPerSecond: 0,
+ });
+ });
+
+ it("updates progress of the download", () => {
+ expect(onDownloadProgressMock).toHaveBeenCalledWith({ percentage: 42 });
+ });
+
+ describe("when downloading resolves", () => {
+ beforeEach(async () => {
+ onDownloadProgressMock.mockClear();
+
+ await downloadUpdateMock.resolve();
+ });
+
+ it("resolves with success", async () => {
+ const actual = await actualPromise;
+
+ expect(actual).toEqual({ downloadWasSuccessful: true });
+ });
+
+ it("does not reset progress of download yet", () => {
+ expect(onDownloadProgressMock).not.toHaveBeenCalled();
+ });
+
+ it("stops watching for download progress", () => {
+ expect(electronUpdaterOffMock).toHaveBeenCalledWith(
+ "download-progress",
+ expect.any(Function),
+ );
+ });
+
+ it("when starting download again, resets progress of download", () => {
+ downloadPlatformUpdate(onDownloadProgressMock);
+
+ expect(onDownloadProgressMock).toHaveBeenCalledWith({ percentage: 0 });
+ });
+ });
+
+ describe("when downloading rejects", () => {
+ let errorStub: Error;
+
+ beforeEach(() => {
+ errorStub = new Error("Some error");
+
+ downloadUpdateMock.reject(errorStub);
+ });
+
+ it("logs error", () => {
+ expect(logErrorMock).toHaveBeenCalledWith("[UPDATE-APP/DOWNLOAD]", errorStub);
+ });
+
+ it("stops watching for download progress", () => {
+ expect(electronUpdaterOffMock).toHaveBeenCalledWith(
+ "download-progress",
+ expect.any(Function),
+ );
+ });
+
+ it("resolves with failure", async () => {
+ const actual = await actualPromise;
+
+ expect(actual).toEqual({ downloadWasSuccessful: false });
+ });
+ });
+ });
+ });
+});
diff --git a/src/main/application-update/download-update/download-update.injectable.ts b/src/main/application-update/download-update/download-update.injectable.ts
new file mode 100644
index 0000000000..d0ac141b9c
--- /dev/null
+++ b/src/main/application-update/download-update/download-update.injectable.ts
@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getInjectable } from "@ogre-tools/injectable";
+import downloadPlatformUpdateInjectable from "../download-platform-update/download-platform-update.injectable";
+import updateIsBeingDownloadedInjectable from "../../../common/application-update/update-is-being-downloaded/update-is-being-downloaded.injectable";
+import discoveredUpdateVersionInjectable from "../../../common/application-update/discovered-update-version/discovered-update-version.injectable";
+import { action, runInAction } from "mobx";
+import type { ProgressOfDownload } from "../../../common/application-update/progress-of-update-download/progress-of-update-download.injectable";
+import progressOfUpdateDownloadInjectable from "../../../common/application-update/progress-of-update-download/progress-of-update-download.injectable";
+
+const downloadUpdateInjectable = getInjectable({
+ id: "download-update",
+
+ instantiate: (di) => {
+ const downloadPlatformUpdate = di.inject(downloadPlatformUpdateInjectable);
+ const downloadingUpdateState = di.inject(updateIsBeingDownloadedInjectable);
+ const discoveredVersionState = di.inject(discoveredUpdateVersionInjectable);
+ const progressOfUpdateDownload = di.inject(progressOfUpdateDownloadInjectable);
+
+ const updateDownloadProgress = action((progressOfDownload: ProgressOfDownload) => {
+ progressOfUpdateDownload.set(progressOfDownload);
+ });
+
+ return async () => {
+ runInAction(() => {
+ progressOfUpdateDownload.set({ percentage: 0 });
+ downloadingUpdateState.set(true);
+ });
+
+ const { downloadWasSuccessful } = await downloadPlatformUpdate(
+ updateDownloadProgress,
+ );
+
+ runInAction(() => {
+ if (!downloadWasSuccessful) {
+ discoveredVersionState.set(null);
+ }
+
+ downloadingUpdateState.set(false);
+ });
+
+ return { downloadWasSuccessful };
+ };
+ },
+});
+
+export default downloadUpdateInjectable;
diff --git a/src/main/application-update/install-application-update-tray-item.injectable.ts b/src/main/application-update/install-application-update-tray-item.injectable.ts
new file mode 100644
index 0000000000..2f938964f8
--- /dev/null
+++ b/src/main/application-update/install-application-update-tray-item.injectable.ts
@@ -0,0 +1,56 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getInjectable } from "@ogre-tools/injectable";
+import { computed } from "mobx";
+import { trayMenuItemInjectionToken } from "../tray/tray-menu-item/tray-menu-item-injection-token";
+import quitAndInstallUpdateInjectable from "../electron-app/features/quit-and-install-update.injectable";
+import discoveredUpdateVersionInjectable from "../../common/application-update/discovered-update-version/discovered-update-version.injectable";
+import updateIsBeingDownloadedInjectable from "../../common/application-update/update-is-being-downloaded/update-is-being-downloaded.injectable";
+import { withErrorSuppression } from "../../common/utils/with-error-suppression/with-error-suppression";
+import { pipeline } from "@ogre-tools/fp";
+import withErrorLoggingInjectable from "../../common/utils/with-error-logging/with-error-logging.injectable";
+
+const installApplicationUpdateTrayItemInjectable = getInjectable({
+ id: "install-update-tray-item",
+
+ instantiate: (di) => {
+ const quitAndInstallUpdate = di.inject(quitAndInstallUpdateInjectable);
+ const discoveredVersionState = di.inject(discoveredUpdateVersionInjectable);
+ const downloadingUpdateState = di.inject(updateIsBeingDownloadedInjectable);
+ const withErrorLoggingFor = di.inject(withErrorLoggingInjectable);
+
+ return {
+ id: "install-update",
+ parentId: null,
+ orderNumber: 50,
+
+ label: computed(() => {
+ const versionToBeInstalled = discoveredVersionState.value.get()?.version;
+
+ return `Install update ${versionToBeInstalled}`;
+ }),
+
+ enabled: computed(() => true),
+
+ visible: computed(
+ () => !!discoveredVersionState.value.get() && !downloadingUpdateState.value.get(),
+ ),
+
+ click: pipeline(
+ quitAndInstallUpdate,
+
+ withErrorLoggingFor(() => "[TRAY]: Update installation failed."),
+
+ // TODO: Find out how to improve typing so that instead of
+ // x => withErrorSuppression(x) there could only be withErrorSuppression
+ (x) => withErrorSuppression(x),
+ ),
+ };
+ },
+
+ injectionToken: trayMenuItemInjectionToken,
+});
+
+export default installApplicationUpdateTrayItemInjectable;
diff --git a/src/main/application-update/periodical-check-for-updates/periodical-check-for-updates.injectable.ts b/src/main/application-update/periodical-check-for-updates/periodical-check-for-updates.injectable.ts
new file mode 100644
index 0000000000..394383ee65
--- /dev/null
+++ b/src/main/application-update/periodical-check-for-updates/periodical-check-for-updates.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 processCheckingForUpdatesInjectable from "../check-for-updates/process-checking-for-updates.injectable";
+
+const periodicalCheckForUpdatesInjectable = getInjectable({
+ id: "periodical-check-for-updates",
+
+ instantiate: (di) => {
+ const processCheckingForUpdates = di.inject(processCheckingForUpdatesInjectable);
+
+ return getStartableStoppable("periodical-check-for-updates", () => {
+ const TWO_HOURS = 1000 * 60 * 60 * 2;
+
+ // Note: intentional orphan promise to make checking for updates happen in the background
+ processCheckingForUpdates();
+
+ const intervalId = setInterval(() => {
+ // Note: intentional orphan promise to make checking for updates happen in the background
+ processCheckingForUpdates();
+ }, TWO_HOURS);
+
+ return () => {
+ clearInterval(intervalId);
+ };
+ });
+ },
+
+ causesSideEffects: true,
+});
+
+export default periodicalCheckForUpdatesInjectable;
diff --git a/src/main/application-update/periodical-check-for-updates/start-checking-for-updates.injectable.ts b/src/main/application-update/periodical-check-for-updates/start-checking-for-updates.injectable.ts
new file mode 100644
index 0000000000..9a9b9cf206
--- /dev/null
+++ b/src/main/application-update/periodical-check-for-updates/start-checking-for-updates.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 periodicalCheckForUpdatesInjectable from "./periodical-check-for-updates.injectable";
+import { afterRootFrameIsReadyInjectionToken } from "../../start-main-application/runnable-tokens/after-root-frame-is-ready-injection-token";
+import updatingIsEnabledInjectable from "../updating-is-enabled.injectable";
+
+const startCheckingForUpdatesInjectable = getInjectable({
+ id: "start-checking-for-updates",
+
+ instantiate: (di) => {
+ const periodicalCheckForUpdates = di.inject(periodicalCheckForUpdatesInjectable);
+ const updatingIsEnabled = di.inject(updatingIsEnabledInjectable);
+
+ return {
+ run: async () => {
+ if (updatingIsEnabled) {
+ await periodicalCheckForUpdates.start();
+ }
+ },
+ };
+ },
+
+ injectionToken: afterRootFrameIsReadyInjectionToken,
+});
+
+export default startCheckingForUpdatesInjectable;
diff --git a/src/main/application-update/periodical-check-for-updates/stop-checking-for-updates.injectable.ts b/src/main/application-update/periodical-check-for-updates/stop-checking-for-updates.injectable.ts
new file mode 100644
index 0000000000..13aefe4d96
--- /dev/null
+++ b/src/main/application-update/periodical-check-for-updates/stop-checking-for-updates.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 { beforeQuitOfFrontEndInjectionToken } from "../../start-main-application/runnable-tokens/before-quit-of-front-end-injection-token";
+import periodicalCheckForUpdatesInjectable from "./periodical-check-for-updates.injectable";
+
+const stopCheckingForUpdatesInjectable = getInjectable({
+ id: "stop-checking-for-updates",
+
+ instantiate: (di) => {
+ const periodicalCheckForUpdates = di.inject(periodicalCheckForUpdatesInjectable);
+
+ return {
+ run: async () => {
+ if (periodicalCheckForUpdates.started) {
+ await periodicalCheckForUpdates.stop();
+ }
+ },
+ };
+ },
+
+ injectionToken: beforeQuitOfFrontEndInjectionToken,
+});
+
+export default stopCheckingForUpdatesInjectable;
diff --git a/src/main/application-update/publish-is-configured.injectable.ts b/src/main/application-update/publish-is-configured.injectable.ts
new file mode 100644
index 0000000000..321adc8a22
--- /dev/null
+++ b/src/main/application-update/publish-is-configured.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 packageJsonInjectable from "../../common/vars/package-json.injectable";
+import { has } from "lodash/fp";
+
+// TOOO: Rename to something less technical
+const publishIsConfiguredInjectable = getInjectable({
+ id: "publish-is-configured",
+
+ instantiate: (di) => {
+ const packageJson = di.inject(packageJsonInjectable);
+
+ return has("build.publish", packageJson);
+ },
+});
+
+export default publishIsConfiguredInjectable;
diff --git a/src/main/application-update/updating-is-enabled.injectable.ts b/src/main/application-update/updating-is-enabled.injectable.ts
new file mode 100644
index 0000000000..df5e264219
--- /dev/null
+++ b/src/main/application-update/updating-is-enabled.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 electronUpdaterIsActiveInjectable from "../electron-app/features/electron-updater-is-active.injectable";
+import publishIsConfiguredInjectable from "./publish-is-configured.injectable";
+
+const updatingIsEnabledInjectable = getInjectable({
+ id: "updating-is-enabled",
+
+ instantiate: (di) => {
+ const electronUpdaterIsActive = di.inject(electronUpdaterIsActiveInjectable);
+ const publishIsConfigured = di.inject(publishIsConfiguredInjectable);
+
+ return electronUpdaterIsActive && publishIsConfigured;
+ },
+});
+
+export default updatingIsEnabledInjectable;
diff --git a/src/main/application-update/watch-if-update-should-happen-on-quit/start-watching-if-update-should-happen-on-quit.injectable.ts b/src/main/application-update/watch-if-update-should-happen-on-quit/start-watching-if-update-should-happen-on-quit.injectable.ts
new file mode 100644
index 0000000000..ef31cf5db5
--- /dev/null
+++ b/src/main/application-update/watch-if-update-should-happen-on-quit/start-watching-if-update-should-happen-on-quit.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 watchIfUpdateShouldHappenOnQuitInjectable from "./watch-if-update-should-happen-on-quit.injectable";
+
+const startWatchingIfUpdateShouldHappenOnQuitInjectable = getInjectable({
+ id: "start-watching-if-update-should-happen-on-quit",
+
+ instantiate: (di) => {
+ const watchIfUpdateShouldHappenOnQuit = di.inject(watchIfUpdateShouldHappenOnQuitInjectable);
+
+ return {
+ run: () => {
+ watchIfUpdateShouldHappenOnQuit.start();
+ },
+ };
+ },
+
+ injectionToken: onLoadOfApplicationInjectionToken,
+});
+
+export default startWatchingIfUpdateShouldHappenOnQuitInjectable;
diff --git a/src/main/application-update/watch-if-update-should-happen-on-quit/stop-watching-if-update-should-happen-on-quit.injectable.ts b/src/main/application-update/watch-if-update-should-happen-on-quit/stop-watching-if-update-should-happen-on-quit.injectable.ts
new file mode 100644
index 0000000000..b66cf927f2
--- /dev/null
+++ b/src/main/application-update/watch-if-update-should-happen-on-quit/stop-watching-if-update-should-happen-on-quit.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 watchIfUpdateShouldHappenOnQuitInjectable from "./watch-if-update-should-happen-on-quit.injectable";
+import { beforeQuitOfBackEndInjectionToken } from "../../start-main-application/runnable-tokens/before-quit-of-back-end-injection-token";
+
+const stopWatchingIfUpdateShouldHappenOnQuitInjectable = getInjectable({
+ id: "stop-watching-if-update-should-happen-on-quit",
+
+ instantiate: (di) => {
+ const watchIfUpdateShouldHappenOnQuit = di.inject(watchIfUpdateShouldHappenOnQuitInjectable);
+
+ return {
+ run: () => {
+ watchIfUpdateShouldHappenOnQuit.stop();
+ },
+ };
+ },
+
+ injectionToken: beforeQuitOfBackEndInjectionToken,
+});
+
+export default stopWatchingIfUpdateShouldHappenOnQuitInjectable;
diff --git a/src/main/application-update/watch-if-update-should-happen-on-quit/watch-if-update-should-happen-on-quit.injectable.ts b/src/main/application-update/watch-if-update-should-happen-on-quit/watch-if-update-should-happen-on-quit.injectable.ts
new file mode 100644
index 0000000000..12ec2d7c6e
--- /dev/null
+++ b/src/main/application-update/watch-if-update-should-happen-on-quit/watch-if-update-should-happen-on-quit.injectable.ts
@@ -0,0 +1,54 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getInjectable } from "@ogre-tools/injectable";
+import { autorun } from "mobx";
+import { getStartableStoppable } from "../../../common/utils/get-startable-stoppable";
+import setUpdateOnQuitInjectable from "../../electron-app/features/set-update-on-quit.injectable";
+import selectedUpdateChannelInjectable from "../../../common/application-update/selected-update-channel/selected-update-channel.injectable";
+import type { UpdateChannel } from "../../../common/application-update/update-channels";
+import discoveredUpdateVersionInjectable from "../../../common/application-update/discovered-update-version/discovered-update-version.injectable";
+
+const watchIfUpdateShouldHappenOnQuitInjectable = getInjectable({
+ id: "watch-if-update-should-happen-on-quit",
+
+ instantiate: (di) => {
+ const setUpdateOnQuit = di.inject(setUpdateOnQuitInjectable);
+ const selectedUpdateChannel = di.inject(selectedUpdateChannelInjectable);
+ const discoveredVersionState = di.inject(discoveredUpdateVersionInjectable);
+
+ return getStartableStoppable("watch-if-update-should-happen-on-quit", () =>
+ autorun(() => {
+ const sufficientlyStableUpdateChannels =
+ getSufficientlyStableUpdateChannels(selectedUpdateChannel.value.get());
+
+ const discoveredVersion = discoveredVersionState.value.get();
+
+ const updateIsDiscoveredFromChannel = discoveredVersion?.updateChannel;
+
+ const updateOnQuit = updateIsDiscoveredFromChannel
+ ? sufficientlyStableUpdateChannels.includes(
+ updateIsDiscoveredFromChannel,
+ )
+ : false;
+
+ setUpdateOnQuit(updateOnQuit);
+ }),
+ );
+ },
+});
+
+const getSufficientlyStableUpdateChannels = (updateChannel: UpdateChannel): UpdateChannel[] => {
+ if (!updateChannel.moreStableUpdateChannel) {
+ return [updateChannel];
+ }
+
+ return [
+ updateChannel,
+
+ ...getSufficientlyStableUpdateChannels(updateChannel.moreStableUpdateChannel),
+ ];
+};
+
+export default watchIfUpdateShouldHappenOnQuitInjectable;
diff --git a/src/main/ask-boolean/__snapshots__/ask-boolean.test.ts.snap b/src/main/ask-boolean/__snapshots__/ask-boolean.test.ts.snap
new file mode 100644
index 0000000000..8176698168
--- /dev/null
+++ b/src/main/ask-boolean/__snapshots__/ask-boolean.test.ts.snap
@@ -0,0 +1,336 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ask-boolean given started when asking multiple questions renders 1`] = `
+
+
+
+
+
+
+
+ info_outline
+
+
+
+
+
+
+ some-title
+
+
+ Some question
+
+
+
+
+
+
+
+
+
+
+ close
+
+
+
+
+
+
+
+
+ info_outline
+
+
+
+
+
+
+ some-other-title
+
+
+ Some other question
+
+
+
+
+
+
+
+
+
+
+ close
+
+
+
+
+
+
+
+`;
+
+exports[`ask-boolean given started when asking multiple questions when answering to first question renders 1`] = `
+
+
+
+
+
+
+
+ info_outline
+
+
+
+
+
+
+ some-other-title
+
+
+ Some other question
+
+
+
+
+
+
+
+
+
+
+ close
+
+
+
+
+
+
+
+`;
+
+exports[`ask-boolean given started when asking question renders 1`] = `
+
+
+
+
+
+
+
+ info_outline
+
+
+
+
+
+
+ some-title
+
+
+ Some question
+
+
+
+
+
+
+
+
+
+
+ close
+
+
+
+
+
+
+
+`;
+
+exports[`ask-boolean given started when asking question when user answers "no" renders 1`] = `
+
+
+
+
+
+`;
+
+exports[`ask-boolean given started when asking question when user answers "yes" renders 1`] = `
+
+
+
+
+
+`;
+
+exports[`ask-boolean given started when asking question when user closes notification without answering the question renders 1`] = `
+
+
+
+
+
+`;
diff --git a/src/main/ask-boolean/ask-boolean-answer-channel-listener.injectable.ts b/src/main/ask-boolean/ask-boolean-answer-channel-listener.injectable.ts
new file mode 100644
index 0000000000..06bc3972eb
--- /dev/null
+++ b/src/main/ask-boolean/ask-boolean-answer-channel-listener.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 type { AskBooleanAnswerChannel } from "../../common/ask-boolean/ask-boolean-answer-channel.injectable";
+import askBooleanAnswerChannelInjectable from "../../common/ask-boolean/ask-boolean-answer-channel.injectable";
+import askBooleanPromiseInjectable from "./ask-boolean-promise.injectable";
+import type { MessageChannelListener } from "../../common/utils/channel/message-channel-listener-injection-token";
+import { messageChannelListenerInjectionToken } from "../../common/utils/channel/message-channel-listener-injection-token";
+
+
+const askBooleanAnswerChannelListenerInjectable = getInjectable({
+ id: "ask-boolean-answer-channel-listener",
+
+ instantiate: (di): MessageChannelListener => ({
+ channel: di.inject(askBooleanAnswerChannelInjectable),
+
+ handler: ({ id, value }) => {
+ const answerPromise = di.inject(askBooleanPromiseInjectable, id);
+
+ answerPromise.resolve(value);
+ },
+ }),
+
+ injectionToken: messageChannelListenerInjectionToken,
+});
+
+export default askBooleanAnswerChannelListenerInjectable;
diff --git a/src/main/ask-boolean/ask-boolean-promise.injectable.ts b/src/main/ask-boolean/ask-boolean-promise.injectable.ts
new file mode 100644
index 0000000000..827714084f
--- /dev/null
+++ b/src/main/ask-boolean/ask-boolean-promise.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";
+
+const askBooleanPromiseInjectable = getInjectable({
+ id: "ask-boolean-promise",
+
+ instantiate: (di, questionId: string) => {
+ void questionId;
+
+ let resolve: (value: boolean) => void;
+
+ const promise = new Promise(_resolve => {
+ resolve = _resolve;
+ });
+
+ return ({
+ promise,
+
+ resolve: (value: boolean) => {
+ resolve(value);
+ },
+ });
+ },
+
+ lifecycle: lifecycleEnum.keyedSingleton({
+ getInstanceKey: (di, questionId: string) => questionId,
+ }),
+});
+
+export default askBooleanPromiseInjectable;
diff --git a/src/main/ask-boolean/ask-boolean.injectable.ts b/src/main/ask-boolean/ask-boolean.injectable.ts
new file mode 100644
index 0000000000..1fa54629b2
--- /dev/null
+++ b/src/main/ask-boolean/ask-boolean.injectable.ts
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getInjectable } from "@ogre-tools/injectable";
+import { messageToChannelInjectionToken } from "../../common/utils/channel/message-to-channel-injection-token";
+import askBooleanQuestionChannelInjectable from "../../common/ask-boolean/ask-boolean-question-channel.injectable";
+import askBooleanPromiseInjectable from "./ask-boolean-promise.injectable";
+import getRandomIdInjectable from "../../common/utils/get-random-id.injectable";
+
+export type AskBoolean = ({
+ title,
+ question,
+}: {
+ title: string;
+ question: string;
+}) => Promise;
+
+const askBooleanInjectable = getInjectable({
+ id: "ask-boolean",
+
+ instantiate: (di): AskBoolean => {
+ const messageToChannel = di.inject(messageToChannelInjectionToken);
+ const askBooleanChannel = di.inject(askBooleanQuestionChannelInjectable);
+ const getRandomId = di.inject(getRandomIdInjectable);
+
+ return async ({ title, question }) => {
+ const id = getRandomId();
+
+ const returnValuePromise = di.inject(askBooleanPromiseInjectable, id);
+
+ await messageToChannel(askBooleanChannel, { id, title, question });
+
+ return await returnValuePromise.promise;
+ };
+ },
+});
+
+export default askBooleanInjectable;
diff --git a/src/main/ask-boolean/ask-boolean.test.ts b/src/main/ask-boolean/ask-boolean.test.ts
new file mode 100644
index 0000000000..79fecdceca
--- /dev/null
+++ b/src/main/ask-boolean/ask-boolean.test.ts
@@ -0,0 +1,206 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
+import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
+import type { AskBoolean } from "./ask-boolean.injectable";
+import askBooleanInjectable from "./ask-boolean.injectable";
+import { getPromiseStatus } from "../../common/test-utils/get-promise-status";
+import type { RenderResult } from "@testing-library/react";
+import { fireEvent } from "@testing-library/react";
+import getRandomIdInjectable from "../../common/utils/get-random-id.injectable";
+
+describe("ask-boolean", () => {
+ let applicationBuilder: ApplicationBuilder;
+ let askBoolean: AskBoolean;
+
+ beforeEach(() => {
+ applicationBuilder = getApplicationBuilder();
+
+ const getRandomIdFake = jest
+ .fn()
+ .mockReturnValueOnce("some-random-id-1")
+ .mockReturnValueOnce("some-random-id-2");
+
+ applicationBuilder.dis.mainDi.override(getRandomIdInjectable, () => getRandomIdFake);
+
+ askBoolean = applicationBuilder.dis.mainDi.inject(askBooleanInjectable);
+ });
+
+ describe("given started", () => {
+ let rendered: RenderResult;
+
+ beforeEach(async () => {
+ rendered = await applicationBuilder.render();
+ });
+
+ describe("when asking question", () => {
+ let actualPromise: Promise;
+
+ beforeEach(() => {
+ actualPromise = askBoolean({
+ title: "some-title",
+ question: "Some question",
+ });
+ });
+
+ it("does not resolve yet", async () => {
+ const promiseStatus = await getPromiseStatus(actualPromise);
+
+ expect(promiseStatus.fulfilled).toBe(false);
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("shows notification", () => {
+ const notification = rendered.getByTestId("ask-boolean-some-random-id-1");
+
+ expect(notification).not.toBeUndefined();
+ });
+
+ describe('when user answers "yes"', () => {
+ beforeEach(() => {
+ const button = rendered.getByTestId("ask-boolean-some-random-id-1-button-yes");
+
+ fireEvent.click(button);
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("does not show notification anymore", () => {
+ const notification = rendered.queryByTestId("ask-boolean-some-random-id-1");
+
+ expect(notification).toBeNull();
+ });
+
+ it("resolves", async () => {
+ const actual = await actualPromise;
+
+ expect(actual).toBe(true);
+ });
+ });
+
+ describe('when user answers "no"', () => {
+ beforeEach(() => {
+ const button = rendered.getByTestId("ask-boolean-some-random-id-1-button-no");
+
+ fireEvent.click(button);
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("does not show notification anymore", () => {
+ const notification = rendered.queryByTestId("ask-boolean-some-random-id-1");
+
+ expect(notification).toBeNull();
+ });
+
+ it("resolves", async () => {
+ const actual = await actualPromise;
+
+ expect(actual).toBe(false);
+ });
+ });
+
+ describe("when user closes notification without answering the question", () => {
+ beforeEach(() => {
+ const button = rendered.getByTestId("close-notification-for-ask-boolean-for-some-random-id-1");
+
+ fireEvent.click(button);
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("does not show notification anymore", () => {
+ const notification = rendered.queryByTestId("ask-boolean-some-random-id-1");
+
+ expect(notification).toBeNull();
+ });
+
+ it("resolves", async () => {
+ const actual = await actualPromise;
+
+ expect(actual).toBe(false);
+ });
+ });
+ });
+
+ describe("when asking multiple questions", () => {
+ let firstQuestionPromise: Promise;
+ let secondQuestionPromise: Promise;
+
+ beforeEach(() => {
+ firstQuestionPromise = askBoolean({
+ title: "some-title",
+ question: "Some question",
+ });
+
+ secondQuestionPromise = askBoolean({
+ title: "some-other-title",
+ question: "Some other question",
+ });
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("shows notification for first question", () => {
+ const notification = rendered.getByTestId("ask-boolean-some-random-id-1");
+
+ expect(notification).not.toBeUndefined();
+ });
+
+ it("shows notification for second question", () => {
+ const notification = rendered.getByTestId("ask-boolean-some-random-id-2");
+
+ expect(notification).not.toBeUndefined();
+ });
+
+ describe("when answering to first question", () => {
+ beforeEach(() => {
+ const button = rendered.getByTestId("ask-boolean-some-random-id-1-button-no");
+
+ fireEvent.click(button);
+ });
+
+ it("renders", () => {
+ expect(rendered.baseElement).toMatchSnapshot();
+ });
+
+ it("does not show notification for first question anymore", () => {
+ const notification = rendered.queryByTestId("ask-boolean-some-random-id-1");
+
+ expect(notification).toBeNull();
+ });
+
+ it("still shows notification for second question", () => {
+ const notification = rendered.getByTestId("ask-boolean-some-random-id-2");
+
+ expect(notification).not.toBeUndefined();
+ });
+
+ it("resolves first question", async () => {
+ const actual = await firstQuestionPromise;
+
+ expect(actual).toBe(false);
+ });
+
+ it("does not resolve second question yet", async () => {
+ const promiseStatus = await getPromiseStatus(secondQuestionPromise);
+
+ expect(promiseStatus.fulfilled).toBe(false);
+ });
+ });
+ });
+ });
+});
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
index 37dfd2f7fd..7e588481a8 100644
--- 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
@@ -17,6 +17,8 @@ const catalogSyncToRendererInjectable = getInjectable({
startCatalogSyncToRenderer(catalogEntityRegistry),
);
},
+
+ causesSideEffects: true,
});
export default catalogSyncToRendererInjectable;
diff --git a/src/main/electron-app/features/electron-updater-is-active.injectable.ts b/src/main/electron-app/features/electron-updater-is-active.injectable.ts
new file mode 100644
index 0000000000..2fe0d7bf06
--- /dev/null
+++ b/src/main/electron-app/features/electron-updater-is-active.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 electronUpdaterInjectable from "./electron-updater.injectable";
+
+const electronUpdaterIsActiveInjectable = getInjectable({
+ id: "electron-updater-is-active",
+
+ instantiate: (di) => {
+ const electronUpdater = di.inject(electronUpdaterInjectable);
+
+ return electronUpdater.isUpdaterActive();
+ },
+});
+
+export default electronUpdaterIsActiveInjectable;
diff --git a/src/main/electron-app/features/electron-updater.injectable.ts b/src/main/electron-app/features/electron-updater.injectable.ts
new file mode 100644
index 0000000000..f9e3335343
--- /dev/null
+++ b/src/main/electron-app/features/electron-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-updater";
+
+const electronUpdaterInjectable = getInjectable({
+ id: "electron-updater",
+ instantiate: () => autoUpdater,
+ causesSideEffects: true,
+});
+
+export default electronUpdaterInjectable;
diff --git a/src/main/electron-app/features/quit-and-install-update.injectable.ts b/src/main/electron-app/features/quit-and-install-update.injectable.ts
new file mode 100644
index 0000000000..6b313e21b0
--- /dev/null
+++ b/src/main/electron-app/features/quit-and-install-update.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 electronUpdaterInjectable from "./electron-updater.injectable";
+
+const quitAndInstallUpdateInjectable = getInjectable({
+ id: "quit-and-install-update",
+
+ instantiate: (di) => {
+ const electronUpdater = di.inject(electronUpdaterInjectable);
+
+ return () => {
+ electronUpdater.quitAndInstall(true, true);
+ };
+ },
+});
+
+export default quitAndInstallUpdateInjectable;
diff --git a/src/main/electron-app/features/set-update-on-quit.injectable.ts b/src/main/electron-app/features/set-update-on-quit.injectable.ts
new file mode 100644
index 0000000000..43693f8eed
--- /dev/null
+++ b/src/main/electron-app/features/set-update-on-quit.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 electronUpdaterInjectable from "./electron-updater.injectable";
+
+const setUpdateOnQuitInjectable = getInjectable({
+ id: "set-update-on-quit",
+
+ instantiate: (di) => {
+ const electronUpdater = di.inject(electronUpdaterInjectable);
+
+ return (updateOnQuit: boolean) => {
+ electronUpdater.autoInstallOnAppQuit = updateOnQuit;
+ };
+ },
+});
+
+export default setUpdateOnQuitInjectable;
diff --git a/src/main/electron-app/runnables/setup-update-checking.injectable.ts b/src/main/electron-app/runnables/setup-update-checking.injectable.ts
deleted file mode 100644
index 918e985265..0000000000
--- a/src/main/electron-app/runnables/setup-update-checking.injectable.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/**
- * Copyright (c) OpenLens Authors. All rights reserved.
- * Licensed under MIT License. See LICENSE in root directory for more information.
- */
-import { getInjectable } from "@ogre-tools/injectable";
-import { afterRootFrameIsReadyInjectionToken } from "../../start-main-application/runnable-tokens/after-root-frame-is-ready-injection-token";
-import startUpdateCheckingInjectable from "../../start-update-checking.injectable";
-
-const setupUpdateCheckingInjectable = getInjectable({
- id: "setup-update-checking",
-
- instantiate: (di) => {
- const startUpdateChecking = di.inject(startUpdateCheckingInjectable);
-
- return {
- run: () => {
- startUpdateChecking();
- },
- };
- },
-
- injectionToken: afterRootFrameIsReadyInjectionToken,
-});
-
-export default setupUpdateCheckingInjectable;
diff --git a/src/main/getDiForUnitTesting.ts b/src/main/getDiForUnitTesting.ts
index 5889e30090..2cc78ec528 100644
--- a/src/main/getDiForUnitTesting.ts
+++ b/src/main/getDiForUnitTesting.ts
@@ -4,12 +4,11 @@
*/
import glob from "glob";
-import { kebabCase, memoize } from "lodash/fp";
+import { kebabCase, memoize, noop } from "lodash/fp";
import type { DiContainer } from "@ogre-tools/injectable";
import { createContainer } from "@ogre-tools/injectable";
import { Environments, setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
import appNameInjectable from "./app-paths/app-name/app-name.injectable";
-import registerChannelInjectable from "./app-paths/register-channel/register-channel.injectable";
import writeJsonFileInjectable from "../common/fs/write-json-file.injectable";
import readJsonFileInjectable from "../common/fs/read-json-file.injectable";
import readFileInjectable from "../common/fs/read-file.injectable";
@@ -29,8 +28,6 @@ import { getAbsolutePathFake } from "../common/test-utils/get-absolute-path-fake
import joinPathsInjectable from "../common/path/join-paths.injectable";
import { joinPathsFake } from "../common/test-utils/join-paths-fake";
import hotbarStoreInjectable from "../common/hotbars/store.injectable";
-import type { GetDiForUnitTestingOptions } from "../test-utils/get-dis-for-unit-testing";
-import isAutoUpdateEnabledInjectable from "./is-auto-update-enabled.injectable";
import appEventBusInjectable from "../common/app-event-bus/app-event-bus.injectable";
import { EventEmitter } from "../common/event-emitter";
import type { AppEvent } from "../common/app-event-bus/event-bus";
@@ -45,7 +42,6 @@ import setupSentryInjectable from "./start-main-application/runnables/setup-sent
import setupShellInjectable from "./start-main-application/runnables/setup-shell.injectable";
import setupSyncingOfWeblinksInjectable from "./start-main-application/runnables/setup-syncing-of-weblinks.injectable";
import stopServicesAndExitAppInjectable from "./stop-services-and-exit-app.injectable";
-import trayInjectable from "./tray/tray.injectable";
import applicationMenuInjectable from "./menu/application-menu.injectable";
import isDevelopmentInjectable from "../common/vars/is-development.injectable";
import setupSystemCaInjectable from "./start-main-application/runnables/setup-system-ca.injectable";
@@ -67,7 +63,7 @@ import type { ClusterFrameInfo } from "../common/cluster-frames";
import { observable } from "mobx";
import waitForElectronToBeReadyInjectable from "./electron-app/features/wait-for-electron-to-be-ready.injectable";
import setupListenerForCurrentClusterFrameInjectable from "./start-main-application/lens-window/current-cluster-frame/setup-listener-for-current-cluster-frame.injectable";
-import ipcMainInjectable from "./app-paths/register-channel/ipc-main/ipc-main.injectable";
+import ipcMainInjectable from "./utils/channel/ipc-main/ipc-main.injectable";
import createElectronWindowForInjectable from "./start-main-application/lens-window/application-window/create-electron-window-for.injectable";
import setupRunnablesAfterWindowIsOpenedInjectable from "./electron-app/runnables/setup-runnables-after-window-is-opened.injectable";
import sendToChannelInElectronBrowserWindowInjectable from "./start-main-application/lens-window/application-window/send-to-channel-in-electron-browser-window.injectable";
@@ -75,10 +71,21 @@ import broadcastMessageInjectable from "../common/ipc/broadcast-message.injectab
import getElectronThemeInjectable from "./electron-app/features/get-electron-theme.injectable";
import syncThemeFromOperatingSystemInjectable from "./electron-app/features/sync-theme-from-operating-system.injectable";
import platformInjectable from "../common/vars/platform.injectable";
-import { noop } from "../renderer/utils";
+import productNameInjectable from "./app-paths/app-name/product-name.injectable";
+import quitAndInstallUpdateInjectable from "./electron-app/features/quit-and-install-update.injectable";
+import electronUpdaterIsActiveInjectable from "./electron-app/features/electron-updater-is-active.injectable";
+import publishIsConfiguredInjectable from "./application-update/publish-is-configured.injectable";
+import checkForPlatformUpdatesInjectable from "./application-update/check-for-platform-updates/check-for-platform-updates.injectable";
import baseBundeledBinariesDirectoryInjectable from "../common/vars/base-bundled-binaries-dir.injectable";
+import setUpdateOnQuitInjectable from "./electron-app/features/set-update-on-quit.injectable";
+import downloadPlatformUpdateInjectable from "./application-update/download-platform-update/download-platform-update.injectable";
+import startCatalogSyncInjectable from "./catalog-sync-to-renderer/start-catalog-sync.injectable";
+import startKubeConfigSyncInjectable from "./start-main-application/runnables/kube-config-sync/start-kube-config-sync.injectable";
+import appVersionInjectable from "../common/get-configuration-file-model/app-version/app-version.injectable";
+import getRandomIdInjectable from "../common/utils/get-random-id.injectable";
+import periodicalCheckForUpdatesInjectable from "./application-update/periodical-check-for-updates/periodical-check-for-updates.injectable";
-export function getDiForUnitTesting(opts: GetDiForUnitTestingOptions = {}) {
+export function getDiForUnitTesting(opts: { doGeneralOverrides?: boolean } = {}) {
const {
doGeneralOverrides = false,
} = opts;
@@ -100,10 +107,11 @@ export function getDiForUnitTesting(opts: GetDiForUnitTestingOptions = {}) {
di.preventSideEffects();
if (doGeneralOverrides) {
+ di.override(getRandomIdInjectable, () => () => "some-irrelevant-random-id");
di.override(hotbarStoreInjectable, () => ({ load: () => {} }));
- di.override(userStoreInjectable, () => ({ startMainReactions: () => {} }) as UserStore);
+ di.override(userStoreInjectable, () => ({ startMainReactions: () => {}, extensionRegistryUrl: { customUrl: "some-custom-url" }}) as UserStore);
di.override(extensionsStoreInjectable, () => ({ isEnabled: (opts) => (void opts, false) }) as ExtensionsStore);
- di.override(clusterStoreInjectable, () => ({ getById: (id) => (void id, {}) as Cluster }) as ClusterStore);
+ di.override(clusterStoreInjectable, () => ({ provideInitialFromMain: () => {}, getById: (id) => (void id, {}) as Cluster }) as ClusterStore);
di.override(fileSystemProvisionerStoreInjectable, () => ({}) as FileSystemProvisionerStore);
overrideOperatingSystem(di);
@@ -114,19 +122,22 @@ export function getDiForUnitTesting(opts: GetDiForUnitTestingOptions = {}) {
di.override(environmentVariablesInjectable, () => ({}));
di.override(commandLineArgumentsInjectable, () => []);
+ di.override(productNameInjectable, () => "some-product-name");
+ di.override(appVersionInjectable, () => "1.0.0");
+
di.override(clusterFramesInjectable, () => observable.map());
di.override(stopServicesAndExitAppInjectable, () => () => {});
di.override(lensResourcesDirInjectable, () => "/irrelevant");
- di.override(trayInjectable, () => ({ start: () => {}, stop: () => {} }));
di.override(applicationMenuInjectable, () => ({ start: () => {}, stop: () => {} }));
+ di.override(periodicalCheckForUpdatesInjectable, () => ({ start: () => {}, stop: () => {}, started: false }));
+
// TODO: Remove usages of globally exported appEventBus to get rid of this
di.override(appEventBusInjectable, () => new EventEmitter<[AppEvent]>());
di.override(appNameInjectable, () => "some-app-name");
- di.override(registerChannelInjectable, () => () => undefined);
di.override(broadcastMessageInjectable, () => (channel) => {
throw new Error(`Tried to broadcast message to channel "${channel}" over IPC without explicit override.`);
});
@@ -182,6 +193,8 @@ const overrideRunnablesHavingSideEffects = (di: DiContainer) => {
setupSystemCaInjectable,
setupListenerForCurrentClusterFrameInjectable,
setupRunnablesAfterWindowIsOpenedInjectable,
+ startCatalogSyncInjectable,
+ startKubeConfigSyncInjectable,
].forEach((injectable) => {
di.override(injectable, () => ({ run: () => {} }));
});
@@ -215,6 +228,13 @@ const overrideElectronFeatures = (di: DiContainer) => {
di.override(ipcMainInjectable, () => ({}));
di.override(getElectronThemeInjectable, () => () => "dark");
di.override(syncThemeFromOperatingSystemInjectable, () => ({ start: () => {}, stop: () => {} }));
+ di.override(quitAndInstallUpdateInjectable, () => () => {});
+ di.override(setUpdateOnQuitInjectable, () => () => {});
+ di.override(downloadPlatformUpdateInjectable, () => async () => ({ downloadWasSuccessful: true }));
+
+ di.override(checkForPlatformUpdatesInjectable, () => () => {
+ throw new Error("Tried to check for platform updates without explicit override.");
+ });
di.override(createElectronWindowForInjectable, () => () => async () => ({
show: () => {},
@@ -234,5 +254,6 @@ const overrideElectronFeatures = (di: DiContainer) => {
);
di.override(setElectronAppPathInjectable, () => () => {});
- di.override(isAutoUpdateEnabledInjectable, () => () => false);
+ di.override(publishIsConfiguredInjectable, () => false);
+ di.override(electronUpdaterIsActiveInjectable, () => false);
};
diff --git a/src/main/is-auto-update-enabled.injectable.ts b/src/main/is-auto-update-enabled.injectable.ts
deleted file mode 100644
index c76bd27e45..0000000000
--- a/src/main/is-auto-update-enabled.injectable.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 { getInjectable } from "@ogre-tools/injectable";
-import { isPublishConfigured } from "../common/vars";
-import { autoUpdater } from "electron-updater";
-
-const isAutoUpdateEnabledInjectable = getInjectable({
- id: "is-auto-update-enabled",
-
- instantiate: () => () => {
- return autoUpdater.isUpdaterActive() && isPublishConfigured;
- },
-
- causesSideEffects: true,
-});
-
-export default isAutoUpdateEnabledInjectable;
diff --git a/src/main/menu/application-menu-items.injectable.ts b/src/main/menu/application-menu-items.injectable.ts
index 417c40ac2e..4ea447f675 100644
--- a/src/main/menu/application-menu-items.injectable.ts
+++ b/src/main/menu/application-menu-items.injectable.ts
@@ -3,7 +3,6 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
-import { checkForUpdates } from "../app-updater";
import { docsUrl, productName, supportUrl } from "../../common/vars";
import { broadcastMessage } from "../../common/ipc";
import { openBrowser } from "../../common/utils";
@@ -12,7 +11,7 @@ import { webContents } from "electron";
import loggerInjectable from "../../common/logger.injectable";
import appNameInjectable from "../app-paths/app-name/app-name.injectable";
import electronMenuItemsInjectable from "./electron-menu-items.injectable";
-import isAutoUpdateEnabledInjectable from "../is-auto-update-enabled.injectable";
+import updatingIsEnabledInjectable from "../application-update/updating-is-enabled.injectable";
import navigateToPreferencesInjectable from "../../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable";
import navigateToExtensionsInjectable from "../../common/front-end-routing/routes/extensions/navigate-to-extensions.injectable";
import navigateToCatalogInjectable from "../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable";
@@ -25,6 +24,7 @@ import showAboutInjectable from "./show-about.injectable";
import applicationWindowInjectable from "../start-main-application/lens-window/application-window/application-window.injectable";
import reloadWindowInjectable from "../start-main-application/lens-window/reload-window.injectable";
import showApplicationWindowInjectable from "../start-main-application/lens-window/show-application-window.injectable";
+import processCheckingForUpdatesInjectable from "../application-update/check-for-updates/process-checking-for-updates.injectable";
function ignoreIf(check: boolean, menuItems: MenuItemOpts[]) {
return check ? [] : menuItems;
@@ -41,7 +41,7 @@ const applicationMenuItemsInjectable = getInjectable({
const logger = di.inject(loggerInjectable);
const appName = di.inject(appNameInjectable);
const isMac = di.inject(isMacInjectable);
- const isAutoUpdateEnabled = di.inject(isAutoUpdateEnabledInjectable);
+ const updatingIsEnabled = di.inject(updatingIsEnabledInjectable);
const electronMenuItems = di.inject(electronMenuItemsInjectable);
const showAbout = di.inject(showAboutInjectable);
const applicationWindow = di.inject(applicationWindowInjectable);
@@ -53,12 +53,11 @@ const applicationMenuItemsInjectable = getInjectable({
const navigateToWelcome = di.inject(navigateToWelcomeInjectable);
const navigateToAddCluster = di.inject(navigateToAddClusterInjectable);
const stopServicesAndExitApp = di.inject(stopServicesAndExitAppInjectable);
+ const processCheckingForUpdates = di.inject(processCheckingForUpdatesInjectable);
+
+ logger.info(`[MENU]: autoUpdateEnabled=${updatingIsEnabled}`);
return computed((): MenuItemOpts[] => {
- const autoUpdateDisabled = !isAutoUpdateEnabled();
-
- logger.info(`[MENU]: autoUpdateDisabled=${autoUpdateDisabled}`);
-
const macAppMenu: MenuItemOpts = {
label: appName,
id: "root",
@@ -70,11 +69,11 @@ const applicationMenuItemsInjectable = getInjectable({
showAbout();
},
},
- ...ignoreIf(autoUpdateDisabled, [
+ ...ignoreIf(!updatingIsEnabled, [
{
label: "Check for updates",
click() {
- checkForUpdates().then(() => showApplicationWindow());
+ processCheckingForUpdates().then(() => showApplicationWindow());
},
},
]),
@@ -282,11 +281,11 @@ const applicationMenuItemsInjectable = getInjectable({
showAbout();
},
},
- ...ignoreIf(autoUpdateDisabled, [
+ ...ignoreIf(!updatingIsEnabled, [
{
label: "Check for updates",
click() {
- checkForUpdates().then(() =>
+ processCheckingForUpdates().then(() =>
showApplicationWindow(),
);
},
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
index dd3feb3020..c63191c71c 100644
--- 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
@@ -11,7 +11,7 @@ import appNameInjectable from "../../../app-paths/app-name/app-name.injectable";
import appEventBusInjectable from "../../../../common/app-event-bus/app-event-bus.injectable";
import { delay } from "../../../../common/utils";
import { bundledExtensionsLoaded } from "../../../../common/ipc/extension-handling";
-import ipcMainInjectable from "../../../app-paths/register-channel/ipc-main/ipc-main.injectable";
+import ipcMainInjectable from "../../../utils/channel/ipc-main/ipc-main.injectable";
const applicationWindowInjectable = getInjectable({
id: "application-window",
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
index c93877ee6a..a44b0ebd4c 100644
--- 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
@@ -58,9 +58,9 @@ const createLensWindowInjectable = getInjectable({
browserWindow?.close();
browserWindow = undefined;
},
- send: async (args: SendToViewArgs) => {
+ send: (args: SendToViewArgs) => {
if (!browserWindow) {
- browserWindow = await createElectronWindow();
+ throw new Error(`Tried to send message to window "${configuration.id}" but the window was closed`);
}
return browserWindow.send(args);
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
index f7273206c9..3e62b0894b 100644
--- 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
@@ -14,7 +14,7 @@ export interface SendToViewArgs {
export interface LensWindow {
show: () => Promise;
close: () => void;
- send: (args: SendToViewArgs) => Promise;
+ send: (args: SendToViewArgs) => void;
visible: boolean;
}
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
index 703ef15f60..7bada3f3bd 100644
--- 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
@@ -36,7 +36,7 @@ const navigateForExtensionInjectable = getInjectable({
(frameInfo) => frameInfo.frameId === frameId,
);
- await applicationWindow.send({
+ applicationWindow.send({
channel: "extension:navigate",
frameInfo,
data: [extId, pageId, params],
diff --git a/src/main/start-main-application/lens-window/navigate.injectable.ts b/src/main/start-main-application/lens-window/navigate.injectable.ts
index c7cbb10d24..f9d80e4205 100644
--- a/src/main/start-main-application/lens-window/navigate.injectable.ts
+++ b/src/main/start-main-application/lens-window/navigate.injectable.ts
@@ -29,7 +29,7 @@ const navigateInjectable = getInjectable({
? IpcRendererNavigationEvents.NAVIGATE_IN_CLUSTER
: IpcRendererNavigationEvents.NAVIGATE_IN_APP;
- await applicationWindow.send({
+ applicationWindow.send({
channel,
frameInfo,
data: [url],
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
index d6d964c7df..80c725e17d 100644
--- 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
@@ -25,6 +25,8 @@ const startKubeConfigSyncInjectable = getInjectable({
};
},
+ causesSideEffects: true,
+
injectionToken: afterRootFrameIsReadyInjectionToken,
});
diff --git a/src/main/start-main-application/runnables/root-frame-rendered-channel-listener/root-frame-rendered-channel-listener.injectable.ts b/src/main/start-main-application/runnables/root-frame-rendered-channel-listener/root-frame-rendered-channel-listener.injectable.ts
new file mode 100644
index 0000000000..83ebf7bf91
--- /dev/null
+++ b/src/main/start-main-application/runnables/root-frame-rendered-channel-listener/root-frame-rendered-channel-listener.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 rootFrameRenderedChannelInjectable from "../../../../common/root-frame-rendered-channel/root-frame-rendered-channel.injectable";
+import { runManyFor } from "../../../../common/runnable/run-many-for";
+import { afterRootFrameIsReadyInjectionToken } from "../../runnable-tokens/after-root-frame-is-ready-injection-token";
+import { messageChannelListenerInjectionToken } from "../../../../common/utils/channel/message-channel-listener-injection-token";
+
+const rootFrameRenderedChannelListenerInjectable = getInjectable({
+ id: "root-frame-rendered-channel-listener",
+
+ instantiate: (di) => {
+ const channel = di.inject(rootFrameRenderedChannelInjectable);
+
+ const runMany = runManyFor(di);
+
+ const runRunnablesAfterRootFrameIsReady = runMany(
+ afterRootFrameIsReadyInjectionToken,
+ );
+
+ return {
+ channel,
+
+ handler: async () => {
+ await runRunnablesAfterRootFrameIsReady();
+ },
+ };
+ },
+
+ injectionToken: messageChannelListenerInjectionToken,
+});
+
+export default rootFrameRenderedChannelListenerInjectable;
diff --git a/src/main/start-main-application/runnables/setup-sentry.injectable.ts b/src/main/start-main-application/runnables/setup-sentry.injectable.ts
index e9f0587450..d100b93cc6 100644
--- a/src/main/start-main-application/runnables/setup-sentry.injectable.ts
+++ b/src/main/start-main-application/runnables/setup-sentry.injectable.ts
@@ -5,7 +5,7 @@
import { getInjectable } from "@ogre-tools/injectable";
import { initializeSentryReporting } from "../../../common/sentry";
import { init } from "@sentry/electron/main";
-import { onLoadOfApplicationInjectionToken } from "../runnable-tokens/on-load-of-application-injection-token";
+import { beforeApplicationIsLoadingInjectionToken } from "../runnable-tokens/before-application-is-loading-injection-token";
const setupSentryInjectable = getInjectable({
id: "setup-sentry",
@@ -18,7 +18,7 @@ const setupSentryInjectable = getInjectable({
causesSideEffects: true,
- injectionToken: onLoadOfApplicationInjectionToken,
+ injectionToken: beforeApplicationIsLoadingInjectionToken,
});
export default setupSentryInjectable;
diff --git a/src/main/start-update-checking.injectable.ts b/src/main/start-update-checking.injectable.ts
deleted file mode 100644
index 4571e70df4..0000000000
--- a/src/main/start-update-checking.injectable.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 { 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/tray/electron-tray/electron-tray.injectable.ts b/src/main/tray/electron-tray/electron-tray.injectable.ts
new file mode 100644
index 0000000000..409e7abf3f
--- /dev/null
+++ b/src/main/tray/electron-tray/electron-tray.injectable.ts
@@ -0,0 +1,116 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getInjectable } from "@ogre-tools/injectable";
+import { Menu, Tray } from "electron";
+import packageJsonInjectable from "../../../common/vars/package-json.injectable";
+import logger from "../../logger";
+import { TRAY_LOG_PREFIX } from "../tray";
+import showApplicationWindowInjectable from "../../start-main-application/lens-window/show-application-window.injectable";
+import type { TrayMenuItem } from "../tray-menu-item/tray-menu-item-injection-token";
+import { pipeline } from "@ogre-tools/fp";
+import { isEmpty, map, filter } from "lodash/fp";
+import isWindowsInjectable from "../../../common/vars/is-windows.injectable";
+import loggerInjectable from "../../../common/logger.injectable";
+import trayIconPathInjectable from "../tray-icon-path.injectable";
+
+const electronTrayInjectable = getInjectable({
+ id: "electron-tray",
+
+ instantiate: (di) => {
+ const packageJson = di.inject(packageJsonInjectable);
+ const showApplicationWindow = di.inject(showApplicationWindowInjectable);
+ const isWindows = di.inject(isWindowsInjectable);
+ const logger = di.inject(loggerInjectable);
+ const trayIconPath = di.inject(trayIconPathInjectable);
+
+ let tray: Tray;
+
+ return {
+ start: () => {
+ tray = new Tray(trayIconPath);
+
+ tray.setToolTip(packageJson.description);
+ tray.setIgnoreDoubleClickEvents(true);
+
+ if (isWindows) {
+ tray.on("click", () => {
+ showApplicationWindow()
+ .catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to open lens`, { error }));
+ });
+ }
+ },
+
+ stop: () => {
+ tray.destroy();
+ },
+
+ setMenuItems: (items: TrayMenuItem[]) => {
+ pipeline(
+ items,
+ convertToElectronMenuTemplate,
+ Menu.buildFromTemplate,
+
+ (template) => {
+ tray.setContextMenu(template);
+ },
+ );
+ },
+ };
+ },
+
+ causesSideEffects: true,
+});
+
+export default electronTrayInjectable;
+
+const convertToElectronMenuTemplate = (trayMenuItems: TrayMenuItem[]) => {
+ const _toTrayMenuOptions = (parentId: string | null) =>
+ pipeline(
+ trayMenuItems,
+
+ filter((item) => item.parentId === parentId),
+
+ map(
+ (trayMenuItem: TrayMenuItem): Electron.MenuItemConstructorOptions => {
+ if (trayMenuItem.separator) {
+ return { id: trayMenuItem.id, type: "separator" };
+ }
+
+ const childItems = _toTrayMenuOptions(trayMenuItem.id);
+
+ return {
+ id: trayMenuItem.id,
+ label: trayMenuItem.label?.get(),
+ enabled: trayMenuItem.enabled.get(),
+ toolTip: trayMenuItem.tooltip,
+
+ ...(isEmpty(childItems)
+ ? {
+ type: "normal",
+ submenu: _toTrayMenuOptions(trayMenuItem.id),
+
+ click: () => {
+ try {
+ trayMenuItem.click?.();
+ } catch (error) {
+ logger.error(
+ `${TRAY_LOG_PREFIX}: clicking item "${trayMenuItem.id} failed."`,
+ { error },
+ );
+ }
+ },
+ }
+ : {
+ type: "submenu",
+ submenu: _toTrayMenuOptions(trayMenuItem.id),
+ }),
+
+ };
+ },
+ ),
+ );
+
+ return _toTrayMenuOptions(null);
+};
diff --git a/src/main/tray/electron-tray/start-tray.injectable.ts b/src/main/tray/electron-tray/start-tray.injectable.ts
new file mode 100644
index 0000000000..1a223ac3a5
--- /dev/null
+++ b/src/main/tray/electron-tray/start-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 { onLoadOfApplicationInjectionToken } from "../../start-main-application/runnable-tokens/on-load-of-application-injection-token";
+import electronTrayInjectable from "./electron-tray.injectable";
+
+const startTrayInjectable = getInjectable({
+ id: "start-tray",
+
+ instantiate: (di) => {
+ const electronTray = di.inject(electronTrayInjectable);
+
+ return {
+ run: () => {
+ electronTray.start();
+ },
+ };
+ },
+
+ injectionToken: onLoadOfApplicationInjectionToken,
+});
+
+export default startTrayInjectable;
diff --git a/src/main/tray/electron-tray/stop-tray.injectable.ts b/src/main/tray/electron-tray/stop-tray.injectable.ts
new file mode 100644
index 0000000000..f66ffb3a64
--- /dev/null
+++ b/src/main/tray/electron-tray/stop-tray.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 electronTrayInjectable from "./electron-tray.injectable";
+import { beforeQuitOfBackEndInjectionToken } from "../../start-main-application/runnable-tokens/before-quit-of-back-end-injection-token";
+import stopReactiveTrayMenuItemsInjectable from "../reactive-tray-menu-items/stop-reactive-tray-menu-items.injectable";
+
+const stopTrayInjectable = getInjectable({
+ id: "stop-tray",
+
+ instantiate: (di) => {
+ const electronTray = di.inject(electronTrayInjectable);
+
+ return {
+ run: () => {
+ electronTray.stop();
+ },
+
+ runAfter: di.inject(stopReactiveTrayMenuItemsInjectable),
+ };
+ },
+
+ injectionToken: beforeQuitOfBackEndInjectionToken,
+});
+
+export default stopTrayInjectable;
diff --git a/src/main/tray/install-tray.injectable.ts b/src/main/tray/install-tray.injectable.ts
deleted file mode 100644
index 716d602101..0000000000
--- a/src/main/tray/install-tray.injectable.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/**
- * Copyright (c) OpenLens Authors. All rights reserved.
- * Licensed under MIT License. See LICENSE in root directory for more information.
- */
-import { getInjectable } from "@ogre-tools/injectable";
-import trayInjectable from "./tray.injectable";
-import { onLoadOfApplicationInjectionToken } from "../start-main-application/runnable-tokens/on-load-of-application-injection-token";
-
-const installTrayInjectable = getInjectable({
- id: "install-tray",
-
- instantiate: (di) => {
- const trayInitializer = di.inject(trayInjectable);
-
- return {
- run: async () => {
- await trayInitializer.start();
- },
- };
- },
-
- injectionToken: onLoadOfApplicationInjectionToken,
-});
-
-export default installTrayInjectable;
diff --git a/src/main/tray/reactive-tray-menu-items/reactive-tray-menu-items.injectable.ts b/src/main/tray/reactive-tray-menu-items/reactive-tray-menu-items.injectable.ts
new file mode 100644
index 0000000000..b11654393a
--- /dev/null
+++ b/src/main/tray/reactive-tray-menu-items/reactive-tray-menu-items.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 { getStartableStoppable } from "../../../common/utils/get-startable-stoppable";
+import { autorun } from "mobx";
+import trayMenuItemsInjectable from "../tray-menu-item/tray-menu-items.injectable";
+import electronTrayInjectable from "../electron-tray/electron-tray.injectable";
+
+const reactiveTrayMenuItemsInjectable = getInjectable({
+ id: "reactive-tray-menu-items",
+
+ instantiate: (di) => {
+ const electronTray = di.inject(electronTrayInjectable);
+ const trayMenuItems = di.inject(trayMenuItemsInjectable);
+
+ return getStartableStoppable("reactive-tray-menu-items", () => autorun(() => {
+ electronTray.setMenuItems(trayMenuItems.get());
+ }));
+ },
+});
+
+export default reactiveTrayMenuItemsInjectable;
diff --git a/src/main/tray/reactive-tray-menu-items/start-reactive-tray-menu-items.injectable.ts b/src/main/tray/reactive-tray-menu-items/start-reactive-tray-menu-items.injectable.ts
new file mode 100644
index 0000000000..63025e6a9a
--- /dev/null
+++ b/src/main/tray/reactive-tray-menu-items/start-reactive-tray-menu-items.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 reactiveTrayMenuItemsInjectable from "./reactive-tray-menu-items.injectable";
+import { onLoadOfApplicationInjectionToken } from "../../start-main-application/runnable-tokens/on-load-of-application-injection-token";
+import startTrayInjectable from "../electron-tray/start-tray.injectable";
+
+const startReactiveTrayMenuItemsInjectable = getInjectable({
+ id: "start-reactive-tray-menu-items",
+
+ instantiate: (di) => {
+ const reactiveTrayMenuItems = di.inject(reactiveTrayMenuItemsInjectable);
+
+ return {
+ run: async () => {
+ await reactiveTrayMenuItems.start();
+ },
+
+ runAfter: di.inject(startTrayInjectable),
+ };
+ },
+
+ injectionToken: onLoadOfApplicationInjectionToken,
+});
+
+export default startReactiveTrayMenuItemsInjectable;
diff --git a/src/main/tray/reactive-tray-menu-items/stop-reactive-tray-menu-items.injectable.ts b/src/main/tray/reactive-tray-menu-items/stop-reactive-tray-menu-items.injectable.ts
new file mode 100644
index 0000000000..384cdc253a
--- /dev/null
+++ b/src/main/tray/reactive-tray-menu-items/stop-reactive-tray-menu-items.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 reactiveTrayMenuItemsInjectable from "./reactive-tray-menu-items.injectable";
+import { beforeQuitOfBackEndInjectionToken } from "../../start-main-application/runnable-tokens/before-quit-of-back-end-injection-token";
+
+const stopReactiveTrayMenuItemsInjectable = getInjectable({
+ id: "stop-reactive-tray-menu-items",
+
+ instantiate: (di) => {
+ const reactiveTrayMenuItems = di.inject(reactiveTrayMenuItemsInjectable);
+
+ return {
+ run: async () => {
+ await reactiveTrayMenuItems.stop();
+ },
+ };
+ },
+
+ injectionToken: beforeQuitOfBackEndInjectionToken,
+});
+
+export default stopReactiveTrayMenuItemsInjectable;
diff --git a/src/main/tray/tray-menu-item/implementations/about-app-tray-item.injectable.ts b/src/main/tray/tray-menu-item/implementations/about-app-tray-item.injectable.ts
new file mode 100644
index 0000000000..5fb1a9f34f
--- /dev/null
+++ b/src/main/tray/tray-menu-item/implementations/about-app-tray-item.injectable.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 { getInjectable } from "@ogre-tools/injectable";
+import productNameInjectable from "../../../app-paths/app-name/product-name.injectable";
+import showApplicationWindowInjectable from "../../../start-main-application/lens-window/show-application-window.injectable";
+import showAboutInjectable from "../../../menu/show-about.injectable";
+import { trayMenuItemInjectionToken } from "../tray-menu-item-injection-token";
+import { computed } from "mobx";
+import withErrorLoggingInjectable from "../../../../common/utils/with-error-logging/with-error-logging.injectable";
+import { withErrorSuppression } from "../../../../common/utils/with-error-suppression/with-error-suppression";
+import { pipeline } from "@ogre-tools/fp";
+
+const aboutAppTrayItemInjectable = getInjectable({
+ id: "about-app-tray-item",
+
+ instantiate: (di) => {
+ const productName = di.inject(productNameInjectable);
+ const showApplicationWindow = di.inject(showApplicationWindowInjectable);
+ const showAbout = di.inject(showAboutInjectable);
+ const withErrorLoggingFor = di.inject(withErrorLoggingInjectable);
+
+ return {
+ id: "about-app",
+ parentId: null,
+ orderNumber: 140,
+ label: computed(() => `About ${productName}`),
+ enabled: computed(() => true),
+ visible: computed(() => true),
+
+ click: pipeline(
+ async () => {
+ await showApplicationWindow();
+
+ await showAbout();
+ },
+
+ withErrorLoggingFor(() => "[TRAY]: Opening of show about failed."),
+
+ // TODO: Find out how to improve typing so that instead of
+ // x => withErrorSuppression(x) there could only be withErrorSuppression
+ (x) => withErrorSuppression(x),
+ ),
+ };
+ },
+
+ injectionToken: trayMenuItemInjectionToken,
+});
+
+export default aboutAppTrayItemInjectable;
diff --git a/src/main/tray/tray-menu-item/implementations/open-app-tray-item.injectable.ts b/src/main/tray/tray-menu-item/implementations/open-app-tray-item.injectable.ts
new file mode 100644
index 0000000000..ff19d7718a
--- /dev/null
+++ b/src/main/tray/tray-menu-item/implementations/open-app-tray-item.injectable.ts
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getInjectable } from "@ogre-tools/injectable";
+import { trayMenuItemInjectionToken } from "../tray-menu-item-injection-token";
+import productNameInjectable from "../../../app-paths/app-name/product-name.injectable";
+import showApplicationWindowInjectable from "../../../start-main-application/lens-window/show-application-window.injectable";
+import { computed } from "mobx";
+import withErrorLoggingInjectable from "../../../../common/utils/with-error-logging/with-error-logging.injectable";
+import { withErrorSuppression } from "../../../../common/utils/with-error-suppression/with-error-suppression";
+import { pipeline } from "@ogre-tools/fp";
+
+const openAppTrayItemInjectable = getInjectable({
+ id: "open-app-tray-item",
+
+ instantiate: (di) => {
+ const productName = di.inject(productNameInjectable);
+ const showApplicationWindow = di.inject(showApplicationWindowInjectable);
+ const withErrorLoggingFor = di.inject(withErrorLoggingInjectable);
+
+ return {
+ id: "open-app",
+ parentId: null,
+ label: computed(() => `Open ${productName}`),
+ orderNumber: 10,
+ enabled: computed(() => true),
+ visible: computed(() => true),
+
+ click: pipeline(
+ async () => {
+ await showApplicationWindow();
+ },
+
+ withErrorLoggingFor(() => "[TRAY]: Opening of application window failed."),
+
+ // TODO: Find out how to improve typing so that instead of
+ // x => withErrorSuppression(x) there could only be withErrorSuppression
+ (x) => withErrorSuppression(x),
+ ),
+ };
+ },
+
+ injectionToken: trayMenuItemInjectionToken,
+});
+
+export default openAppTrayItemInjectable;
diff --git a/src/main/tray/tray-menu-item/implementations/open-preferences-tray-item.injectable.ts b/src/main/tray/tray-menu-item/implementations/open-preferences-tray-item.injectable.ts
new file mode 100644
index 0000000000..8c062f6a29
--- /dev/null
+++ b/src/main/tray/tray-menu-item/implementations/open-preferences-tray-item.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 { trayMenuItemInjectionToken } from "../tray-menu-item-injection-token";
+import navigateToPreferencesInjectable from "../../../../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable";
+import { computed } from "mobx";
+import { withErrorSuppression } from "../../../../common/utils/with-error-suppression/with-error-suppression";
+import { pipeline } from "@ogre-tools/fp";
+import withErrorLoggingInjectable from "../../../../common/utils/with-error-logging/with-error-logging.injectable";
+
+const openPreferencesTrayItemInjectable = getInjectable({
+ id: "open-preferences-tray-item",
+
+ instantiate: (di) => {
+ const navigateToPreferences = di.inject(navigateToPreferencesInjectable);
+ const withErrorLoggingFor = di.inject(withErrorLoggingInjectable);
+
+ return {
+ id: "open-preferences",
+ parentId: null,
+ label: computed(() => "Preferences"),
+ orderNumber: 20,
+ enabled: computed(() => true),
+ visible: computed(() => true),
+
+ click: pipeline(
+ navigateToPreferences,
+
+ withErrorLoggingFor(() => "[TRAY]: Opening of preferences failed."),
+
+ // TODO: Find out how to improve typing so that instead of
+ // x => withErrorSuppression(x) there could only be withErrorSuppression
+ (x) => withErrorSuppression(x),
+ ),
+ };
+ },
+
+ injectionToken: trayMenuItemInjectionToken,
+});
+
+export default openPreferencesTrayItemInjectable;
diff --git a/src/main/tray/tray-menu-item/implementations/quit-app-separator-tray-item.injectable.ts b/src/main/tray/tray-menu-item/implementations/quit-app-separator-tray-item.injectable.ts
new file mode 100644
index 0000000000..de83a92fe6
--- /dev/null
+++ b/src/main/tray/tray-menu-item/implementations/quit-app-separator-tray-item.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 { trayMenuItemInjectionToken } from "../tray-menu-item-injection-token";
+import { computed } from "mobx";
+
+const quitAppSeparatorTrayItemInjectable = getInjectable({
+ id: "quit-app-separator-tray-item",
+
+ instantiate: () => ({
+ id: "quit-app-separator",
+ parentId: null,
+ orderNumber: 149,
+ enabled: computed(() => true),
+ visible: computed(() => true),
+ separator: true,
+ }),
+
+ injectionToken: trayMenuItemInjectionToken,
+});
+
+export default quitAppSeparatorTrayItemInjectable;
diff --git a/src/main/tray/tray-menu-item/implementations/quit-app-tray-item.injectable.ts b/src/main/tray/tray-menu-item/implementations/quit-app-tray-item.injectable.ts
new file mode 100644
index 0000000000..894a823511
--- /dev/null
+++ b/src/main/tray/tray-menu-item/implementations/quit-app-tray-item.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 { trayMenuItemInjectionToken } from "../tray-menu-item-injection-token";
+import { computed } from "mobx";
+import stopServicesAndExitAppInjectable from "../../../stop-services-and-exit-app.injectable";
+import { withErrorSuppression } from "../../../../common/utils/with-error-suppression/with-error-suppression";
+import { pipeline } from "@ogre-tools/fp";
+import withErrorLoggingInjectable from "../../../../common/utils/with-error-logging/with-error-logging.injectable";
+
+const quitAppTrayItemInjectable = getInjectable({
+ id: "quit-app-tray-item",
+
+ instantiate: (di) => {
+ const stopServicesAndExitApp = di.inject(stopServicesAndExitAppInjectable);
+ const withErrorLoggingFor = di.inject(withErrorLoggingInjectable);
+
+ return {
+ id: "quit-app",
+ parentId: null,
+ orderNumber: 150,
+ label: computed(() => "Quit App"),
+ enabled: computed(() => true),
+ visible: computed(() => true),
+
+ click: pipeline(
+ stopServicesAndExitApp,
+
+ withErrorLoggingFor(() => "[TRAY]: Quitting application failed."),
+
+ // TODO: Find out how to improve typing so that instead of
+ // x => withErrorSuppression(x) there could only be withErrorSuppression
+ (x) => withErrorSuppression(x),
+ ),
+ };
+ },
+
+ injectionToken: trayMenuItemInjectionToken,
+});
+
+export default quitAppTrayItemInjectable;
diff --git a/src/main/tray/tray-menu-item/tray-menu-item-injection-token.ts b/src/main/tray/tray-menu-item/tray-menu-item-injection-token.ts
new file mode 100644
index 0000000000..f8e9d7c6cc
--- /dev/null
+++ b/src/main/tray/tray-menu-item/tray-menu-item-injection-token.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 { getInjectionToken } from "@ogre-tools/injectable";
+import type { IComputedValue } from "mobx";
+import type { LensMainExtension } from "../../../extensions/lens-main-extension";
+
+export interface TrayMenuItem {
+ id: string;
+ parentId: string | null;
+ orderNumber: number;
+ enabled: IComputedValue;
+ visible: IComputedValue;
+
+ label?: IComputedValue;
+ click?: () => Promise | void;
+ tooltip?: string;
+ separator?: boolean;
+ extension?: LensMainExtension;
+}
+
+export const trayMenuItemInjectionToken = getInjectionToken({
+ id: "tray-menu-item",
+});
diff --git a/src/main/tray/tray-menu-item/tray-menu-item-registrator.injectable.ts b/src/main/tray/tray-menu-item/tray-menu-item-registrator.injectable.ts
new file mode 100644
index 0000000000..6cbd9e5d33
--- /dev/null
+++ b/src/main/tray/tray-menu-item/tray-menu-item-registrator.injectable.ts
@@ -0,0 +1,90 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { pipeline } from "@ogre-tools/fp";
+import { flatMap, kebabCase } from "lodash/fp";
+import type { Injectable } from "@ogre-tools/injectable";
+import { getInjectable } from "@ogre-tools/injectable";
+import { computed } from "mobx";
+import { extensionRegistratorInjectionToken } from "../../../extensions/extension-loader/extension-registrator-injection-token";
+import type { LensMainExtension } from "../../../extensions/lens-main-extension";
+import type { TrayMenuItem } from "./tray-menu-item-injection-token";
+import { trayMenuItemInjectionToken } from "./tray-menu-item-injection-token";
+import type { TrayMenuRegistration } from "../tray-menu-registration";
+import { withErrorSuppression } from "../../../common/utils/with-error-suppression/with-error-suppression";
+import type { WithErrorLoggingFor } from "../../../common/utils/with-error-logging/with-error-logging.injectable";
+import withErrorLoggingInjectable from "../../../common/utils/with-error-logging/with-error-logging.injectable";
+
+const trayMenuItemRegistratorInjectable = getInjectable({
+ id: "tray-menu-item-registrator",
+
+ instantiate: (di) => (extension, installationCounter) => {
+ const mainExtension = extension as LensMainExtension;
+ const withErrorLoggingFor = di.inject(withErrorLoggingInjectable);
+
+ pipeline(
+ mainExtension.trayMenus,
+
+ flatMap(toItemInjectablesFor(mainExtension, installationCounter, withErrorLoggingFor)),
+
+ (injectables) => di.register(...injectables),
+ );
+ },
+
+ injectionToken: extensionRegistratorInjectionToken,
+});
+
+export default trayMenuItemRegistratorInjectable;
+
+const toItemInjectablesFor = (extension: LensMainExtension, installationCounter: number, withErrorLoggingFor: WithErrorLoggingFor) => {
+ const _toItemInjectables = (parentId: string | null) => (registration: TrayMenuRegistration): Injectable[] => {
+ const trayItemId = registration.id || kebabCase(registration.label || "");
+ const id = `${trayItemId}-tray-menu-item-for-extension-${extension.sanitizedExtensionId}-instance-${installationCounter}`;
+
+ const parentInjectable = getInjectable({
+ id,
+
+ instantiate: () => ({
+ id,
+ parentId,
+ orderNumber: 100,
+
+ separator: registration.type === "separator",
+
+ label: computed(() => registration.label || ""),
+ tooltip: registration.toolTip,
+
+ click: pipeline(
+ () => {
+ registration.click?.(registration);
+ },
+
+ withErrorLoggingFor(() => `[TRAY]: Clicking of tray item "${trayItemId}" from extension "${extension.sanitizedExtensionId}" failed.`),
+
+ // TODO: Find out how to improve typing so that instead of
+ // x => withErrorSuppression(x) there could only be withErrorSuppression
+ (x) => withErrorSuppression(x),
+ ),
+
+ enabled: computed(() => !!registration.enabled),
+ visible: computed(() => true),
+ }),
+
+ injectionToken: trayMenuItemInjectionToken,
+ });
+
+ const childMenuItems = registration.submenu || [];
+
+ const childInjectables = childMenuItems.flatMap(_toItemInjectables(id));
+
+ return [
+ parentInjectable,
+ ...childInjectables,
+ ];
+ };
+
+ return _toItemInjectables(null);
+};
+
+
diff --git a/src/main/tray/tray-menu-item/tray-menu-items.injectable.ts b/src/main/tray/tray-menu-item/tray-menu-items.injectable.ts
new file mode 100644
index 0000000000..c29482007d
--- /dev/null
+++ b/src/main/tray/tray-menu-item/tray-menu-items.injectable.ts
@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getInjectable } from "@ogre-tools/injectable";
+import { computed } from "mobx";
+
+
+import mainExtensionsInjectable from "../../../extensions/main-extensions.injectable";
+import type { TrayMenuItem } from "./tray-menu-item-injection-token";
+import { trayMenuItemInjectionToken } from "./tray-menu-item-injection-token";
+import { pipeline } from "@ogre-tools/fp";
+import { filter, overSome, sortBy } from "lodash/fp";
+import type { LensMainExtension } from "../../../extensions/lens-main-extension";
+
+const trayMenuItemsInjectable = getInjectable({
+ id: "tray-menu-items",
+
+ instantiate: (di) => {
+ const extensions = di.inject(mainExtensionsInjectable);
+
+ return computed(() => {
+ const enabledExtensions = extensions.get();
+
+ return pipeline(
+ di.injectMany(trayMenuItemInjectionToken),
+
+ filter((item) =>
+ overSome([
+ isNonExtensionItem,
+ isEnabledExtensionItemFor(enabledExtensions),
+ ])(item),
+ ),
+
+ filter(item => item.visible.get()),
+ items => sortBy("orderNumber", items),
+ );
+ });
+ },
+});
+
+const isNonExtensionItem = (item: TrayMenuItem) => !item.extension;
+
+const isEnabledExtensionItemFor =
+ (enabledExtensions: LensMainExtension[]) => (item: TrayMenuItem) =>
+ !!enabledExtensions.find((extension) => extension === item.extension);
+
+
+export default trayMenuItemsInjectable;
diff --git a/src/main/tray/tray.injectable.ts b/src/main/tray/tray.injectable.ts
deleted file mode 100644
index 0e61062d50..0000000000
--- a/src/main/tray/tray.injectable.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-/**
- * Copyright (c) OpenLens Authors. All rights reserved.
- * Licensed under MIT License. See LICENSE in root directory for more information.
- */
-import { getInjectable } from "@ogre-tools/injectable";
-import { initTray } from "./tray";
-import trayMenuItemsInjectable from "./tray-menu-items.injectable";
-import navigateToPreferencesInjectable from "../../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable";
-import stopServicesAndExitAppInjectable from "../stop-services-and-exit-app.injectable";
-import { getStartableStoppable } from "../../common/utils/get-startable-stoppable";
-import isAutoUpdateEnabledInjectable from "../is-auto-update-enabled.injectable";
-import showAboutInjectable from "../menu/show-about.injectable";
-import showApplicationWindowInjectable from "../start-main-application/lens-window/show-application-window.injectable";
-import trayIconPathInjectable from "./tray-icon-path.injectable";
-
-const trayInjectable = getInjectable({
- id: "tray",
-
- instantiate: (di) => {
- const trayMenuItems = di.inject(trayMenuItemsInjectable);
- const navigateToPreferences = di.inject(navigateToPreferencesInjectable);
- const stopServicesAndExitApp = di.inject(stopServicesAndExitAppInjectable);
- const isAutoUpdateEnabled = di.inject(isAutoUpdateEnabledInjectable);
- const showApplicationWindow = di.inject(showApplicationWindowInjectable);
- const showAboutPopup = di.inject(showAboutInjectable);
- const trayIconPath = di.inject(trayIconPathInjectable);
-
- return getStartableStoppable("build-of-tray", () =>
- initTray(
- trayMenuItems,
- navigateToPreferences,
- stopServicesAndExitApp,
- isAutoUpdateEnabled,
- showApplicationWindow,
- showAboutPopup,
- trayIconPath,
- ),
- );
- },
-});
-
-export default trayInjectable;
diff --git a/src/main/tray/tray.ts b/src/main/tray/tray.ts
index c5d0b47ab1..4d7e39c344 100644
--- a/src/main/tray/tray.ts
+++ b/src/main/tray/tray.ts
@@ -7,25 +7,22 @@ import packageInfo from "../../../package.json";
import { Menu, Tray } from "electron";
import type { IComputedValue } from "mobx";
import { autorun } from "mobx";
-import { checkForUpdates } from "../app-updater";
import logger from "../logger";
-import { isWindows, productName } from "../../common/vars";
+import { isWindows } from "../../common/vars";
import type { Disposer } from "../../common/utils";
-import { disposer, toJS } from "../../common/utils";
-import type { TrayMenuRegistration } from "./tray-menu-registration";
+import { disposer } from "../../common/utils";
+import type { TrayMenuItem } from "./tray-menu-item/tray-menu-item-injection-token";
+import { pipeline } from "@ogre-tools/fp";
+import { filter, isEmpty, map } from "lodash/fp";
-const TRAY_LOG_PREFIX = "[TRAY]";
+export const TRAY_LOG_PREFIX = "[TRAY]";
// note: instance of Tray should be saved somewhere, otherwise it disappears
export let tray: Tray | null = null;
export function initTray(
- trayMenuItems: IComputedValue,
- navigateToPreferences: () => void,
- stopServicesAndExitApp: () => void,
- isAutoUpdateEnabled: () => boolean,
+ trayMenuItems: IComputedValue,
showApplicationWindow: () => Promise,
- showAbout: () => void,
trayIconPath: string,
): Disposer {
tray = new Tray(trayIconPath);
@@ -42,7 +39,9 @@ export function initTray(
return disposer(
autorun(() => {
try {
- const menu = createTrayMenu(toJS(trayMenuItems.get()), navigateToPreferences, stopServicesAndExitApp, isAutoUpdateEnabled, showApplicationWindow, showAbout);
+ const options = toTrayMenuOptions(trayMenuItems.get());
+
+ const menu = Menu.buildFromTemplate(options);
tray?.setContextMenu(menu);
} catch (error) {
@@ -56,66 +55,45 @@ export function initTray(
);
}
-function getMenuItemConstructorOptions(trayItem: TrayMenuRegistration): Electron.MenuItemConstructorOptions {
- return {
- ...trayItem,
- submenu: trayItem.submenu ? trayItem.submenu.map(getMenuItemConstructorOptions) : undefined,
- click: trayItem.click ? () => {
- trayItem.click?.(trayItem);
- } : undefined,
- };
-}
+const toTrayMenuOptions = (trayMenuItems: TrayMenuItem[]) => {
+ const _toTrayMenuOptions = (parentId: string | null) =>
+ pipeline(
+ trayMenuItems,
-function createTrayMenu(
- extensionTrayItems: TrayMenuRegistration[],
- navigateToPreferences: () => void,
- stopServicesAndExitApp: () => void,
- isAutoUpdateEnabled: () => boolean,
- showApplicationWindow: () => Promise,
- showAbout: () => void,
-): Menu {
- let template: Electron.MenuItemConstructorOptions[] = [
- {
- label: `Open ${productName}`,
- click() {
- showApplicationWindow().catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to open lens`, { error }));
- },
- },
- {
- label: "Preferences",
- click() {
- navigateToPreferences();
- },
- },
- ];
+ filter((item) => item.parentId === parentId),
- if (isAutoUpdateEnabled()) {
- template.push({
- label: "Check for updates",
- click() {
- checkForUpdates()
- .then(() => showApplicationWindow());
- },
- });
- }
+ map(
+ (trayMenuItem: TrayMenuItem): Electron.MenuItemConstructorOptions => {
+ if (trayMenuItem.separator) {
+ return { id: trayMenuItem.id, type: "separator" };
+ }
- template = template.concat(extensionTrayItems.map(getMenuItemConstructorOptions));
+ const childItems = _toTrayMenuOptions(trayMenuItem.id);
+
+ return {
+ id: trayMenuItem.id,
+ label: trayMenuItem.label?.get(),
+ enabled: trayMenuItem.enabled.get(),
+ toolTip: trayMenuItem.tooltip,
+
+ ...(isEmpty(childItems)
+ ? {
+ type: "normal",
+ submenu: _toTrayMenuOptions(trayMenuItem.id),
+
+ click: () => {
+ trayMenuItem.click?.();
+ },
+ }
+ : {
+ type: "submenu",
+ submenu: _toTrayMenuOptions(trayMenuItem.id),
+ }),
+ };
+ },
+ ),
+ );
+
+ return _toTrayMenuOptions(null);
+};
- return Menu.buildFromTemplate(template.concat([
- {
- label: `About ${productName}`,
- click() {
- showApplicationWindow()
- .then(showAbout)
- .catch(error => logger.error(`${TRAY_LOG_PREFIX}: Failed to show Lens About view`, { error }));
- },
- },
- { type: "separator" },
- {
- label: "Quit App",
- click() {
- stopServicesAndExitApp();
- },
- },
- ]));
-}
diff --git a/src/main/tray/uninstall-tray.injectable.ts b/src/main/tray/uninstall-tray.injectable.ts
deleted file mode 100644
index 41b3cd676c..0000000000
--- a/src/main/tray/uninstall-tray.injectable.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/**
- * Copyright (c) OpenLens Authors. All rights reserved.
- * Licensed under MIT License. See LICENSE in root directory for more information.
- */
-import { getInjectable } from "@ogre-tools/injectable";
-import trayInjectable from "./tray.injectable";
-import { beforeQuitOfBackEndInjectionToken } from "../start-main-application/runnable-tokens/before-quit-of-back-end-injection-token";
-
-const uninstallTrayInjectable = getInjectable({
- id: "uninstall-tray",
-
- instantiate: (di) => {
- const trayInitializer = di.inject(trayInjectable);
-
- return {
- run: async () => {
- await trayInitializer.stop();
- },
- };
- },
-
- injectionToken: beforeQuitOfBackEndInjectionToken,
-});
-
-export default uninstallTrayInjectable;
diff --git a/src/main/utils/__test__/update-channel.test.ts b/src/main/utils/__test__/update-channel.test.ts
deleted file mode 100644
index e0c20e4707..0000000000
--- a/src/main/utils/__test__/update-channel.test.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-/**
- * Copyright (c) OpenLens Authors. All rights reserved.
- * Licensed under MIT License. See LICENSE in root directory for more information.
- */
-
-import { nextUpdateChannel } from "../update-channel";
-
-describe("nextUpdateChannel", () => {
- it("returns latest if current channel is latest", () => {
- expect(nextUpdateChannel("latest", "latest")).toEqual("latest");
- });
-
- it("returns beta if current channel is alpha", () => {
- expect(nextUpdateChannel("alpha", "alpha")).toEqual("beta");
- expect(nextUpdateChannel("beta", "alpha")).toEqual("beta");
- expect(nextUpdateChannel("rc", "alpha")).toEqual("beta");
- expect(nextUpdateChannel("latest", "alpha")).toEqual("beta");
- });
-
- it("returns latest if current channel is beta", () => {
- expect(nextUpdateChannel("alpha", "beta")).toEqual("latest");
- expect(nextUpdateChannel("beta", "beta")).toEqual("latest");
- expect(nextUpdateChannel("rc", "beta")).toEqual("latest");
- expect(nextUpdateChannel("latest", "beta")).toEqual("latest");
- });
-
- it("returns default if current channel is unknown", () => {
- expect(nextUpdateChannel("alpha", "rc")).toEqual("alpha");
- expect(nextUpdateChannel("beta", "rc")).toEqual("beta");
- expect(nextUpdateChannel("rc", "rc")).toEqual("rc");
- expect(nextUpdateChannel("latest", "rc")).toEqual("latest");
- });
-});
diff --git a/src/main/utils/channel/channel-listeners/enlist-message-channel-listener.injectable.ts b/src/main/utils/channel/channel-listeners/enlist-message-channel-listener.injectable.ts
new file mode 100644
index 0000000000..6b7fa9b8df
--- /dev/null
+++ b/src/main/utils/channel/channel-listeners/enlist-message-channel-listener.injectable.ts
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getInjectable } from "@ogre-tools/injectable";
+import type { IpcMainEvent } from "electron";
+import ipcMainInjectable from "../ipc-main/ipc-main.injectable";
+import { enlistMessageChannelListenerInjectionToken } from "../../../../common/utils/channel/enlist-message-channel-listener-injection-token";
+import { pipeline } from "@ogre-tools/fp";
+import { tentativeParseJson } from "../../../../common/utils/tentative-parse-json";
+
+const enlistMessageChannelListenerInjectable = getInjectable({
+ id: "enlist-message-channel-listener-for-main",
+
+ instantiate: (di) => {
+ const ipcMain = di.inject(ipcMainInjectable);
+
+ return ({ channel, handler }) => {
+ const nativeOnCallback = (_: IpcMainEvent, message: unknown) => {
+ pipeline(
+ message,
+ tentativeParseJson,
+ handler,
+ );
+ };
+
+ ipcMain.on(channel.id, nativeOnCallback);
+
+ return () => {
+ ipcMain.off(channel.id, nativeOnCallback);
+ };
+ };
+ },
+
+ injectionToken: enlistMessageChannelListenerInjectionToken,
+});
+
+export default enlistMessageChannelListenerInjectable;
diff --git a/src/main/utils/channel/channel-listeners/enlist-message-channel-listener.test.ts b/src/main/utils/channel/channel-listeners/enlist-message-channel-listener.test.ts
new file mode 100644
index 0000000000..3bd0398d8e
--- /dev/null
+++ b/src/main/utils/channel/channel-listeners/enlist-message-channel-listener.test.ts
@@ -0,0 +1,97 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
+import ipcMainInjectable from "../ipc-main/ipc-main.injectable";
+import type { EnlistMessageChannelListener } from "../../../../common/utils/channel/enlist-message-channel-listener-injection-token";
+import { enlistMessageChannelListenerInjectionToken } from "../../../../common/utils/channel/enlist-message-channel-listener-injection-token";
+import type { IpcMain, IpcMainEvent } from "electron";
+
+describe("enlist message channel listener in main", () => {
+ let enlistMessageChannelListener: EnlistMessageChannelListener;
+ let ipcMainStub: IpcMain;
+ let onMock: jest.Mock;
+ let offMock: jest.Mock;
+
+ beforeEach(() => {
+ const di = getDiForUnitTesting({ doGeneralOverrides: true });
+
+ onMock = jest.fn();
+ offMock = jest.fn();
+
+ ipcMainStub = {
+ on: onMock,
+ off: offMock,
+ } as unknown as IpcMain;
+
+ di.override(ipcMainInjectable, () => ipcMainStub);
+
+ enlistMessageChannelListener = di.inject(
+ enlistMessageChannelListenerInjectionToken,
+ );
+ });
+
+ describe("when called", () => {
+ let handlerMock: jest.Mock;
+ let disposer: () => void;
+
+ beforeEach(() => {
+ handlerMock = jest.fn();
+
+ disposer = enlistMessageChannelListener({
+ channel: { id: "some-channel-id" },
+ handler: handlerMock,
+ });
+ });
+
+ it("does not call handler yet", () => {
+ expect(handlerMock).not.toHaveBeenCalled();
+ });
+
+ it("registers the listener", () => {
+ expect(onMock).toHaveBeenCalledWith(
+ "some-channel-id",
+ expect.any(Function),
+ );
+ });
+
+ it("does not de-register the listener yet", () => {
+ expect(offMock).not.toHaveBeenCalled();
+ });
+
+ describe("when message arrives", () => {
+ beforeEach(() => {
+ onMock.mock.calls[0][1]({} as IpcMainEvent, "some-message");
+ });
+
+ it("calls the handler with the message", () => {
+ expect(handlerMock).toHaveBeenCalledWith("some-message");
+ });
+
+ it("when disposing the listener, de-registers the listener", () => {
+ disposer();
+
+ expect(offMock).toHaveBeenCalledWith("some-channel-id", expect.any(Function));
+ });
+ });
+
+ it("given number as message, when message arrives, calls the handler with the message", () => {
+ onMock.mock.calls[0][1]({} as IpcMainEvent, 42);
+
+ expect(handlerMock).toHaveBeenCalledWith(42);
+ });
+
+ it("given boolean as message, when message arrives, calls the handler with the message", () => {
+ onMock.mock.calls[0][1]({} as IpcMainEvent, true);
+
+ expect(handlerMock).toHaveBeenCalledWith(true);
+ });
+
+ it("given stringified object as message, when message arrives, calls the handler with the message", () => {
+ onMock.mock.calls[0][1]({} as IpcMainEvent, JSON.stringify({ some: "object" }));
+
+ expect(handlerMock).toHaveBeenCalledWith({ some: "object" });
+ });
+ });
+});
diff --git a/src/main/utils/channel/channel-listeners/enlist-request-channel-listener.injectable.ts b/src/main/utils/channel/channel-listeners/enlist-request-channel-listener.injectable.ts
new file mode 100644
index 0000000000..6f118288f3
--- /dev/null
+++ b/src/main/utils/channel/channel-listeners/enlist-request-channel-listener.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 { IpcMainInvokeEvent } from "electron";
+import ipcMainInjectable from "../ipc-main/ipc-main.injectable";
+import { enlistRequestChannelListenerInjectionToken } from "../../../../common/utils/channel/enlist-request-channel-listener-injection-token";
+import { pipeline } from "@ogre-tools/fp";
+import { tentativeParseJson } from "../../../../common/utils/tentative-parse-json";
+import { tentativeStringifyJson } from "../../../../common/utils/tentative-stringify-json";
+
+const enlistRequestChannelListenerInjectable = getInjectable({
+ id: "enlist-request-channel-listener-for-main",
+
+ instantiate: (di) => {
+ const ipcMain = di.inject(ipcMainInjectable);
+
+ return ({ channel, handler }) => {
+ const nativeHandleCallback = (_: IpcMainInvokeEvent, request: unknown) =>
+ pipeline(request, tentativeParseJson, handler, tentativeStringifyJson);
+
+ ipcMain.handle(channel.id, nativeHandleCallback);
+
+ return () => {
+ ipcMain.off(channel.id, nativeHandleCallback);
+ };
+ };
+ },
+
+ injectionToken: enlistRequestChannelListenerInjectionToken,
+});
+
+export default enlistRequestChannelListenerInjectable;
diff --git a/src/main/utils/channel/channel-listeners/enlist-request-channel-listener.test.ts b/src/main/utils/channel/channel-listeners/enlist-request-channel-listener.test.ts
new file mode 100644
index 0000000000..12a5e9af74
--- /dev/null
+++ b/src/main/utils/channel/channel-listeners/enlist-request-channel-listener.test.ts
@@ -0,0 +1,147 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { getDiForUnitTesting } from "../../../getDiForUnitTesting";
+import ipcMainInjectable from "../ipc-main/ipc-main.injectable";
+import type { IpcMain, IpcMainInvokeEvent } from "electron";
+import type { EnlistRequestChannelListener } from "../../../../common/utils/channel/enlist-request-channel-listener-injection-token";
+import { enlistRequestChannelListenerInjectionToken } from "../../../../common/utils/channel/enlist-request-channel-listener-injection-token";
+import { getPromiseStatus } from "../../../../common/test-utils/get-promise-status";
+import type { AsyncFnMock } from "@async-fn/jest";
+import asyncFn from "@async-fn/jest";
+
+describe("enlist request channel listener in main", () => {
+ let enlistRequestChannelListener: EnlistRequestChannelListener;
+ let ipcMainStub: IpcMain;
+ let handleMock: jest.Mock;
+ let offMock: jest.Mock;
+
+ beforeEach(() => {
+ const di = getDiForUnitTesting({ doGeneralOverrides: true });
+
+ handleMock = jest.fn();
+ offMock = jest.fn();
+
+ ipcMainStub = {
+ handle: handleMock,
+ off: offMock,
+ } as unknown as IpcMain;
+
+ di.override(ipcMainInjectable, () => ipcMainStub);
+
+ enlistRequestChannelListener = di.inject(
+ enlistRequestChannelListenerInjectionToken,
+ );
+ });
+
+ describe("when called", () => {
+ let handlerMock: AsyncFnMock<(message: any) => any>;
+ let disposer: () => void;
+
+ beforeEach(() => {
+ handlerMock = asyncFn();
+
+ disposer = enlistRequestChannelListener({
+ channel: { id: "some-channel-id" },
+ handler: handlerMock,
+ });
+ });
+
+ it("does not call handler yet", () => {
+ expect(handlerMock).not.toHaveBeenCalled();
+ });
+
+ it("registers the listener", () => {
+ expect(handleMock).toHaveBeenCalledWith(
+ "some-channel-id",
+ expect.any(Function),
+ );
+ });
+
+ it("does not de-register the listener yet", () => {
+ expect(offMock).not.toHaveBeenCalled();
+ });
+
+ describe("when request arrives", () => {
+ let actualPromise: Promise;
+
+ beforeEach(() => {
+ actualPromise = handleMock.mock.calls[0][1](
+ {} as IpcMainInvokeEvent,
+ "some-request",
+ );
+ });
+
+ it("calls the handler with the request", () => {
+ expect(handlerMock).toHaveBeenCalledWith("some-request");
+ });
+
+ it("does not resolve yet", async () => {
+ const promiseStatus = await getPromiseStatus(actualPromise);
+
+ expect(promiseStatus.fulfilled).toBe(false);
+ });
+
+ describe("when handler resolves with response, listener resolves with the response", () => {
+ beforeEach(async () => {
+ await handlerMock.resolve("some-response");
+ });
+
+ it("resolves with the response", async () => {
+ const actual = await actualPromise;
+
+ expect(actual).toBe('"some-response"');
+ });
+
+ it("when disposing the listener, de-registers the listener", () => {
+ disposer();
+
+ expect(offMock).toHaveBeenCalledWith("some-channel-id", expect.any(Function));
+ });
+ });
+
+ it("given number as response, when handler resolves with response, listener resolves with stringified response", async () => {
+ await handlerMock.resolve(42);
+
+ const actual = await actualPromise;
+
+ expect(actual).toBe("42");
+ });
+
+ it("given boolean as response, when handler resolves with response, listener resolves with stringified response", async () => {
+ await handlerMock.resolve(true);
+
+ const actual = await actualPromise;
+
+ expect(actual).toBe("true");
+ });
+
+ it("given object as response, when handler resolves with response, listener resolves with stringified response", async () => {
+ await handlerMock.resolve({ some: "object" });
+
+ const actual = await actualPromise;
+
+ expect(actual).toBe(JSON.stringify({ some: "object" }));
+ });
+ });
+
+ it("given number as request, when request arrives, calls the handler with the request", () => {
+ handleMock.mock.calls[0][1]({} as IpcMainInvokeEvent, 42);
+
+ expect(handlerMock).toHaveBeenCalledWith(42);
+ });
+
+ it("given boolean as request, when request arrives, calls the handler with the request", () => {
+ handleMock.mock.calls[0][1]({} as IpcMainInvokeEvent, true);
+
+ expect(handlerMock).toHaveBeenCalledWith(true);
+ });
+
+ it("given stringified object as request, when request arrives, calls the handler with the request", () => {
+ handleMock.mock.calls[0][1]({} as IpcMainInvokeEvent, JSON.stringify({ some: "object" }));
+
+ expect(handlerMock).toHaveBeenCalledWith({ some: "object" });
+ });
+ });
+});
diff --git a/src/main/utils/channel/channel-listeners/start-listening-of-channels.injectable.ts b/src/main/utils/channel/channel-listeners/start-listening-of-channels.injectable.ts
new file mode 100644
index 0000000000..96fea0a2f0
--- /dev/null
+++ b/src/main/utils/channel/channel-listeners/start-listening-of-channels.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 listeningOfChannelsInjectable from "../../../../common/utils/channel/listening-of-channels.injectable";
+
+const startListeningOfChannelsInjectable = getInjectable({
+ id: "start-listening-of-channels-main",
+
+ instantiate: (di) => {
+ const listeningOfChannels = di.inject(listeningOfChannelsInjectable);
+
+ return {
+ run: async () => {
+ await listeningOfChannels.start();
+ },
+ };
+ },
+
+ injectionToken: onLoadOfApplicationInjectionToken,
+});
+
+export default startListeningOfChannelsInjectable;
diff --git a/src/main/app-paths/register-channel/ipc-main/ipc-main.injectable.ts b/src/main/utils/channel/ipc-main/ipc-main.injectable.ts
similarity index 100%
rename from src/main/app-paths/register-channel/ipc-main/ipc-main.injectable.ts
rename to src/main/utils/channel/ipc-main/ipc-main.injectable.ts
diff --git a/src/main/utils/channel/message-to-channel.injectable.ts b/src/main/utils/channel/message-to-channel.injectable.ts
new file mode 100644
index 0000000000..00e588a16a
--- /dev/null
+++ b/src/main/utils/channel/message-to-channel.injectable.ts
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+import { lensWindowInjectionToken } from "../../start-main-application/lens-window/application-window/lens-window-injection-token";
+import { pipeline } from "@ogre-tools/fp";
+import { getInjectable } from "@ogre-tools/injectable";
+import { filter } from "lodash/fp";
+import { messageToChannelInjectionToken } from "../../../common/utils/channel/message-to-channel-injection-token";
+import type { MessageChannel } from "../../../common/utils/channel/message-channel-injection-token";
+import { tentativeStringifyJson } from "../../../common/utils/tentative-stringify-json";
+
+const messageToChannelInjectable = getInjectable({
+ id: "message-to-channel",
+
+ instantiate: (di) => {
+ const getAllLensWindows = () => di.injectMany(lensWindowInjectionToken);
+
+ // TODO: Figure out way to improve typing in internals
+ // Notice that this should be injected using "messageToChannelInjectionToken" which is typed correctly.
+ return (channel: MessageChannel, message?: unknown) => {
+ const stringifiedMessage = tentativeStringifyJson(message);
+
+
+ const visibleWindows = pipeline(
+ getAllLensWindows(),
+ filter((lensWindow) => !!lensWindow.visible),
+ );
+
+ visibleWindows.forEach((lensWindow) =>
+ lensWindow.send({ channel: channel.id, data: stringifiedMessage ? [stringifiedMessage] : [] }),
+ );
+ };
+ },
+
+ injectionToken: messageToChannelInjectionToken,
+});
+
+export default messageToChannelInjectable;
diff --git a/src/main/utils/channel/message-to-channel.test.ts b/src/main/utils/channel/message-to-channel.test.ts
new file mode 100644
index 0000000000..cf2fc46549
--- /dev/null
+++ b/src/main/utils/channel/message-to-channel.test.ts
@@ -0,0 +1,167 @@
+/**
+ * Copyright (c) OpenLens Authors. All rights reserved.
+ * Licensed under MIT License. See LICENSE in root directory for more information.
+ */
+
+import type { MessageToChannel } from "../../../common/utils/channel/message-to-channel-injection-token";
+import { messageToChannelInjectionToken } from "../../../common/utils/channel/message-to-channel-injection-token";
+import closeAllWindowsInjectable from "../../start-main-application/lens-window/hide-all-windows/close-all-windows.injectable";
+import type { MessageChannel } from "../../../common/utils/channel/message-channel-injection-token";
+import { getDiForUnitTesting } from "../../getDiForUnitTesting";
+import createLensWindowInjectable from "../../start-main-application/lens-window/application-window/create-lens-window.injectable";
+import type { LensWindow } from "../../start-main-application/lens-window/application-window/lens-window-injection-token";
+import { lensWindowInjectionToken } from "../../start-main-application/lens-window/application-window/lens-window-injection-token";
+import type { DiContainer } from "@ogre-tools/injectable";
+import { getInjectable } from "@ogre-tools/injectable";
+import sendToChannelInElectronBrowserWindowInjectable from "../../start-main-application/lens-window/application-window/send-to-channel-in-electron-browser-window.injectable";
+
+describe("message to channel from main", () => {
+ let messageToChannel: MessageToChannel;
+ let someTestWindow: LensWindow;
+ let someOtherTestWindow: LensWindow;
+ let sendToChannelInBrowserMock: jest.Mock;
+
+ beforeEach(() => {
+ const di = getDiForUnitTesting({ doGeneralOverrides: true });
+
+ sendToChannelInBrowserMock = jest.fn();
+ di.override(sendToChannelInElectronBrowserWindowInjectable, () => sendToChannelInBrowserMock);
+
+ someTestWindow = createTestWindow(di, "some-test-window-id");
+ someOtherTestWindow = createTestWindow(di, "some-other-test-window-id");
+
+ messageToChannel = di.inject(messageToChannelInjectionToken);
+
+ const closeAllWindows = di.inject(closeAllWindowsInjectable);
+
+ closeAllWindows();
+ });
+
+ it("given no visible windows, when messaging to channel, does not message to any window", () => {
+ messageToChannel(someChannel, "some-message");
+
+ expect(sendToChannelInBrowserMock).not.toHaveBeenCalled();
+ });
+
+ describe("given visible window", () => {
+ beforeEach(async () => {
+ await someTestWindow.show();
+ });
+
+ it("when messaging to channel, messages to window", () => {
+ messageToChannel(someChannel, "some-message");
+
+ expect(sendToChannelInBrowserMock.mock.calls).toEqual([
+ [
+ null,
+
+ {
+ channel: "some-channel",
+ data: ['"some-message"'],
+ },
+ ],
+ ]);
+ });
+
+ it("given boolean as message, when messaging to channel, messages to window with stringified message", () => {
+ messageToChannel(someChannel, true);
+
+ expect(sendToChannelInBrowserMock.mock.calls).toEqual([
+ [
+ null,
+
+ {
+ channel: "some-channel",
+ data: ["true"],
+ },
+ ],
+ ]);
+ });
+
+ it("given number as message, when messaging to channel, messages to window with stringified message", () => {
+ messageToChannel(someChannel, 42);
+
+ expect(sendToChannelInBrowserMock.mock.calls).toEqual([
+ [
+ null,
+
+ {
+ channel: "some-channel",
+ data: ["42"],
+ },
+ ],
+ ]);
+ });
+
+ it("given object as message, when messaging to channel, messages to window with stringified message", () => {
+ messageToChannel(someChannel, { some: "object" });
+
+ expect(sendToChannelInBrowserMock.mock.calls).toEqual([
+ [
+ null,
+
+ {
+ channel: "some-channel",
+ data: [JSON.stringify({ some: "object" })],
+ },
+ ],
+ ]);
+ });
+ });
+
+ it("given multiple visible windows, when messaging to channel, messages to window", async () => {
+ await someTestWindow.show();
+ await someOtherTestWindow.show();
+
+ messageToChannel(someChannel, "some-message");
+
+ expect(sendToChannelInBrowserMock.mock.calls).toEqual([
+ [
+ null,
+
+ {
+ channel: "some-channel",
+ data: ['"some-message"'],
+ },
+ ],
+
+ [
+ null,
+
+ {
+ channel: "some-channel",
+ data: ['"some-message"'],
+ },
+ ],
+ ]);
+ });
+});
+
+const someChannel: MessageChannel = { id: "some-channel" };
+
+const createTestWindow = (di: DiContainer, id: string) => {
+ const testWindowInjectable = getInjectable({
+ id,
+
+ instantiate: (di) => {
+ const createLensWindow = di.inject(createLensWindowInjectable);
+
+ return createLensWindow({
+ id,
+ title: "Some test window",
+ defaultHeight: 42,
+ defaultWidth: 42,
+ getContentSource: () => ({ url: "some-content-url" }),
+ resizable: true,
+ windowFrameUtilitiesAreShown: false,
+ centered: false,
+ });
+ },
+
+ injectionToken: lensWindowInjectionToken,
+ });
+
+ di.register(testWindowInjectable);
+
+ return di.inject(testWindowInjectable);
+};
diff --git a/src/main/utils/sync-box/sync-box-initial-value-channel-listener.injectable.ts b/src/main/utils/sync-box/sync-box-initial-value-channel-listener.injectable.ts
new file mode 100644
index 0000000000..5eb043291a
--- /dev/null
+++ b/src/main/utils/sync-box/sync-box-initial-value-channel-listener.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 syncBoxInitialValueChannelInjectable from "../../../common/utils/sync-box/sync-box-initial-value-channel.injectable";
+import { syncBoxInjectionToken } from "../../../common/utils/sync-box/sync-box-injection-token";
+import { requestChannelListenerInjectionToken } from "../../../common/utils/channel/request-channel-listener-injection-token";
+
+const syncBoxInitialValueChannelListenerInjectable = getInjectable({
+ id: "sync-box-initial-value-channel-listener",
+
+ instantiate: (di) => {
+ const channel = di.inject(syncBoxInitialValueChannelInjectable);
+ const syncBoxes = di.injectMany(syncBoxInjectionToken);
+
+ return {
+ channel,
+
+ handler: () =>
+ syncBoxes.map((box) => ({
+ id: box.id,
+ value: box.value.get(),
+ })),
+ };
+ },
+
+ injectionToken: requestChannelListenerInjectionToken,
+});
+
+export default syncBoxInitialValueChannelListenerInjectable;
diff --git a/src/main/utils/update-channel.ts b/src/main/utils/update-channel.ts
deleted file mode 100644
index 598d0f0bfd..0000000000
--- a/src/main/utils/update-channel.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.
- */
-
-/**
- * Compute the next update channel from the current updating channel
- * @param defaultChannel The default (initial) channel to check
- * @param channel The current channel that did not have a new version associated with it
- * @returns The channel name of the next release version
- */
-export function nextUpdateChannel(defaultChannel: string, channel: string | null): string {
- switch (channel) {
- case "alpha":
- return "beta";
- case "beta":
- return "latest"; // there is no RC currently
- default:
- return defaultChannel;
- }
-}
diff --git a/src/renderer/app-paths/get-value-from-registered-channel/get-value-from-registered-channel.injectable.ts b/src/renderer/app-paths/get-value-from-registered-channel/get-value-from-registered-channel.injectable.ts
deleted file mode 100644
index b9ddee1007..0000000000
--- a/src/renderer/app-paths/get-value-from-registered-channel/get-value-from-registered-channel.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 ipcRendererInjectable from "./ipc-renderer/ipc-renderer.injectable";
-import { getValueFromRegisteredChannel } from "./get-value-from-registered-channel";
-import type { Channel } from "../../../common/ipc-channel/channel";
-
-export type GetValueFromRegisteredChannel = , TInstance>(channel: TChannel) => Promise;
-
-const getValueFromRegisteredChannelInjectable = getInjectable({
- id: "get-value-from-registered-channel",
-
- instantiate: (di) => getValueFromRegisteredChannel({
- ipcRenderer: di.inject(ipcRendererInjectable),
- }),
-});
-
-export default getValueFromRegisteredChannelInjectable;
diff --git a/src/renderer/app-paths/get-value-from-registered-channel/get-value-from-registered-channel.ts b/src/renderer/app-paths/get-value-from-registered-channel/get-value-from-registered-channel.ts
deleted file mode 100644
index 8e0c953784..0000000000
--- a/src/renderer/app-paths/get-value-from-registered-channel/get-value-from-registered-channel.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-/**
- * Copyright (c) OpenLens Authors. All rights reserved.
- * Licensed under MIT License. See LICENSE in root directory for more information.
- */
-import type { IpcRenderer } from "electron";
-import type { Channel } from "../../../common/ipc-channel/channel";
-
-interface Dependencies {
- ipcRenderer: IpcRenderer;
-}
-
-export const getValueFromRegisteredChannel = ({ ipcRenderer }: Dependencies) =>
- , TInstance>(
- channel: TChannel,
- ): Promise =>
- ipcRenderer.invoke(channel.name);
diff --git a/src/renderer/app-paths/get-value-from-registered-channel/register-ipc-channel-listener.injectable.ts b/src/renderer/app-paths/get-value-from-registered-channel/register-ipc-channel-listener.injectable.ts
deleted file mode 100644
index 151d77f097..0000000000
--- a/src/renderer/app-paths/get-value-from-registered-channel/register-ipc-channel-listener.injectable.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/**
- * Copyright (c) OpenLens Authors. All rights reserved.
- * Licensed under MIT License. See LICENSE in root directory for more information.
- */
-import { getInjectable } from "@ogre-tools/injectable";
-import ipcRendererInjectable from "./ipc-renderer/ipc-renderer.injectable";
-import type {
- IpcChannelListener,
-} from "../../ipc-channel-listeners/ipc-channel-listener-injection-token";
-
-const registerIpcChannelListenerInjectable = getInjectable({
- id: "register-ipc-channel-listener",
-
- instantiate: (di) => {
- const ipc = di.inject(ipcRendererInjectable);
-
- return ({ channel, handle }: IpcChannelListener) => {
- ipc.on(channel.name, (_, data) => {
- handle(data);
- });
- };
- },
-});
-
-export default registerIpcChannelListenerInjectable;
diff --git a/src/renderer/app-paths/setup-app-paths.injectable.ts b/src/renderer/app-paths/setup-app-paths.injectable.ts
index e6cf30f0dd..14242347f4 100644
--- a/src/renderer/app-paths/setup-app-paths.injectable.ts
+++ b/src/renderer/app-paths/setup-app-paths.injectable.ts
@@ -3,27 +3,29 @@
* 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";
+import appPathsChannelInjectable from "../../common/app-paths/app-paths-channel.injectable";
+import { requestFromChannelInjectionToken } from "../../common/utils/channel/request-from-channel-injection-token";
const setupAppPathsInjectable = getInjectable({
id: "setup-app-paths",
- instantiate: (di) => ({
- run: async () => {
- const getValueFromRegisteredChannel = di.inject(
- getValueFromRegisteredChannelInjectable,
- );
+ instantiate: (di) => {
+ const requestFromChannel = di.inject(requestFromChannelInjectionToken);
+ const appPathsChannel = di.inject(appPathsChannelInjectable);
+ const appPathsState = di.inject(appPathsStateInjectable);
- const syncAppPaths = await getValueFromRegisteredChannel(appPathsIpcChannel);
+ return {
+ run: async () => {
+ const appPaths = await requestFromChannel(
+ appPathsChannel,
+ );
- const appPathsState = di.inject(appPathsStateInjectable);
-
- appPathsState.set(syncAppPaths);
- },
- }),
+ appPathsState.set(appPaths);
+ },
+ };
+ },
injectionToken: beforeFrameStartsInjectionToken,
});
diff --git a/src/renderer/application-update/application-update-status-listener.injectable.ts b/src/renderer/application-update/application-update-status-listener.injectable.ts
new file mode 100644
index 0000000000..69ba23608c
--- /dev/null
+++ b/src/renderer/application-update/application-update-status-listener.injectable.ts
@@ -0,0 +1,57 @@
+/**
+ * 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 { ApplicationUpdateStatusChannel, ApplicationUpdateStatusEventId } from "../../common/application-update/application-update-status-channel.injectable";
+import applicationUpdateStatusChannelInjectable from "../../common/application-update/application-update-status-channel.injectable";
+import showInfoNotificationInjectable from "../components/notifications/show-info-notification.injectable";
+import type { MessageChannelListener } from "../../common/utils/channel/message-channel-listener-injection-token";
+import { messageChannelListenerInjectionToken } from "../../common/utils/channel/message-channel-listener-injection-token";
+
+const applicationUpdateStatusListenerInjectable = getInjectable({
+ id: "application-update-status-listener",
+
+ instantiate: (di): MessageChannelListener => {
+ const channel = di.inject(applicationUpdateStatusChannelInjectable);
+ const showInfoNotification = di.inject(showInfoNotificationInjectable);
+
+ const eventHandlers: Record void }> = {
+ "checking-for-updates": {
+ handle: () => {
+ showInfoNotification("Checking for updates...");
+ },
+ },
+
+ "no-updates-available": {
+ handle: () => {
+ showInfoNotification("No new updates available");
+ },
+ },
+
+ "download-for-update-started": {
+ handle: (version) => {
+ showInfoNotification(`Download for version ${version} started...`);
+ },
+ },
+
+ "download-for-update-failed": {
+ handle: () => {
+ showInfoNotification("Download of update failed");
+ },
+ },
+ };
+
+ return {
+ channel,
+
+ handler: ({ eventId, version }) => {
+ eventHandlers[eventId].handle(version);
+ },
+ };
+ },
+
+ injectionToken: messageChannelListenerInjectionToken,
+});
+
+export default applicationUpdateStatusListenerInjectable;
diff --git a/src/renderer/ask-boolean/ask-boolean-question-channel-listener.injectable.tsx b/src/renderer/ask-boolean/ask-boolean-question-channel-listener.injectable.tsx
new file mode 100644
index 0000000000..5e9adff4cc
--- /dev/null
+++ b/src/renderer/ask-boolean/ask-boolean-question-channel-listener.injectable.tsx
@@ -0,0 +1,107 @@
+/**
+ * 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 { AskBooleanQuestionChannel } from "../../common/ask-boolean/ask-boolean-question-channel.injectable";
+import askBooleanQuestionChannelInjectable from "../../common/ask-boolean/ask-boolean-question-channel.injectable";
+import showInfoNotificationInjectable from "../components/notifications/show-info-notification.injectable";
+import { Button } from "../components/button";
+import React from "react";
+import { messageToChannelInjectionToken } from "../../common/utils/channel/message-to-channel-injection-token";
+import askBooleanAnswerChannelInjectable from "../../common/ask-boolean/ask-boolean-answer-channel.injectable";
+import notificationsStoreInjectable from "../components/notifications/notifications-store.injectable";
+import type { MessageChannelListener } from "../../common/utils/channel/message-channel-listener-injection-token";
+import { messageChannelListenerInjectionToken } from "../../common/utils/channel/message-channel-listener-injection-token";
+
+const askBooleanQuestionChannelListenerInjectable = getInjectable({
+ id: "ask-boolean-question-channel-listener",
+
+ instantiate: (di): MessageChannelListener => {
+ const questionChannel = di.inject(askBooleanQuestionChannelInjectable);
+ const showInfoNotification = di.inject(showInfoNotificationInjectable);
+ const messageToChannel = di.inject(messageToChannelInjectionToken);
+ const answerChannel = di.inject(askBooleanAnswerChannelInjectable);
+ const notificationsStore = di.inject(notificationsStoreInjectable);
+
+ const sendAnswerFor = (id: string) => (value: boolean) => {
+ messageToChannel(answerChannel, { id, value });
+ };
+
+ const closeNotification = (notificationId: string) => {
+ notificationsStore.remove(notificationId);
+ };
+
+ const sendAnswerAndCloseNotificationFor = (sendAnswer: (value: boolean) => void, notificationId: string) => (value: boolean) => () => {
+ sendAnswer(value);
+ closeNotification(notificationId);
+ };
+
+ return {
+ channel: questionChannel,
+
+ handler: ({ id: questionId, title, question }) => {
+ const notificationId = `ask-boolean-for-${questionId}`;
+
+ const sendAnswer = sendAnswerFor(questionId);
+ const sendAnswerAndCloseNotification = sendAnswerAndCloseNotificationFor(sendAnswer, notificationId);
+
+ showInfoNotification(
+ ,
+
+ {
+ id: notificationId,
+ timeout: 0,
+ onClose: () => sendAnswer(false),
+ },
+ );
+ },
+ };
+ },
+
+ injectionToken: messageChannelListenerInjectionToken,
+});
+
+export default askBooleanQuestionChannelListenerInjectable;
+
+const AskBoolean = ({
+ id,
+ title,
+ message,
+ onNo,
+ onYes,
+}: {
+ id: string;
+ title: string;
+ message: string;
+ onNo: () => void;
+ onYes: () => void;
+}) => (
+
+ {title}
+
{message}
+
+
+
+
+
+
+
+);
diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx
index b2fa7700b9..fabb17cb5a 100644
--- a/src/renderer/bootstrap.tsx
+++ b/src/renderer/bootstrap.tsx
@@ -46,8 +46,7 @@ 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";
+import startFrameInjectable from "./start-frame/start-frame.injectable";
configurePackages(); // global packages
registerCustomThemes(); // monaco editor themes
@@ -68,9 +67,9 @@ export async function bootstrap(di: DiContainer) {
initializeSentryReporting(init);
}
- const beforeFrameStarts = runManyFor(di)(beforeFrameStartsInjectionToken);
+ const startFrame = di.inject(startFrameInjectable);
- await beforeFrameStarts();
+ await startFrame();
// TODO: Consolidate import time side-effect to setup time
bindEvents();
diff --git a/src/renderer/components/+catalog/catalog.test.tsx b/src/renderer/components/+catalog/catalog.test.tsx
index 1fd7e3f3f8..808226727c 100644
--- a/src/renderer/components/+catalog/catalog.test.tsx
+++ b/src/renderer/components/+catalog/catalog.test.tsx
@@ -26,8 +26,6 @@ import appVersionInjectable from "../../../common/get-configuration-file-model/a
import type { AppEvent } from "../../../common/app-event-bus/event-bus";
import appEventBusInjectable from "../../../common/app-event-bus/app-event-bus.injectable";
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();
@@ -107,13 +105,7 @@ describe("", () => {
catalogEntityItem = createMockCatalogEntity(onRun);
catalogEntityRegistry = di.inject(catalogEntityRegistryInjectable);
- UserStore.createInstance(); // TODO: replace with DI
-
di.override(catalogEntityRegistryInjectable, () => catalogEntityRegistry);
- di.override(ipcRendererInjectable, () => ({
- on: jest.fn(),
- invoke: jest.fn(), // TODO: replace with proper mocking via the IPC bridge
- } as never));
emitEvent = jest.fn();
@@ -129,7 +121,6 @@ describe("", () => {
afterEach(() => {
CatalogEntityDetailRegistry.resetInstance();
- UserStore.resetInstance();
jest.clearAllMocks();
jest.restoreAllMocks();
mockFs.restore();
diff --git a/src/renderer/components/+extensions/__tests__/extensions.test.tsx b/src/renderer/components/+extensions/__tests__/extensions.test.tsx
index 7ec77838fc..66f0830c3d 100644
--- a/src/renderer/components/+extensions/__tests__/extensions.test.tsx
+++ b/src/renderer/components/+extensions/__tests__/extensions.test.tsx
@@ -7,7 +7,6 @@ import "@testing-library/jest-dom/extend-expect";
import { fireEvent, screen, waitFor } from "@testing-library/react";
import fse from "fs-extra";
import React from "react";
-import { UserStore } from "../../../../common/user-store";
import type { ExtensionDiscovery } from "../../../../extensions/extension-discovery/extension-discovery";
import type { ExtensionLoader } from "../../../../extensions/extension-loader";
import { ConfirmDialog } from "../../confirm-dialog";
@@ -96,13 +95,10 @@ describe("Extensions", () => {
});
extensionDiscovery.uninstallExtension = jest.fn(() => Promise.resolve());
-
- UserStore.createInstance();
});
afterEach(() => {
mockFs.restore();
- UserStore.resetInstance();
});
it("disables uninstall and disable buttons while uninstalling", async () => {
diff --git a/src/renderer/components/+preferences/application.tsx b/src/renderer/components/+preferences/application.tsx
index bbda49f1a0..d3c0fd4414 100644
--- a/src/renderer/components/+preferences/application.tsx
+++ b/src/renderer/components/+preferences/application.tsx
@@ -12,7 +12,7 @@ import type { UserStore } from "../../../common/user-store";
import { Input } from "../input";
import { Switch } from "../switch";
import moment from "moment-timezone";
-import { updateChannels, defaultExtensionRegistryUrl, defaultUpdateChannel, defaultLocaleTimezone, defaultExtensionRegistryUrlLocation } from "../../../common/user-store/preferences-helpers";
+import { defaultExtensionRegistryUrl, defaultLocaleTimezone, defaultExtensionRegistryUrlLocation } from "../../../common/user-store/preferences-helpers";
import type { IComputedValue } from "mobx";
import { runInAction } from "mobx";
import { isUrl } from "../input/input_validators";
@@ -24,11 +24,17 @@ import { Preferences } from "./preferences";
import userStoreInjectable from "../../../common/user-store/user-store.injectable";
import themeStoreInjectable from "../../themes/store.injectable";
import { defaultThemeId } from "../../../common/vars";
+import { updateChannels } from "../../../common/application-update/update-channels";
+import { map, toPairs } from "lodash/fp";
+import { pipeline } from "@ogre-tools/fp";
+import type { SelectedUpdateChannel } from "../../../common/application-update/selected-update-channel/selected-update-channel.injectable";
+import selectedUpdateChannelInjectable from "../../../common/application-update/selected-update-channel/selected-update-channel.injectable";
interface Dependencies {
appPreferenceItems: IComputedValue;
userStore: UserStore;
themeStore: ThemeStore;
+ selectedUpdateChannel: SelectedUpdateChannel;
}
const timezoneOptions = moment.tz.names()
@@ -36,10 +42,16 @@ const timezoneOptions = moment.tz.names()
value: timezone,
label: timezone.replace("_", " "),
}));
-const updateChannelOptions = Array.from(updateChannels, ([channel, { label }]) => ({
- value: channel,
- label,
-}));
+
+const updateChannelOptions = pipeline(
+ toPairs(updateChannels),
+
+ map(([, channel]) => ({
+ value: channel.id,
+ label: channel.label,
+ })),
+);
+
const extensionInstallRegistryOptions = [
{
value: "default",
@@ -55,7 +67,7 @@ const extensionInstallRegistryOptions = [
},
] as const;
-const NonInjectedApplication: React.FC = ({ appPreferenceItems, userStore, themeStore }) => {
+const NonInjectedApplication: React.FC = ({ appPreferenceItems, userStore, themeStore, selectedUpdateChannel }) => {
const [customUrl, setCustomUrl] = React.useState(userStore.extensionRegistryUrl.customUrl || "");
const themeOptions = [
{
@@ -144,8 +156,8 @@ const NonInjectedApplication: React.FC = ({ appPreferenceItems, us