From ad1d9427fee1cec0e16a41143077338bb28240a7 Mon Sep 17 00:00:00 2001 From: Janne Savolainen Date: Wed, 19 Oct 2022 11:02:00 +0300 Subject: [PATCH] Implement registrator for preference items Co-authored-by: Mikko Aspiala Signed-off-by: Janne Savolainen --- .../extension-adding-preference-tabs.test.tsx | 64 +++- ...to-extension-specific-preferences.test.tsx | 320 ++++++------------ ...vigation-to-telemetry-preferences.test.tsx | 38 ++- .../extension-preference-item.tsx | 35 ++ ...trator-for-preference-items.injectable.tsx | 138 ++++++++ .../renderer/get-preference-page.tsx | 2 +- ...ent-preference-tab-composite.injectable.ts | 11 +- .../preference-item-injection-token.ts | 11 +- .../preference-items/preference-tab-root.tsx | 6 +- .../preferences-composite.injectable.ts | 34 +- .../preferences-navigation.tsx | 6 +- .../preferences/renderer/preferences.tsx | 26 +- .../+preferences/extension-settings.tsx | 39 --- .../components/+preferences/extensions.tsx | 7 +- 14 files changed, 436 insertions(+), 301 deletions(-) create mode 100644 src/features/preferences/renderer/compliance-for-legacy-extension-api/extension-preference-item.tsx create mode 100644 src/features/preferences/renderer/compliance-for-legacy-extension-api/registrator-for-preference-items.injectable.tsx delete mode 100644 src/renderer/components/+preferences/extension-settings.tsx diff --git a/src/features/preferences/extension-adding-preference-tabs.test.tsx b/src/features/preferences/extension-adding-preference-tabs.test.tsx index 47810df727..06b2a2ea43 100644 --- a/src/features/preferences/extension-adding-preference-tabs.test.tsx +++ b/src/features/preferences/extension-adding-preference-tabs.test.tsx @@ -7,6 +7,8 @@ import type { IObservableValue } from "mobx"; import { runInAction, computed, observable } from "mobx"; import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; +import { getSingleElement, queryAllElements, querySingleElement } from "../../renderer/components/test-utils/discovery-of-html-elements"; +import React from "react"; describe("preferences: extension adding preference tabs", () => { let builder: ApplicationBuilder; @@ -49,11 +51,43 @@ describe("preferences: extension adding preference tabs", () => { visible: computed(() => someObservable.get()), }, ], + + appPreferences: [ + { + title: "some-title", + id: "some-preference-item-id", + showInPreferencesTab: "some-preference-tab-id", + + components: { + Hint: () =>
, + Input: () =>
, + }, + }, + { + title: "some-other-title", + id: "some-other-preference-item-id", + showInPreferencesTab: "some-other-preference-tab-id", + + components: { + Hint: () =>
, + Input: () =>
, + }, + }, + { + title: "some-another-title", + id: "some-another-preference-item-id", + showInPreferencesTab: "some-preference-tab-id-with-controlled-visibility", + + components: { + Hint: () =>
, + Input: () =>
, + }, + }, + ], }, }; builder.extensions.enable(testExtension); - }); it("renders", () => { @@ -61,20 +95,25 @@ describe("preferences: extension adding preference tabs", () => { }); it("shows tabs in order", () => { - const actual = rendered.queryAllByTestId(/tab-link-for-extension-some-extension-nav-item-(.*)/).map(x => x.dataset.testid); + const actual = queryAllElements("preference-tab-link")( + rendered, + ).attributeValues.filter((value) => + value?.startsWith("extension-some-extension"), + ); expect(actual).toEqual([ - "tab-link-for-extension-some-extension-nav-item-some-other-preference-tab-id", - "tab-link-for-extension-some-extension-nav-item-some-preference-tab-id", + "extension-some-extension-some-other-preference-tab-id", + "extension-some-extension-some-preference-tab-id", ]); }); it("does not show hidden tab", () => { - const actual = rendered.queryByTestId( - "tab-link-for-extension-some-extension-nav-item-some-preference-tab-id-with-controlled-visibility", - ); + const actual = querySingleElement( + "preference-tab-link", + "extension-some-extension-some-preference-tab-id-with-controlled-visibility", + )(rendered); - expect(actual).not.toBeInTheDocument(); + expect(actual).toBeNull(); }); it("when item becomes visible, shows the tab", () => { @@ -82,11 +121,12 @@ describe("preferences: extension adding preference tabs", () => { someObservable.set(true); }); - const actual = rendered.queryByTestId( - "tab-link-for-extension-some-extension-nav-item-some-preference-tab-id-with-controlled-visibility", - ); + const actual = getSingleElement( + "preference-tab-link", + "extension-some-extension-some-preference-tab-id-with-controlled-visibility", + )(rendered); - expect(actual).toBeInTheDocument(); + expect(actual).not.toBeNull(); }); }); }); diff --git a/src/features/preferences/navigation-to-extension-specific-preferences.test.tsx b/src/features/preferences/navigation-to-extension-specific-preferences.test.tsx index cb44c1770c..7ae53037e5 100644 --- a/src/features/preferences/navigation-to-extension-specific-preferences.test.tsx +++ b/src/features/preferences/navigation-to-extension-specific-preferences.test.tsx @@ -7,8 +7,9 @@ import type { ApplicationBuilder } from "../../renderer/components/test-utils/ge import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import React from "react"; import "@testing-library/jest-dom/extend-expect"; -import extensionPreferencesRouteInjectable from "../../common/front-end-routing/routes/preferences/extension/extension-preferences-route.injectable"; import type { FakeExtensionOptions } from "../../renderer/components/test-utils/get-extension-fake"; +import { getSingleElement, queryAllElements, querySingleElement } from "../../renderer/components/test-utils/discovery-of-html-elements"; +import logErrorInjectable from "../../common/log-error.injectable"; describe("preferences - navigation to extension specific preferences", () => { let builder: ApplicationBuilder; @@ -19,9 +20,14 @@ describe("preferences - navigation to extension specific preferences", () => { describe("given in preferences, when rendered", () => { let rendered: RenderResult; + let logErrorMock: jest.Mock; beforeEach(async () => { - builder.beforeWindowStart(() => { + logErrorMock = jest.fn(); + + builder.beforeWindowStart((windowDi) => { + windowDi.override(logErrorInjectable, () => logErrorMock); + builder.preferences.navigate(); }); @@ -33,7 +39,8 @@ describe("preferences - navigation to extension specific preferences", () => { }); it("does not show extension preferences yet", () => { - const page = rendered.queryByTestId("extension-preferences-page"); + // Todo: check if query is correct. + const page = querySingleElement("preference-page", "extension")(rendered); expect(page).toBeNull(); }); @@ -52,7 +59,7 @@ describe("preferences - navigation to extension specific preferences", () => { ); builder.preferences.navigation.click( - "extension-some-test-extension-id", + "some-test-extension-id", ); }); @@ -61,13 +68,19 @@ describe("preferences - navigation to extension specific preferences", () => { }); it("doesn't show preferences from unrelated extension", () => { - const actual = rendered.queryByTestId("extension-preference-item-for-some-other-preference-item-id"); + const actual = querySingleElement( + "preference-page", + "preference-item-for-extension-some-other-test-extension-id-page", + )(rendered); expect(actual).toBeNull(); }); it("shows preferences from related extension", () => { - const actual = rendered.getByTestId("extension-preference-item-for-some-preference-item-id"); + const actual = getSingleElement( + "preference-page", + "preference-item-for-extension-some-test-extension-id-page", + )(rendered); expect(actual).not.toBeNull(); }); @@ -104,21 +117,18 @@ describe("preferences - navigation to extension specific preferences", () => { expect(rendered.container).toMatchSnapshot(); }); - it("shows link for extension preferences", () => { - const actual = rendered.getByTestId("tab-link-for-extension-some-test-extension-id"); - - expect(actual).not.toBeNull(); - }); - it("link should not be active", () => { - const actual = rendered.getByTestId("tab-link-for-extension-some-test-extension-id"); + const actual = getSingleElement( + "preference-tab-link", + "some-test-extension-id", + )(rendered); expect(actual).not.toHaveClass("active"); }); describe("when navigating to extension preferences using navigation", () => { beforeEach(() => { - builder.preferences.navigation.click("extension-some-test-extension-id"); + builder.preferences.navigation.click("some-test-extension-id"); }); it("renders", () => { @@ -126,31 +136,23 @@ describe("preferences - navigation to extension specific preferences", () => { }); it("shows proper page title", () => { - const title = rendered.getByText("some-test-extension-id preferences"); + const title = getSingleElement("preference-page-title")(rendered); - expect(title).toBeInTheDocument(); + expect(title).toHaveTextContent("some-test-extension-id preferences"); }); - it("shows extension specific preferences", () => { - const page = rendered.getByTestId("extension-preferences-page"); + it("shows only extension specific preference items", () => { + const actual = queryAllElements( + "preference-item", + )(rendered).attributeValues; - expect(page).not.toBeNull(); - }); - - it("shows extension specific preference item", () => { - const actual = rendered.getByTestId("extension-preference-item-for-some-preference-item-id"); - - expect(actual).not.toBeNull(); - }); - - it("does not show unrelated preference tab items", () => { - const actual = rendered.queryByTestId("extension-preference-item-for-some-unrelated-preference-item-id"); - - expect(actual).toBeNull(); + expect(actual).toEqual([ + "preference-item-for-extension-some-test-extension-id-item-some-preference-item-id", + ]); }); it("link is active", () => { - const actual = rendered.getByTestId("tab-link-for-extension-some-test-extension-id"); + const actual = getSingleElement("preference-tab-link", "some-test-extension-id")(rendered); expect(actual).toHaveClass("active"); }); @@ -164,14 +166,21 @@ describe("preferences - navigation to extension specific preferences", () => { expect(rendered.baseElement).toMatchSnapshot(); }); - it("shows the error message about extension not being present", () => { - expect(rendered.getByTestId("error-for-extension-not-being-present")).toBeInTheDocument(); + it("does not show any preference page", () => { + const actual = querySingleElement("preference-page")(rendered); + + expect(actual).toBeNull(); }); - it("when extension is enabled again, does not show the error message anymore", () => { + it("when extension is enabled again, shows the preference page", () => { builder.extensions.enable(extensionStubWithExtensionSpecificPreferenceItems); - expect(rendered.queryByTestId("error-for-extension-not-being-present")).not.toBeInTheDocument(); + const actual = getSingleElement( + "preference-page", + "preference-item-for-extension-some-test-extension-id-page", + )(rendered); + + expect(actual).not.toBeNull(); }); }); }); @@ -179,90 +188,104 @@ describe("preferences - navigation to extension specific preferences", () => { describe("given extension with registered tab", () => { beforeEach(() => { - builder.extensions.enable(extensionStubWithWithRegisteredTab); + builder.extensions.enable(extensionStubWithRegisteredTab); + }); + + it("logs error", () => { + expect( + logErrorMock.mock.calls[0][0].startsWith( + "Tried to create preferences, but encountered references to unknown ids", + ), + ).toBe(true); + }); + + it("renders", () => { + expect(rendered.baseElement).toMatchSnapshot(); }); it("shows extension tab in general area", () => { - const actual = rendered.getByTestId("tab-link-for-extension-registered-tab-page-id-nav-item-metrics-extension-tab"); + const generalTabGroup = getSingleElement( + "preference-tab-group", + "general-tab-group", + )(rendered); - expect(actual).toMatchSnapshot(); + const actual = getSingleElement( + "preference-tab-link", + "extension-registered-tab-page-id-metrics-extension-tab", + )(generalTabGroup); + + expect(actual).not.toBeNull(); }); - it("does not show custom settings block", () => { - const actual = rendered.queryByTestId("extension-settings"); + it("does not show tab group for extensions for there being no content", () => { + const actual = querySingleElement( + "preference-tab-group", + "extensions-tab-group", + )(rendered); - expect(actual).not.toBeInTheDocument(); + expect(actual).toBeNull(); }); describe("when navigating to specific extension tab", () => { beforeEach(() => { - builder.preferences.navigation.click("extension-registered-tab-page-id-nav-item-metrics-extension-tab"); + builder.preferences.navigation.click("extension-registered-tab-page-id-metrics-extension-tab"); }); + it("renders", () => { expect(rendered.container).toMatchSnapshot(); }); + it("shows related preferences for this tab", () => { const actual = rendered.getByTestId("metrics-preference-item-hint"); expect(actual).toBeInTheDocument(); }); + it("does not show unrelated preferences for this tab", () => { const actual = rendered.queryByTestId("survey-preference-item-hint"); expect(actual).not.toBeInTheDocument(); }); - it("shows page title", () => { - const pageTitle = rendered.queryByTestId("extension-preferences-page-title"); - expect(pageTitle).toBeInTheDocument(); + it("shows correct page title", () => { + const pageTitle = getSingleElement("preference-page-title")(rendered); + + expect(pageTitle).toHaveTextContent("Metrics tab"); }); - it("shows page title same as tab title", () => { - const pageTitle = rendered.queryByTestId("extension-preferences-page-title"); - const tabs = extensionStubWithWithRegisteredTab.rendererOptions?.appPreferenceTabs; - const tabTitle = tabs && tabs[0].title; - - expect(pageTitle?.innerHTML).toBe(tabTitle); - }); - }); - }); - - describe("given extension with few registered tabs", () => { - const tabs = [ - "tab-link-for-extension-hello-world-tab-page-id-nav-item-hello-extension-tab", - "tab-link-for-extension-hello-world-tab-page-id-nav-item-logs-extension-tab", - ]; - - beforeEach(() => { - builder.extensions.enable(extensionStubWithWithRegisteredTabs); - }); - - it.each(tabs)("shows '%s' tab in general area", (tab) => { - const tabElement = rendered.getByTestId(tab); - - expect(tabElement).toBeInTheDocument(); }); }); describe("given extensions with tabs having same id", () => { beforeEach(() => { - builder.extensions.enable(extensionStubWithWithRegisteredTab, extensionStubWithWithSameRegisteredTab); + builder.extensions.enable( + extensionStubWithRegisteredTab, + extensionStubWithSameRegisteredTab, + ); }); it("shows tab from the first extension", () => { - const actual = rendered.getByTestId("tab-link-for-extension-registered-tab-page-id-nav-item-metrics-extension-tab"); + const actual = getSingleElement( + "preference-tab-link", + "extension-registered-tab-page-id-metrics-extension-tab", + )(rendered); expect(actual).toBeInTheDocument(); }); it("shows tab from the second extension", () => { - const actual = rendered.getByTestId("tab-link-for-extension-duplicated-tab-page-id-nav-item-metrics-extension-tab"); + const actual = getSingleElement( + "preference-tab-link", + "extension-duplicated-tab-page-id-metrics-extension-tab", + )(rendered); expect(actual).toBeInTheDocument(); }); describe("when navigating to first extension tab", () => { beforeEach(() => { - builder.preferences.navigation.click("extension-registered-tab-page-id-nav-item-metrics-extension-tab"); + builder.preferences.navigation.click( + "extension-registered-tab-page-id-metrics-extension-tab", + ); }); it("renders", () => { @@ -270,21 +293,20 @@ describe("preferences - navigation to extension specific preferences", () => { }); it("shows related preferences for this tab", () => { - const actual = rendered.getByTestId("metrics-preference-item-hint"); + const actual = + queryAllElements("preference-item")(rendered).attributeValues; - expect(actual).toBeInTheDocument(); - }); - - it("does not show unrelated preferences for this tab", () => { - const actual = rendered.queryByTestId("another-metrics-preference-item-hint"); - - expect(actual).not.toBeInTheDocument(); + expect(actual).toEqual([ + "preference-item-for-extension-registered-tab-page-id-item-metrics-preference-item-id", + ]); }); }); describe("when navigating to second extension tab", () => { beforeEach(() => { - builder.preferences.navigation.click("extension-duplicated-tab-page-id-nav-item-metrics-extension-tab"); + builder.preferences.navigation.click( + "extension-duplicated-tab-page-id-metrics-extension-tab", + ); }); it("renders", () => { @@ -292,74 +314,16 @@ describe("preferences - navigation to extension specific preferences", () => { }); it("shows related preferences for this tab", () => { - const actual = rendered.getByTestId("another-metrics-preference-item-hint"); + const actual = + queryAllElements("preference-item")(rendered).attributeValues; - expect(actual).toBeInTheDocument(); - }); - - it("does not show unrelated preferences for this tab", () => { - const actual = rendered.queryByTestId("metrics-preference-item-hint"); - - expect(actual).not.toBeInTheDocument(); + expect(actual).toEqual([ + "preference-item-for-extension-duplicated-tab-page-id-item-another-metrics-preference-item-id", + ]); }); }); }); }); - - describe("when navigating to extension specific tab", () => { - let rendered: RenderResult; - - beforeEach(async () => { - builder.beforeWindowStart((windowDi) => { - const extensionRoute = windowDi.inject(extensionPreferencesRouteInjectable); - - const params = { parameters: { - extensionId: "duplicated-tab-page-id", - tabId: "metrics-extension-tab", - }}; - - builder.preferences.navigateTo(extensionRoute, params); - }); - - builder.extensions.enable(extensionStubWithWithSameRegisteredTab, extensionUsingSomeoneElseTab); - rendered = await builder.render(); - }); - - it("renders", () => { - expect(rendered.container).toMatchSnapshot(); - }); - - it("does render related preferences for specific tab", () => { - expect(rendered.getByTestId("another-metrics-preference-item-hint")).toBeInTheDocument(); - }); - - it("does not render related preferences for specific tab", () => { - expect(rendered.queryByTestId("my-preferences-item-hint")).not.toBeInTheDocument(); - }); - }); - - describe("when navigating to someone else extension specific tab", () => { - let rendered: RenderResult; - - beforeEach(async () => { - builder.beforeWindowStart((windowDi) => { - const extensionRoute = windowDi.inject(extensionPreferencesRouteInjectable); - const params = { parameters: { - extensionId: "extension-using-someone-else-tab-id", - tabId: "metrics-extension-tab", - }}; - - builder.preferences.navigateTo(extensionRoute, params); - }); - - builder.extensions.enable(extensionStubWithWithSameRegisteredTab, extensionUsingSomeoneElseTab); - rendered = await builder.render(); - }); - - it("renders", () => { - expect(rendered.container).toMatchSnapshot(); - }); - }); }); const extensionStubWithExtensionSpecificPreferenceItems: FakeExtensionOptions = { @@ -436,7 +400,7 @@ const extensionStubWithShowInPreferencesTab: FakeExtensionOptions = { }, }; -const extensionStubWithWithRegisteredTab: FakeExtensionOptions = { +const extensionStubWithRegisteredTab: FakeExtensionOptions = { id: "registered-tab-page-id", name: "registered-tab-page-id", @@ -482,47 +446,7 @@ const extensionStubWithWithRegisteredTab: FakeExtensionOptions = { }, }; -const extensionStubWithWithRegisteredTabs: FakeExtensionOptions = { - id: "hello-world-tab-page-id", - name: "hello-world-tab-page-id", - - rendererOptions: { - appPreferences: [ - { - title: "Hello world", - id: "hello-preference-item-id", - showInPreferencesTab: "hello-extension-tab", - - components: { - Hint: () =>
, - Input: () =>
, - }, - }, - { - title: "Logs", - id: "logs-preference-item-id", - showInPreferencesTab: "logs-extension-tab", - - components: { - Hint: () =>
, - Input: () =>
, - }, - }, - ], - - appPreferenceTabs: [{ - title: "Metrics tab", - id: "hello-extension-tab", - orderNumber: 100, - }, { - title: "Logs tab", - id: "logs-extension-tab", - orderNumber: 200, - }], - }, -}; - -const extensionStubWithWithSameRegisteredTab: FakeExtensionOptions = { +const extensionStubWithSameRegisteredTab: FakeExtensionOptions = { id: "duplicated-tab-page-id", name: "duplicated-tab-page-id", @@ -547,23 +471,3 @@ const extensionStubWithWithSameRegisteredTab: FakeExtensionOptions = { }], }, }; - -const extensionUsingSomeoneElseTab: FakeExtensionOptions = { - id: "extension-using-someone-else-tab-id", - name: "extension-using-someone-else-tab-id", - - rendererOptions: { - appPreferences: [ - { - title: "My preferences", - id: "my-preferences-item-id", - showInPreferencesTab: "metrics-extension-tab", - - components: { - Hint: () =>
, - Input: () =>
, - }, - }, - ], - }, -}; diff --git a/src/features/preferences/navigation-to-telemetry-preferences.test.tsx b/src/features/preferences/navigation-to-telemetry-preferences.test.tsx index 5997814336..675c3d4fce 100644 --- a/src/features/preferences/navigation-to-telemetry-preferences.test.tsx +++ b/src/features/preferences/navigation-to-telemetry-preferences.test.tsx @@ -9,7 +9,7 @@ import { getApplicationBuilder } from "../../renderer/components/test-utils/get- import navigateToTelemetryPreferencesInjectable from "./common/navigate-to-telemetry-preferences.injectable"; import sentryDataSourceNameInjectable from "../../common/vars/sentry-dsn-url.injectable"; import type { FakeExtensionOptions } from "../../renderer/components/test-utils/get-extension-fake"; -import { getSingleElement, querySingleElement } from "../../renderer/components/test-utils/discovery-of-html-elements"; +import { getSingleElement, queryAllElements, querySingleElement } from "../../renderer/components/test-utils/discovery-of-html-elements"; describe("preferences - navigation to telemetry preferences", () => { let builder: ApplicationBuilder; @@ -43,7 +43,10 @@ describe("preferences - navigation to telemetry preferences", () => { }); it("does not show link for telemetry preferences", () => { - const actual = rendered.queryByTestId("tab-link-for-telemetry"); + const actual = querySingleElement( + "preference-tab-link", + "telemetry", + )(rendered); expect(actual).toBeNull(); }); @@ -60,7 +63,10 @@ describe("preferences - navigation to telemetry preferences", () => { }); it("shows link for telemetry preferences", () => { - const actual = rendered.getByTestId("tab-link-for-telemetry"); + const actual = getSingleElement( + "preference-tab-link", + "telemetry", + )(rendered); expect(actual).not.toBeNull(); }); @@ -84,11 +90,12 @@ describe("preferences - navigation to telemetry preferences", () => { }); it("shows extension telemetry preference items", () => { - const actual = rendered.getByTestId( - "telemetry-preference-item-for-some-telemetry-preference-item-id", - ); + const actual = + queryAllElements("preference-item")(rendered).attributeValues; - expect(actual).not.toBeNull(); + expect(actual).toEqual([ + "preference-item-for-extension-some-test-extension-name-item-some-telemetry-preference-item-id", + ]); }); }); }); @@ -110,7 +117,10 @@ describe("preferences - navigation to telemetry preferences", () => { }, }); - const actual = rendered.queryByTestId("tab-link-for-telemetry"); + const actual = querySingleElement( + "preference-tab-link", + "telemetry", + )(rendered); expect(actual).toBeNull(); }); @@ -139,9 +149,11 @@ describe("preferences - navigation to telemetry preferences", () => { }); it("allows configuration of automatic error reporting", () => { - const actual = rendered.getByTestId("telemetry-preferences-for-automatic-error-reporting"); + const actual = queryAllElements( + "preference-item", + )(rendered).attributeValues; - expect(actual).not.toBeNull(); + expect(actual).toEqual(["automatic-error-reporting"]); }); }); }); @@ -168,9 +180,11 @@ describe("preferences - navigation to telemetry preferences", () => { }); it("does not allow configuration of automatic error reporting", () => { - const actual = rendered.queryByTestId("telemetry-preferences-for-automatic-error-reporting"); + const actual = queryAllElements( + "preference-item", + )(rendered).attributeValues; - expect(actual).toBeNull(); + expect(actual).toEqual([]); }); }); }); diff --git a/src/features/preferences/renderer/compliance-for-legacy-extension-api/extension-preference-item.tsx b/src/features/preferences/renderer/compliance-for-legacy-extension-api/extension-preference-item.tsx new file mode 100644 index 0000000000..65018b4db3 --- /dev/null +++ b/src/features/preferences/renderer/compliance-for-legacy-extension-api/extension-preference-item.tsx @@ -0,0 +1,35 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { SubTitle } from "../../../../renderer/components/layout/sub-title"; +import type { AppPreferenceRegistration } from "../../../../renderer/components/+preferences/app-preferences/app-preference-registration"; +import React from "react"; + +export interface ExtensionSettingsProps { + registration: AppPreferenceRegistration; +} + +export function ExtensionPreferenceItem({ registration }: ExtensionSettingsProps) { + const { + title, + id, + components: { Hint, Input }, + } = registration; + + return ( + +
+ + +
+ +
+
+
+
+ ); +} diff --git a/src/features/preferences/renderer/compliance-for-legacy-extension-api/registrator-for-preference-items.injectable.tsx b/src/features/preferences/renderer/compliance-for-legacy-extension-api/registrator-for-preference-items.injectable.tsx new file mode 100644 index 0000000000..9d819ce4a6 --- /dev/null +++ b/src/features/preferences/renderer/compliance-for-legacy-extension-api/registrator-for-preference-items.injectable.tsx @@ -0,0 +1,138 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import React from "react"; +import { getInjectable } from "@ogre-tools/injectable"; +import type { LensRendererExtension } from "../../../../extensions/lens-renderer-extension"; +import { preferenceItemInjectionToken } from "../preference-items/preference-item-injection-token"; +import { extensionRegistratorInjectionToken } from "../../../../extensions/extension-loader/extension-registrator-injection-token"; +import { getPreferencePage } from "../get-preference-page"; +import { ExtensionPreferenceItem } from "./extension-preference-item"; +import { computed } from "mobx"; + +const registratorForPreferenceItemsInjectable = getInjectable({ + id: "registrator-for-preference-items", + + instantiate: () => (ext) => { + const extension = ext as LensRendererExtension; + + const commonId = `preference-item-for-extension-${extension.sanitizedExtensionId}`; + + const tabId = `${commonId}-primary-tab`; + const primaryTabInjectable = getInjectable({ + id: tabId, + + instantiate: () => ({ + kind: "tab" as const, + id: tabId, + parentId: "extensions-tab-group", + pathId: extension.sanitizedExtensionId, + label: extension.name, + orderNumber: 10, + }), + + injectionToken: preferenceItemInjectionToken, + }); + + const pageId = `${commonId}-page`; + const primaryPageInjectable = getInjectable({ + id: pageId, + + instantiate: () => ({ + kind: "page" as const, + id: pageId, + parentId: tabId, + orderNumber: 0, + Component: getPreferencePage(`${extension.name} preferences`), + childrenSeparator: () =>
, + }), + + injectionToken: preferenceItemInjectionToken, + }); + + const additionalTabs = extension.appPreferenceTabs.map( + (registration) => { + const additionalTabId = `${commonId}-additional-tab-${registration.id}`; + + return getInjectable({ + id: additionalTabId, + + instantiate: () => ({ + kind: "tab" as const, + id: additionalTabId, + parentId: "general-tab-group", + pathId: `extension-${extension.sanitizedExtensionId}-${registration.id}`, + label: registration.title, + isShown: computed(() => registration.visible?.get() ?? true), + orderNumber: registration.orderNumber || 100, + }), + + injectionToken: preferenceItemInjectionToken, + }); + }, + ); + + const additionalPages = extension.appPreferenceTabs.map( + (registration) => { + const additionalPageId = `${commonId}-additional-page-${registration.id}`; + const additionalTabId = `${commonId}-additional-tab-${registration.id}`; + + return getInjectable({ + id: additionalPageId, + + instantiate: () => ({ + kind: "page" as const, + id: additionalPageId, + parentId: additionalTabId, + Component: getPreferencePage(registration.title), + }), + + injectionToken: preferenceItemInjectionToken, + }); + }, + ); + + const items = extension.appPreferences.map((registration, i) => { + const itemId = `${commonId}-item-${registration.id ?? i}`; + + return getInjectable({ + id: itemId, + + instantiate: () => ({ + kind: "item" as const, + id: itemId, + + // Note: Legacy extensions considered telemetry as magic string, and so does this code + parentId: registration.showInPreferencesTab + ? registration.showInPreferencesTab === "telemetry" + ? "telemetry-page" + : `${commonId}-additional-page-${registration.showInPreferencesTab}` + : pageId, + + orderNumber: i * 10, + + Component: () => ( + + ), + + childrenSeparator: () =>
, + }), + + injectionToken: preferenceItemInjectionToken, + }); + }); + + return [ + primaryTabInjectable, + ...additionalTabs, + primaryPageInjectable, + ...additionalPages, + ...items, + ]; + }, + + injectionToken: extensionRegistratorInjectionToken, +}); + +export default registratorForPreferenceItemsInjectable; diff --git a/src/features/preferences/renderer/get-preference-page.tsx b/src/features/preferences/renderer/get-preference-page.tsx index 2ebbb60459..544293a218 100644 --- a/src/features/preferences/renderer/get-preference-page.tsx +++ b/src/features/preferences/renderer/get-preference-page.tsx @@ -7,7 +7,7 @@ import type { PreferencePageComponent } from "./preference-items/preference-item export const getPreferencePage = (label: string): PreferencePageComponent => ({ children, item }) => (
-

{label}

+

{label}

{children}
diff --git a/src/features/preferences/renderer/preference-items/current-preference-tab-composite.injectable.ts b/src/features/preferences/renderer/preference-items/current-preference-tab-composite.injectable.ts index 5ce058152a..d45344be31 100644 --- a/src/features/preferences/renderer/preference-items/current-preference-tab-composite.injectable.ts +++ b/src/features/preferences/renderer/preference-items/current-preference-tab-composite.injectable.ts @@ -11,7 +11,6 @@ import preferencesRouteInjectable from "../../common/preferences-route.injectabl import { filter, map } from "lodash/fp"; import { pipeline } from "@ogre-tools/fp"; import { normalizeComposite } from "../../../application-menu/main/menu-items/get-composite/normalize-composite/normalize-composite"; -import { findExactlyOne } from "../../../../common/utils/find-exactly-one/find-exactly-one"; import preferencesCompositeInjectable from "./preferences-composite.injectable"; import type { PreferenceTabsRoot } from "./preference-tab-root"; @@ -26,12 +25,18 @@ const currentPreferenceTabCompositeInjectable = getInjectable({ return computed(() => { const { preferenceTabId } = routePathParameters.get(); - return pipeline( + const tabComposites = pipeline( normalizeComposite(preferencesComposite.get()), map(([, composite]) => composite), filter(isPreferenceTab), - findExactlyOne(hasMatchingPathId(preferenceTabId)), + filter(hasMatchingPathId(preferenceTabId)), ); + + if (tabComposites.length === 0) { + return undefined; + } + + return tabComposites[0]; }); }, }); diff --git a/src/features/preferences/renderer/preference-items/preference-item-injection-token.ts b/src/features/preferences/renderer/preference-items/preference-item-injection-token.ts index 86bec6f3bc..c4d3de94fd 100644 --- a/src/features/preferences/renderer/preference-items/preference-item-injection-token.ts +++ b/src/features/preferences/renderer/preference-items/preference-item-injection-token.ts @@ -3,6 +3,7 @@ * 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 React from "react"; export type PreferenceItemComponent = React.ComponentType<{ @@ -21,7 +22,7 @@ export interface PreferenceTab { pathId: string; label: string; orderNumber: number; - isShown?: boolean; + isShown?: IComputedValue | boolean; } export interface PreferenceTabGroup { @@ -30,7 +31,7 @@ export interface PreferenceTabGroup { parentId: "preference-tabs"; label: string; orderNumber: number; - isShown?: boolean; + isShown?: IComputedValue | boolean; iconName?: string; } @@ -38,7 +39,7 @@ export interface PreferencePage { kind: "page"; id: string; parentId: string; - isShown?: boolean; + isShown?: IComputedValue | boolean; childrenSeparator?: () => React.ReactElement; Component: PreferencePageComponent; } @@ -47,7 +48,7 @@ export interface PreferenceGroup { kind: "group"; id: string; parentId: string; - isShown?: boolean; + isShown?: IComputedValue | boolean; childrenSeparator?: () => React.ReactElement; } @@ -57,7 +58,7 @@ export interface PreferenceItem { id: string; parentId: string; orderNumber: number; - isShown?: boolean; + isShown?: IComputedValue | boolean; childrenSeparator?: () => React.ReactElement; } diff --git a/src/features/preferences/renderer/preference-items/preference-tab-root.tsx b/src/features/preferences/renderer/preference-items/preference-tab-root.tsx index 7e2292b955..1575357526 100644 --- a/src/features/preferences/renderer/preference-items/preference-tab-root.tsx +++ b/src/features/preferences/renderer/preference-items/preference-tab-root.tsx @@ -2,13 +2,15 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ +import type { IComputedValue } from "mobx"; +import { computed } from "mobx"; import React from "react"; export interface PreferenceTabsRoot { kind: "preference-tabs-root"; id: string; parentId: undefined; - isShown: true; + isShown: IComputedValue; childrenSeparator: () => React.ReactElement; } @@ -16,6 +18,6 @@ export const preferenceTabsRoot: PreferenceTabsRoot = { kind: "preference-tabs-root" as const, id: "preference-tabs", parentId: undefined, - isShown: true, + isShown: computed(() => true as const), childrenSeparator: () =>
, }; diff --git a/src/features/preferences/renderer/preference-items/preferences-composite.injectable.ts b/src/features/preferences/renderer/preference-items/preferences-composite.injectable.ts index 6720e44bb7..d0af642dd8 100644 --- a/src/features/preferences/renderer/preference-items/preferences-composite.injectable.ts +++ b/src/features/preferences/renderer/preference-items/preferences-composite.injectable.ts @@ -11,6 +11,8 @@ import getComposite from "../../../application-menu/main/menu-items/get-composit import { filter } from "lodash/fp"; import { pipeline } from "@ogre-tools/fp"; import { preferenceTabsRoot } from "./preference-tab-root"; +import logErrorInjectable from "../../../../common/log-error.injectable"; +import { isBoolean } from "../../../../common/utils"; const preferencesCompositeInjectable = getInjectable({ id: "preferences-composite", @@ -18,17 +20,41 @@ const preferencesCompositeInjectable = getInjectable({ instantiate: (di) => { const computedInjectMany = di.inject(computedInjectManyInjectable); const preferenceItems = computedInjectMany(preferenceItemInjectionToken); + const logError = di.inject(logErrorInjectable); return computed(() => pipeline( [preferenceTabsRoot, ...preferenceItems.get()], - filter(isShown), - (items) => getComposite({ source: items }), + + filter((item: PreferenceTypes) => { + if (item.isShown === undefined) { + return true; + } + + if (isBoolean(item.isShown)) { + return item.isShown; + } + + return item.isShown.get(); + }), + + (items) => + getComposite({ + source: items, + + handleMissingParentIds: (ids) => { + logError( + `Tried to create preferences, but encountered references to unknown ids: "${ids.missingParentIds.join( + '", "', + )}". Available ids are: "${ids.availableParentIds.join( + '", "', + )}"`, + ); + }, + }), ), ); }, }); -const isShown = (item: PreferenceTypes) => item.isShown ?? true; - export default preferencesCompositeInjectable; diff --git a/src/features/preferences/renderer/preference-navigation/preferences-navigation.tsx b/src/features/preferences/renderer/preference-navigation/preferences-navigation.tsx index 617ac089f8..7e4b17c667 100644 --- a/src/features/preferences/renderer/preference-navigation/preferences-navigation.tsx +++ b/src/features/preferences/renderer/preference-navigation/preferences-navigation.tsx @@ -63,8 +63,8 @@ const toNavigationHierarchy = (composite: Composite -
+
+
{value.iconName && ( {toNavigationHierarchy} - +
); } diff --git a/src/features/preferences/renderer/preferences.tsx b/src/features/preferences/renderer/preferences.tsx index 6683ba3229..cd719f0e18 100644 --- a/src/features/preferences/renderer/preferences.tsx +++ b/src/features/preferences/renderer/preferences.tsx @@ -18,7 +18,7 @@ import { PreferencesNavigation } from "./preference-navigation/preferences-navig interface Dependencies { closePreferences: () => void; - pageComposite: IComputedValue>; + pageComposite: IComputedValue | undefined>; } const NonInjectedPreferences = observer(({ @@ -35,7 +35,16 @@ const NonInjectedPreferences = observer(({ closeButtonProps={{ "data-testid": "close-preferences" }} back={closePreferences} > - {toPreferenceItemHierarchy(composite)} + {composite ? ( + toPreferenceItemHierarchy(composite) + ) : ( +
+ No preferences found +
+ )} ); }); @@ -58,13 +67,14 @@ const toPreferenceItemHierarchy = (composite: Composite) => { const Component = value.Component; return ( - - - {toPreferenceItemHierarchy} - - +
+ + + {toPreferenceItemHierarchy} + + +
); - } // eslint-disable-next-line no-fallthrough diff --git a/src/renderer/components/+preferences/extension-settings.tsx b/src/renderer/components/+preferences/extension-settings.tsx deleted file mode 100644 index 277580a303..0000000000 --- a/src/renderer/components/+preferences/extension-settings.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Copyright (c) OpenLens Authors. All rights reserved. - * Licensed under MIT License. See LICENSE in root directory for more information. - */ -import { SubTitle } from "../layout/sub-title"; -import type { AppPreferenceRegistration } from "./app-preferences/app-preference-registration"; -import type { DOMAttributes } from "react"; -import React from "react"; -import { cssNames } from "../../../renderer/utils"; - -export interface ExtensionSettingsProps extends DOMAttributes { - setting: AppPreferenceRegistration; - size: "small" | "normal"; -} - -export function ExtensionSettings({ setting, size, ...props }: ExtensionSettingsProps) { - const { - title, - id, - components: { Hint, Input }, - } = setting; - - return ( - -
- - -
- -
-
-
-
- ); -} diff --git a/src/renderer/components/+preferences/extensions.tsx b/src/renderer/components/+preferences/extensions.tsx index d3be6ae4d0..9f396a6507 100644 --- a/src/renderer/components/+preferences/extensions.tsx +++ b/src/renderer/components/+preferences/extensions.tsx @@ -9,7 +9,7 @@ import { observer } from "mobx-react"; import React from "react"; import type { RegisteredAppPreference } from "./app-preferences/app-preference-registration"; import extensionPreferencesModelInjectable from "./extension-preference-model.injectable"; -import { ExtensionSettings } from "./extension-settings"; +import { ExtensionPreferenceItem } from "../../../features/preferences/renderer/compliance-for-legacy-extension-api/extension-preference-item"; interface Dependencies { model: IComputedValue<{ @@ -36,10 +36,9 @@ const NonInjectedExtensions = ({ model }: Dependencies) => {
)} {preferenceItems.map((preferenceItem, index) => ( - ))}