diff --git a/js/apps/account-ui/src/content/ContentComponent.tsx b/js/apps/account-ui/src/content/ContentComponent.tsx new file mode 100644 index 0000000000..15efefbb36 --- /dev/null +++ b/js/apps/account-ui/src/content/ContentComponent.tsx @@ -0,0 +1,54 @@ +import { Spinner } from "@patternfly/react-core"; +import { Suspense, lazy, useMemo, useState } from "react"; +import { useParams } from "react-router-dom"; +import { environment } from "../environment"; +import { joinPath } from "../utils/joinPath"; +import { usePromise } from "../utils/usePromise"; +import { fetchContentJson } from "./ContentRenderer"; +import { ContentItem, ModulePageDef, isExpansion } from "./content"; +import { ContentComponentParams } from "../routes"; + +function findComponent( + content: ContentItem[], + componentId: string, +): string | undefined { + for (const item of content) { + if ("path" in item && item.path === componentId) { + return (item as ModulePageDef).modulePath; + } + if (isExpansion(item)) { + return findComponent(item.content, componentId); + } + } + return undefined; +} + +const ContentComponent = () => { + const [content, setContent] = useState(); + const { componentId } = useParams(); + + usePromise((signal) => fetchContentJson({ signal }), setContent); + const modulePath = useMemo( + () => findComponent(content || [], componentId!), + [content, componentId], + ); + + return ( + }> + {modulePath && } + + ); +}; + +type ComponentProps = { + modulePath: string; +}; + +const Component = ({ modulePath }: ComponentProps) => { + const Element = lazy( + () => import(joinPath(environment.resourceUrl, modulePath)), + ); + return ; +}; + +export default ContentComponent; diff --git a/js/apps/account-ui/src/content/ContentRenderer.tsx b/js/apps/account-ui/src/content/ContentRenderer.tsx new file mode 100644 index 0000000000..7a4bdbbbd6 --- /dev/null +++ b/js/apps/account-ui/src/content/ContentRenderer.tsx @@ -0,0 +1,101 @@ +import { NavExpandable, Spinner } from "@patternfly/react-core"; +import { TFunction } from "i18next"; +import { Suspense, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useParams } from "react-router-dom"; +import { CallOptions } from "../api/methods"; +import { environment } from "../environment"; +import { TFuncKey } from "../i18n"; +import { NavLink } from "../root/PageNav"; +import { ContentComponentParams } from "../routes"; +import { joinPath } from "../utils/joinPath"; +import { usePromise } from "../utils/usePromise"; +import { + ContentItem, + Expansion, + PageDef, + isChildOf, + isExpansion, +} from "./content"; + +export async function fetchContentJson( + opts: CallOptions = {}, +): Promise { + const response = await fetch( + joinPath(environment.resourceUrl, "/content.json"), + opts, + ); + return await response.json(); +} + +function createNavItem(page: PageDef, activePage: string, t: TFunction) { + return ( + + {t(page.label as TFuncKey)} + + ); +} + +function createExpandableNav( + item: Expansion, + t: TFunction, + activePage: string, + groupNum: number, +) { + return ( + + {createNavItems(t, activePage, item.content, groupNum + 1)} + + ); +} + +function createNavItems( + t: TFunction, + activePage: string, + contentParam: ContentItem[], + groupNum: number, +) { + return contentParam.map((item: ContentItem) => { + if (isExpansion(item)) { + return createExpandableNav(item, t, activePage!, groupNum); + } else { + const page: PageDef = item as PageDef; + return createNavItem(page, activePage, t); + } + }); +} + +type MenuProps = { + content: ContentItem[]; +}; + +const Menu = ({ content }: MenuProps) => { + const { t } = useTranslation(); + const { componentId: activePage } = useParams(); + + const groupNum = 0; + return content.map((item) => { + if (isExpansion(item)) { + return createExpandableNav(item, t, activePage!, groupNum); + } else { + const page: PageDef = item as PageDef; + return createNavItem(page, activePage!, t); + } + }); +}; + +export const ContentMenu = () => { + const [content, setContent] = useState(); + + usePromise((signal) => fetchContentJson({ signal }), setContent); + + return ( + }> + {content && } + + ); +}; diff --git a/js/apps/account-ui/src/content/content.ts b/js/apps/account-ui/src/content/content.ts new file mode 100644 index 0000000000..44b46fd7c4 --- /dev/null +++ b/js/apps/account-ui/src/content/content.ts @@ -0,0 +1,38 @@ +export interface ContentItem { + id?: string; + label: string; + labelParams?: string[]; + hidden?: string; +} + +export interface Expansion extends ContentItem { + content: ContentItem[]; +} + +export interface PageDef extends ContentItem { + path: string; +} + +export interface ComponentPageDef extends PageDef { + component: React.ComponentType; +} + +export interface ModulePageDef extends PageDef { + modulePath: string; + componentName: string; +} + +export function isExpansion( + contentItem: ContentItem, +): contentItem is Expansion { + return "content" in contentItem; +} + +export function isChildOf(parent: Expansion, child: string): boolean { + for (const item of parent.content) { + if (isExpansion(item) && isChildOf(item, child)) return true; + if ("path" in item && item.path === child) return true; + } + + return false; +} diff --git a/js/apps/account-ui/src/root/PageNav.tsx b/js/apps/account-ui/src/root/PageNav.tsx index d4fd853fbb..dcba03455d 100644 --- a/js/apps/account-ui/src/root/PageNav.tsx +++ b/js/apps/account-ui/src/root/PageNav.tsx @@ -18,6 +18,7 @@ import { useLinkClickHandler, useLocation, } from "react-router-dom"; +import { ContentMenu } from "../content/ContentRenderer"; import { environment } from "../environment"; import { TFuncKey } from "../i18n"; @@ -79,6 +80,7 @@ export const PageNav = () => ( nav={