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:
parent
41dd1d2161
commit
2163fae7ae
7 changed files with 102 additions and 220 deletions
26
js/apps/account-ui/public/content.json
Normal file
26
js/apps/account-ui/public/content.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
|
@ -2,29 +2,29 @@ import { Spinner } from "@patternfly/react-core";
|
||||||
import { Suspense, lazy, useMemo, useState } from "react";
|
import { Suspense, lazy, useMemo, useState } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { environment } from "../environment";
|
import { environment } from "../environment";
|
||||||
|
import { ContentComponentParams } from "../routes";
|
||||||
import { joinPath } from "../utils/joinPath";
|
import { joinPath } from "../utils/joinPath";
|
||||||
import { usePromise } from "../utils/usePromise";
|
import { usePromise } from "../utils/usePromise";
|
||||||
import { fetchContentJson } from "./ContentRenderer";
|
import fetchContentJson from "./fetchContent";
|
||||||
import { ContentItem, ModulePageDef, isExpansion } from "./content";
|
import { MenuItem } from "../root/PageNav";
|
||||||
import { ContentComponentParams } from "../routes";
|
|
||||||
|
|
||||||
function findComponent(
|
function findComponent(
|
||||||
content: ContentItem[],
|
content: MenuItem[],
|
||||||
componentId: string,
|
componentId: string,
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
for (const item of content) {
|
for (const item of content) {
|
||||||
if ("path" in item && item.path === componentId) {
|
if ("path" in item && item.path === componentId && "modulePath" in item) {
|
||||||
return (item as ModulePageDef).modulePath;
|
return item.modulePath;
|
||||||
}
|
}
|
||||||
if (isExpansion(item)) {
|
if ("children" in item) {
|
||||||
return findComponent(item.content, componentId);
|
return findComponent(item.children, componentId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ContentComponent = () => {
|
const ContentComponent = () => {
|
||||||
const [content, setContent] = useState<ContentItem[]>();
|
const [content, setContent] = useState<MenuItem[]>();
|
||||||
const { componentId } = useParams<ContentComponentParams>();
|
const { componentId } = useParams<ContentComponentParams>();
|
||||||
|
|
||||||
usePromise((signal) => fetchContentJson({ signal }), setContent);
|
usePromise((signal) => fetchContentJson({ signal }), setContent);
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -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;
|
|
||||||
}
|
|
14
js/apps/account-ui/src/content/fetchContent.ts
Normal file
14
js/apps/account-ui/src/content/fetchContent.ts
Normal 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();
|
||||||
|
}
|
|
@ -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 = {
|
export type Environment = {
|
||||||
/** The URL to the root of the auth server. */
|
/** The URL to the root of the auth server. */
|
||||||
authUrl: string;
|
authUrl: string;
|
||||||
|
@ -14,19 +28,7 @@ export type Environment = {
|
||||||
/** The locale of the user */
|
/** The locale of the user */
|
||||||
locale: string;
|
locale: string;
|
||||||
/** Feature flags */
|
/** Feature flags */
|
||||||
features: {
|
features: Feature;
|
||||||
isRegistrationEmailAsUsername: boolean;
|
|
||||||
isEditUserNameAllowed: boolean;
|
|
||||||
isInternationalizationEnabled: boolean;
|
|
||||||
isLinkedAccountsEnabled: boolean;
|
|
||||||
isEventsEnabled: boolean;
|
|
||||||
isMyResourcesEnabled: boolean;
|
|
||||||
isTotpConfigured: boolean;
|
|
||||||
deleteAccountAllowed: boolean;
|
|
||||||
updateEmailFeatureEnabled: boolean;
|
|
||||||
updateEmailActionEnabled: boolean;
|
|
||||||
isViewGroupsEnabled: boolean;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// The default environment, used during development.
|
// The default environment, used during development.
|
||||||
|
|
|
@ -4,11 +4,14 @@ import {
|
||||||
NavItem,
|
NavItem,
|
||||||
NavList,
|
NavList,
|
||||||
PageSidebar,
|
PageSidebar,
|
||||||
|
Spinner,
|
||||||
} from "@patternfly/react-core";
|
} from "@patternfly/react-core";
|
||||||
import {
|
import {
|
||||||
PropsWithChildren,
|
PropsWithChildren,
|
||||||
MouseEvent as ReactMouseEvent,
|
MouseEvent as ReactMouseEvent,
|
||||||
|
Suspense,
|
||||||
useMemo,
|
useMemo,
|
||||||
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
|
@ -18,79 +21,55 @@ import {
|
||||||
useLinkClickHandler,
|
useLinkClickHandler,
|
||||||
useLocation,
|
useLocation,
|
||||||
} from "react-router-dom";
|
} from "react-router-dom";
|
||||||
import { ContentMenu } from "../content/ContentRenderer";
|
import fetchContentJson from "../content/fetchContent";
|
||||||
import { environment } from "../environment";
|
|
||||||
import { TFuncKey } from "../i18n";
|
import { TFuncKey } from "../i18n";
|
||||||
|
import { usePromise } from "../utils/usePromise";
|
||||||
|
import { Feature, environment } from "../environment";
|
||||||
|
|
||||||
type RootMenuItem = {
|
type RootMenuItem = {
|
||||||
label: TFuncKey;
|
label: TFuncKey;
|
||||||
path: string;
|
path: string;
|
||||||
isHidden?: boolean;
|
isHidden?: keyof Feature;
|
||||||
|
modulePath?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type MenuItemWithChildren = {
|
type MenuItemWithChildren = {
|
||||||
label: TFuncKey;
|
label: TFuncKey;
|
||||||
children: MenuItem[];
|
children: MenuItem[];
|
||||||
isHidden?: boolean;
|
isHidden?: keyof Feature;
|
||||||
};
|
};
|
||||||
|
|
||||||
type MenuItem = RootMenuItem | MenuItemWithChildren;
|
export type MenuItem = RootMenuItem | MenuItemWithChildren;
|
||||||
|
|
||||||
const menuItems: MenuItem[] = [
|
export const PageNav = () => {
|
||||||
{
|
const [menuItems, setMenuItems] = useState<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 = () => (
|
usePromise((signal) => fetchContentJson({ signal }), setMenuItems);
|
||||||
<PageSidebar
|
return (
|
||||||
nav={
|
<PageSidebar
|
||||||
<Nav>
|
nav={
|
||||||
<NavList>
|
<Nav>
|
||||||
<ContentMenu />
|
<NavList>
|
||||||
{menuItems
|
<Suspense fallback={<Spinner />}>
|
||||||
.filter((menuItem) => !menuItem.isHidden)
|
{menuItems
|
||||||
.map((menuItem) => (
|
?.filter((menuItem) =>
|
||||||
<NavMenuItem key={menuItem.label as string} menuItem={menuItem} />
|
menuItem.isHidden
|
||||||
))}
|
? environment.features[menuItem.isHidden]
|
||||||
</NavList>
|
: true,
|
||||||
</Nav>
|
)
|
||||||
}
|
.map((menuItem) => (
|
||||||
/>
|
<NavMenuItem
|
||||||
);
|
key={menuItem.label as string}
|
||||||
|
menuItem={menuItem}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Suspense>
|
||||||
|
</NavList>
|
||||||
|
</Nav>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
type NavMenuItemProps = {
|
type NavMenuItemProps = {
|
||||||
menuItem: MenuItem;
|
menuItem: MenuItem;
|
||||||
|
|
Loading…
Reference in a new issue