use content json also for own menu (#25744)

* use content json also for own menu

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* change isHidden with isVisible

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 2024-01-10 09:38:02 +01:00 committed by GitHub
parent 41dd1d2161
commit 2163fae7ae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 102 additions and 220 deletions

View file

@ -0,0 +1,26 @@
[
{ "label": "personalInfo", "path": "/" },
{
"label": "accountSecurity",
"children": [
{ "label": "signingIn", "path": "account-security/signing-in" },
{ "label": "deviceActivity", "path": "account-security/device-activity" },
{
"label": "linkedAccounts",
"path": "account-security/linked-accounts",
"isVisible": "isLinkedAccountsEnabled"
}
]
},
{ "label": "applications", "path": "applications" },
{
"label": "groups",
"path": "groups",
"isVisible": "isViewGroupsEnabled"
},
{
"label": "resources",
"path": "resources",
"isVisible": "isMyResourcesEnabled"
}
]

View file

@ -2,29 +2,29 @@ import { Spinner } from "@patternfly/react-core";
import { Suspense, lazy, useMemo, useState } from "react";
import { useParams } from "react-router-dom";
import { environment } from "../environment";
import { ContentComponentParams } from "../routes";
import { joinPath } from "../utils/joinPath";
import { usePromise } from "../utils/usePromise";
import { fetchContentJson } from "./ContentRenderer";
import { ContentItem, ModulePageDef, isExpansion } from "./content";
import { ContentComponentParams } from "../routes";
import fetchContentJson from "./fetchContent";
import { MenuItem } from "../root/PageNav";
function findComponent(
content: ContentItem[],
content: MenuItem[],
componentId: string,
): string | undefined {
for (const item of content) {
if ("path" in item && item.path === componentId) {
return (item as ModulePageDef).modulePath;
if ("path" in item && item.path === componentId && "modulePath" in item) {
return item.modulePath;
}
if (isExpansion(item)) {
return findComponent(item.content, componentId);
if ("children" in item) {
return findComponent(item.children, componentId);
}
}
return undefined;
}
const ContentComponent = () => {
const [content, setContent] = useState<ContentItem[]>();
const [content, setContent] = useState<MenuItem[]>();
const { componentId } = useParams<ContentComponentParams>();
usePromise((signal) => fetchContentJson({ signal }), setContent);

View file

@ -1,101 +0,0 @@
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

@ -1,38 +0,0 @@
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

@ -0,0 +1,14 @@
import { CallOptions } from "../api/methods";
import { environment } from "../environment";
import { MenuItem } from "../root/PageNav";
import { joinPath } from "../utils/joinPath";
export default async function fetchContentJson(
opts: CallOptions = {},
): Promise<MenuItem[]> {
const response = await fetch(
joinPath(environment.resourceUrl, "/content.json"),
opts,
);
return await response.json();
}

View file

@ -1,3 +1,17 @@
export type Feature = {
isRegistrationEmailAsUsername: boolean;
isEditUserNameAllowed: boolean;
isInternationalizationEnabled: boolean;
isLinkedAccountsEnabled: boolean;
isEventsEnabled: boolean;
isMyResourcesEnabled: boolean;
isTotpConfigured: boolean;
deleteAccountAllowed: boolean;
updateEmailFeatureEnabled: boolean;
updateEmailActionEnabled: boolean;
isViewGroupsEnabled: boolean;
};
export type Environment = {
/** The URL to the root of the auth server. */
authUrl: string;
@ -14,19 +28,7 @@ export type Environment = {
/** The locale of the user */
locale: string;
/** Feature flags */
features: {
isRegistrationEmailAsUsername: boolean;
isEditUserNameAllowed: boolean;
isInternationalizationEnabled: boolean;
isLinkedAccountsEnabled: boolean;
isEventsEnabled: boolean;
isMyResourcesEnabled: boolean;
isTotpConfigured: boolean;
deleteAccountAllowed: boolean;
updateEmailFeatureEnabled: boolean;
updateEmailActionEnabled: boolean;
isViewGroupsEnabled: boolean;
};
features: Feature;
};
// The default environment, used during development.

View file

@ -4,11 +4,14 @@ import {
NavItem,
NavList,
PageSidebar,
Spinner,
} from "@patternfly/react-core";
import {
PropsWithChildren,
MouseEvent as ReactMouseEvent,
Suspense,
useMemo,
useState,
} from "react";
import { useTranslation } from "react-i18next";
import {
@ -18,79 +21,55 @@ import {
useLinkClickHandler,
useLocation,
} from "react-router-dom";
import { ContentMenu } from "../content/ContentRenderer";
import { environment } from "../environment";
import fetchContentJson from "../content/fetchContent";
import { TFuncKey } from "../i18n";
import { usePromise } from "../utils/usePromise";
import { Feature, environment } from "../environment";
type RootMenuItem = {
label: TFuncKey;
path: string;
isHidden?: boolean;
isHidden?: keyof Feature;
modulePath?: string;
};
type MenuItemWithChildren = {
label: TFuncKey;
children: MenuItem[];
isHidden?: boolean;
isHidden?: keyof Feature;
};
type MenuItem = RootMenuItem | MenuItemWithChildren;
export type MenuItem = RootMenuItem | MenuItemWithChildren;
const menuItems: MenuItem[] = [
{
label: "personalInfo",
path: "/",
},
{
label: "accountSecurity",
children: [
{
label: "signingIn",
path: "account-security/signing-in",
},
{
label: "deviceActivity",
path: "account-security/device-activity",
},
{
label: "linkedAccounts",
path: "account-security/linked-accounts",
isHidden: !environment.features.isLinkedAccountsEnabled,
},
],
},
{
label: "applications",
path: "applications",
},
{
label: "groups",
path: "groups",
isHidden: !environment.features.isViewGroupsEnabled,
},
{
label: "resources",
path: "resources",
isHidden: !environment.features.isMyResourcesEnabled,
},
];
export const PageNav = () => {
const [menuItems, setMenuItems] = useState<MenuItem[]>();
export const PageNav = () => (
usePromise((signal) => fetchContentJson({ signal }), setMenuItems);
return (
<PageSidebar
nav={
<Nav>
<NavList>
<ContentMenu />
<Suspense fallback={<Spinner />}>
{menuItems
.filter((menuItem) => !menuItem.isHidden)
?.filter((menuItem) =>
menuItem.isHidden
? environment.features[menuItem.isHidden]
: true,
)
.map((menuItem) => (
<NavMenuItem key={menuItem.label as string} menuItem={menuItem} />
<NavMenuItem
key={menuItem.label as string}
menuItem={menuItem}
/>
))}
</Suspense>
</NavList>
</Nav>
}
/>
);
};
type NavMenuItemProps = {
menuItem: MenuItem;