diff --git a/src/common/routes/preferences.ts b/src/common/routes/preferences.ts index 9b1932fd3d..701b8bfa58 100644 --- a/src/common/routes/preferences.ts +++ b/src/common/routes/preferences.ts @@ -50,6 +50,10 @@ export const extensionRoute: RouteProps = { path: `${preferencesRoute.path}/extensions`, }; +export const extensionSettingsRoute: RouteProps = { + path: `${preferencesRoute.path}/extension-settings/:extensionId?`, +}; + export const preferencesURL = buildURL(preferencesRoute.path); export const appURL = buildURL(appRoute.path); export const proxyURL = buildURL(proxyRoute.path); @@ -57,3 +61,4 @@ export const kubernetesURL = buildURL(kubernetesRoute.path); export const editorURL = buildURL(editorRoute.path); export const telemetryURL = buildURL(telemetryRoute.path); export const extensionURL = buildURL(extensionRoute.path); +export const extensionSettingsURL = buildURL<{ extensionId?: string }>(extensionSettingsRoute.path); diff --git a/src/extensions/registries/app-preference-registry.ts b/src/extensions/registries/app-preference-registry.ts index ac7e81fd07..26b878714e 100644 --- a/src/extensions/registries/app-preference-registry.ts +++ b/src/extensions/registries/app-preference-registry.ts @@ -43,7 +43,7 @@ export interface RegisteredAppPreference extends AppPreferenceRegistration { export class AppPreferenceRegistry extends BaseRegistry { getRegisteredItem(item: AppPreferenceRegistration, extension: LensExtension): RegisteredAppPreference { return { - extensionId: extension.id, + extensionId: extension.manifest.name, id: item.id || item.title.toLowerCase().replace(/[^0-9a-zA-Z]+/g, "-"), ...item, }; diff --git a/src/renderer/components/+preferences/__tests__/preferences.test.tsx b/src/renderer/components/+preferences/__tests__/preferences.test.tsx new file mode 100644 index 0000000000..9503b3216a --- /dev/null +++ b/src/renderer/components/+preferences/__tests__/preferences.test.tsx @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import React from "react"; +import Preferences from "../preferences"; +import { DiRender, renderFor } from "../../test-utils/renderFor"; +import type { ConfigurableDependencyInjectionContainer } from "@ogre-tools/injectable"; +import userExtensionsInjectable from "../../+extensions/user-extensions/user-extensions.injectable"; +import { getDiForUnitTesting } from "../../getDiForUnitTesting"; +import { AppPreferenceRegistry } from "../../../../extensions/registries"; +import { computed } from "mobx"; +import { MemoryRouter } from "react-router-dom"; + +describe("Preferences", () => { + let di: ConfigurableDependencyInjectionContainer; + let render: DiRender; + + beforeEach(async () => { + di = getDiForUnitTesting(); + render = renderFor(di); + + AppPreferenceRegistry.createInstance(); + }); + + it("renders w/o errors", () => { + di.override(userExtensionsInjectable, () => { + return computed(() => [] as any); + }); + + const { container } = render(); + + expect(container).toBeInstanceOf(HTMLElement); + }); + + it("doesn't render extension settings tabs if no extensions found", () => { + di.override(userExtensionsInjectable, () => { + return computed(() => [] as any); + }); + + const { getByTestId } = render(); + + expect(getByTestId("custom-settings")).not.toBeInTheDocument(); + }); +}); diff --git a/src/renderer/components/+preferences/extension-settings-page.tsx b/src/renderer/components/+preferences/extension-settings-page.tsx new file mode 100644 index 0000000000..38836da0b0 --- /dev/null +++ b/src/renderer/components/+preferences/extension-settings-page.tsx @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import { observer } from "mobx-react"; +import React from "react"; +import { matchPath, RouteComponentProps } from "react-router"; +import { extensionSettingsRoute } from "../../../common/routes"; +import { AppPreferenceRegistry } from "../../../extensions/registries"; +import { ExtensionSettings } from "./extension-settings"; + +interface Props extends RouteComponentProps<{ extensionId?: string }> { +} + +export const ExtensionSettingsPage = observer((props: Props) => { + // https://github.com/remix-run/react-router/issues/5870#issuecomment-394194338 + const match = matchPath<{ extensionId: string }>(props.history.location.pathname, { + path: extensionSettingsRoute.path, + exact: true, + }); + const extensionId = decodeURIComponent(match.params.extensionId); + const settings = AppPreferenceRegistry.getInstance().getItems(); + const currentSettings = settings.filter(setting => setting.extensionId == extensionId); + + const renderContent = () => { + if (!currentSettings) { + return ( +
No settings found
+ ); + } + + return settings.filter(e => !e.showInPreferencesTab).map((setting) => + , + ); + }; + + return ( +
+

{extensionId} settings

+ {renderContent()} +
+ ); +}); diff --git a/src/renderer/components/+preferences/preferences.tsx b/src/renderer/components/+preferences/preferences.tsx index 2c84232b2a..1d5a8843a2 100644 --- a/src/renderer/components/+preferences/preferences.tsx +++ b/src/renderer/components/+preferences/preferences.tsx @@ -38,6 +38,7 @@ import { editorRoute, telemetryRoute, telemetryURL, + extensionSettingsURL, } from "../../../common/routes"; import { AppPreferenceRegistry } from "../../../extensions/registries/app-preference-registry"; import { navigateWithoutHistoryChange, navigation } from "../../navigation"; @@ -53,6 +54,7 @@ import { sentryDsn } from "../../../common/vars"; import { withInjectables } from "@ogre-tools/injectable-react"; import type { InstalledExtension } from "../../../extensions/extension-discovery"; import userExtensionsInjectable from "../+extensions/user-extensions/user-extensions.injectable"; +import { ExtensionSettingsPage } from "./extension-settings-page"; interface Dependencies { userExtensions: IComputedValue; @@ -68,13 +70,19 @@ class Preferences extends React.Component { } renderNavigation() { - const extensions = AppPreferenceRegistry.getInstance().getItems(); - const telemetryExtensions = extensions.filter(e => e.showInPreferencesTab == "telemetry"); + const preferenceRegistries = AppPreferenceRegistry.getInstance().getItems(); + const telemetryExtensions = preferenceRegistries.filter(e => e.showInPreferencesTab == "telemetry"); const currentLocation = navigation.location.pathname; const isActive = (route: RouteProps) => !!matchPath(currentLocation, { path: route.path, exact: route.exact }); + const extensions = this.props.userExtensions.get().filter(extension => + preferenceRegistries.some(registry => registry.extensionId.includes(extension.manifest.name) && !registry.showInPreferencesTab), + ); return ( - navigateWithoutHistoryChange({ pathname: url })}> + { + console.log("URL ", url) + navigateWithoutHistoryChange({ pathname: url }); + }}>
Preferences
@@ -83,9 +91,21 @@ class Preferences extends React.Component { {(telemetryExtensions.length > 0 || !!sentryDsn) && } - {extensions.filter(e => !e.showInPreferencesTab).length > 0 && + {preferenceRegistries.filter(e => !e.showInPreferencesTab).length > 0 && } + {extensions.length > 0 && ( +
+
+ {extensions.map(extension => ( + + ))} +
+ )}
); } @@ -104,6 +124,7 @@ class Preferences extends React.Component { + diff --git a/src/renderer/components/cluster-manager/cluster-manager.tsx b/src/renderer/components/cluster-manager/cluster-manager.tsx index 04af444c30..7ffcd69187 100644 --- a/src/renderer/components/cluster-manager/cluster-manager.tsx +++ b/src/renderer/components/cluster-manager/cluster-manager.tsx @@ -26,7 +26,7 @@ import { Redirect, Route, Switch } from "react-router"; import { disposeOnUnmount, observer } from "mobx-react"; import { BottomBar } from "./bottom-bar"; import { Catalog, previousActiveTab } from "../+catalog"; -import { Preferences } from "../+preferences"; +import Preferences from "../+preferences/preferences"; import { AddCluster } from "../+add-cluster"; import { ClusterView } from "./cluster-view"; import { GlobalPageRegistry } from "../../../extensions/registries/page-registry";