diff --git a/src/features/preferences/renderer/preference-items/current-preference-tab-composite.injectable.ts b/src/features/preferences/renderer/preference-items/current-preference-tab-composite.injectable.ts new file mode 100644 index 0000000000..d2d13c9822 --- /dev/null +++ b/src/features/preferences/renderer/preference-items/current-preference-tab-composite.injectable.ts @@ -0,0 +1,73 @@ +/** + * 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 { computedInjectManyInjectable } from "@ogre-tools/injectable-extension-for-mobx"; +import { computed } from "mobx"; +import type { PreferenceTab, PreferenceTypes } from "./preference-item-injection-token"; +import { preferenceItemInjectionToken } from "./preference-item-injection-token"; +import type { Composite } from "../../../application-menu/main/menu-items/get-composite/get-composite"; +import getComposite from "../../../application-menu/main/menu-items/get-composite/get-composite"; +import routePathParametersInjectable from "../../../../renderer/routes/route-path-parameters.injectable"; +import preferencesRouteInjectable from "../preferences-route.injectable"; +import { filter, find } from "lodash/fp"; +import { pipeline } from "@ogre-tools/fp"; + +interface PreferenceTabsRoot { + kind: "preference-tabs-root"; + id: string; + parentId: undefined; + isShown: true; +} + +const preferenceTabRoot: PreferenceTabsRoot = { + kind: "preference-tabs-root" as const, + id: "preference-tabs", + parentId: undefined, + isShown: true, +}; + +const currentPreferenceTabCompositeInjectable = getInjectable({ + id: "current-preference-page-composite", + + instantiate: (di) => { + const computedInjectMany = di.inject(computedInjectManyInjectable); + const preferenceItems = computedInjectMany(preferenceItemInjectionToken); + const preferencesRoute = di.inject(preferencesRouteInjectable); + const routePathParameters = di.inject(routePathParametersInjectable, preferencesRoute); + + return computed(() => { + const { preferenceTabId } = routePathParameters.get(); + + const tabComposite = pipeline( + [preferenceTabRoot, ...preferenceItems.get()], + filter(isShown), + (items) => getComposite({ source: items }), + (rootComposite) => rootComposite.children, + filter(isPreferenceTab), + find(hasMatchingPathId(preferenceTabId)), + ); + + if (!tabComposite) { + throw new Error( + `Tried to open preferences but no tab exists for ID "${preferenceTabId}"`, + ); + } + + return tabComposite; + }); + }, +}); + +const isShown = (item: PreferenceTypes) => item.isShown ?? true; + +const isPreferenceTab = (composite: Composite): composite is Composite => + composite.value.kind === "tab"; + +const hasMatchingPathId = + (preferenceTabId: string) => + ({ value: { pathId }}: Composite) => + pathId === preferenceTabId; + +export default currentPreferenceTabCompositeInjectable; diff --git a/src/features/preferences/renderer/preference-items/preference-item-injection-token.ts b/src/features/preferences/renderer/preference-items/preference-item-injection-token.ts new file mode 100644 index 0000000000..d36ebc48ed --- /dev/null +++ b/src/features/preferences/renderer/preference-items/preference-item-injection-token.ts @@ -0,0 +1,53 @@ +/** + * 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 React from "react"; + +export type PreferenceItemComponent = React.ComponentType<{ children: React.ReactElement }>; + +export interface PreferenceTab { + kind: "tab"; + id: string; + parentId: "preference-tabs"; + pathId: string; + testId: string; + label: string; + orderNumber: number; + isShown?: boolean; +} + +export interface PreferencePage { + kind: "page"; + id: string; + parentId: string; + isShown?: boolean; + childrenSeparator?: () => React.ReactElement; + Component: PreferenceItemComponent; +} + +export interface PreferenceGroup { + kind: "group"; + id: string; + parentId: string; + isShown?: boolean; + childrenSeparator?: () => React.ReactElement; +} + +export interface PreferenceItem { + kind: "item"; + Component: PreferenceItemComponent; + id: string; + parentId: string; + orderNumber: number; + isShown?: boolean; + childrenSeparator?: () => React.ReactElement; +} + +export type PreferenceTypes = PreferenceTab | PreferenceItem | PreferencePage | PreferenceGroup; + +export const preferenceItemInjectionToken = getInjectionToken({ + id: "preference-item-injection-token", +}); + diff --git a/src/features/preferences/renderer/preferences-route-component.injectable.ts b/src/features/preferences/renderer/preferences-route-component.injectable.ts new file mode 100644 index 0000000000..17c7e44a2e --- /dev/null +++ b/src/features/preferences/renderer/preferences-route-component.injectable.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { routeSpecificComponentInjectionToken } from "../../../renderer/routes/route-specific-component-injection-token"; +import { Preferences } from "./preferences"; +import preferencesRouteInjectable from "./preferences-route.injectable"; + +const preferencesRouteComponentInjectable = getInjectable({ + id: "preferences-route-component", + + instantiate: (di) => ({ + route: di.inject(preferencesRouteInjectable), + Component: Preferences, + }), + + injectionToken: routeSpecificComponentInjectionToken, +}); + +export default preferencesRouteComponentInjectable; diff --git a/src/features/preferences/renderer/preferences-route.injectable.ts b/src/features/preferences/renderer/preferences-route.injectable.ts new file mode 100644 index 0000000000..bb54c8e7aa --- /dev/null +++ b/src/features/preferences/renderer/preferences-route.injectable.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import { getInjectable } from "@ogre-tools/injectable"; +import { computed } from "mobx"; +import { frontEndRouteInjectionToken } from "../../../common/front-end-routing/front-end-route-injection-token"; + +const preferencesRouteInjectable = getInjectable({ + id: "preferences-route", + + instantiate: () => ({ + path: "/preferences2/:preferenceTabId", + clusterFrame: false, + isEnabled: computed(() => true), + }), + + injectionToken: frontEndRouteInjectionToken, +}); + +export default preferencesRouteInjectable; diff --git a/src/features/preferences/renderer/preferences.tsx b/src/features/preferences/renderer/preferences.tsx new file mode 100644 index 0000000000..25fb626ada --- /dev/null +++ b/src/features/preferences/renderer/preferences.tsx @@ -0,0 +1,103 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import "../../../renderer/components/+preferences/preferences.scss"; +import React from "react"; + +import { SettingLayout } from "../../../renderer/components/layout/setting-layout"; +import { PreferencesNavigation } from "../../../renderer/components/+preferences/preferences-navigation/preferences-navigation"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import closePreferencesInjectable from "../../../renderer/components/+preferences/close-preferences.injectable"; +import currentPreferenceTabCompositeInjectable from "./preference-items/current-preference-tab-composite.injectable"; +import type { Composite } from "../../application-menu/main/menu-items/get-composite/get-composite"; +import type { PreferenceTypes, PreferenceTab } from "./preference-items/preference-item-injection-token"; +import type { IComputedValue } from "mobx"; +import { Map } from "../../../renderer/components/map/map"; + +interface Dependencies { + closePreferences: () => void; + pageComposite: IComputedValue>; +} + +const NonInjectedPreferences = ({ + closePreferences, + pageComposite, +}: Dependencies) => { + const composite = pageComposite.get(); + + return ( + } + className="Preferences" + contentGaps={false} + closeButtonProps={{ "data-testid": "close-preferences" }} + back={closePreferences} + data-testid={composite.value.testId} + > + {toPreferenceItemHierarchy(composite)} + + ); +}; + +const toPreferenceItemHierarchy = (composite: Composite) => { + switch (composite.value.kind) { + + case "group": { + return ( +
+ + {toPreferenceItemHierarchy} + +
+ ); + } + + case "item": + + // eslint-disable-next-line no-fallthrough + case "page": { + const Component = composite.value.Component; + + return ( + + + {toPreferenceItemHierarchy} + + + ); + } + + case "tab": { + return ( + + {toPreferenceItemHierarchy} + + ); + } + + default: { + // Note: this will fail at transpilation time, if all ApplicationMenuItemTypes + // are not handled in switch/case. + const _exhaustiveCheck: never = composite.value; + + // Note: this code is unreachable, it is here to make ts not complain about + // _exhaustiveCheck not being used. + // See: https://www.typescriptlang.org/docs/handbook/2/narrowing.html#exhaustiveness-checking + throw new Error(`Tried to create preferences, but foreign item was encountered: ${_exhaustiveCheck} ${composite.value}`); + } + } +}; + +export const Preferences = withInjectables( + NonInjectedPreferences, + + { + getProps: (di, props) => ({ + closePreferences: di.inject(closePreferencesInjectable), + pageComposite: di.inject(currentPreferenceTabCompositeInjectable), + ...props, + }), + }, +); +