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:
Erik Jan de Wit 2023-11-30 16:20:49 +01:00 committed by GitHub
parent 4279bbc6b5
commit 2e4cd78f61
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 207 additions and 1 deletions

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

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

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

View file

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

View file

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