1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Show extension preferences in separate page (#5284)

Co-authored-by: Alex Andreev <alex.andreev.email@gmail.com>
Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
Alex Andreev 2022-06-21 18:11:31 +03:00 committed by GitHub
parent 91ee7bd785
commit 0784085bf4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 2535 additions and 204 deletions

View File

@ -208,11 +208,11 @@
"@hapi/subtext": "^7.0.4", "@hapi/subtext": "^7.0.4",
"@kubernetes/client-node": "^0.16.3", "@kubernetes/client-node": "^0.16.3",
"@material-ui/styles": "^4.11.5", "@material-ui/styles": "^4.11.5",
"@ogre-tools/fp": "8.0.0", "@ogre-tools/fp": "9.0.0",
"@ogre-tools/injectable": "8.0.0", "@ogre-tools/injectable": "9.0.0",
"@ogre-tools/injectable-extension-for-auto-registration": "8.0.0", "@ogre-tools/injectable-extension-for-auto-registration": "9.0.0",
"@ogre-tools/injectable-extension-for-mobx": "8.0.0", "@ogre-tools/injectable-extension-for-mobx": "9.0.0",
"@ogre-tools/injectable-react": "8.0.0", "@ogre-tools/injectable-react": "9.0.0",
"@sentry/electron": "^3.0.7", "@sentry/electron": "^3.0.7",
"@sentry/integrations": "^6.19.3", "@sentry/integrations": "^6.19.3",
"@side/jest-runtime": "^1.0.1", "@side/jest-runtime": "^1.0.1",

View File

@ -250,6 +250,7 @@ const testNavigationItemInjectable = getInjectable({
return { return {
id: "some-test-preference-navigation-item-id", id: "some-test-preference-navigation-item-id",
label: "Some preference navigation item", label: "Some preference navigation item",
parent: "general",
isActive: routeIsActive, isActive: routeIsActive,
isVisible: testRoute.isEnabled, isVisible: testRoute.isEnabled,
navigate: navigateToPreferenceTab(testRoute), navigate: navigateToPreferenceTab(testRoute),

View File

@ -6,8 +6,13 @@ import type { RenderResult } from "@testing-library/react";
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import React from "react"; 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 { 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", () => { describe("preferences - navigation to extension specific preferences", () => {
let applicationBuilder: ApplicationBuilder; let applicationBuilder: ApplicationBuilder;
@ -43,10 +48,65 @@ describe("preferences - navigation to extension specific preferences", () => {
expect(actual).toBeNull(); 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", () => { describe("when extension with specific preferences is enabled", () => {
let testExtension: TestExtension;
beforeEach(() => { beforeEach(() => {
const getRendererExtensionFake = getRendererExtensionFakeFor(applicationBuilder); const getRendererExtensionFake = getRendererExtensionFakeFor(applicationBuilder);
const testExtension = getRendererExtensionFake(extensionStubWithExtensionSpecificPreferenceItems);
testExtension = getRendererExtensionFake(
extensionStubWithExtensionSpecificPreferenceItems,
);
applicationBuilder.extensions.renderer.enable(testExtension); applicationBuilder.extensions.renderer.enable(testExtension);
}); });
@ -56,20 +116,32 @@ describe("preferences - navigation to extension specific preferences", () => {
}); });
it("shows link for extension 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(); 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", () => { describe("when navigating to extension preferences using navigation", () => {
beforeEach(() => { beforeEach(() => {
applicationBuilder.preferences.navigation.click("extensions"); applicationBuilder.preferences.navigation.click("extension-some-test-extension-id");
}); });
it("renders", () => { it("renders", () => {
expect(rendered.container).toMatchSnapshot(); 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", () => { it("shows extension specific preferences", () => {
const page = rendered.getByTestId("extension-preferences-page"); const page = rendered.getByTestId("extension-preferences-page");
@ -87,14 +159,234 @@ describe("preferences - navigation to extension specific preferences", () => {
expect(actual).toBeNull(); 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 = { const extensionStubWithExtensionSpecificPreferenceItems: FakeExtensionData = {
id: "some-extension-id", id: "some-test-extension-id",
name: "some-extension-name", name: "some-test-extension-id",
appPreferences: [ appPreferences: [
{ {
title: "Some preference item", 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: () => <div data-testid="some-other-preference-item-hint" />,
Input: () => <div data-testid="some-other-preference-item-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: () => <div data-testid="very-other-preference-item-hint" />,
Input: () => <div data-testid="very-other-preference-item-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: () => <div data-testid="metrics-preference-item-hint" />,
Input: () => <div data-testid="metrics-preference-item-input" />,
},
},
{
title: "Menu item",
id: "menu-preference-item-id",
showInPreferencesTab: "menu-extension-tab",
components: {
Hint: () => <div data-testid="menu-preference-item-hint" />,
Input: () => <div data-testid="menu-preference-item-input" />,
},
},
{
title: "Survey item",
id: "survey-preference-item-id",
showInPreferencesTab: "survey-extension-tab",
components: {
Hint: () => <div data-testid="survey-preference-item-hint" />,
Input: () => <div data-testid="survey-preference-item-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: () => <div data-testid="hello-preference-item-hint" />,
Input: () => <div data-testid="hello-preference-item-input" />,
},
},
{
title: "Logs",
id: "logs-preference-item-id",
showInPreferencesTab: "logs-extension-tab",
components: {
Hint: () => <div data-testid="logs-preference-item-hint" />,
Input: () => <div data-testid="logs-preference-item-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: () => <div data-testid="another-metrics-preference-item-hint" />,
Input: () => <div data-testid="another-metrics-preference-item-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: () => <div data-testid="my-preferences-item-hint" />,
Input: () => <div data-testid="my-preferences-item-input" />,
},
},
],
};

View File

@ -5,12 +5,18 @@
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { computed } from "mobx"; import { computed } from "mobx";
import { frontEndRouteInjectionToken } from "../../../front-end-route-injection-token"; 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({ const extensionPreferencesRouteInjectable = getInjectable({
id: "extension-preferences-route", id: "extension-preferences-route",
instantiate: () => ({ instantiate: (): Route<ExtensionPreferenceRouteParams> => ({
path: "/preferences/extensions", path: "/preferences/extension/:extensionId/:tabId?",
clusterFrame: false, clusterFrame: false,
isEnabled: computed(() => true), isEnabled: computed(() => true),
}), }),

View File

@ -13,7 +13,10 @@ const navigateToExtensionPreferencesInjectable = getInjectable({
const navigateToRoute = di.inject(navigateToRouteInjectionToken); const navigateToRoute = di.inject(navigateToRouteInjectionToken);
const route = di.inject(extensionPreferencesRouteInjectable); const route = di.inject(extensionPreferencesRouteInjectable);
return () => navigateToRoute(route); return (extensionId: string, tabId?: string) => navigateToRoute(route, { parameters: {
extensionId,
tabId,
}});
}, },
}); });

View File

@ -15,7 +15,7 @@ describe("runManyFor", () => {
let actualPromise: Promise<void>; let actualPromise: Promise<void>;
beforeEach(() => { beforeEach(() => {
const rootDi = createContainer(); const rootDi = createContainer("irrelevant");
runMock = asyncFn(); runMock = asyncFn();
@ -67,7 +67,7 @@ describe("runManyFor", () => {
let actualPromise: Promise<void>; let actualPromise: Promise<void>;
beforeEach(() => { beforeEach(() => {
const di = createContainer(); const di = createContainer("irrelevant");
runMock = asyncFn(); runMock = asyncFn();
@ -170,7 +170,7 @@ describe("runManyFor", () => {
}); });
it("given invalid hierarchy, when running runnables, throws", () => { it("given invalid hierarchy, when running runnables, throws", () => {
const rootDi = createContainer(); const rootDi = createContainer("irrelevant");
const runMock = asyncFn<(...args: unknown[]) => void>(); const runMock = asyncFn<(...args: unknown[]) => void>();
@ -218,7 +218,7 @@ describe("runManyFor", () => {
let runMock: AsyncFnMock<(...args: unknown[]) => Promise<void>>; let runMock: AsyncFnMock<(...args: unknown[]) => Promise<void>>;
beforeEach(() => { beforeEach(() => {
const rootDi = createContainer(); const rootDi = createContainer("irrelevant");
runMock = asyncFn(); runMock = asyncFn();

View File

@ -11,7 +11,7 @@ describe("runManySyncFor", () => {
let runMock: jest.Mock; let runMock: jest.Mock;
beforeEach(() => { beforeEach(() => {
const rootDi = createContainer(); const rootDi = createContainer("irrelevant");
runMock = jest.fn(); runMock = jest.fn();
@ -50,7 +50,7 @@ describe("runManySyncFor", () => {
let runMock: jest.Mock<(arg: string) => void>; let runMock: jest.Mock<(arg: string) => void>;
beforeEach(() => { beforeEach(() => {
const di = createContainer(); const di = createContainer("irrelevant");
runMock = jest.fn(); runMock = jest.fn();
@ -99,7 +99,7 @@ describe("runManySyncFor", () => {
}); });
it("given invalid hierarchy, when running runnables, throws", () => { it("given invalid hierarchy, when running runnables, throws", () => {
const rootDi = createContainer(); const rootDi = createContainer("irrelevant");
const runMock = jest.fn(); const runMock = jest.fn();
@ -147,7 +147,7 @@ describe("runManySyncFor", () => {
let runMock: jest.Mock<(arg: string, arg2: string) => void>; let runMock: jest.Mock<(arg: string, arg2: string) => void>;
beforeEach(() => { beforeEach(() => {
const rootDi = createContainer(); const rootDi = createContainer("irrelevant");
runMock = jest.fn(); runMock = jest.fn();

View File

@ -21,7 +21,7 @@ describe("asLegacyGlobalObjectForExtensionApiWithModifications", () => {
}; };
beforeEach(() => { beforeEach(() => {
di = createContainer(); di = createContainer("irrelevant");
jest.spyOn(di, "inject"); jest.spyOn(di, "inject");

View File

@ -26,6 +26,7 @@ import { pipeline } from "@ogre-tools/fp";
import { getExtensionRoutePath } from "../renderer/routes/for-extension"; import { getExtensionRoutePath } from "../renderer/routes/for-extension";
import type { LensRendererExtensionDependencies } from "./lens-extension-set-dependencies"; import type { LensRendererExtensionDependencies } from "./lens-extension-set-dependencies";
import type { KubeObjectHandlerRegistration } from "../renderer/kube-object/handler"; 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<LensRendererExtensionDependencies> { export class LensRendererExtension extends LensExtension<LensRendererExtensionDependencies> {
globalPages: registries.PageRegistration[] = []; globalPages: registries.PageRegistration[] = [];
@ -33,6 +34,7 @@ export class LensRendererExtension extends LensExtension<LensRendererExtensionDe
clusterPageMenus: registries.ClusterPageMenuRegistration[] = []; clusterPageMenus: registries.ClusterPageMenuRegistration[] = [];
kubeObjectStatusTexts: KubeObjectStatusRegistration[] = []; kubeObjectStatusTexts: KubeObjectStatusRegistration[] = [];
appPreferences: AppPreferenceRegistration[] = []; appPreferences: AppPreferenceRegistration[] = [];
appPreferenceTabs: AppPreferenceTabRegistration[] = [];
entitySettings: registries.EntitySettingRegistration[] = []; entitySettings: registries.EntitySettingRegistration[] = [];
statusBarItems: StatusBarRegistration[] = []; statusBarItems: StatusBarRegistration[] = [];
kubeObjectDetailItems: registries.KubeObjectDetailRegistration[] = []; kubeObjectDetailItems: registries.KubeObjectDetailRegistration[] = [];

View File

@ -8,7 +8,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"; import { Environments, setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
export const getDi = () => { export const getDi = () => {
const di = createContainer(); const di = createContainer("main");
registerMobX(di); registerMobX(di);

View File

@ -105,7 +105,7 @@ export function getDiForUnitTesting(opts: { doGeneralOverrides?: boolean } = {})
doGeneralOverrides = false, doGeneralOverrides = false,
} = opts; } = opts;
const di = createContainer(); const di = createContainer("main");
registerMobX(di); registerMobX(di);

View File

@ -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;
}

View File

@ -7,7 +7,7 @@ import { filter, map } from "lodash/fp";
import { extensionRegistratorInjectionToken } from "../../../extensions/extension-loader/extension-registrator-injection-token"; import { extensionRegistratorInjectionToken } from "../../../extensions/extension-loader/extension-registrator-injection-token";
import type { LensRendererExtension } from "../../../extensions/lens-renderer-extension"; import type { LensRendererExtension } from "../../../extensions/lens-renderer-extension";
import { pipeline } from "@ogre-tools/fp"; import { pipeline } from "@ogre-tools/fp";
import { extensionPreferenceItemInjectionToken } from "./extension-preference-items.injectable"; import { extensionPreferenceItemInjectionToken } from "./extension-preference-items-injection-token";
const extensionPreferenceItemRegistratorInjectable = getInjectable({ const extensionPreferenceItemRegistratorInjectable = getInjectable({
id: "extension-preference-item-registrator", id: "extension-preference-item-registrator",
@ -34,6 +34,7 @@ const extensionPreferenceItemRegistratorInjectable = getInjectable({
instantiate: () => ({ instantiate: () => ({
id: registration.id || id, id: registration.id || id,
title: registration.title, title: registration.title,
extension,
components: { components: {
Hint: registration.components.Hint, Hint: registration.components.Hint,

View File

@ -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<ExtensionPreferenceItem>({
id: "extension-preference-item-injection-token",
});

View File

@ -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;

View File

@ -8,21 +8,39 @@ import type { IComputedValue } from "mobx";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import React from "react"; import React from "react";
import type { RegisteredAppPreference } from "./app-preferences/app-preference-registration"; import type { RegisteredAppPreference } from "./app-preferences/app-preference-registration";
import extensionPreferencesModelInjectable from "./extension-preference-model.injectable";
import { ExtensionSettings } from "./extension-settings"; import { ExtensionSettings } from "./extension-settings";
import { Preferences } from "./preferences"; import { Preferences } from "./preferences";
import extensionsPreferenceItemsInjectable from "./extension-preference-items.injectable";
interface Dependencies { interface Dependencies {
preferenceItems: IComputedValue<RegisteredAppPreference[]>; model: IComputedValue<{
preferenceItems: RegisteredAppPreference[];
extensionName?: string;
}>;
} }
const NonInjectedExtensions = ({ preferenceItems }: Dependencies) => ( const NonInjectedExtensions = ({ model }: Dependencies) => {
const { extensionName, preferenceItems } = model.get();
return (
<Preferences data-testid="extension-preferences-page"> <Preferences data-testid="extension-preferences-page">
<section id="extensions"> <section id="extensions">
<h2>Extensions</h2> <h2>
{preferenceItems.get().map((preferenceItem) => ( {extensionName}
{" "}
preferences
</h2>
{!extensionName && (
<div
className="flex items-center"
data-testid="error-for-extension-not-being-present"
>
No extension found
</div>
)}
{preferenceItems.map((preferenceItem, index) => (
<ExtensionSettings <ExtensionSettings
key={preferenceItem.id} key={`${preferenceItem.id}-${index}`}
setting={preferenceItem} setting={preferenceItem}
size="small" size="small"
data-testid={`extension-preference-item-for-${preferenceItem.id}`} data-testid={`extension-preference-item-for-${preferenceItem.id}`}
@ -31,13 +49,14 @@ const NonInjectedExtensions = ({ preferenceItems }: Dependencies) => (
</section> </section>
</Preferences> </Preferences>
); );
};
export const Extensions = withInjectables<Dependencies>( export const Extensions = withInjectables<Dependencies>(
observer(NonInjectedExtensions), observer(NonInjectedExtensions),
{ {
getProps: (di) => ({ getProps: (di) => ({
preferenceItems: di.inject(extensionsPreferenceItemsInjectable), model: di.inject(extensionPreferencesModelInjectable),
}), }),
}, },
); );

View File

@ -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);
}

View File

@ -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",
},
];

View File

@ -24,6 +24,7 @@ const applicationPreferencesNavigationItemInjectable = getInjectable({
return { return {
id: "application", id: "application",
label: "App", label: "App",
parent: "general",
navigate: navigateToPreferenceTab(route), navigate: navigateToPreferenceTab(route),
isActive: routeIsActive, isActive: routeIsActive,
isVisible: computed(() => true), isVisible: computed(() => true),

View File

@ -24,6 +24,7 @@ const editorPreferencesNavigationItemInjectable = getInjectable({
return { return {
id: "editor", id: "editor",
label: "Editor", label: "Editor",
parent: "general",
navigate: navigateToPreferenceTab(route), navigate: navigateToPreferenceTab(route),
isActive: routeIsActive, isActive: routeIsActive,
isVisible: computed(() => true), isVisible: computed(() => true),

View File

@ -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;

View File

@ -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<PreferenceNavigationItem[]>;
}
const NonInjectedExtensionsNavGroup = observer((props: Dependencies) => {
if (!props.navigationItems.get().length) {
return null;
}
return (
<div data-testid="extension-settings">
<hr/>
<div className="header flex items-center">
<Icon
material="extension"
smallest
className="mr-3"
/>
{" "}
Extensions
</div>
<div>
{props.navigationItems.get().map(item => (
<PreferencesNavigationTab
key={item.id}
item={item}
data-testid={`tab-link-for-${item.id}`}
/>
))}
</div>
</div>
);
});
export const ExtensionsNavGroup = withInjectables<Dependencies>(
NonInjectedExtensionsNavGroup,
{
getProps: (di) => ({
navigationItems: di.inject(preferenceNavigationItemsForGroupInjectable, "extensions"),
}),
},
);

View File

@ -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;

View File

@ -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;

View File

@ -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<PreferenceNavigationItem[]>;
}
const NonInjectedGeneralNavGroup = observer((props: Dependencies) => {
if (!props.navigationItems.get().length) {
return null;
}
return (
<React.Fragment>
<div className="header">Preferences</div>
{props.navigationItems.get().map(item => (
<PreferencesNavigationTab
key={item.id}
item={item}
data-testid={`tab-link-for-${item.id}`}
/>
))}
</React.Fragment>
);
});
export const GeneralNavGroup = withInjectables<Dependencies>(
NonInjectedGeneralNavGroup,
{
getProps: (di) => ({
navigationItems: di.inject(preferenceNavigationItemsForGroupInjectable, "general"),
}),
},
);

View File

@ -27,6 +27,7 @@ const kubernetesPreferencesNavigationItemInjectable = getInjectable({
return { return {
id: "kubernetes", id: "kubernetes",
label: "Kubernetes", label: "Kubernetes",
parent: "general",
navigate: navigateToPreferenceTab(route), navigate: navigateToPreferenceTab(route),
isActive: routeIsActive, isActive: routeIsActive,
isVisible: computed(() => true), isVisible: computed(() => true),

View File

@ -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;

View File

@ -4,6 +4,7 @@
*/ */
import { pipeline } from "@ogre-tools/fp"; import { pipeline } from "@ogre-tools/fp";
import { getInjectable, getInjectionToken } from "@ogre-tools/injectable"; import { getInjectable, getInjectionToken } from "@ogre-tools/injectable";
import { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx";
import { filter, orderBy } from "lodash/fp"; import { filter, orderBy } from "lodash/fp";
import type { IComputedValue } from "mobx"; import type { IComputedValue } from "mobx";
import { computed } from "mobx"; import { computed } from "mobx";
@ -20,19 +21,24 @@ export interface PreferenceNavigationItem {
isVisible: IComputedValue<boolean>; isVisible: IComputedValue<boolean>;
navigate: () => void; navigate: () => void;
orderNumber: number; orderNumber: number;
parent: string;
} }
const preferenceNavigationItemsInjectable = getInjectable({ const preferenceNavigationItemsInjectable = getInjectable({
id: "preference-navigation-items", id: "preference-navigation-items",
instantiate: (di) => instantiate: (di) => {
computed((): PreferenceNavigationItem[] => const computedInjectMany = di.inject(computedInjectManyInjectable);
const navigationItems = computedInjectMany(preferenceNavigationItemInjectionToken);
return computed((): PreferenceNavigationItem[] =>
pipeline( pipeline(
di.injectMany(preferenceNavigationItemInjectionToken), navigationItems.get(),
filter((item) => !!item.isVisible.get()), filter((item) => !!item.isVisible.get()),
(items) => orderBy([(item) => item.orderNumber], ["asc"], items), (items) => orderBy([(item) => item.orderNumber], ["asc"], items),
), ),
), );
},
}); });
export default preferenceNavigationItemsInjectable; export default preferenceNavigationItemsInjectable;

View File

@ -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<HTMLElement> {
item: PreferenceNavigationItem;
}
export const PreferencesNavigationTab = observer(({ item }: PreferenceNavigationTabProps) => (
<Tab
value={item}
label={item.label}
data-testid={`tab-link-for-${item.id}`}
active={item.isActive.get()}
/>
));

View File

@ -2,61 +2,23 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * 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 React from "react";
import { Tabs } from "../../tabs";
import { ExtensionsNavGroup } from "./extensions-nav-group";
import { GeneralNavGroup } from "./general-nav-group";
import type { import type {
PreferenceNavigationItem, PreferenceNavigationItem,
} from "./preference-navigation-items.injectable"; } from "./preference-navigation-items.injectable";
import preferenceNavigationItemsInjectable from "./preference-navigation-items.injectable";
import { observer } from "mobx-react"; export const PreferencesNavigation = () => {
return (
interface Dependencies {
navigationItems: IComputedValue<PreferenceNavigationItem[]>;
}
const NonInjectedPreferencesNavigation = ({
navigationItems,
}: Dependencies) => (
<Tabs <Tabs
className="flex column" className="flex column"
scrollable={false} scrollable={false}
onChange={(item: PreferenceNavigationItem) => item.navigate()} onChange={(item: PreferenceNavigationItem) => item.navigate()}
> >
<div className="header">Preferences</div> <GeneralNavGroup/>
<ExtensionsNavGroup/>
{navigationItems.get().map((item) => (
<PreferencesNavigationTab
key={item.id}
item={item}
data-testid={`tab-link-for-${item.id}`}
/>
))}
</Tabs> </Tabs>
); );
};
interface PreferenceNavigationTabProps extends React.DOMAttributes<HTMLElement> {
item: PreferenceNavigationItem;
}
const PreferencesNavigationTab = observer(({ item }: PreferenceNavigationTabProps) => (
<Tab
value={item}
label={item.label}
data-testid={`tab-link-for-${item.id}`}
active={item.isActive.get()}
/>
));
export const PreferencesNavigation = withInjectables<Dependencies>(
NonInjectedPreferencesNavigation,
{
getProps: (di) => ({
navigationItems: di.inject(preferenceNavigationItemsInjectable),
}),
},
);

View File

@ -24,6 +24,7 @@ const proxyPreferencesNavigationItemInjectable = getInjectable({
return { return {
id: "proxy", id: "proxy",
label: "Proxy", label: "Proxy",
parent: "general",
navigate: navigateToPreferenceTab(route), navigate: navigateToPreferenceTab(route),
isActive: routeIsActive, isActive: routeIsActive,
isVisible: computed(() => true), isVisible: computed(() => true),

View File

@ -32,6 +32,7 @@ const terminalPreferencesNavigationItemInjectable = getInjectable({
return { return {
id: "telemetry", id: "telemetry",
label: "Telemetry", label: "Telemetry",
parent: "general",
navigate: navigateToPreferenceTab(route), navigate: navigateToPreferenceTab(route),
isActive: routeIsActive, isActive: routeIsActive,

View File

@ -24,6 +24,7 @@ const terminalPreferencesNavigationItemInjectable = getInjectable({
return { return {
id: "terminal", id: "terminal",
label: "Terminal", label: "Terminal",
parent: "general",
navigate: navigateToPreferenceTab(route), navigate: navigateToPreferenceTab(route),
isActive: routeIsActive, isActive: routeIsActive,
isVisible: computed(() => true), isVisible: computed(() => true),

View File

@ -34,7 +34,7 @@
.sidebar { .sidebar {
width: 218px; width: 218px;
padding: 60px 0 60px 20px; padding: 60px 10px 60px 20px;
h2 { h2 {
font-size: 15px; font-size: 15px;
@ -52,10 +52,6 @@
margin-right: 20px; margin-right: 20px;
height: 1px; height: 1px;
border-top: thin solid var(--hrColor); border-top: thin solid var(--hrColor);
&:first-child {
display: none;
}
} }
.Tabs { .Tabs {
@ -63,7 +59,6 @@
padding: 6px 10px; padding: 6px 10px;
font-size: 13px; font-size: 13px;
font-weight: 800; font-weight: 800;
line-height: 16px;
text-transform: uppercase; text-transform: uppercase;
color: var(--textColorPrimary); color: var(--textColorPrimary);
@ -76,14 +71,10 @@
padding: 6px 10px; padding: 6px 10px;
margin-bottom: 2px; margin-bottom: 2px;
border-radius: 4px; border-radius: 4px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
font-size: 15px; font-size: 15px;
line-height: 20px; line-height: 20px;
cursor: pointer; cursor: pointer;
border-top-right-radius: 0; white-space: normal;
border-bottom-right-radius: 0;
&::after { &::after {
content: none; content: none;

View File

@ -55,6 +55,9 @@ import { openMenu } from "react-select-event";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import { StatusBar } from "../status-bar/status-bar"; import { StatusBar } from "../status-bar/status-bar";
import lensProxyPortInjectable from "../../../main/lens-proxy/lens-proxy-port.injectable"; 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 type { LensMainExtension } from "../../../extensions/lens-main-extension";
import trayMenuItemsInjectable from "../../../main/tray/tray-menu-item/tray-menu-items.injectable"; import trayMenuItemsInjectable from "../../../main/tray/tray-menu-item/tray-menu-items.injectable";
import type { LensExtension } from "../../../extensions/lens-extension"; import type { LensExtension } from "../../../extensions/lens-extension";
@ -100,6 +103,7 @@ export interface ApplicationBuilder {
preferences: { preferences: {
close: () => void; close: () => void;
navigate: () => void; navigate: () => void;
navigateTo: (route: Route<any>, params: Partial<NavigateToRouteOptions<any>>) => void;
navigation: { navigation: {
click: (id: string) => void; click: (id: string) => void;
}; };
@ -332,6 +336,12 @@ export const getApplicationBuilder = () => {
navigateToPreferences(); navigateToPreferences();
}, },
navigateTo: (route: Route<any>, params: Partial<NavigateToRouteOptions<any>>) => {
const navigateToRoute = rendererDi.inject(navigateToRouteInjectionToken);
navigateToRoute(route, params);
},
navigation: { navigation: {
click: (id: string) => { click: (id: string) => {
const link = rendered.queryByTestId(`tab-link-for-${id}`); const link = rendered.queryByTestId(`tab-link-for-${id}`);
@ -546,7 +556,7 @@ const disableExtensionsFor = <T extends ObservableSet>(
runInAction(() => { runInAction(() => {
extension.deregister(); extension.deregister();
extensionState.delete(extension); extensionState.delete(instance);
}); });
}); });
}; };

View File

@ -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"; import { Environments, setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
export const getDi = () => { export const getDi = () => {
const di = createContainer(); const di = createContainer("renderer");
registerMobX(di); registerMobX(di);

View File

@ -58,7 +58,7 @@ export const getDiForUnitTesting = (opts: { doGeneralOverrides?: boolean } = {})
doGeneralOverrides = false, doGeneralOverrides = false,
} = opts; } = opts;
const di = createContainer(); const di = createContainer("renderer");
registerMobX(di); registerMobX(di);

View File

@ -1189,46 +1189,46 @@
"@nodelib/fs.scandir" "2.1.5" "@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0" fastq "^1.6.0"
"@ogre-tools/fp@8.0.0", "@ogre-tools/fp@^8.0.0": "@ogre-tools/fp@9.0.0", "@ogre-tools/fp@^9.0.0":
version "8.0.0" version "9.0.0"
resolved "https://registry.yarnpkg.com/@ogre-tools/fp/-/fp-8.0.0.tgz#dd2319a96ce3d2edd3ee2bc2acca07a94a77bf3b" resolved "https://registry.yarnpkg.com/@ogre-tools/fp/-/fp-9.0.0.tgz#926cd4f13b52961156161feeeafddf22a0ad39c0"
integrity sha512-8OpGUbG3avGtx6ASz3XNnK/KCyPW25RPp8oYzzU0zihKU5D4QKHy9qNkQ3npurzeg4d1k4BsgkeX+805nqtZOA== integrity sha512-kMUgzhdjHuph0UWteOfyXNGBavZJX23NOA5su6fx9NdTzWhl9yB5Uf6Q//nOvL9COftjZDwnAgIaDU4MPdjyqA==
dependencies: dependencies:
lodash "^4.17.21" lodash "^4.17.21"
"@ogre-tools/injectable-extension-for-auto-registration@8.0.0": "@ogre-tools/injectable-extension-for-auto-registration@9.0.0":
version "8.0.0" version "9.0.0"
resolved "https://registry.yarnpkg.com/@ogre-tools/injectable-extension-for-auto-registration/-/injectable-extension-for-auto-registration-8.0.0.tgz#3a443f1f1c9b564baa78cca6a3c81ac4102660b0" resolved "https://registry.yarnpkg.com/@ogre-tools/injectable-extension-for-auto-registration/-/injectable-extension-for-auto-registration-9.0.0.tgz#69463737ef4f7777db4703964b8a72a5fb82d6b3"
integrity sha512-DX1bxn8mDwek+W/SaI5WmDHmkY3B3njs3X4pOvqRtiMis3GaWHzeCZeK3q3Iv5cd14FTW8AsfKtujPmLklNf/A== integrity sha512-+3I9Z0GfA04zZoj7Nw5WhJLDFLJgr5xv8Kp1zPDuT9/OvE9EA6hzAqakMDLbvn1zZOJjkJCGk44x6UjSQJp/9w==
dependencies: dependencies:
"@ogre-tools/fp" "^8.0.0" "@ogre-tools/fp" "^9.0.0"
"@ogre-tools/injectable" "^8.0.0" "@ogre-tools/injectable" "^9.0.0"
lodash "^4.17.21" lodash "^4.17.21"
"@ogre-tools/injectable-extension-for-mobx@8.0.0": "@ogre-tools/injectable-extension-for-mobx@9.0.0":
version "8.0.0" version "9.0.0"
resolved "https://registry.yarnpkg.com/@ogre-tools/injectable-extension-for-mobx/-/injectable-extension-for-mobx-8.0.0.tgz#80ff506011e078050dd8dcb72660d17181db6d34" resolved "https://registry.yarnpkg.com/@ogre-tools/injectable-extension-for-mobx/-/injectable-extension-for-mobx-9.0.0.tgz#ed14df39b266e521272977821d3e05bcbd647577"
integrity sha512-m8gU3cEFHl9IMZLcvvoS7hVxS6p6nG3jdf6fY6MUZE0u9hx4bZuUdWYoylGQizy0FyRFQ2/m5xhTH2VdtXqx8w== integrity sha512-9Hrtr7AdibcD+Fqn2qNsjiOUakAACO55TB1IqNsOJMMuqQHVB5SFZTHBzdVRqqqY2MwQvWYvd4xfy+beItD/xw==
dependencies: dependencies:
"@ogre-tools/fp" "^8.0.0" "@ogre-tools/fp" "^9.0.0"
"@ogre-tools/injectable" "^8.0.0" "@ogre-tools/injectable" "^9.0.0"
lodash "^4.17.21" lodash "^4.17.21"
"@ogre-tools/injectable-react@8.0.0": "@ogre-tools/injectable-react@9.0.0":
version "8.0.0" version "9.0.0"
resolved "https://registry.yarnpkg.com/@ogre-tools/injectable-react/-/injectable-react-8.0.0.tgz#b2d8db4bb697ba2822d71ec73e6cabee30f70f34" resolved "https://registry.yarnpkg.com/@ogre-tools/injectable-react/-/injectable-react-9.0.0.tgz#c5c510e893a5c1d7994d8709f70606967cabdec2"
integrity sha512-rZa38fm3UzGa/09qC765Za8xJiSPOYHJGsob8UOt2JQIt/BecTNXPMHexfxy9W+DIdAer+YruUgedChdk9nvdQ== integrity sha512-vGQrwkcWibRUWFPbu392riBYY4dXK051FxwyMsDYNRqmvaLo8HuumwjzS1DWS7db/P9Li+Kc+Ms670xIZepcpA==
dependencies: dependencies:
"@ogre-tools/fp" "^8.0.0" "@ogre-tools/fp" "^9.0.0"
"@ogre-tools/injectable" "^8.0.0" "@ogre-tools/injectable" "^9.0.0"
lodash "^4.17.21" lodash "^4.17.21"
"@ogre-tools/injectable@8.0.0", "@ogre-tools/injectable@^8.0.0": "@ogre-tools/injectable@9.0.0", "@ogre-tools/injectable@^9.0.0":
version "8.0.0" version "9.0.0"
resolved "https://registry.yarnpkg.com/@ogre-tools/injectable/-/injectable-8.0.0.tgz#ea4f98bd2466149add94d4f6a9beb7be03729da7" resolved "https://registry.yarnpkg.com/@ogre-tools/injectable/-/injectable-9.0.0.tgz#0819bc7b5fbae0a467f3250b10b4adb003268067"
integrity sha512-59p+8uGqwVQ5IpGpgfn3RA+wXzn1tjnPdFWO3GLEgjyp5dWBaMKufCpCFRvvb9sP6B68qo19aLfy/uSm4AXduw== integrity sha512-z9X86Q9AEkkilLu9V33j/aXv/IUoG944AdfN6WX2zZgJqRNjESN9spoOMqdKqib6JmEjCRxpMvaMwHLQSh14fg==
dependencies: dependencies:
"@ogre-tools/fp" "^8.0.0" "@ogre-tools/fp" "^9.0.0"
lodash "^4.17.21" lodash "^4.17.21"
"@panva/asn1.js@^1.0.0": "@panva/asn1.js@^1.0.0":