Prevent duplicate app windows (#5533)

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>
Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>
Co-authored-by: Iku-turso <mikko.aspiala@gmail.com>
This commit is contained in:
Janne SavolainenandMikko Aspiala authored and GitHub committed 2022-06-15 16:40:34 -04:00
1 parent 5c1f0daf50
commit 6451df1f17
19 files changed
+528 -109

No files matched your search

@@ -5,23 +5,19 @@
import { getInjectable } from "@ogre-tools/injectable";
import { beforeQuitOfFrontEndInjectionToken } from "../../../start-main-application/runnable-tokens/before-quit-of-front-end-injection-token";
import electronAppInjectable from "../../electron-app.injectable";
import { lensWindowInjectionToken } from "../../../start-main-application/lens-window/application-window/lens-window-injection-token";
import { pipeline } from "@ogre-tools/fp";
import { filter, isEmpty } from "lodash/fp";
import { isEmpty } from "lodash/fp";
import getVisibleWindowsInjectable from "../../../start-main-application/lens-window/get-visible-windows.injectable";
const hideDockForLastClosedWindowInjectable = getInjectable({
id: "hide-dock-when-there-are-no-windows",
instantiate: (di) => {
const app = di.inject(electronAppInjectable);
const getLensWindows = () => di.injectMany(lensWindowInjectionToken);
const getVisibleWindows = di.inject(getVisibleWindowsInjectable);
return {
run: () => {
const visibleWindows = pipeline(
getLensWindows(),
filter(window => !!window.visible),
);
const visibleWindows = getVisibleWindows();
if (isEmpty(visibleWindows)) {
app.dock?.hide();
+8 -3
View File
@@ -64,7 +64,7 @@ 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 "./utils/channel/ipc-main/ipc-main.injectable";
import createElectronWindowForInjectable from "./start-main-application/lens-window/application-window/create-electron-window-for.injectable";
import createElectronWindowForInjectable from "./start-main-application/lens-window/application-window/create-electron-window.injectable";
import setupRunnablesAfterWindowIsOpenedInjectable from "./electron-app/runnables/setup-runnables-after-window-is-opened.injectable";
import sendToChannelInElectronBrowserWindowInjectable from "./start-main-application/lens-window/application-window/send-to-channel-in-electron-browser-window.injectable";
import broadcastMessageInjectable from "../common/ipc/broadcast-message.injectable";
@@ -97,6 +97,7 @@ import installHelmChartInjectable from "./helm/helm-service/install-helm-chart.i
import listHelmReleasesInjectable from "./helm/helm-service/list-helm-releases.injectable";
import rollbackHelmReleaseInjectable from "./helm/helm-service/rollback-helm-release.injectable";
import updateHelmReleaseInjectable from "./helm/helm-service/update-helm-release.injectable";
import waitUntilBundledExtensionsAreLoadedInjectable from "./start-main-application/lens-window/application-window/wait-until-bundled-extensions-are-loaded.injectable";
export function getDiForUnitTesting(opts: { doGeneralOverrides?: boolean } = {}) {
const {
@@ -120,6 +121,7 @@ export function getDiForUnitTesting(opts: { doGeneralOverrides?: boolean } = {})
di.preventSideEffects();
if (doGeneralOverrides) {
di.override(waitUntilBundledExtensionsAreLoadedInjectable, () => async () => {});
di.override(getRandomIdInjectable, () => () => "some-irrelevant-random-id");
di.override(hotbarStoreInjectable, () => ({ load: () => {} }));
di.override(userStoreInjectable, () => ({ startMainReactions: () => {}, extensionRegistryUrl: { customUrl: "some-custom-url" }}) as UserStore);
@@ -242,7 +244,7 @@ const overrideElectronFeatures = (di: DiContainer) => {
di.override(getCommandLineSwitchInjectable, () => () => "irrelevant");
di.override(requestSingleInstanceLockInjectable, () => () => true);
di.override(disableHardwareAccelerationInjectable, () => () => {});
di.override(shouldStartHiddenInjectable, () => true);
di.override(shouldStartHiddenInjectable, () => false);
di.override(showMessagePopupInjectable, () => () => {});
di.override(waitForElectronToBeReadyInjectable, () => () => Promise.resolve());
di.override(ipcMainInjectable, () => ({}));
@@ -256,7 +258,7 @@ const overrideElectronFeatures = (di: DiContainer) => {
throw new Error("Tried to check for platform updates without explicit override.");
});
di.override(createElectronWindowForInjectable, () => () => async () => ({
di.override(createElectronWindowForInjectable, () => () => ({
show: () => {},
close: () => {},
@@ -266,6 +268,9 @@ const overrideElectronFeatures = (di: DiContainer) => {
sendFake(null, arg);
},
loadFile: async () => {},
loadUrl: async () => {},
}));
di.override(
@@ -9,9 +9,7 @@ import lensProxyPortInjectable from "../../../lens-proxy/lens-proxy-port.injecta
import isMacInjectable from "../../../../common/vars/is-mac.injectable";
import appNameInjectable from "../../../app-paths/app-name/app-name.injectable";
import appEventBusInjectable from "../../../../common/app-event-bus/app-event-bus.injectable";
import { delay } from "../../../../common/utils";
import { bundledExtensionsLoaded } from "../../../../common/ipc/extension-handling";
import ipcMainInjectable from "../../../utils/channel/ipc-main/ipc-main.injectable";
import waitUntilBundledExtensionsAreLoadedInjectable from "./wait-until-bundled-extensions-are-loaded.injectable";
const applicationWindowInjectable = getInjectable({
id: "application-window",
@@ -21,7 +19,7 @@ const applicationWindowInjectable = getInjectable({
const isMac = di.inject(isMacInjectable);
const applicationName = di.inject(appNameInjectable);
const appEventBus = di.inject(appEventBusInjectable);
const ipcMain = di.inject(ipcMainInjectable);
const waitUntilBundledExtensionsAreLoaded = di.inject(waitUntilBundledExtensionsAreLoadedInjectable);
const lensProxyPort = di.inject(lensProxyPortInjectable);
return createLensWindow({
@@ -45,14 +43,7 @@ const applicationWindowInjectable = getInjectable({
onDomReady: () => {
appEventBus.emit({ name: "app", action: "dom-ready" });
},
beforeOpen: async () => {
const viewHasLoaded = new Promise<void>((resolve) => {
ipcMain.once(bundledExtensionsLoaded, () => resolve());
});
await viewHasLoaded;
await delay(50); // wait just a bit longer to let the first round of rendering happen
},
beforeOpen: waitUntilBundledExtensionsAreLoaded,
});
},
@@ -8,7 +8,7 @@ import applicationWindowStateInjectable from "./application-window-state.injecta
import { BrowserWindow } from "electron";
import { openBrowser } from "../../../../common/utils";
import sendToChannelInElectronBrowserWindowInjectable from "./send-to-channel-in-electron-browser-window.injectable";
import type { LensWindow } from "./create-lens-window.injectable";
import type { ElectronWindow } from "./create-lens-window.injectable";
import type { RequireExactlyOne } from "type-fest";
export type ElectronWindowTitleBarStyle = "hiddenInset" | "hidden" | "default" | "customButtonsOnHover";
@@ -38,21 +38,16 @@ export interface ElectronWindowConfiguration {
onDomReady?: () => void;
}
export type CreateElectronWindow = () => Promise<LensWindow>;
export type CreateElectronWindowFor = (config: ElectronWindowConfiguration) => CreateElectronWindow;
export type CreateElectronWindow = (config: ElectronWindowConfiguration) => ElectronWindow;
function isFileSource(src: ContentSource): src is FileSource {
return typeof (src as FileSource).file === "string";
}
const createElectronWindowInjectable = getInjectable({
id: "create-electron-window",
const createElectronWindowFor = getInjectable({
id: "create-electron-window-for",
instantiate: (di): CreateElectronWindowFor => {
instantiate: (di): CreateElectronWindow => {
const logger = di.inject(loggerInjectable);
const sendToChannelInLensWindow = di.inject(sendToChannelInElectronBrowserWindowInjectable);
return (configuration) => async () => {
return (configuration) => {
const applicationWindowState = di.inject(
applicationWindowStateInjectable,
{
@@ -172,19 +167,23 @@ const createElectronWindowFor = getInjectable({
return { action: "deny" };
});
const contentSource = configuration.getContentSource();
if (isFileSource(contentSource)) {
logger.info(`[CREATE-ELECTRON-WINDOW]: Loading content for window "${configuration.id}" from file: ${contentSource.file}...`);
await browserWindow.loadFile(contentSource.file);
} else {
logger.info(`[CREATE-ELECTRON-WINDOW]: Loading content for window "${configuration.id}" from url: ${contentSource.url}...`);
await browserWindow.loadURL(contentSource.url);
}
await configuration.beforeOpen?.();
return {
loadFile: async (filePath) => {
logger.info(
`[CREATE-ELECTRON-WINDOW]: Loading content for window "${configuration.id}" from file: ${filePath}...`,
);
await browserWindow.loadFile(filePath);
},
loadUrl: async (url) => {
logger.info(
`[CREATE-ELECTRON-WINDOW]: Loading content for window "${configuration.id}" from url: ${url}...`,
);
await browserWindow.loadURL(url);
},
show: () => browserWindow.show(),
close: () => browserWindow.close(),
send: (args) => sendToChannelInLensWindow(browserWindow, args),
@@ -195,4 +194,4 @@ const createElectronWindowFor = getInjectable({
causesSideEffects: true,
});
export default createElectronWindowFor;
export default createElectronWindowInjectable;
@@ -3,14 +3,17 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { SendToViewArgs } from "./lens-window-injection-token";
import type { ContentSource, ElectronWindowTitleBarStyle } from "./create-electron-window-for.injectable";
import createElectronWindowForInjectable from "./create-electron-window-for.injectable";
import type { LensWindow, SendToViewArgs } from "./lens-window-injection-token";
import type { ContentSource, ElectronWindowTitleBarStyle } from "./create-electron-window.injectable";
import createElectronWindowForInjectable from "./create-electron-window.injectable";
import assert from "assert";
export interface LensWindow {
export interface ElectronWindow {
show: () => void;
close: () => void;
send: (args: SendToViewArgs) => void;
loadFile: (filePath: string) => Promise<void>;
loadUrl: (url: string) => Promise<void>;
}
export interface LensWindowConfiguration {
@@ -33,31 +36,69 @@ const createLensWindowInjectable = getInjectable({
id: "create-lens-window",
instantiate: (di) => {
const createElectronWindowFor = di.inject(createElectronWindowForInjectable);
const createElectronWindow = di.inject(createElectronWindowForInjectable);
return (configuration: LensWindowConfiguration) => {
let browserWindow: LensWindow | undefined;
return (configuration: LensWindowConfiguration): LensWindow => {
let browserWindow: ElectronWindow | undefined;
const createElectronWindow = createElectronWindowFor({
...configuration,
onClose: () => browserWindow = undefined,
});
let windowIsShown = false;
let windowIsStarting = false;
const showWindow = () => {
assert(browserWindow);
browserWindow.show();
windowIsShown = true;
};
return {
get visible() {
return !!browserWindow;
id: configuration.id,
get isVisible() {
return windowIsShown;
},
show: async () => {
get isStarting() {
return windowIsStarting;
},
start: async () => {
if (!browserWindow) {
browserWindow = await createElectronWindow();
windowIsStarting = true;
browserWindow = createElectronWindow({
...configuration,
onClose: () => {
browserWindow = undefined;
windowIsShown = false;
},
});
const { file: filePathForContent, url: urlForContent } =
configuration.getContentSource();
if (filePathForContent) {
await browserWindow.loadFile(filePathForContent);
} else if (urlForContent) {
await browserWindow.loadUrl(urlForContent);
}
await configuration.beforeOpen?.();
}
browserWindow.show();
showWindow();
windowIsStarting = false;
},
show: showWindow,
close: () => {
browserWindow?.close();
browserWindow = undefined;
windowIsShown = false;
},
send: (args: SendToViewArgs) => {
if (!browserWindow) {
throw new Error(`Tried to send message to window "${configuration.id}" but the window was closed`);
@@ -12,10 +12,13 @@ export interface SendToViewArgs {
}
export interface LensWindow {
show: () => Promise<void>;
id: string;
start: () => Promise<void>;
close: () => void;
show: () => void;
send: (args: SendToViewArgs) => void;
visible: boolean;
isVisible: boolean;
isStarting: boolean;
}
export const lensWindowInjectionToken = getInjectionToken<LensWindow>({
@@ -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 { bundledExtensionsLoaded } from "../../../../common/ipc/extension-handling";
import { delay } from "../../../../common/utils";
import ipcMainInjectable from "../../../utils/channel/ipc-main/ipc-main.injectable";
const waitUntilBundledExtensionsAreLoadedInjectable = getInjectable({
id: "wait-until-bundled-extensions-are-loaded",
instantiate: (di) => {
const ipcMain = di.inject(ipcMainInjectable);
return async () => {
const viewHasLoaded = new Promise<void>((resolve) => {
ipcMain.once(bundledExtensionsLoaded, () => resolve());
});
await viewHasLoaded;
await delay(50); // wait just a bit longer to let the first round of rendering happen
};
},
causesSideEffects: true,
});
export default waitUntilBundledExtensionsAreLoadedInjectable;
@@ -0,0 +1,24 @@
/**
* 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 { getInjectable } from "@ogre-tools/injectable";
import { filter } from "lodash/fp";
import { lensWindowInjectionToken } from "./application-window/lens-window-injection-token";
const getVisibleWindowsInjectable = getInjectable({
id: "get-visible-windows",
instantiate: (di) => {
const getAllLensWindows = () => di.injectMany(lensWindowInjectionToken);
return () =>
pipeline(
getAllLensWindows(),
filter((lensWindow) => !!lensWindow.isVisible),
);
},
});
export default getVisibleWindowsInjectable;
@@ -5,25 +5,36 @@
import { getInjectable } from "@ogre-tools/injectable";
import splashWindowInjectable from "./splash-window/splash-window.injectable";
import applicationWindowInjectable from "./application-window/application-window.injectable";
import { identity, some } from "lodash/fp";
const someIsTruthy = some(identity);
const showApplicationWindowInjectable = getInjectable({
id: "show-application-window",
instantiate: (di) => {
const applicationWindow = di.inject(applicationWindowInjectable);
const splashWindow = di.inject(
splashWindowInjectable,
);
const splashWindow = di.inject(splashWindowInjectable);
return async () => {
if (applicationWindow.visible) {
if (applicationWindow.isStarting) {
applicationWindow.show();
splashWindow.close();
return;
}
await splashWindow.show();
const windowIsAlreadyBeingShown = someIsTruthy([
applicationWindow.isVisible,
splashWindow.isStarting,
]);
await applicationWindow.show();
if (windowIsAlreadyBeingShown) {
return;
}
await splashWindow.start();
await applicationWindow.start();
splashWindow.close();
};
@@ -49,7 +49,7 @@ const startMainApplicationInjectable = getInjectable({
await beforeApplicationIsLoading();
if (!shouldStartHidden) {
await splashWindow.show();
await splashWindow.start();
}
await onLoadOfApplication();
@@ -60,7 +60,7 @@ const startMainApplicationInjectable = getInjectable({
if (deepLinkUrl) {
await openDeepLink(deepLinkUrl);
} else {
await applicationWindow.show();
await applicationWindow.start();
}
splashWindow.close();
@@ -2,32 +2,24 @@
* 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";
import getVisibleWindowsInjectable from "../../start-main-application/lens-window/get-visible-windows.injectable";
const messageToChannelInjectable = getInjectable({
id: "message-to-channel",
instantiate: (di) => {
const getAllLensWindows = () => di.injectMany(lensWindowInjectionToken);
const getVisibleWindows = di.inject(getVisibleWindowsInjectable);
// TODO: Figure out way to improve typing in internals
// Notice that this should be injected using "messageToChannelInjectionToken" which is typed correctly.
return (channel: MessageChannel<any>, message?: unknown) => {
const stringifiedMessage = tentativeStringifyJson(message);
const visibleWindows = pipeline(
getAllLensWindows(),
filter((lensWindow) => !!lensWindow.visible),
);
visibleWindows.forEach((lensWindow) =>
getVisibleWindows().forEach((lensWindow) =>
lensWindow.send({ channel: channel.id, data: stringifiedMessage ? [stringifiedMessage] : [] }),
);
};
@@ -43,9 +43,9 @@ describe("message to channel from main", () => {
expect(sendToChannelInBrowserMock).not.toHaveBeenCalled();
});
describe("given visible window", () => {
describe("given started window", () => {
beforeEach(async () => {
await someTestWindow.show();
await someTestWindow.start();
});
it("when messaging to channel, messages to window", () => {
@@ -109,9 +109,9 @@ describe("message to channel from main", () => {
});
});
it("given multiple visible windows, when messaging to channel, messages to window", async () => {
await someTestWindow.show();
await someOtherTestWindow.show();
it("given multiple started windows, when messaging to channel, messages to window", async () => {
await someTestWindow.start();
await someOtherTestWindow.start();
messageToChannel(someChannel, "some-message");