extend the account-ui with a content.json (#24885)
* extend the account-ui with a content.json Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * use click nav Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> * bit more performant Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> --------- Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
This commit is contained in:
parent
4279bbc6b5
commit
2e4cd78f61
5 changed files with 207 additions and 1 deletions
54
js/apps/account-ui/src/content/ContentComponent.tsx
Normal file
54
js/apps/account-ui/src/content/ContentComponent.tsx
Normal file
|
@ -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<ContentItem[]>();
|
||||
const { componentId } = useParams<ContentComponentParams>();
|
||||
|
||||
usePromise((signal) => fetchContentJson({ signal }), setContent);
|
||||
const modulePath = useMemo(
|
||||
() => findComponent(content || [], componentId!),
|
||||
[content, componentId],
|
||||
);
|
||||
|
||||
return (
|
||||
<Suspense fallback={<Spinner />}>
|
||||
{modulePath && <Component modulePath={modulePath} />}
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
type ComponentProps = {
|
||||
modulePath: string;
|
||||
};
|
||||
|
||||
const Component = ({ modulePath }: ComponentProps) => {
|
||||
const Element = lazy(
|
||||
() => import(joinPath(environment.resourceUrl, modulePath)),
|
||||
);
|
||||
return <Element />;
|
||||
};
|
||||
|
||||
export default ContentComponent;
|
101
js/apps/account-ui/src/content/ContentRenderer.tsx
Normal file
101
js/apps/account-ui/src/content/ContentRenderer.tsx
Normal file
|
@ -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<ContentItem[]> {
|
||||
const response = await fetch(
|
||||
joinPath(environment.resourceUrl, "/content.json"),
|
||||
opts,
|
||||
);
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
function createNavItem(page: PageDef, activePage: string, t: TFunction) {
|
||||
return (
|
||||
<NavLink to={"content/" + page.path} isActive={activePage === page.path}>
|
||||
{t(page.label as TFuncKey)}
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
|
||||
function createExpandableNav(
|
||||
item: Expansion,
|
||||
t: TFunction,
|
||||
activePage: string,
|
||||
groupNum: number,
|
||||
) {
|
||||
return (
|
||||
<NavExpandable
|
||||
key={item.id}
|
||||
title={t(item.label as TFuncKey)}
|
||||
isExpanded={isChildOf(item, activePage)}
|
||||
>
|
||||
{createNavItems(t, activePage, item.content, groupNum + 1)}
|
||||
</NavExpandable>
|
||||
);
|
||||
}
|
||||
|
||||
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<ContentComponentParams>();
|
||||
|
||||
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<ContentItem[]>();
|
||||
|
||||
usePromise((signal) => fetchContentJson({ signal }), setContent);
|
||||
|
||||
return (
|
||||
<Suspense fallback={<Spinner />}>
|
||||
{content && <Menu content={content} />}
|
||||
</Suspense>
|
||||
);
|
||||
};
|
38
js/apps/account-ui/src/content/content.ts
Normal file
38
js/apps/account-ui/src/content/content.ts
Normal file
|
@ -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;
|
||||
}
|
|
@ -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={
|
||||
<Nav>
|
||||
<NavList>
|
||||
<ContentMenu />
|
||||
{menuItems
|
||||
.filter((menuItem) => !menuItem.isHidden)
|
||||
.map((menuItem) => (
|
||||
|
@ -138,7 +140,7 @@ type NavLinkProps = {
|
|||
isActive: boolean;
|
||||
};
|
||||
|
||||
const NavLink = ({
|
||||
export const NavLink = ({
|
||||
to,
|
||||
isActive,
|
||||
children,
|
||||
|
|
|
@ -11,6 +11,7 @@ const Applications = lazy(() => import("./applications/Applications"));
|
|||
const Groups = lazy(() => import("./groups/Groups"));
|
||||
const PersonalInfo = lazy(() => import("./personal-info/PersonalInfo"));
|
||||
const Resources = lazy(() => import("./resources/Resources"));
|
||||
const ContentComponent = lazy(() => import("./content/ContentComponent"));
|
||||
|
||||
export const DeviceActivityRoute: RouteObject = {
|
||||
path: "account-security/device-activity",
|
||||
|
@ -42,6 +43,15 @@ export const ResourcesRoute: RouteObject = {
|
|||
element: <Resources />,
|
||||
};
|
||||
|
||||
export type ContentComponentParams = {
|
||||
componentId: string;
|
||||
};
|
||||
|
||||
export const ContentRoute: RouteObject = {
|
||||
path: "/content/:componentId",
|
||||
element: <ContentComponent />,
|
||||
};
|
||||
|
||||
export const PersonalInfoRoute: IndexRouteObject = {
|
||||
index: true,
|
||||
element: <PersonalInfo />,
|
||||
|
@ -60,6 +70,7 @@ export const RootRoute: RouteObject = {
|
|||
GroupsRoute,
|
||||
PersonalInfoRoute,
|
||||
ResourcesRoute,
|
||||
ContentRoute,
|
||||
],
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue