diff --git a/package.json b/package.json index ca4cce21df..75074300bb 100644 --- a/package.json +++ b/package.json @@ -208,11 +208,11 @@ "@hapi/subtext": "^7.0.4", "@kubernetes/client-node": "^0.16.3", "@material-ui/styles": "^4.11.5", - "@ogre-tools/fp": "8.0.0", - "@ogre-tools/injectable": "8.0.0", - "@ogre-tools/injectable-extension-for-auto-registration": "8.0.0", - "@ogre-tools/injectable-extension-for-mobx": "8.0.0", - "@ogre-tools/injectable-react": "8.0.0", + "@ogre-tools/fp": "9.0.0", + "@ogre-tools/injectable": "9.0.0", + "@ogre-tools/injectable-extension-for-auto-registration": "9.0.0", + "@ogre-tools/injectable-extension-for-mobx": "9.0.0", + "@ogre-tools/injectable-react": "9.0.0", "@sentry/electron": "^3.0.7", "@sentry/integrations": "^6.19.3", "@side/jest-runtime": "^1.0.1", diff --git a/src/behaviours/preferences/__snapshots__/navigation-to-extension-specific-preferences.test.tsx.snap b/src/behaviours/preferences/__snapshots__/navigation-to-extension-specific-preferences.test.tsx.snap index 2adb0f198f..79787b5d8d 100644 --- a/src/behaviours/preferences/__snapshots__/navigation-to-extension-specific-preferences.test.tsx.snap +++ b/src/behaviours/preferences/__snapshots__/navigation-to-extension-specific-preferences.test.tsx.snap @@ -1,5 +1,835 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`preferences - navigation to extension specific preferences given in preferences, when rendered given extension with registered tab shows extension tab in general area 1`] = ` + +`; + +exports[`preferences - navigation to extension specific preferences given in preferences, when rendered given extension with registered tab when navigating to specific extension tab renders 1`] = ` +
+
+
+
+
+
+ +
+
+
+

+ registered-tab-page-id + + preferences +

+
+
+ License item + +
+
+
+
+
+
+
+
+
+
+
+
+
+ + + close + + +
+ +
+
+
+
+
+
+
+`; + +exports[`preferences - navigation to extension specific preferences given in preferences, when rendered given extensions with tabs having same id when navigating to first extension tab renders 1`] = ` +
+
+
+
+
+
+ +
+
+
+

+ registered-tab-page-id + + preferences +

+
+
+ License item + +
+
+
+
+
+
+
+
+
+
+
+
+
+ + + close + + +
+ +
+
+
+
+
+
+
+`; + +exports[`preferences - navigation to extension specific preferences given in preferences, when rendered given extensions with tabs having same id when navigating to second extension tab renders 1`] = ` +
+
+
+
+
+
+ +
+
+
+

+ duplicated-tab-page-id + + preferences +

+
+
+ Another metrics + +
+
+
+
+
+
+
+
+
+
+
+
+
+ + + close + + +
+ +
+
+
+
+
+
+
+`; + +exports[`preferences - navigation to extension specific preferences given in preferences, when rendered given multiple extensions with specific preferences, when navigating to extension specific preferences page renders 1`] = ` +
+
+
+
+
+
+ +
+
+
+

+ some-test-extension-id + + preferences +

+
+
+ Some preference item + +
+
+
+
+
+
+
+
+
+
+
+
+
+ + + close + + +
+ +
+
+
+
+
+
+
+`; + exports[`preferences - navigation to extension specific preferences given in preferences, when rendered renders 1`] = `
@@ -1181,16 +2034,39 @@ exports[`preferences - navigation to extension specific preferences given in pre
@@ -1206,7 +2082,9 @@ exports[`preferences - navigation to extension specific preferences given in pre id="extensions" >

- Extensions + some-test-extension-id + + preferences

`; + +exports[`preferences - navigation to extension specific preferences given in preferences, when rendered when extension with specific preferences is enabled when navigating to extension preferences using navigation when extension is disabled renders 1`] = ` + +
+
+
+
+
+
+ +
+
+
+

+ + preferences +

+
+ No extension found +
+
+
+
+
+
+
+ + + close + + +
+ +
+
+
+
+
+
+
+ +`; + +exports[`preferences - navigation to extension specific preferences when navigating to extension specific tab renders 1`] = ` +
+
+
+
+
+
+ +
+
+
+

+ duplicated-tab-page-id + + preferences +

+
+
+ Another metrics + +
+
+
+
+
+
+
+
+
+
+
+
+
+ + + close + + +
+ +
+
+
+
+
+
+
+`; + +exports[`preferences - navigation to extension specific preferences when navigating to someone else extension specific tab renders 1`] = ` +
+
+
+
+
+
+ +
+
+
+

+ extension-using-someone-else-tab-id + + preferences +

+
+
+ My preferences + +
+
+
+
+
+
+
+
+
+
+
+
+
+ + + close + + +
+ +
+
+
+
+
+
+
+`; diff --git a/src/behaviours/preferences/closing-preferences.test.tsx b/src/behaviours/preferences/closing-preferences.test.tsx index 0ca2c91da7..dddd7e18dc 100644 --- a/src/behaviours/preferences/closing-preferences.test.tsx +++ b/src/behaviours/preferences/closing-preferences.test.tsx @@ -250,6 +250,7 @@ const testNavigationItemInjectable = getInjectable({ return { id: "some-test-preference-navigation-item-id", label: "Some preference navigation item", + parent: "general", isActive: routeIsActive, isVisible: testRoute.isEnabled, navigate: navigateToPreferenceTab(testRoute), diff --git a/src/behaviours/preferences/navigation-to-extension-specific-preferences.test.tsx b/src/behaviours/preferences/navigation-to-extension-specific-preferences.test.tsx index 5eb9b2f384..bded3769f2 100644 --- a/src/behaviours/preferences/navigation-to-extension-specific-preferences.test.tsx +++ b/src/behaviours/preferences/navigation-to-extension-specific-preferences.test.tsx @@ -6,8 +6,13 @@ import type { RenderResult } from "@testing-library/react"; import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import React from "react"; -import type { FakeExtensionData } from "../../renderer/components/test-utils/get-renderer-extension-fake"; +import "@testing-library/jest-dom/extend-expect"; +import type { FakeExtensionData, TestExtension } from "../../renderer/components/test-utils/get-renderer-extension-fake"; import { getRendererExtensionFakeFor } from "../../renderer/components/test-utils/get-renderer-extension-fake"; +import type { DiContainer } from "@ogre-tools/injectable"; +import { getDiForUnitTesting } from "../../renderer/getDiForUnitTesting"; +import extensionPreferencesRouteInjectable from "../../common/front-end-routing/routes/preferences/extension/extension-preferences-route.injectable"; + describe("preferences - navigation to extension specific preferences", () => { let applicationBuilder: ApplicationBuilder; @@ -43,10 +48,65 @@ describe("preferences - navigation to extension specific preferences", () => { expect(actual).toBeNull(); }); + describe("given multiple extensions with specific preferences, when navigating to extension specific preferences page", () => { + beforeEach(async () => { + const getRendererExtensionFake = getRendererExtensionFakeFor(applicationBuilder); + const someTestExtension = getRendererExtensionFake(extensionStubWithExtensionSpecificPreferenceItems); + const someOtherTestExtension = getRendererExtensionFake(someOtherExtensionStubWithExtensionSpecificPreferenceItems); + + await applicationBuilder.extensions.renderer.enable(someTestExtension, someOtherTestExtension); + applicationBuilder.preferences.navigation.click("extension-some-test-extension-id"); + }); + + it("renders", () => { + expect(rendered.container).toMatchSnapshot(); + }); + + it("doesn't show preferences from unrelated extension", () => { + const actual = rendered.queryByTestId("extension-preference-item-for-some-other-preference-item-id"); + + expect(actual).toBeNull(); + }); + + it("shows preferences from related extension", () => { + const actual = rendered.getByTestId("extension-preference-item-for-some-preference-item-id"); + + expect(actual).not.toBeNull(); + }); + }); + + describe("given multiple extensions with and without specific preferences", () => { + beforeEach(async () => { + const getRendererExtensionFake = getRendererExtensionFakeFor(applicationBuilder); + const someTestExtension = getRendererExtensionFake(extensionStubWithExtensionSpecificPreferenceItems); + const extensionWithoutPreferences = getRendererExtensionFake(extensionStubWithoutPreferences); + const extensionWithSpecificTab = getRendererExtensionFake(extensionStubWithShowInPreferencesTab); + + await applicationBuilder.extensions.renderer.enable(someTestExtension, extensionWithoutPreferences, extensionWithSpecificTab); + }); + + it("doesn't show link for extension without preferences", () => { + const actual = rendered.queryByTestId("tab-link-for-extension-without-preferences-id"); + + expect(actual).toBeNull(); + }); + + it("doesn't show link for preferences intended for a specific tab", () => { + const actual = rendered.queryByTestId("tab-link-for-extension-specified-preferences-page-id"); + + expect(actual).toBeNull(); + }); + }); + describe("when extension with specific preferences is enabled", () => { + let testExtension: TestExtension; + beforeEach(() => { const getRendererExtensionFake = getRendererExtensionFakeFor(applicationBuilder); - const testExtension = getRendererExtensionFake(extensionStubWithExtensionSpecificPreferenceItems); + + testExtension = getRendererExtensionFake( + extensionStubWithExtensionSpecificPreferenceItems, + ); applicationBuilder.extensions.renderer.enable(testExtension); }); @@ -56,20 +116,32 @@ describe("preferences - navigation to extension specific preferences", () => { }); it("shows link for extension preferences", () => { - const actual = rendered.getByTestId("tab-link-for-extensions"); + 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"); + + expect(actual).not.toHaveClass("active"); + }); + describe("when navigating to extension preferences using navigation", () => { beforeEach(() => { - applicationBuilder.preferences.navigation.click("extensions"); + applicationBuilder.preferences.navigation.click("extension-some-test-extension-id"); }); it("renders", () => { expect(rendered.container).toMatchSnapshot(); }); + it("shows proper page title", () => { + const title = rendered.getByText("some-test-extension-id preferences"); + + expect(title).toBeInTheDocument(); + }); + it("shows extension specific preferences", () => { const page = rendered.getByTestId("extension-preferences-page"); @@ -87,14 +159,234 @@ describe("preferences - navigation to extension specific preferences", () => { expect(actual).toBeNull(); }); + + it("link is active", () => { + const actual = rendered.getByTestId("tab-link-for-extension-some-test-extension-id"); + + expect(actual).toHaveClass("active"); + }); + + describe("when extension is disabled", () => { + beforeEach(() => { + applicationBuilder.extensions.renderer.disable(testExtension); + }); + + it("renders", () => { + 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("when extension is enabled again, does not show the error message anymore", () => { + applicationBuilder.extensions.renderer.enable(testExtension); + + expect(rendered.queryByTestId("error-for-extension-not-being-present")).not.toBeInTheDocument(); + }); + }); }); }); + + describe("given extension with registered tab", () => { + beforeEach(async () => { + const getRendererExtensionFake = getRendererExtensionFakeFor(applicationBuilder); + const extension = getRendererExtensionFake(extensionStubWithWithRegisteredTab); + + await applicationBuilder.extensions.renderer.enable(extension); + }); + + it("shows extension tab in general area", () => { + const actual = rendered.getByTestId("tab-link-for-extension-registered-tab-page-id-nav-item-metrics-extension-tab"); + + expect(actual).toMatchSnapshot(); + }); + + it("does not show custom settings block", () => { + const actual = rendered.queryByTestId("extension-settings"); + + expect(actual).not.toBeInTheDocument(); + }); + + describe("when navigating to specific extension tab", () => { + beforeEach(() => { + applicationBuilder.preferences.navigation.click("extension-registered-tab-page-id-nav-item-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(); + }); + }); + }); + + 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(async () => { + const getRendererExtensionFake = getRendererExtensionFakeFor(applicationBuilder); + const extension = getRendererExtensionFake(extensionStubWithWithRegisteredTabs); + + await applicationBuilder.extensions.renderer.enable(extension); + }); + + 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(async () => { + const getRendererExtensionFake = getRendererExtensionFakeFor(applicationBuilder); + const extension = getRendererExtensionFake(extensionStubWithWithRegisteredTab); + const otherExtension = getRendererExtensionFake(extensionStubWithWithSameRegisteredTab); + + await applicationBuilder.extensions.renderer.enable(extension, otherExtension); + }); + + it("shows tab from the first extension", () => { + const actual = rendered.getByTestId("tab-link-for-extension-registered-tab-page-id-nav-item-metrics-extension-tab"); + + 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"); + + expect(actual).toBeInTheDocument(); + }); + + describe("when navigating to first extension tab", () => { + beforeEach(() => { + applicationBuilder.preferences.navigation.click("extension-registered-tab-page-id-nav-item-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("another-metrics-preference-item-hint"); + + expect(actual).not.toBeInTheDocument(); + }); + }); + + describe("when navigating to second extension tab", () => { + beforeEach(() => { + applicationBuilder.preferences.navigation.click("extension-duplicated-tab-page-id-nav-item-metrics-extension-tab"); + }); + + it("renders", () => { + expect(rendered.container).toMatchSnapshot(); + }); + + it("shows related preferences for this tab", () => { + const actual = rendered.getByTestId("another-metrics-preference-item-hint"); + + expect(actual).toBeInTheDocument(); + }); + + it("does not show unrelated preferences for this tab", () => { + const actual = rendered.queryByTestId("metrics-preference-item-hint"); + + expect(actual).not.toBeInTheDocument(); + }); + }); + }); + }); + + describe("when navigating to extension specific tab", () => { + let rendered: RenderResult; + let di: DiContainer; + + beforeEach(async () => { + di = getDiForUnitTesting({ doGeneralOverrides: true }); + + const getRendererExtensionFake = getRendererExtensionFakeFor(applicationBuilder); + const extension = getRendererExtensionFake(extensionStubWithWithSameRegisteredTab); + const otherExtension = getRendererExtensionFake(extensionUsingSomeoneElseTab); + + applicationBuilder.beforeRender(() => { + const extensionRoute = di.inject(extensionPreferencesRouteInjectable); + const params = { parameters: { + extensionId: "duplicated-tab-page-id", + tabId: "metrics-extension-tab", + }}; + + applicationBuilder.preferences.navigateTo(extensionRoute, params); + }); + + await applicationBuilder.extensions.renderer.enable(extension, otherExtension); + rendered = await applicationBuilder.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; + let di: DiContainer; + + beforeEach(async () => { + di = getDiForUnitTesting({ doGeneralOverrides: true }); + + const getRendererExtensionFake = getRendererExtensionFakeFor(applicationBuilder); + const extension = getRendererExtensionFake(extensionStubWithWithSameRegisteredTab); + const extensionUsingOtherTab = getRendererExtensionFake(extensionUsingSomeoneElseTab); + + applicationBuilder.beforeRender(() => { + const extensionRoute = di.inject(extensionPreferencesRouteInjectable); + const params = { parameters: { + extensionId: "extension-using-someone-else-tab-id", + tabId: "metrics-extension-tab", + }}; + + applicationBuilder.preferences.navigateTo(extensionRoute, params); + }); + + await applicationBuilder.extensions.renderer.enable(extension, extensionUsingOtherTab); + rendered = await applicationBuilder.render(); + }); + + it("renders", () => { + expect(rendered.container).toMatchSnapshot(); + }); }); }); const extensionStubWithExtensionSpecificPreferenceItems: FakeExtensionData = { - id: "some-extension-id", - name: "some-extension-name", + id: "some-test-extension-id", + name: "some-test-extension-id", appPreferences: [ { title: "Some preference item", @@ -119,3 +411,166 @@ const extensionStubWithExtensionSpecificPreferenceItems: FakeExtensionData = { ], }; +const someOtherExtensionStubWithExtensionSpecificPreferenceItems: FakeExtensionData = { + id: "some-other-test-extension-id", + name: "some-other-test-extension-id", + + appPreferences: [ + { + title: "Test preference item", + id: "some-other-preference-item-id", + + components: { + Hint: () =>
, + Input: () =>
, + }, + }, + ], +}; + +const extensionStubWithoutPreferences: FakeExtensionData = { + id: "without-preferences-id", + name: "without-preferences-id", +}; + +const extensionStubWithShowInPreferencesTab: FakeExtensionData = { + id: "specified-preferences-page-id", + name: "specified-preferences-page-name", + + appPreferences: [ + { + title: "Test preference item", + id: "very-other-preference-item-id", + showInPreferencesTab: "some-tab", + + components: { + Hint: () =>
, + Input: () =>
, + }, + }, + ], +}; + +const extensionStubWithWithRegisteredTab: FakeExtensionData = { + id: "registered-tab-page-id", + name: "registered-tab-page-id", + + appPreferences: [ + { + title: "License item", + id: "metrics-preference-item-id", + showInPreferencesTab: "metrics-extension-tab", + + components: { + Hint: () =>
, + Input: () =>
, + }, + }, + { + title: "Menu item", + id: "menu-preference-item-id", + showInPreferencesTab: "menu-extension-tab", + + components: { + Hint: () =>
, + Input: () =>
, + }, + }, + { + title: "Survey item", + id: "survey-preference-item-id", + showInPreferencesTab: "survey-extension-tab", + + components: { + Hint: () =>
, + Input: () =>
, + }, + }, + ], + + appPreferenceTabs: [{ + title: "Metrics tab", + id: "metrics-extension-tab", + orderNumber: 100, + }], +}; + +const extensionStubWithWithRegisteredTabs: FakeExtensionData = { + id: "hello-world-tab-page-id", + name: "hello-world-tab-page-id", + + 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: FakeExtensionData = { + id: "duplicated-tab-page-id", + name: "duplicated-tab-page-id", + + appPreferences: [ + { + title: "Another metrics", + id: "another-metrics-preference-item-id", + showInPreferencesTab: "metrics-extension-tab", + + components: { + Hint: () =>
, + Input: () =>
, + }, + }, + ], + + appPreferenceTabs: [{ + title: "Metrics tab", + id: "metrics-extension-tab", + orderNumber: 100, + }], +}; + +const extensionUsingSomeoneElseTab: FakeExtensionData = { + id: "extension-using-someone-else-tab-id", + name: "extension-using-someone-else-tab-id", + + appPreferences: [ + { + title: "My preferences", + id: "my-preferences-item-id", + showInPreferencesTab: "metrics-extension-tab", + + components: { + Hint: () =>
, + Input: () =>
, + }, + }, + ], +}; diff --git a/src/common/front-end-routing/routes/preferences/extension/extension-preferences-route.injectable.ts b/src/common/front-end-routing/routes/preferences/extension/extension-preferences-route.injectable.ts index 01a87fa3c2..38d25b4114 100644 --- a/src/common/front-end-routing/routes/preferences/extension/extension-preferences-route.injectable.ts +++ b/src/common/front-end-routing/routes/preferences/extension/extension-preferences-route.injectable.ts @@ -5,12 +5,18 @@ import { getInjectable } from "@ogre-tools/injectable"; import { computed } from "mobx"; import { frontEndRouteInjectionToken } from "../../../front-end-route-injection-token"; +import type { Route } from "../../../front-end-route-injection-token"; + +interface ExtensionPreferenceRouteParams { + extensionId: string; + tabId?: string; +} const extensionPreferencesRouteInjectable = getInjectable({ id: "extension-preferences-route", - instantiate: () => ({ - path: "/preferences/extensions", + instantiate: (): Route => ({ + path: "/preferences/extension/:extensionId/:tabId?", clusterFrame: false, isEnabled: computed(() => true), }), diff --git a/src/common/front-end-routing/routes/preferences/extension/navigate-to-extension-preferences.injectable.ts b/src/common/front-end-routing/routes/preferences/extension/navigate-to-extension-preferences.injectable.ts index 6e2f80d864..c51c45ed28 100644 --- a/src/common/front-end-routing/routes/preferences/extension/navigate-to-extension-preferences.injectable.ts +++ b/src/common/front-end-routing/routes/preferences/extension/navigate-to-extension-preferences.injectable.ts @@ -13,7 +13,10 @@ const navigateToExtensionPreferencesInjectable = getInjectable({ const navigateToRoute = di.inject(navigateToRouteInjectionToken); const route = di.inject(extensionPreferencesRouteInjectable); - return () => navigateToRoute(route); + return (extensionId: string, tabId?: string) => navigateToRoute(route, { parameters: { + extensionId, + tabId, + }}); }, }); diff --git a/src/common/runnable/run-many-for.test.ts b/src/common/runnable/run-many-for.test.ts index 193c1fd178..35db17952a 100644 --- a/src/common/runnable/run-many-for.test.ts +++ b/src/common/runnable/run-many-for.test.ts @@ -15,7 +15,7 @@ describe("runManyFor", () => { let actualPromise: Promise; beforeEach(() => { - const rootDi = createContainer(); + const rootDi = createContainer("irrelevant"); runMock = asyncFn(); @@ -67,7 +67,7 @@ describe("runManyFor", () => { let actualPromise: Promise; beforeEach(() => { - const di = createContainer(); + const di = createContainer("irrelevant"); runMock = asyncFn(); @@ -170,7 +170,7 @@ describe("runManyFor", () => { }); it("given invalid hierarchy, when running runnables, throws", () => { - const rootDi = createContainer(); + const rootDi = createContainer("irrelevant"); const runMock = asyncFn<(...args: unknown[]) => void>(); @@ -218,7 +218,7 @@ describe("runManyFor", () => { let runMock: AsyncFnMock<(...args: unknown[]) => Promise>; beforeEach(() => { - const rootDi = createContainer(); + const rootDi = createContainer("irrelevant"); runMock = asyncFn(); diff --git a/src/common/runnable/run-many-sync-for.test.ts b/src/common/runnable/run-many-sync-for.test.ts index 215b1a3b15..fe47516725 100644 --- a/src/common/runnable/run-many-sync-for.test.ts +++ b/src/common/runnable/run-many-sync-for.test.ts @@ -11,7 +11,7 @@ describe("runManySyncFor", () => { let runMock: jest.Mock; beforeEach(() => { - const rootDi = createContainer(); + const rootDi = createContainer("irrelevant"); runMock = jest.fn(); @@ -50,7 +50,7 @@ describe("runManySyncFor", () => { let runMock: jest.Mock<(arg: string) => void>; beforeEach(() => { - const di = createContainer(); + const di = createContainer("irrelevant"); runMock = jest.fn(); @@ -99,7 +99,7 @@ describe("runManySyncFor", () => { }); it("given invalid hierarchy, when running runnables, throws", () => { - const rootDi = createContainer(); + const rootDi = createContainer("irrelevant"); const runMock = jest.fn(); @@ -147,7 +147,7 @@ describe("runManySyncFor", () => { let runMock: jest.Mock<(arg: string, arg2: string) => void>; beforeEach(() => { - const rootDi = createContainer(); + const rootDi = createContainer("irrelevant"); runMock = jest.fn(); diff --git a/src/extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api-with-modifications.test.ts b/src/extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api-with-modifications.test.ts index 3a69f0d563..ebfc77f40e 100644 --- a/src/extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api-with-modifications.test.ts +++ b/src/extensions/as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api-with-modifications.test.ts @@ -21,7 +21,7 @@ describe("asLegacyGlobalObjectForExtensionApiWithModifications", () => { }; beforeEach(() => { - di = createContainer(); + di = createContainer("irrelevant"); jest.spyOn(di, "inject"); diff --git a/src/extensions/lens-renderer-extension.ts b/src/extensions/lens-renderer-extension.ts index 1ed9164cb0..25b1e15061 100644 --- a/src/extensions/lens-renderer-extension.ts +++ b/src/extensions/lens-renderer-extension.ts @@ -26,6 +26,7 @@ import { pipeline } from "@ogre-tools/fp"; import { getExtensionRoutePath } from "../renderer/routes/for-extension"; import type { LensRendererExtensionDependencies } from "./lens-extension-set-dependencies"; import type { KubeObjectHandlerRegistration } from "../renderer/kube-object/handler"; +import type { AppPreferenceTabRegistration } from "../renderer/components/+preferences/app-preference-tab/app-preference-tab-registration"; export class LensRendererExtension extends LensExtension { globalPages: registries.PageRegistration[] = []; @@ -33,6 +34,7 @@ export class LensRendererExtension extends LensExtension { - const di = createContainer(); + const di = createContainer("main"); registerMobX(di); diff --git a/src/main/getDiForUnitTesting.ts b/src/main/getDiForUnitTesting.ts index 45fedce511..f17a4e6217 100644 --- a/src/main/getDiForUnitTesting.ts +++ b/src/main/getDiForUnitTesting.ts @@ -105,7 +105,7 @@ export function getDiForUnitTesting(opts: { doGeneralOverrides?: boolean } = {}) doGeneralOverrides = false, } = opts; - const di = createContainer(); + const di = createContainer("main"); registerMobX(di); diff --git a/src/renderer/components/+preferences/app-preference-tab/app-preference-tab-registration.ts b/src/renderer/components/+preferences/app-preference-tab/app-preference-tab-registration.ts new file mode 100644 index 0000000000..2fed65d79e --- /dev/null +++ b/src/renderer/components/+preferences/app-preference-tab/app-preference-tab-registration.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +export interface AppPreferenceTabRegistration { + title: string; + id: string; + orderNumber?: number; +} + diff --git a/src/renderer/components/+preferences/extension-preference-item-registrator.injectable.ts b/src/renderer/components/+preferences/extension-preference-item-registrator.injectable.ts index 1129699bbd..8182cfd2af 100644 --- a/src/renderer/components/+preferences/extension-preference-item-registrator.injectable.ts +++ b/src/renderer/components/+preferences/extension-preference-item-registrator.injectable.ts @@ -7,7 +7,7 @@ import { filter, map } from "lodash/fp"; import { extensionRegistratorInjectionToken } from "../../../extensions/extension-loader/extension-registrator-injection-token"; import type { LensRendererExtension } from "../../../extensions/lens-renderer-extension"; import { pipeline } from "@ogre-tools/fp"; -import { extensionPreferenceItemInjectionToken } from "./extension-preference-items.injectable"; +import { extensionPreferenceItemInjectionToken } from "./extension-preference-items-injection-token"; const extensionPreferenceItemRegistratorInjectable = getInjectable({ id: "extension-preference-item-registrator", @@ -34,6 +34,7 @@ const extensionPreferenceItemRegistratorInjectable = getInjectable({ instantiate: () => ({ id: registration.id || id, title: registration.title, + extension, components: { Hint: registration.components.Hint, diff --git a/src/renderer/components/+preferences/extension-preference-items-injection-token.ts b/src/renderer/components/+preferences/extension-preference-items-injection-token.ts new file mode 100644 index 0000000000..6c634f60da --- /dev/null +++ b/src/renderer/components/+preferences/extension-preference-items-injection-token.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getInjectionToken } from "@ogre-tools/injectable"; +import type { LensRendererExtension } from "../../../extensions/lens-renderer-extension"; +import type { RegisteredAppPreference } from "./app-preferences/app-preference-registration"; + + interface ExtensionPreferenceItem extends RegisteredAppPreference { + extension: LensRendererExtension; + } + +export const extensionPreferenceItemInjectionToken = getInjectionToken({ + id: "extension-preference-item-injection-token", +}); + diff --git a/src/renderer/components/+preferences/extension-preference-model.injectable.ts b/src/renderer/components/+preferences/extension-preference-model.injectable.ts new file mode 100644 index 0000000000..5e4fd06de0 --- /dev/null +++ b/src/renderer/components/+preferences/extension-preference-model.injectable.ts @@ -0,0 +1,32 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import extensionPreferencesRouteInjectable from "../../../common/front-end-routing/routes/preferences/extension/extension-preferences-route.injectable"; +import rendererExtensionsInjectable from "../../../extensions/renderer-extensions.injectable"; +import routePathParametersInjectable from "../../routes/route-path-parameters.injectable"; +import { getExtensionPreferenceItems } from "./get-extension-preference-items"; + +const extensionPreferencesModelInjectable = getInjectable({ + id: "extension-preferences-model", + + instantiate: (di) => { + const route = di.inject(extensionPreferencesRouteInjectable); + const pathParameters = di.inject(routePathParametersInjectable, route); + const extensions = di.inject(rendererExtensionsInjectable); + + return computed(() => { + const { extensionId, tabId } = pathParameters.get(); + const targetExtension = extensions.get().find((extension) => extension.sanitizedExtensionId === extensionId); + + return { + extensionName: targetExtension?.manifest.name, + preferenceItems: getExtensionPreferenceItems(targetExtension, tabId), + }; + }); + }, +}); + +export default extensionPreferencesModelInjectable; diff --git a/src/renderer/components/+preferences/extensions.tsx b/src/renderer/components/+preferences/extensions.tsx index 930b74c8ee..94b56a2716 100644 --- a/src/renderer/components/+preferences/extensions.tsx +++ b/src/renderer/components/+preferences/extensions.tsx @@ -8,36 +8,55 @@ import type { IComputedValue } from "mobx"; 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 { Preferences } from "./preferences"; -import extensionsPreferenceItemsInjectable from "./extension-preference-items.injectable"; interface Dependencies { - preferenceItems: IComputedValue; + model: IComputedValue<{ + preferenceItems: RegisteredAppPreference[]; + extensionName?: string; + }>; } -const NonInjectedExtensions = ({ preferenceItems }: Dependencies) => ( - -
-

Extensions

- {preferenceItems.get().map((preferenceItem) => ( - - ))} -
-
-); +const NonInjectedExtensions = ({ model }: Dependencies) => { + const { extensionName, preferenceItems } = model.get(); + + return ( + +
+

+ {extensionName} + {" "} + preferences +

+ {!extensionName && ( +
+ No extension found +
+ )} + {preferenceItems.map((preferenceItem, index) => ( + + ))} +
+
+ ); +}; export const Extensions = withInjectables( observer(NonInjectedExtensions), { getProps: (di) => ({ - preferenceItems: di.inject(extensionsPreferenceItemsInjectable), + model: di.inject(extensionPreferencesModelInjectable), }), }, ); diff --git a/src/renderer/components/+preferences/get-extension-preference-items.ts b/src/renderer/components/+preferences/get-extension-preference-items.ts new file mode 100644 index 0000000000..a9c2237d18 --- /dev/null +++ b/src/renderer/components/+preferences/get-extension-preference-items.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { LensRendererExtension } from "../../../extensions/lens-renderer-extension"; +import type { RegisteredAppPreference } from "./app-preferences/app-preference-registration"; + +export function getExtensionPreferenceItems(extension?: LensRendererExtension, tabId?: string): RegisteredAppPreference[] { + if (!extension) { + return []; + } + + const preferences = extension.appPreferences.map(preference => ({ + id: preference.id || preference.title, + ...preference, + })); + + if (tabId) { + return preferences.filter(preference => preference.showInPreferencesTab == tabId); + } + + return preferences.filter(preference => !preference.showInPreferencesTab); +} diff --git a/src/renderer/components/+preferences/preferences-navigation/__tests__/preferences-navigation.test.tsx b/src/renderer/components/+preferences/preferences-navigation/__tests__/preferences-navigation.test.tsx new file mode 100644 index 0000000000..0d4547ae6a --- /dev/null +++ b/src/renderer/components/+preferences/preferences-navigation/__tests__/preferences-navigation.test.tsx @@ -0,0 +1,152 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import "@testing-library/jest-dom/extend-expect"; +import type { RenderResult } from "@testing-library/react"; +import { computed } from "mobx"; +import { noop } from "../../../../utils"; +import type { ApplicationBuilder } from "../../../test-utils/get-application-builder"; +import { getApplicationBuilder } from "../../../test-utils/get-application-builder"; +import type { PreferenceNavigationItem } from "../preference-navigation-items.injectable"; +import preferenceNavigationItemsInjectable from "../preference-navigation-items.injectable"; + +describe.only("preferences - navigation block with links", () => { + let applicationBuilder: ApplicationBuilder; + + beforeEach(() => { + applicationBuilder = getApplicationBuilder(); + }); + + describe("given in preferences, when rendered", () => { + let renderer: RenderResult; + + describe("when general navigation items passed", () => { + beforeEach(async () => { + applicationBuilder.beforeApplicationStart(({ rendererDi }) => { + rendererDi.override(preferenceNavigationItemsInjectable, () => + computed(() => generalNavItems), + ); + }); + + applicationBuilder.beforeRender(() => { + applicationBuilder.preferences.navigate(); + }); + + renderer = await applicationBuilder.render(); + }); + + const links = ["General", "Proxy"]; + + it.each(links)("renders link with text content %s", (link) => { + expect(renderer.container).toHaveTextContent(link); + }); + + it("does not show custom settings block", () => { + expect( + renderer.queryByTestId("extension-settings"), + ).not.toBeInTheDocument(); + }); + }); + + describe("when general + extension navigation items passed", () => { + beforeEach(async () => { + applicationBuilder.beforeApplicationStart(({ rendererDi }) => { + rendererDi.override(preferenceNavigationItemsInjectable, () => + computed(() => [...generalNavItems, ...extensionNavItems]), + ); + }); + + applicationBuilder.beforeRender(() => { + applicationBuilder.preferences.navigate(); + }); + + renderer = await applicationBuilder.render(); + }); + + const generalLinks = ["General", "Proxy"]; + + it.each(generalLinks)( + "renders general link with text content %s", + (link) => { + expect(renderer.container).toHaveTextContent(link); + }, + ); + + it("shows custom settings block", () => { + expect( + renderer.queryByTestId("extension-settings"), + ).toBeInTheDocument(); + }); + + const extensionLinks = ["lensapp-node-menu", "lensapp-pod-menu"]; + + it.each(extensionLinks)("shows extension navigation item %s", (link) => { + expect( + renderer.getByTestId( + `tab-link-for-extension-preferences-navigation-item-${link}`, + ), + ).toBeInTheDocument(); + }); + + it("renders extension navigation items inside custom settings block", () => { + const settingsBlock = renderer.getByTestId("extension-settings"); + + expect(settingsBlock).toHaveTextContent("lensapp-node-menu"); + }); + }); + }); +}); + +const generalNavItems: PreferenceNavigationItem[] = [ + { + id: "general", + label: "General", + isActive: computed(() => false), + isVisible: computed(() => true), + navigate: () => noop, + orderNumber: 0, + parent: "general", + }, + { + id: "proxy", + label: "Proxy", + isActive: computed(() => false), + isVisible: computed(() => true), + navigate: () => noop, + orderNumber: 1, + parent: "general", + }, +]; + +const extensionNavItems = [ + { + id: "extension-preferences-navigation-item-lensapp-node-menu", + label: "lensapp-node-menu", + isActive: computed(() => false), + isVisible: computed(() => true), + navigate: () => noop, + orderNumber: 0, + parent: "extensions", + }, + { + id: "extension-preferences-navigation-item-lensapp-pod-menu", + label: "lensapp-pod-menu", + isActive: computed(() => false), + isVisible: computed(() => true), + navigate: () => noop, + orderNumber: 0, + parent: "extensions", + }, + { + id: "extension-preferences-navigation-item-metrics-plugin", + label: "metrics-plugin", + isActive: computed(() => false), + isVisible: computed(() => false), + navigate: () => noop, + orderNumber: 0, + parent: "extensions", + }, +]; + diff --git a/src/renderer/components/+preferences/preferences-navigation/application-preferences-navigation-item.injectable.ts b/src/renderer/components/+preferences/preferences-navigation/application-preferences-navigation-item.injectable.ts index 0b333f4fed..a3e35d35a9 100644 --- a/src/renderer/components/+preferences/preferences-navigation/application-preferences-navigation-item.injectable.ts +++ b/src/renderer/components/+preferences/preferences-navigation/application-preferences-navigation-item.injectable.ts @@ -24,6 +24,7 @@ const applicationPreferencesNavigationItemInjectable = getInjectable({ return { id: "application", label: "App", + parent: "general", navigate: navigateToPreferenceTab(route), isActive: routeIsActive, isVisible: computed(() => true), diff --git a/src/renderer/components/+preferences/preferences-navigation/editor-preferences-navigation-item.injectable.ts b/src/renderer/components/+preferences/preferences-navigation/editor-preferences-navigation-item.injectable.ts index f5738cf985..686a688aa6 100644 --- a/src/renderer/components/+preferences/preferences-navigation/editor-preferences-navigation-item.injectable.ts +++ b/src/renderer/components/+preferences/preferences-navigation/editor-preferences-navigation-item.injectable.ts @@ -24,6 +24,7 @@ const editorPreferencesNavigationItemInjectable = getInjectable({ return { id: "editor", label: "Editor", + parent: "general", navigate: navigateToPreferenceTab(route), isActive: routeIsActive, isVisible: computed(() => true), diff --git a/src/renderer/components/+preferences/preferences-navigation/extension-tab-preferences-navigation-item.injectable.ts b/src/renderer/components/+preferences/preferences-navigation/extension-tab-preferences-navigation-item.injectable.ts new file mode 100644 index 0000000000..8cd0c4895b --- /dev/null +++ b/src/renderer/components/+preferences/preferences-navigation/extension-tab-preferences-navigation-item.injectable.ts @@ -0,0 +1,60 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import { map } from "lodash/fp"; + +import routeIsActiveInjectable from "../../../routes/route-is-active.injectable"; +import { preferenceNavigationItemInjectionToken } from "./preference-navigation-items.injectable"; + +import type { LensRendererExtension } from "../../../../extensions/lens-renderer-extension"; +import { extensionRegistratorInjectionToken } from "../../../../extensions/extension-loader/extension-registrator-injection-token"; +import { pipeline } from "@ogre-tools/fp"; +import extensionPreferencesRouteInjectable from "../../../../common/front-end-routing/routes/preferences/extension/extension-preferences-route.injectable"; +import navigateToExtensionPreferencesInjectable from "../../../../common/front-end-routing/routes/preferences/extension/navigate-to-extension-preferences.injectable"; +import type { LensExtension } from "../../../../extensions/lens-extension"; +import routePathParametersInjectable from "../../../routes/route-path-parameters.injectable"; + +const extensionSpecificTabNavigationItemRegistratorInjectable = getInjectable({ + id: "extension-specific-tab-preferences-navigation-items", + + instantiate: (di) => { + return (ext: LensExtension) => { + const extension = ext as LensRendererExtension; + const navigateToExtensionPreferences = di.inject( + navigateToExtensionPreferencesInjectable, + ); + const route = di.inject(extensionPreferencesRouteInjectable); + const routeIsActive = di.inject(routeIsActiveInjectable, route); + const pathParameters = di.inject(routePathParametersInjectable, route); + + return pipeline( + extension.appPreferenceTabs, + + map((tab) => { + const id = `extension-${extension.sanitizedExtensionId}-nav-item-${tab.id}`; + const isActive = computed(() => routeIsActive.get() && pathParameters.get().tabId === tab.id); + + return getInjectable({ + id, + injectionToken: preferenceNavigationItemInjectionToken, + instantiate: () => ({ + id, + label: tab.title, + parent: "general", + orderNumber: tab.orderNumber || 100, + navigate: () => navigateToExtensionPreferences(extension.sanitizedExtensionId, tab.id), + isVisible: computed(() => true), + isActive, + }), + }); + }), + ); + }; + }, + injectionToken: extensionRegistratorInjectionToken, +}); + +export default extensionSpecificTabNavigationItemRegistratorInjectable; diff --git a/src/renderer/components/+preferences/preferences-navigation/extensions-nav-group.tsx b/src/renderer/components/+preferences/preferences-navigation/extensions-nav-group.tsx new file mode 100644 index 0000000000..fd0c4158f6 --- /dev/null +++ b/src/renderer/components/+preferences/preferences-navigation/extensions-nav-group.tsx @@ -0,0 +1,56 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import React from "react"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import type { IComputedValue } from "mobx"; +import type { PreferenceNavigationItem } from "./preference-navigation-items.injectable"; +import { Icon } from "../../icon"; +import { PreferencesNavigationTab } from "./preference-navigation-tab"; +import preferenceNavigationItemsForGroupInjectable from "./preference-navigation-items-for-group.injectable"; +import { observer } from "mobx-react"; + +interface Dependencies { + navigationItems: IComputedValue; +} + +const NonInjectedExtensionsNavGroup = observer((props: Dependencies) => { + if (!props.navigationItems.get().length) { + return null; + } + + return ( +
+
+
+ + {" "} + Extensions +
+
+ {props.navigationItems.get().map(item => ( + + ))} +
+
+ ); +}); + +export const ExtensionsNavGroup = withInjectables( + NonInjectedExtensionsNavGroup, + + { + getProps: (di) => ({ + navigationItems: di.inject(preferenceNavigationItemsForGroupInjectable, "extensions"), + }), + }, +); diff --git a/src/renderer/components/+preferences/preferences-navigation/extensions-preferences-navigation-item-registrator.injectable.ts b/src/renderer/components/+preferences/preferences-navigation/extensions-preferences-navigation-item-registrator.injectable.ts new file mode 100644 index 0000000000..64b152e573 --- /dev/null +++ b/src/renderer/components/+preferences/preferences-navigation/extensions-preferences-navigation-item-registrator.injectable.ts @@ -0,0 +1,57 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import extensionPreferencesRouteInjectable from "../../../../common/front-end-routing/routes/preferences/extension/extension-preferences-route.injectable"; +import navigateToExtensionPreferencesInjectable from "../../../../common/front-end-routing/routes/preferences/extension/navigate-to-extension-preferences.injectable"; +import { extensionRegistratorInjectionToken } from "../../../../extensions/extension-loader/extension-registrator-injection-token"; +import type { LensRendererExtension } from "../../../../extensions/lens-renderer-extension"; +import routeIsActiveInjectable from "../../../routes/route-is-active.injectable"; +import routePathParametersInjectable from "../../../routes/route-path-parameters.injectable"; +import { preferenceNavigationItemInjectionToken } from "./preference-navigation-items.injectable"; + +const extensionPreferencesNavigationItemRegistratorInjectable = getInjectable({ + id: "extension-preferences-navigation-item", + + instantiate: (di) => { + return (ext) => { + const extension = ext as LensRendererExtension; + const navigateToExtensionPreferences = di.inject( + navigateToExtensionPreferencesInjectable, + ); + + const extensionHasPreferences = extension.appPreferences.length > 0; + const extensionHasGeneralPreferences = extension.appPreferences.some(preferences => + !preferences.showInPreferencesTab, + ); + const isVisible = computed(() => extensionHasPreferences && extensionHasGeneralPreferences); + const extensionRoute = di.inject(extensionPreferencesRouteInjectable); + const pathParameters = di.inject(routePathParametersInjectable, extensionRoute); + const routeIsActive = di.inject(routeIsActiveInjectable, extensionRoute); + const isActive = computed(() => routeIsActive.get() && pathParameters.get().extensionId === extension.sanitizedExtensionId); + const id = `extension-preferences-navigation-item-${extension.sanitizedExtensionId}`; + + const injectable = getInjectable({ + id, + injectionToken: preferenceNavigationItemInjectionToken, + instantiate: () => ({ + id: `extension-${extension.sanitizedExtensionId}`, + label: `${extension.name}`, + navigate: () => navigateToExtensionPreferences(extension.sanitizedExtensionId), + isActive, + isVisible, + orderNumber: 20, + parent: "extensions", + }), + }); + + return [injectable]; + }; + }, + + injectionToken: extensionRegistratorInjectionToken, +}); + +export default extensionPreferencesNavigationItemRegistratorInjectable; diff --git a/src/renderer/components/+preferences/preferences-navigation/extensions-preferences-navigation-item.injectable.ts b/src/renderer/components/+preferences/preferences-navigation/extensions-preferences-navigation-item.injectable.ts deleted file mode 100644 index 029e9ba896..0000000000 --- a/src/renderer/components/+preferences/preferences-navigation/extensions-preferences-navigation-item.injectable.ts +++ /dev/null @@ -1,51 +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 { preferenceNavigationItemInjectionToken } from "./preference-navigation-items.injectable"; -import routeIsActiveInjectable from "../../../routes/route-is-active.injectable"; -import { computed } from "mobx"; -import extensionsPreferenceItemsInjectable from "../extension-preference-items.injectable"; -import extensionPreferencesRouteInjectable from "../../../../common/front-end-routing/routes/preferences/extension/extension-preferences-route.injectable"; -import navigateToPreferenceTabInjectable from "./navigate-to-preference-tab.injectable"; - -const extensionsPreferencesNavigationItemInjectable = getInjectable({ - id: "extension-preferences-navigation-item", - - instantiate: (di) => { - const preferenceItems = di.inject( - extensionsPreferenceItemsInjectable, - ); - - const navigateToPreferenceTab = di.inject( - navigateToPreferenceTabInjectable, - ); - - const route = di.inject( - extensionPreferencesRouteInjectable, - ); - - const routeIsActive = di.inject( - routeIsActiveInjectable, - route, - ); - - return { - id: "extensions", - label: "Extensions", - navigate: navigateToPreferenceTab(route), - isActive: routeIsActive, - - isVisible: computed( - () => preferenceItems.get().length > 0, - ), - - orderNumber: 70, - }; - }, - - injectionToken: preferenceNavigationItemInjectionToken, -}); - -export default extensionsPreferencesNavigationItemInjectable; diff --git a/src/renderer/components/+preferences/preferences-navigation/general-nav-group.tsx b/src/renderer/components/+preferences/preferences-navigation/general-nav-group.tsx new file mode 100644 index 0000000000..0dc3b4693e --- /dev/null +++ b/src/renderer/components/+preferences/preferences-navigation/general-nav-group.tsx @@ -0,0 +1,45 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import React from "react"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import type { IComputedValue } from "mobx"; +import type { PreferenceNavigationItem } from "./preference-navigation-items.injectable"; +import { PreferencesNavigationTab } from "./preference-navigation-tab"; +import preferenceNavigationItemsForGroupInjectable from "./preference-navigation-items-for-group.injectable"; +import { observer } from "mobx-react"; + +interface Dependencies { + navigationItems: IComputedValue; +} + +const NonInjectedGeneralNavGroup = observer((props: Dependencies) => { + if (!props.navigationItems.get().length) { + return null; + } + + return ( + +
Preferences
+ + {props.navigationItems.get().map(item => ( + + ))} +
+ ); +}); + +export const GeneralNavGroup = withInjectables( + NonInjectedGeneralNavGroup, + + { + getProps: (di) => ({ + navigationItems: di.inject(preferenceNavigationItemsForGroupInjectable, "general"), + }), + }, +); diff --git a/src/renderer/components/+preferences/preferences-navigation/kubernetes-preferences-navigation-item.injectable.ts b/src/renderer/components/+preferences/preferences-navigation/kubernetes-preferences-navigation-item.injectable.ts index ec3429a212..6147021087 100644 --- a/src/renderer/components/+preferences/preferences-navigation/kubernetes-preferences-navigation-item.injectable.ts +++ b/src/renderer/components/+preferences/preferences-navigation/kubernetes-preferences-navigation-item.injectable.ts @@ -27,6 +27,7 @@ const kubernetesPreferencesNavigationItemInjectable = getInjectable({ return { id: "kubernetes", label: "Kubernetes", + parent: "general", navigate: navigateToPreferenceTab(route), isActive: routeIsActive, isVisible: computed(() => true), diff --git a/src/renderer/components/+preferences/preferences-navigation/preference-navigation-items-for-group.injectable.ts b/src/renderer/components/+preferences/preferences-navigation/preference-navigation-items-for-group.injectable.ts new file mode 100644 index 0000000000..9c63626f2b --- /dev/null +++ b/src/renderer/components/+preferences/preferences-navigation/preference-navigation-items-for-group.injectable.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { + getInjectable, + lifecycleEnum, +} from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import type { PreferenceNavigationItem } from "./preference-navigation-items.injectable"; +import preferenceNavigationItemsInjectable from "./preference-navigation-items.injectable"; + +const preferenceNavigationItemsForGroupInjectable = getInjectable({ + id: "preference-navigation-items-for-group", + + instantiate: (di, group: string) => { + const preferenceNavigationItems = di.inject(preferenceNavigationItemsInjectable); + + return computed((): PreferenceNavigationItem[] => + preferenceNavigationItems.get().filter((item) => item.parent == group), + ); + }, + + lifecycle: lifecycleEnum.keyedSingleton({ + getInstanceKey: (di, group: string) => group, + }), +}); + +export default preferenceNavigationItemsForGroupInjectable; + diff --git a/src/renderer/components/+preferences/preferences-navigation/preference-navigation-items.injectable.ts b/src/renderer/components/+preferences/preferences-navigation/preference-navigation-items.injectable.ts index e72d3f7b4d..8340ff003e 100644 --- a/src/renderer/components/+preferences/preferences-navigation/preference-navigation-items.injectable.ts +++ b/src/renderer/components/+preferences/preferences-navigation/preference-navigation-items.injectable.ts @@ -4,6 +4,7 @@ */ import { pipeline } from "@ogre-tools/fp"; import { getInjectable, getInjectionToken } from "@ogre-tools/injectable"; +import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx"; import { filter, orderBy } from "lodash/fp"; import type { IComputedValue } from "mobx"; import { computed } from "mobx"; @@ -20,19 +21,24 @@ export interface PreferenceNavigationItem { isVisible: IComputedValue; navigate: () => void; orderNumber: number; + parent: string; } const preferenceNavigationItemsInjectable = getInjectable({ id: "preference-navigation-items", - instantiate: (di) => - computed((): PreferenceNavigationItem[] => + instantiate: (di) => { + const computedInjectMany = di.inject(computedInjectManyInjectable); + const navigationItems = computedInjectMany(preferenceNavigationItemInjectionToken); + + return computed((): PreferenceNavigationItem[] => pipeline( - di.injectMany(preferenceNavigationItemInjectionToken), + navigationItems.get(), filter((item) => !!item.isVisible.get()), (items) => orderBy([(item) => item.orderNumber], ["asc"], items), ), - ), + ); + }, }); export default preferenceNavigationItemsInjectable; diff --git a/src/renderer/components/+preferences/preferences-navigation/preference-navigation-tab.tsx b/src/renderer/components/+preferences/preferences-navigation/preference-navigation-tab.tsx new file mode 100644 index 0000000000..a236c74ea9 --- /dev/null +++ b/src/renderer/components/+preferences/preferences-navigation/preference-navigation-tab.tsx @@ -0,0 +1,21 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { observer } from "mobx-react"; +import React from "react"; +import { Tab } from "../../tabs"; +import type { PreferenceNavigationItem } from "./preference-navigation-items.injectable"; + +interface PreferenceNavigationTabProps extends React.DOMAttributes { + item: PreferenceNavigationItem; +} + +export const PreferencesNavigationTab = observer(({ item }: PreferenceNavigationTabProps) => ( + +)); diff --git a/src/renderer/components/+preferences/preferences-navigation/preferences-navigation.tsx b/src/renderer/components/+preferences/preferences-navigation/preferences-navigation.tsx index b728412f97..55830433cd 100644 --- a/src/renderer/components/+preferences/preferences-navigation/preferences-navigation.tsx +++ b/src/renderer/components/+preferences/preferences-navigation/preferences-navigation.tsx @@ -2,61 +2,23 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { Tab, Tabs } from "../../tabs"; - -import { withInjectables } from "@ogre-tools/injectable-react"; -import type { IComputedValue } from "mobx"; import React from "react"; +import { Tabs } from "../../tabs"; +import { ExtensionsNavGroup } from "./extensions-nav-group"; +import { GeneralNavGroup } from "./general-nav-group"; import type { PreferenceNavigationItem, } from "./preference-navigation-items.injectable"; -import preferenceNavigationItemsInjectable from "./preference-navigation-items.injectable"; -import { observer } from "mobx-react"; - -interface Dependencies { - navigationItems: IComputedValue; -} - -const NonInjectedPreferencesNavigation = ({ - navigationItems, -}: Dependencies) => ( - item.navigate()} - > -
Preferences
- - {navigationItems.get().map((item) => ( - - ))} -
-); - -interface PreferenceNavigationTabProps extends React.DOMAttributes { - item: PreferenceNavigationItem; -} - -const PreferencesNavigationTab = observer(({ item }: PreferenceNavigationTabProps) => ( - -)); - -export const PreferencesNavigation = withInjectables( - NonInjectedPreferencesNavigation, - - { - getProps: (di) => ({ - navigationItems: di.inject(preferenceNavigationItemsInjectable), - }), - }, -); +export const PreferencesNavigation = () => { + return ( + item.navigate()} + > + + + + ); +}; diff --git a/src/renderer/components/+preferences/preferences-navigation/proxy-preferences-navigation-item.injectable.ts b/src/renderer/components/+preferences/preferences-navigation/proxy-preferences-navigation-item.injectable.ts index d4a588db87..fd0b835d12 100644 --- a/src/renderer/components/+preferences/preferences-navigation/proxy-preferences-navigation-item.injectable.ts +++ b/src/renderer/components/+preferences/preferences-navigation/proxy-preferences-navigation-item.injectable.ts @@ -24,6 +24,7 @@ const proxyPreferencesNavigationItemInjectable = getInjectable({ return { id: "proxy", label: "Proxy", + parent: "general", navigate: navigateToPreferenceTab(route), isActive: routeIsActive, isVisible: computed(() => true), diff --git a/src/renderer/components/+preferences/preferences-navigation/telemetry-preferences-navigation-item.injectable.ts b/src/renderer/components/+preferences/preferences-navigation/telemetry-preferences-navigation-item.injectable.ts index f6576696c6..1f95c74a0d 100644 --- a/src/renderer/components/+preferences/preferences-navigation/telemetry-preferences-navigation-item.injectable.ts +++ b/src/renderer/components/+preferences/preferences-navigation/telemetry-preferences-navigation-item.injectable.ts @@ -32,6 +32,7 @@ const terminalPreferencesNavigationItemInjectable = getInjectable({ return { id: "telemetry", label: "Telemetry", + parent: "general", navigate: navigateToPreferenceTab(route), isActive: routeIsActive, diff --git a/src/renderer/components/+preferences/preferences-navigation/terminal-preferences-navigation-item.injectable.ts b/src/renderer/components/+preferences/preferences-navigation/terminal-preferences-navigation-item.injectable.ts index b623148c10..f5ad555c3b 100644 --- a/src/renderer/components/+preferences/preferences-navigation/terminal-preferences-navigation-item.injectable.ts +++ b/src/renderer/components/+preferences/preferences-navigation/terminal-preferences-navigation-item.injectable.ts @@ -24,6 +24,7 @@ const terminalPreferencesNavigationItemInjectable = getInjectable({ return { id: "terminal", label: "Terminal", + parent: "general", navigate: navigateToPreferenceTab(route), isActive: routeIsActive, isVisible: computed(() => true), diff --git a/src/renderer/components/layout/setting-layout.scss b/src/renderer/components/layout/setting-layout.scss index 7e7ed325cb..244b88b3dc 100644 --- a/src/renderer/components/layout/setting-layout.scss +++ b/src/renderer/components/layout/setting-layout.scss @@ -34,7 +34,7 @@ .sidebar { width: 218px; - padding: 60px 0 60px 20px; + padding: 60px 10px 60px 20px; h2 { font-size: 15px; @@ -52,10 +52,6 @@ margin-right: 20px; height: 1px; border-top: thin solid var(--hrColor); - - &:first-child { - display: none; - } } .Tabs { @@ -63,7 +59,6 @@ padding: 6px 10px; font-size: 13px; font-weight: 800; - line-height: 16px; text-transform: uppercase; color: var(--textColorPrimary); @@ -76,14 +71,10 @@ padding: 6px 10px; margin-bottom: 2px; border-radius: 4px; - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; font-size: 15px; line-height: 20px; cursor: pointer; - border-top-right-radius: 0; - border-bottom-right-radius: 0; + white-space: normal; &::after { content: none; diff --git a/src/renderer/components/test-utils/get-application-builder.tsx b/src/renderer/components/test-utils/get-application-builder.tsx index ae54d4f71c..42401e1288 100644 --- a/src/renderer/components/test-utils/get-application-builder.tsx +++ b/src/renderer/components/test-utils/get-application-builder.tsx @@ -55,6 +55,9 @@ import { openMenu } from "react-select-event"; import userEvent from "@testing-library/user-event"; import { StatusBar } from "../status-bar/status-bar"; import lensProxyPortInjectable from "../../../main/lens-proxy/lens-proxy-port.injectable"; +import type { Route } from "../../../common/front-end-routing/front-end-route-injection-token"; +import type { NavigateToRouteOptions } from "../../../common/front-end-routing/navigate-to-route-injection-token"; +import { navigateToRouteInjectionToken } from "../../../common/front-end-routing/navigate-to-route-injection-token"; import type { LensMainExtension } from "../../../extensions/lens-main-extension"; import trayMenuItemsInjectable from "../../../main/tray/tray-menu-item/tray-menu-items.injectable"; import type { LensExtension } from "../../../extensions/lens-extension"; @@ -100,6 +103,7 @@ export interface ApplicationBuilder { preferences: { close: () => void; navigate: () => void; + navigateTo: (route: Route, params: Partial>) => void; navigation: { click: (id: string) => void; }; @@ -332,6 +336,12 @@ export const getApplicationBuilder = () => { navigateToPreferences(); }, + navigateTo: (route: Route, params: Partial>) => { + const navigateToRoute = rendererDi.inject(navigateToRouteInjectionToken); + + navigateToRoute(route, params); + }, + navigation: { click: (id: string) => { const link = rendered.queryByTestId(`tab-link-for-${id}`); @@ -546,7 +556,7 @@ const disableExtensionsFor = ( runInAction(() => { extension.deregister(); - extensionState.delete(extension); + extensionState.delete(instance); }); }); }; diff --git a/src/renderer/getDi.tsx b/src/renderer/getDi.tsx index 094af6292f..3380d25c06 100644 --- a/src/renderer/getDi.tsx +++ b/src/renderer/getDi.tsx @@ -9,7 +9,7 @@ import { registerMobX } from "@ogre-tools/injectable-extension-for-mobx"; import { Environments, setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; export const getDi = () => { - const di = createContainer(); + const di = createContainer("renderer"); registerMobX(di); diff --git a/src/renderer/getDiForUnitTesting.tsx b/src/renderer/getDiForUnitTesting.tsx index 78ecf47438..2f030c8ab9 100644 --- a/src/renderer/getDiForUnitTesting.tsx +++ b/src/renderer/getDiForUnitTesting.tsx @@ -58,7 +58,7 @@ export const getDiForUnitTesting = (opts: { doGeneralOverrides?: boolean } = {}) doGeneralOverrides = false, } = opts; - const di = createContainer(); + const di = createContainer("renderer"); registerMobX(di); diff --git a/yarn.lock b/yarn.lock index 8632bb789f..d8ee6d4e89 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1189,46 +1189,46 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@ogre-tools/fp@8.0.0", "@ogre-tools/fp@^8.0.0": - version "8.0.0" - resolved "https://registry.yarnpkg.com/@ogre-tools/fp/-/fp-8.0.0.tgz#dd2319a96ce3d2edd3ee2bc2acca07a94a77bf3b" - integrity sha512-8OpGUbG3avGtx6ASz3XNnK/KCyPW25RPp8oYzzU0zihKU5D4QKHy9qNkQ3npurzeg4d1k4BsgkeX+805nqtZOA== +"@ogre-tools/fp@9.0.0", "@ogre-tools/fp@^9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@ogre-tools/fp/-/fp-9.0.0.tgz#926cd4f13b52961156161feeeafddf22a0ad39c0" + integrity sha512-kMUgzhdjHuph0UWteOfyXNGBavZJX23NOA5su6fx9NdTzWhl9yB5Uf6Q//nOvL9COftjZDwnAgIaDU4MPdjyqA== dependencies: lodash "^4.17.21" -"@ogre-tools/injectable-extension-for-auto-registration@8.0.0": - version "8.0.0" - resolved "https://registry.yarnpkg.com/@ogre-tools/injectable-extension-for-auto-registration/-/injectable-extension-for-auto-registration-8.0.0.tgz#3a443f1f1c9b564baa78cca6a3c81ac4102660b0" - integrity sha512-DX1bxn8mDwek+W/SaI5WmDHmkY3B3njs3X4pOvqRtiMis3GaWHzeCZeK3q3Iv5cd14FTW8AsfKtujPmLklNf/A== +"@ogre-tools/injectable-extension-for-auto-registration@9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@ogre-tools/injectable-extension-for-auto-registration/-/injectable-extension-for-auto-registration-9.0.0.tgz#69463737ef4f7777db4703964b8a72a5fb82d6b3" + integrity sha512-+3I9Z0GfA04zZoj7Nw5WhJLDFLJgr5xv8Kp1zPDuT9/OvE9EA6hzAqakMDLbvn1zZOJjkJCGk44x6UjSQJp/9w== dependencies: - "@ogre-tools/fp" "^8.0.0" - "@ogre-tools/injectable" "^8.0.0" + "@ogre-tools/fp" "^9.0.0" + "@ogre-tools/injectable" "^9.0.0" lodash "^4.17.21" -"@ogre-tools/injectable-extension-for-mobx@8.0.0": - version "8.0.0" - resolved "https://registry.yarnpkg.com/@ogre-tools/injectable-extension-for-mobx/-/injectable-extension-for-mobx-8.0.0.tgz#80ff506011e078050dd8dcb72660d17181db6d34" - integrity sha512-m8gU3cEFHl9IMZLcvvoS7hVxS6p6nG3jdf6fY6MUZE0u9hx4bZuUdWYoylGQizy0FyRFQ2/m5xhTH2VdtXqx8w== +"@ogre-tools/injectable-extension-for-mobx@9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@ogre-tools/injectable-extension-for-mobx/-/injectable-extension-for-mobx-9.0.0.tgz#ed14df39b266e521272977821d3e05bcbd647577" + integrity sha512-9Hrtr7AdibcD+Fqn2qNsjiOUakAACO55TB1IqNsOJMMuqQHVB5SFZTHBzdVRqqqY2MwQvWYvd4xfy+beItD/xw== dependencies: - "@ogre-tools/fp" "^8.0.0" - "@ogre-tools/injectable" "^8.0.0" + "@ogre-tools/fp" "^9.0.0" + "@ogre-tools/injectable" "^9.0.0" lodash "^4.17.21" -"@ogre-tools/injectable-react@8.0.0": - version "8.0.0" - resolved "https://registry.yarnpkg.com/@ogre-tools/injectable-react/-/injectable-react-8.0.0.tgz#b2d8db4bb697ba2822d71ec73e6cabee30f70f34" - integrity sha512-rZa38fm3UzGa/09qC765Za8xJiSPOYHJGsob8UOt2JQIt/BecTNXPMHexfxy9W+DIdAer+YruUgedChdk9nvdQ== +"@ogre-tools/injectable-react@9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@ogre-tools/injectable-react/-/injectable-react-9.0.0.tgz#c5c510e893a5c1d7994d8709f70606967cabdec2" + integrity sha512-vGQrwkcWibRUWFPbu392riBYY4dXK051FxwyMsDYNRqmvaLo8HuumwjzS1DWS7db/P9Li+Kc+Ms670xIZepcpA== dependencies: - "@ogre-tools/fp" "^8.0.0" - "@ogre-tools/injectable" "^8.0.0" + "@ogre-tools/fp" "^9.0.0" + "@ogre-tools/injectable" "^9.0.0" lodash "^4.17.21" -"@ogre-tools/injectable@8.0.0", "@ogre-tools/injectable@^8.0.0": - version "8.0.0" - resolved "https://registry.yarnpkg.com/@ogre-tools/injectable/-/injectable-8.0.0.tgz#ea4f98bd2466149add94d4f6a9beb7be03729da7" - integrity sha512-59p+8uGqwVQ5IpGpgfn3RA+wXzn1tjnPdFWO3GLEgjyp5dWBaMKufCpCFRvvb9sP6B68qo19aLfy/uSm4AXduw== +"@ogre-tools/injectable@9.0.0", "@ogre-tools/injectable@^9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@ogre-tools/injectable/-/injectable-9.0.0.tgz#0819bc7b5fbae0a467f3250b10b4adb003268067" + integrity sha512-z9X86Q9AEkkilLu9V33j/aXv/IUoG944AdfN6WX2zZgJqRNjESN9spoOMqdKqib6JmEjCRxpMvaMwHLQSh14fg== dependencies: - "@ogre-tools/fp" "^8.0.0" + "@ogre-tools/fp" "^9.0.0" lodash "^4.17.21" "@panva/asn1.js@^1.0.0":