diff --git a/packages/core/src/common/protocol-handler/router.ts b/packages/core/src/common/protocol-handler/router.ts index 383972c601..f5659b2234 100644 --- a/packages/core/src/common/protocol-handler/router.ts +++ b/packages/core/src/common/protocol-handler/router.ts @@ -12,6 +12,7 @@ import { RoutingError, RoutingErrorType } from "./error"; import type { ExtensionsStore } from "../../extensions/extensions-store/extensions-store"; import type { LensExtension } from "../../extensions/lens-extension"; import type { RouteHandler, RouteParams } from "./registration"; +import type { IComputedValue } from "mobx"; import { when } from "mobx"; import type { Logger } from "../logger"; import type { FindExtensionInstanceByName } from "../../features/extensions/loader/common/find-instance-by-name.injectable"; @@ -61,13 +62,13 @@ export function foldAttemptResults(mainAttempt: RouteAttempt, rendererAttempt: R export interface LensProtocolRouterDependencies { readonly extensionsStore: ExtensionsStore; readonly logger: Logger; - readonly internalRoutes: Map; + readonly internalRoutes: IComputedValue>; findExtensionInstanceByName: FindExtensionInstanceByName; } export const extensionUrlDeepLinkingSchema = `/:${EXTENSION_PUBLISHER_MATCH}(@[A-Za-z0-9_]+)?/:${EXTENSION_NAME_MATCH}`; -export abstract class LensProtocolRouter { +export class LensProtocolRouter { constructor(protected readonly dependencies: LensProtocolRouterDependencies) {} /** @@ -75,8 +76,8 @@ export abstract class LensProtocolRouter { * @param url the parsed URL that initiated the `lens://` protocol * @returns true if a route has been found */ - protected _routeToInternal(url: Url>): RouteAttempt { - return this._route(this.dependencies.internalRoutes.entries(), url); + routeToInternal(url: Url>): RouteAttempt { + return this._route(this.dependencies.internalRoutes.get().entries(), url); } /** @@ -216,7 +217,7 @@ export abstract class LensProtocolRouter { * Note: this function modifies its argument, do not reuse * @param url the protocol request URI that was "open"-ed */ - protected async _routeToExtension(url: Url>): Promise { + async routeToExtension(url: Url>): Promise { const extension = await this._findMatchingExtensionByName(url); if (typeof extension === "string") { diff --git a/packages/core/src/features/deep-linking/common/channels.ts b/packages/core/src/features/deep-linking/common/channels.ts new file mode 100644 index 0000000000..7b9104dfa0 --- /dev/null +++ b/packages/core/src/features/deep-linking/common/channels.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { RouteAttempt } from "../../../common/protocol-handler"; +import type { MessageChannel } from "../../../common/utils/channel/message-channel-listener-injection-token"; + +export interface DeepLinkingRouteAttempt { + url: string; + previous: RouteAttempt; + target: "internal" | "external"; +} + +export const deepLinkingRouteAttemptChannel: MessageChannel = { + id: "deep-linking-route-attempt", +}; + +export interface InvalidDeepLinkingAttempt { + error: string; + url: string; +} + +export const invalidDeepLinkingAttemptChannel: MessageChannel = { + id: "invalid-deep-linking-attempt", +}; diff --git a/packages/core/src/features/deep-linking/main/send-deep-linking-attempt.injectable.ts b/packages/core/src/features/deep-linking/main/send-deep-linking-attempt.injectable.ts new file mode 100644 index 0000000000..c628e17f28 --- /dev/null +++ b/packages/core/src/features/deep-linking/main/send-deep-linking-attempt.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 { MessageChannelSender } from "../../../common/utils/channel/message-to-channel-injection-token"; +import { sendMessageToChannelInjectionToken } from "../../../common/utils/channel/message-to-channel-injection-token"; +import { deepLinkingRouteAttemptChannel } from "../common/channels"; + +export type SendDeepLinkingAttempt = MessageChannelSender; + +const sendDeepLinkingAttemptInjectable = getInjectable({ + id: "send-deep-linking-attempt", + instantiate: (di): SendDeepLinkingAttempt => { + const sendMessageToChannel = di.inject(sendMessageToChannelInjectionToken); + + return (attempt) => sendMessageToChannel(deepLinkingRouteAttemptChannel, attempt); + }, +}); + +export default sendDeepLinkingAttemptInjectable; diff --git a/packages/core/src/features/deep-linking/main/send-invalid-deep-linking-attempt.injectable.ts b/packages/core/src/features/deep-linking/main/send-invalid-deep-linking-attempt.injectable.ts new file mode 100644 index 0000000000..cba1d9e35e --- /dev/null +++ b/packages/core/src/features/deep-linking/main/send-invalid-deep-linking-attempt.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 { MessageChannelSender } from "../../../common/utils/channel/message-to-channel-injection-token"; +import { sendMessageToChannelInjectionToken } from "../../../common/utils/channel/message-to-channel-injection-token"; +import { invalidDeepLinkingAttemptChannel } from "../common/channels"; + +export type SendInvalidDeepLinkingAttempt = MessageChannelSender; + +const sendInvalidDeepLinkingAttemptInjectable = getInjectable({ + id: "send-invalid-deep-linking-attempt", + instantiate: (di): SendInvalidDeepLinkingAttempt => { + const sendMessageToChannel = di.inject(sendMessageToChannelInjectionToken); + + return (attempt) => sendMessageToChannel(invalidDeepLinkingAttemptChannel, attempt); + }, +}); + +export default sendInvalidDeepLinkingAttemptInjectable; diff --git a/packages/core/src/features/deep-linking/renderer/internal-deep-linking-routes.injectable.ts b/packages/core/src/features/deep-linking/renderer/internal-deep-linking-routes.injectable.ts index 60c00c4029..1b1203dc4b 100644 --- a/packages/core/src/features/deep-linking/renderer/internal-deep-linking-routes.injectable.ts +++ b/packages/core/src/features/deep-linking/renderer/internal-deep-linking-routes.injectable.ts @@ -3,19 +3,26 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; +import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx"; +import { computed } from "mobx"; import { pathToRegexp } from "path-to-regexp"; import { internalDeepLinkingRouteInjectionToken } from "../common/internal-handler-token"; const internalDeepLinkingRoutesInjectable = getInjectable({ id: "internal-deep-linking-routes", instantiate: (di) => { - const registrations = di.injectMany(internalDeepLinkingRouteInjectionToken); + const computedInjectMany = di.inject(computedInjectManyInjectable); + const registrations = computedInjectMany(internalDeepLinkingRouteInjectionToken); - return new Map(registrations.map(registration => { - pathToRegexp(registration.path); // verify now that the schema is valid + return computed(() => new Map(( + registrations + .get() + .map(registration => { + pathToRegexp(registration.path); // verify now that the schema is valid - return [registration.path, registration.handler]; - })); + return [registration.path, registration.handler]; + }) + ))); }, }); diff --git a/packages/core/src/features/deep-linking/renderer/invalid-route-attempt.injectable.tsx b/packages/core/src/features/deep-linking/renderer/invalid-route-attempt.injectable.tsx new file mode 100644 index 0000000000..cb11adf29e --- /dev/null +++ b/packages/core/src/features/deep-linking/renderer/invalid-route-attempt.injectable.tsx @@ -0,0 +1,33 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import React from "react"; +import { getMessageChannelListenerInjectable } from "../../../common/utils/channel/message-channel-listener-injection-token"; +import showErrorNotificationInjectable from "../../../renderer/components/notifications/show-error-notification.injectable"; +import { invalidDeepLinkingAttemptChannel } from "../common/channels"; + +const invalidRouteAttemptListenerInjectable = getMessageChannelListenerInjectable({ + channel: invalidDeepLinkingAttemptChannel, + id: "main", + handler: (di) => { + const showErrorNotification = di.inject(showErrorNotificationInjectable); + + return (attempt) => void showErrorNotification(( + <> +

+ {"Failed to route "} + {attempt.url} + . +

+

+ Error: + {" "} + {attempt.error} +

+ + )); + }, +}); + +export default invalidRouteAttemptListenerInjectable; diff --git a/packages/core/src/features/deep-linking/renderer/lens-protocol-router-renderer.injectable.ts b/packages/core/src/features/deep-linking/renderer/lens-protocol-router-renderer.injectable.ts new file mode 100644 index 0000000000..00291a1062 --- /dev/null +++ b/packages/core/src/features/deep-linking/renderer/lens-protocol-router-renderer.injectable.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import extensionsStoreInjectable from "../../../extensions/extensions-store/extensions-store.injectable"; +import findExtensionInstanceByNameInjectable from "../../extensions/loader/common/find-instance-by-name.injectable"; +import internalDeepLinkingRoutesInjectable from "./internal-deep-linking-routes.injectable"; +import protocolHandlerLoggerInjectable from "../../../common/protocol-handler/logger.injectable"; +import { LensProtocolRouter } from "../../../common/protocol-handler"; + +const deepLinkingRouterInjectable = getInjectable({ + id: "deep-linking-router", + instantiate: (di) => new LensProtocolRouter({ + extensionsStore: di.inject(extensionsStoreInjectable), + logger: di.inject(protocolHandlerLoggerInjectable), + findExtensionInstanceByName: di.inject(findExtensionInstanceByNameInjectable), + internalRoutes: di.inject(internalDeepLinkingRoutesInjectable), + }), +}); + +export default deepLinkingRouterInjectable; diff --git a/packages/core/src/features/deep-linking/renderer/route-attempt.injectable.tsx b/packages/core/src/features/deep-linking/renderer/route-attempt.injectable.tsx new file mode 100644 index 0000000000..d7d13b56e6 --- /dev/null +++ b/packages/core/src/features/deep-linking/renderer/route-attempt.injectable.tsx @@ -0,0 +1,50 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getMessageChannelListenerInjectable } from "../../../common/utils/channel/message-channel-listener-injection-token"; +import showShortInfoNotificationInjectable from "../../../renderer/components/notifications/show-short-info.injectable"; +import deepLinkingRouterInjectable from "./lens-protocol-router-renderer.injectable"; +import { deepLinkingRouteAttemptChannel } from "../common/channels"; +import Url from "url-parse"; +import React from "react"; +import { foldAttemptResults, RouteAttempt } from "../../../common/protocol-handler"; + +const deepLinkingRouteAttemptListenerInjectable = getMessageChannelListenerInjectable({ + channel: deepLinkingRouteAttemptChannel, + id: "main", + handler: (di) => { + const router = di.inject(deepLinkingRouterInjectable); + const showShortInfoNotification = di.inject(showShortInfoNotificationInjectable); + + return async (attempt) => { + const url = new Url(attempt.url, true); + const currentAttempt = attempt.target === "internal" + ? router.routeToInternal(url) + : await router.routeToExtension(url); + + switch (foldAttemptResults(attempt.previous, currentAttempt)) { + case RouteAttempt.MISSING: + showShortInfoNotification(( +

+ {"Unknown action "} + {attempt.url} + {". Are you on the latest version of the extension?"} +

+ )); + break; + case RouteAttempt.MISSING_EXTENSION: + showShortInfoNotification(( +

+ {"Missing extension for action "} + {attempt.url} + {". Not able to find extension in our known list. Try installing it manually."} +

+ )); + break; + } + }; + }, +}); + +export default deepLinkingRouteAttemptListenerInjectable; diff --git a/packages/core/src/main/electron-app/runnables/setup-deep-linking.injectable.ts b/packages/core/src/main/electron-app/runnables/setup-deep-linking.injectable.ts index 5f82d60f42..9c063031ea 100644 --- a/packages/core/src/main/electron-app/runnables/setup-deep-linking.injectable.ts +++ b/packages/core/src/main/electron-app/runnables/setup-deep-linking.injectable.ts @@ -4,7 +4,7 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import electronAppInjectable from "../electron-app.injectable"; -import openDeepLinkInjectable from "../../protocol-handler/lens-protocol-router-main/open-deep-link-for-url/open-deep-link.injectable"; +import openDeepLinkInjectable from "../../protocol-handler/lens-protocol-router-main/open-deep-link.injectable"; import loggerInjectable from "../../../common/logger.injectable"; import commandLineArgumentsInjectable from "../../utils/command-line-arguments.injectable"; import { pipeline } from "@ogre-tools/fp"; diff --git a/packages/core/src/main/protocol-handler/__test__/router.test.ts b/packages/core/src/main/protocol-handler/__test__/router.test.ts index bdb77f1b3c..ff462abd80 100644 --- a/packages/core/src/main/protocol-handler/__test__/router.test.ts +++ b/packages/core/src/main/protocol-handler/__test__/router.test.ts @@ -5,7 +5,6 @@ import * as uuid from "uuid"; -import { ProtocolHandlerExtension, ProtocolHandlerInternal, ProtocolHandlerInvalid } from "../../../common/protocol-handler"; import { noop } from "../../../common/utils"; import type { LensProtocolRouterMain } from "../lens-protocol-router-main/lens-protocol-router-main"; import { getDiForUnitTesting } from "../../getDiForUnitTesting"; @@ -16,7 +15,14 @@ import type { ObservableMap } from "mobx"; import { runInAction } from "mobx"; import extensionInstancesInjectable from "../../../extensions/extension-loader/extension-instances.injectable"; import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable"; -import broadcastMessageInjectable from "../../../common/ipc/broadcast-message.injectable"; +import type { SendDeepLinkingAttempt } from "../../../features/deep-linking/main/send-deep-linking-attempt.injectable"; +import type { SendInvalidDeepLinkingAttempt } from "../../../features/deep-linking/main/send-invalid-deep-linking-attempt.injectable"; +import sendDeepLinkingAttemptInjectable from "../../../features/deep-linking/main/send-deep-linking-attempt.injectable"; +import sendInvalidDeepLinkingAttemptInjectable from "../../../features/deep-linking/main/send-invalid-deep-linking-attempt.injectable"; +import type { DiContainer } from "@ogre-tools/injectable"; +import { getInjectable } from "@ogre-tools/injectable"; +import type { InternalRouteRegistration } from "../../../features/deep-linking/common/internal-handler-token"; +import { internalDeepLinkingRouteInjectionToken } from "../../../features/deep-linking/common/internal-handler-token"; import { LensMainExtension } from "../../../extensions/lens-main-extension"; function throwIfDefined(val: any): void { @@ -29,10 +35,12 @@ describe("protocol router tests", () => { let extensionInstances: ObservableMap; let lpr: LensProtocolRouterMain; let enabledExtensions: Set; - let broadcastMessageMock: jest.Mock; + let sendDeepLinkingAttemptMock: jest.MockedFunction; + let sendInvalidDeepLinkingAttemptMock: jest.MockedFunction; + let di: DiContainer; beforeEach(async () => { - const di = getDiForUnitTesting({ doGeneralOverrides: true }); + di = getDiForUnitTesting({ doGeneralOverrides: true }); enabledExtensions = new Set(); @@ -42,8 +50,11 @@ describe("protocol router tests", () => { di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data"); - broadcastMessageMock = jest.fn(); - di.override(broadcastMessageInjectable, () => broadcastMessageMock); + sendDeepLinkingAttemptMock = jest.fn(); + di.override(sendDeepLinkingAttemptInjectable, () => sendDeepLinkingAttemptMock); + + sendInvalidDeepLinkingAttemptMock = jest.fn(); + di.override(sendInvalidDeepLinkingAttemptInjectable, () => sendInvalidDeepLinkingAttemptMock); extensionInstances = di.inject(extensionInstancesInjectable); lpr = di.inject(lensProtocolRouterMainInjectable); @@ -55,16 +66,31 @@ describe("protocol router tests", () => { it("should broadcast invalid protocol on non-lens URLs", async () => { await lpr.route("https://google.ca"); - expect(broadcastMessageMock).toBeCalledWith(ProtocolHandlerInvalid, "invalid protocol", "https://google.ca"); + expect(sendInvalidDeepLinkingAttemptMock).toBeCalledWith({ + error: "invalid protocol", + url: "https://google.ca", + }); }); it("should broadcast invalid host on non internal or non extension URLs", async () => { await lpr.route("lens://foobar"); - expect(broadcastMessageMock).toBeCalledWith(ProtocolHandlerInvalid, "invalid host", "lens://foobar"); + expect(sendInvalidDeepLinkingAttemptMock).toBeCalledWith({ + error: "invalid host", + url: "lens://foobar", + }); }); it("should broadcast internal route when called with valid host", async () => { - lpr.addInternalHandler("/", noop); + runInAction(() => { + di.register(getInjectable({ + id: "some-id", + instantiate: () => ({ + path: "/", + handler: noop, + }), + injectionToken: internalDeepLinkingRouteInjectionToken, + })); + }); try { expect(await lpr.route("lens://app")).toBeUndefined(); @@ -72,7 +98,11 @@ describe("protocol router tests", () => { expect(throwIfDefined(error)).not.toThrow(); } - expect(broadcastMessageMock).toHaveBeenCalledWith(ProtocolHandlerInternal, "lens://app", "matched"); + expect(sendDeepLinkingAttemptMock).toHaveBeenCalledWith({ + url: "lens://app", + previous: "matched", + target: "internal", + }); }); it("should broadcast external route when called with valid host", async () => { @@ -105,13 +135,26 @@ describe("protocol router tests", () => { expect(throwIfDefined(error)).not.toThrow(); } - expect(broadcastMessageMock).toHaveBeenCalledWith(ProtocolHandlerExtension, "lens://extension/@mirantis/minikube", "matched"); + expect(sendDeepLinkingAttemptMock).toHaveBeenCalledWith({ + url: "lens://extension/@mirantis/minikube", + previous: "matched", + target: "external", + }); }); it("should call handler if matches", async () => { let called = false; - lpr.addInternalHandler("/page", () => { called = true; }); + runInAction(() => { + di.register(getInjectable({ + id: "some-id", + instantiate: () => ({ + path: "/page", + handler: () => { called = true; }, + }), + injectionToken: internalDeepLinkingRouteInjectionToken, + })); + }); try { expect(await lpr.route("lens://app/page")).toBeUndefined(); @@ -120,14 +163,35 @@ describe("protocol router tests", () => { } expect(called).toBe(true); - expect(broadcastMessageMock).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page", "matched"); + + expect(sendDeepLinkingAttemptMock).toHaveBeenCalledWith({ + url: "lens://app/page", + previous: "matched", + target: "internal", + }); }); it("should call most exact handler", async () => { let called: any = 0; - lpr.addInternalHandler("/page", () => { called = 1; }); - lpr.addInternalHandler("/page/:id", params => { called = params.pathname.id; }); + runInAction(() => { + di.register(getInjectable({ + id: "some-id", + instantiate: () => ({ + path: "/page", + handler: () => { called = 1; }, + }), + injectionToken: internalDeepLinkingRouteInjectionToken, + })); + di.register(getInjectable({ + id: "some-other-id", + instantiate: () => ({ + path: "/page/:id", + handler: params => { called = params.pathname.id; }, + } as InternalRouteRegistration), + injectionToken: internalDeepLinkingRouteInjectionToken, + })); + }); try { expect(await lpr.route("lens://app/page/foo")).toBeUndefined(); @@ -136,7 +200,11 @@ describe("protocol router tests", () => { } expect(called).toBe("foo"); - expect(broadcastMessageMock).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page/foo", "matched"); + expect(sendDeepLinkingAttemptMock).toHaveBeenCalledWith({ + url: "lens://app/page/foo", + previous: "matched", + target: "internal", + }); }); it("should call most exact handler for an extension", async () => { @@ -176,7 +244,11 @@ describe("protocol router tests", () => { } expect(called).toBe("foob"); - expect(broadcastMessageMock).toBeCalledWith(ProtocolHandlerExtension, "lens://extension/@foobar/icecream/page/foob", "matched"); + expect(sendDeepLinkingAttemptMock).toHaveBeenCalledWith({ + url: "lens://extension/@foobar/icecream/page/foob", + previous: "matched", + target: "external", + }); }); it("should work with non-org extensions", async () => { @@ -245,20 +317,67 @@ describe("protocol router tests", () => { expect(called).toBe(1); - expect(broadcastMessageMock).toBeCalledWith(ProtocolHandlerExtension, "lens://extension/icecream/page", "matched"); + expect(sendDeepLinkingAttemptMock).toHaveBeenCalledWith({ + url: "lens://extension/icecream/page", + previous: "matched", + target: "external", + }); }); it("should throw if urlSchema is invalid", () => { - expect(() => lpr.addInternalHandler("/:@", noop)).toThrowError(); + expect(() => { + runInAction(() => { + di.register(getInjectable({ + id: "some-id", + instantiate: () => ({ + path: "/:@", + handler: noop, + }), + injectionToken: internalDeepLinkingRouteInjectionToken, + })); + }); + }).toThrowError(); }); it("should call most exact handler with 3 found handlers", async () => { let called: any = 0; - lpr.addInternalHandler("/", () => { called = 2; }); - lpr.addInternalHandler("/page", () => { called = 1; }); - lpr.addInternalHandler("/page/foo", () => { called = 3; }); - lpr.addInternalHandler("/page/bar", () => { called = 4; }); + runInAction(() => { + di.register( + getInjectable({ + id: "some-id-2", + instantiate: () => ({ + path: "/", + handler: () => { called = 2; }, + }), + injectionToken: internalDeepLinkingRouteInjectionToken, + }), + getInjectable({ + id: "some-id-1", + instantiate: () => ({ + path: "/page", + handler: () => { called = 1; }, + }), + injectionToken: internalDeepLinkingRouteInjectionToken, + }), + getInjectable({ + id: "some-id-3", + instantiate: () => ({ + path: "/page/foo", + handler: () => { called = 3; }, + }), + injectionToken: internalDeepLinkingRouteInjectionToken, + }), + getInjectable({ + id: "some-id-4", + instantiate: () => ({ + path: "/page/bar", + handler: () => { called = 4; }, + }), + injectionToken: internalDeepLinkingRouteInjectionToken, + }), + ); + }); try { expect(await lpr.route("lens://app/page/foo/bar/bat")).toBeUndefined(); @@ -267,15 +386,44 @@ describe("protocol router tests", () => { } expect(called).toBe(3); - expect(broadcastMessageMock).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page/foo/bar/bat", "matched"); + expect(sendDeepLinkingAttemptMock).toHaveBeenCalledWith({ + url: "lens://app/page/foo/bar/bat", + previous: "matched", + target: "internal", + }); }); it("should call most exact handler with 2 found handlers", async () => { let called: any = 0; - lpr.addInternalHandler("/", () => { called = 2; }); - lpr.addInternalHandler("/page", () => { called = 1; }); - lpr.addInternalHandler("/page/bar", () => { called = 4; }); + runInAction(() => { + di.register( + getInjectable({ + id: "some-id-2", + instantiate: () => ({ + path: "/", + handler: () => { called = 2; }, + }), + injectionToken: internalDeepLinkingRouteInjectionToken, + }), + getInjectable({ + id: "some-id-1", + instantiate: () => ({ + path: "/page", + handler: () => { called = 1; }, + }), + injectionToken: internalDeepLinkingRouteInjectionToken, + }), + getInjectable({ + id: "some-id-4", + instantiate: () => ({ + path: "/page/bar", + handler: () => { called = 4; }, + }), + injectionToken: internalDeepLinkingRouteInjectionToken, + }), + ); + }); try { expect(await lpr.route("lens://app/page/foo/bar/bat")).toBeUndefined(); @@ -284,6 +432,10 @@ describe("protocol router tests", () => { } expect(called).toBe(1); - expect(broadcastMessageMock).toBeCalledWith(ProtocolHandlerInternal, "lens://app/page/foo/bar/bat", "matched"); + expect(sendDeepLinkingAttemptMock).toHaveBeenCalledWith({ + url: "lens://app/page/foo/bar/bat", + previous: "matched", + target: "internal", + }); }); }); diff --git a/packages/core/src/main/protocol-handler/index.ts b/packages/core/src/main/protocol-handler/index.ts deleted file mode 100644 index 973d27c28c..0000000000 --- a/packages/core/src/main/protocol-handler/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -export * from "./lens-protocol-router-main/lens-protocol-router-main"; diff --git a/packages/core/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable.ts b/packages/core/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable.ts index 4551f493c2..25ac4574ab 100644 --- a/packages/core/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable.ts +++ b/packages/core/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.injectable.ts @@ -6,10 +6,11 @@ import { getInjectable } from "@ogre-tools/injectable"; import { LensProtocolRouterMain } from "./lens-protocol-router-main"; import extensionsStoreInjectable from "../../../extensions/extensions-store/extensions-store.injectable"; import showApplicationWindowInjectable from "../../start-main-application/lens-window/show-application-window.injectable"; -import broadcastMessageInjectable from "../../../common/ipc/broadcast-message.injectable"; import findExtensionInstanceByNameInjectable from "../../../features/extensions/loader/common/find-instance-by-name.injectable"; import internalDeepLinkingRoutesInjectable from "../../../features/deep-linking/renderer/internal-deep-linking-routes.injectable"; import protocolHandlerLoggerInjectable from "../../../common/protocol-handler/logger.injectable"; +import sendDeepLinkingAttemptInjectable from "../../../features/deep-linking/main/send-deep-linking-attempt.injectable"; +import sendInvalidDeepLinkingAttemptInjectable from "../../../features/deep-linking/main/send-invalid-deep-linking-attempt.injectable"; const lensProtocolRouterMainInjectable = getInjectable({ id: "lens-protocol-router-main", @@ -17,10 +18,11 @@ const lensProtocolRouterMainInjectable = getInjectable({ instantiate: (di) => new LensProtocolRouterMain({ extensionsStore: di.inject(extensionsStoreInjectable), showApplicationWindow: di.inject(showApplicationWindowInjectable), - broadcastMessage: di.inject(broadcastMessageInjectable), logger: di.inject(protocolHandlerLoggerInjectable), findExtensionInstanceByName: di.inject(findExtensionInstanceByNameInjectable), internalRoutes: di.inject(internalDeepLinkingRoutesInjectable), + sendDeepLinkingAttempt: di.inject(sendDeepLinkingAttemptInjectable), + sendInvalidDeepLinkingAttempt: di.inject(sendInvalidDeepLinkingAttemptInjectable), }), }); diff --git a/packages/core/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.ts b/packages/core/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.ts index 04ff7e43ec..3ce8254c5b 100644 --- a/packages/core/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.ts +++ b/packages/core/src/main/protocol-handler/lens-protocol-router-main/lens-protocol-router-main.ts @@ -8,9 +8,9 @@ import URLParse from "url-parse"; import type { LensExtension } from "../../../extensions/lens-extension"; import { observable, when } from "mobx"; import type { LensProtocolRouterDependencies, RouteAttempt } from "../../../common/protocol-handler"; -import { ProtocolHandlerInvalid } from "../../../common/protocol-handler"; import { disposer, noop } from "../../../common/utils"; -import type { BroadcastMessage } from "../../../common/ipc/broadcast-message.injectable"; +import type { SendDeepLinkingAttempt } from "../../../features/deep-linking/main/send-deep-linking-attempt.injectable"; +import type { SendInvalidDeepLinkingAttempt } from "../../../features/deep-linking/main/send-invalid-deep-linking-attempt.injectable"; export interface FallbackHandler { (name: string): Promise; @@ -35,7 +35,8 @@ function checkHost(url: URLParse): boolean { export interface LensProtocolRouterMainDependencies extends LensProtocolRouterDependencies { showApplicationWindow: () => Promise; - broadcastMessage: BroadcastMessage; + sendDeepLinkingAttempt: SendDeepLinkingAttempt; + sendInvalidDeepLinkingAttempt: SendInvalidDeepLinkingAttempt; } export class LensProtocolRouterMain extends proto.LensProtocolRouter { @@ -74,12 +75,15 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter { this.dependencies.logger.info(`routing ${url.toString()}`); if (routeInternally) { - this._routeToInternal(url); + this.routeToInternal(url); } else { - await this._routeToExtension(url); + await this.routeToExtension(url); } } catch (error) { - this.dependencies.broadcastMessage(ProtocolHandlerInvalid, error ? String(error) : "unknown error", rawUrl); + this.dependencies.sendInvalidDeepLinkingAttempt({ + error: String(error), + url: rawUrl, + }); if (error instanceof proto.RoutingError) { this.dependencies.logger.error(`${error}`, { url: error.url }); @@ -113,10 +117,13 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter { return ""; } - protected _routeToInternal(url: URLParse>): RouteAttempt { - const rawUrl = url.toString(); // for sending to renderer - const attempt = super._routeToInternal(url); - const broadcastToRenderer = () => this.dependencies.broadcastMessage(proto.ProtocolHandlerInternal, rawUrl, attempt); + routeToInternal(url: URLParse>): RouteAttempt { + const attempt = super.routeToInternal(url); + const broadcastToRenderer = () => this.dependencies.sendDeepLinkingAttempt({ + previous: attempt, + target: "internal", + url: url.toString(), + }); if (this.rendererLoaded.get()) { broadcastToRenderer(); @@ -127,18 +134,20 @@ export class LensProtocolRouterMain extends proto.LensProtocolRouter { return attempt; } - protected async _routeToExtension(url: URLParse>): Promise { - const rawUrl = url.toString(); // for sending to renderer - + async routeToExtension(url: URLParse>): Promise { /** * This needs to be done first, so that the missing extension handlers can * be called before notifying the renderer. * - * Note: this needs to clone the url because _routeToExtension modifies its + * Note: this needs to clone the url because routeToExtension modifies its * argument. */ - const attempt = await super._routeToExtension(new URLParse(url.toString(), true)); - const broadcastToRenderer = () => this.dependencies.broadcastMessage(proto.ProtocolHandlerExtension, rawUrl, attempt); + const attempt = await super.routeToExtension(new URLParse(url.toString(), true)); + const broadcastToRenderer = () => this.dependencies.sendDeepLinkingAttempt({ + previous: attempt, + target: "external", + url: url.toString(), + }); if (this.rendererLoaded.get()) { broadcastToRenderer(); diff --git a/packages/core/src/main/protocol-handler/lens-protocol-router-main/open-deep-link-for-url/open-deep-link.injectable.ts b/packages/core/src/main/protocol-handler/lens-protocol-router-main/open-deep-link.injectable.ts similarity index 52% rename from packages/core/src/main/protocol-handler/lens-protocol-router-main/open-deep-link-for-url/open-deep-link.injectable.ts rename to packages/core/src/main/protocol-handler/lens-protocol-router-main/open-deep-link.injectable.ts index 71e42b9f3c..52c7bb52db 100644 --- a/packages/core/src/main/protocol-handler/lens-protocol-router-main/open-deep-link-for-url/open-deep-link.injectable.ts +++ b/packages/core/src/main/protocol-handler/lens-protocol-router-main/open-deep-link.injectable.ts @@ -3,18 +3,11 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import lensProtocolRouterMainInjectable from "../lens-protocol-router-main.injectable"; +import lensProtocolRouterMainInjectable from "./lens-protocol-router-main.injectable"; const openDeepLinkInjectable = getInjectable({ id: "open-deep-link", - - instantiate: (di) => { - const getProtocolRouter = () => di.inject(lensProtocolRouterMainInjectable); - - return async (url: string) => { - await getProtocolRouter().route(url); - }; - }, + instantiate: (di) => async (url: string) => di.inject(lensProtocolRouterMainInjectable).route(url), }); export default openDeepLinkInjectable; diff --git a/packages/core/src/main/start-main-application/runnables/show-initial-window.injectable.ts b/packages/core/src/main/start-main-application/runnables/show-initial-window.injectable.ts index 0ce02bfa85..042db9e1f6 100644 --- a/packages/core/src/main/start-main-application/runnables/show-initial-window.injectable.ts +++ b/packages/core/src/main/start-main-application/runnables/show-initial-window.injectable.ts @@ -4,7 +4,7 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import shouldStartHiddenInjectable from "../../electron-app/features/should-start-hidden.injectable"; -import openDeepLinkInjectable from "../../protocol-handler/lens-protocol-router-main/open-deep-link-for-url/open-deep-link.injectable"; +import openDeepLinkInjectable from "../../protocol-handler/lens-protocol-router-main/open-deep-link.injectable"; import commandLineArgumentsInjectable from "../../utils/command-line-arguments.injectable"; import createFirstApplicationWindowInjectable from "../lens-window/application-window/create-first-application-window.injectable"; import splashWindowInjectable from "../lens-window/splash-window/splash-window.injectable"; diff --git a/packages/core/src/renderer/frames/root-frame/init-root-frame.injectable.ts b/packages/core/src/renderer/frames/root-frame/init-root-frame.injectable.ts index f785f5c6c8..c2e6ed543e 100644 --- a/packages/core/src/renderer/frames/root-frame/init-root-frame.injectable.ts +++ b/packages/core/src/renderer/frames/root-frame/init-root-frame.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 lensProtocolRouterRendererInjectable from "../../protocol-handler/lens-protocol-router-renderer.injectable"; import registerIpcListenersInjectable from "../../ipc/register-ipc-listeners.injectable"; import loggerInjectable from "../../../common/logger.injectable"; import unmountRootComponentInjectable from "../../window/unmount-root-component.injectable"; @@ -12,13 +11,10 @@ const initRootFrameInjectable = getInjectable({ id: "init-root-frame", instantiate: (di) => { const registerIpcListeners = di.inject(registerIpcListenersInjectable); - const lensProtocolRouterRenderer = di.inject(lensProtocolRouterRendererInjectable); const logger = di.inject(loggerInjectable); const unmountRootComponent = di.inject(unmountRootComponentInjectable); return async () => { - lensProtocolRouterRenderer.init(); - registerIpcListeners(); window.addEventListener("beforeunload", () => { diff --git a/packages/core/src/renderer/protocol-handler/lens-protocol-router-renderer.injectable.ts b/packages/core/src/renderer/protocol-handler/lens-protocol-router-renderer.injectable.ts deleted file mode 100644 index 3a3f283de5..0000000000 --- a/packages/core/src/renderer/protocol-handler/lens-protocol-router-renderer.injectable.ts +++ /dev/null @@ -1,27 +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 { LensProtocolRouterRenderer } from "./lens-protocol-router-renderer"; -import extensionsStoreInjectable from "../../extensions/extensions-store/extensions-store.injectable"; -import showErrorNotificationInjectable from "../components/notifications/show-error-notification.injectable"; -import showShortInfoNotificationInjectable from "../components/notifications/show-short-info.injectable"; -import findExtensionInstanceByNameInjectable from "../../features/extensions/loader/common/find-instance-by-name.injectable"; -import internalDeepLinkingRoutesInjectable from "../../features/deep-linking/renderer/internal-deep-linking-routes.injectable"; -import protocolHandlerLoggerInjectable from "../../common/protocol-handler/logger.injectable"; - -const lensProtocolRouterRendererInjectable = getInjectable({ - id: "lens-protocol-router-renderer", - - instantiate: (di) => new LensProtocolRouterRenderer({ - extensionsStore: di.inject(extensionsStoreInjectable), - logger: di.inject(protocolHandlerLoggerInjectable), - showErrorNotification: di.inject(showErrorNotificationInjectable), - showShortInfoNotification: di.inject(showShortInfoNotificationInjectable), - findExtensionInstanceByName: di.inject(findExtensionInstanceByNameInjectable), - internalRoutes: di.inject(internalDeepLinkingRoutesInjectable), - }), -}); - -export default lensProtocolRouterRendererInjectable; diff --git a/packages/core/src/renderer/protocol-handler/lens-protocol-router-renderer.tsx b/packages/core/src/renderer/protocol-handler/lens-protocol-router-renderer.tsx deleted file mode 100644 index 66561788a1..0000000000 --- a/packages/core/src/renderer/protocol-handler/lens-protocol-router-renderer.tsx +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ - -import React from "react"; -import { ipcRenderer } from "electron"; -import * as proto from "../../common/protocol-handler"; -import Url from "url-parse"; -import type { LensProtocolRouterDependencies } from "../../common/protocol-handler"; -import { foldAttemptResults, ProtocolHandlerInvalid, RouteAttempt } from "../../common/protocol-handler"; -import type { ShowNotification } from "../components/notifications"; - -interface Dependencies extends LensProtocolRouterDependencies { - showShortInfoNotification: ShowNotification; - showErrorNotification: ShowNotification; -} - -export class LensProtocolRouterRenderer extends proto.LensProtocolRouter { - constructor(protected readonly dependencies: Dependencies) { - super(dependencies); - } - - /** - * This function is needed to be called early on in the renderers lifetime. - */ - public init(): void { - ipcRenderer.on(proto.ProtocolHandlerInternal, (event, rawUrl: string, mainAttemptResult: RouteAttempt) => { - const rendererAttempt = this._routeToInternal(new Url(rawUrl, true)); - - if (foldAttemptResults(mainAttemptResult, rendererAttempt) === RouteAttempt.MISSING) { - this.dependencies.showShortInfoNotification(( -

- {"Unknown action "} - {rawUrl} - {". Are you on the latest version?"} -

- )); - } - }); - ipcRenderer.on(proto.ProtocolHandlerExtension, async (event, rawUrl: string, mainAttemptResult: RouteAttempt) => { - const rendererAttempt = await this._routeToExtension(new Url(rawUrl, true)); - - switch (foldAttemptResults(mainAttemptResult, rendererAttempt)) { - case RouteAttempt.MISSING: - this.dependencies.showShortInfoNotification(( -

- {"Unknown action "} - {rawUrl} - {". Are you on the latest version of the extension?"} -

- )); - break; - case RouteAttempt.MISSING_EXTENSION: - this.dependencies.showShortInfoNotification(( -

- {"Missing extension for action "} - {rawUrl} - {". Not able to find extension in our known list. Try installing it manually."} -

- )); - break; - } - }); - ipcRenderer.on(ProtocolHandlerInvalid, (event, error: string, rawUrl: string) => { - this.dependencies.showErrorNotification(( - <> -

- {"Failed to route "} - {rawUrl} - . -

-

- Error: - {" "} - {error} -

- - )); - }); - } -}