Use new React Router API for the Admin UI (#19361)

This commit is contained in:
Jon Koops 2023-03-27 18:34:53 +02:00 committed by GitHub
parent 7f85453ac8
commit f0057157da
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 90 additions and 65 deletions

View file

@ -3,25 +3,24 @@ import { Page } from "@patternfly/react-core";
import type Keycloak from "keycloak-js";
import { PropsWithChildren, Suspense } from "react";
import { ErrorBoundary } from "react-error-boundary";
import { HashRouter as Router, Route, Routes } from "react-router-dom";
import { Outlet } from "react-router-dom";
import { Help } from "ui-shared";
import { Header } from "./PageHeader";
import { PageNav } from "./PageNav";
import { AlertProvider } from "./components/alert/Alerts";
import { PageBreadCrumbs } from "./components/bread-crumb/PageBreadCrumbs";
import { ErrorRenderer } from "./components/error/ErrorRenderer";
import { Help } from "ui-shared";
import { KeycloakSpinner } from "./components/keycloak-spinner/KeycloakSpinner";
import { AccessContextProvider, useAccess } from "./context/access/Access";
import { AdminClientContext } from "./context/auth/AdminClient";
import { RealmContextProvider } from "./context/realm-context/RealmContext";
import { RealmsProvider } from "./context/RealmsContext";
import { RecentRealmsProvider } from "./context/RecentRealms";
import { AccessContextProvider } from "./context/access/Access";
import { AdminClientContext } from "./context/auth/AdminClient";
import { RealmContextProvider } from "./context/realm-context/RealmContext";
import { ServerInfoProvider } from "./context/server-info/ServerInfoProvider";
import { WhoAmIContextProvider } from "./context/whoami/WhoAmI";
import { ForbiddenSection } from "./ForbiddenSection";
import { SubGroups } from "./groups/SubGroupsContext";
import { Header } from "./PageHeader";
import { PageNav } from "./PageNav";
import { AppRouteObject, routes } from "./routes";
import { AuthWall } from "./root/AuthWall";
export const mainPageContentId = "kc-main-content-page-container";
@ -35,43 +34,25 @@ const AppContexts = ({
keycloak,
adminClient,
}: PropsWithChildren<AdminClientProps>) => (
<Router>
<AdminClientContext.Provider value={{ keycloak, adminClient }}>
<WhoAmIContextProvider>
<RealmsProvider>
<RealmContextProvider>
<RecentRealmsProvider>
<AccessContextProvider>
<Help>
<AlertProvider>
<SubGroups>{children}</SubGroups>
</AlertProvider>
</Help>
</AccessContextProvider>
</RecentRealmsProvider>
</RealmContextProvider>
</RealmsProvider>
</WhoAmIContextProvider>
</AdminClientContext.Provider>
</Router>
<AdminClientContext.Provider value={{ keycloak, adminClient }}>
<WhoAmIContextProvider>
<RealmsProvider>
<RealmContextProvider>
<RecentRealmsProvider>
<AccessContextProvider>
<Help>
<AlertProvider>
<SubGroups>{children}</SubGroups>
</AlertProvider>
</Help>
</AccessContextProvider>
</RecentRealmsProvider>
</RealmContextProvider>
</RealmsProvider>
</WhoAmIContextProvider>
</AdminClientContext.Provider>
);
// If someone tries to go directly to a route they don't
// have access to, show forbidden page.
type SecuredRouteProps = { route: AppRouteObject };
const SecuredRoute = ({ route }: SecuredRouteProps) => {
const { hasAccess } = useAccess();
const accessAllowed =
route.handle.access instanceof Array
? hasAccess(...route.handle.access)
: hasAccess(route.handle.access);
if (accessAllowed)
return <Suspense fallback={<KeycloakSpinner />}>{route.element}</Suspense>;
return <ForbiddenSection permissionNeeded={route.handle.access} />;
};
export const App = ({ keycloak, adminClient }: AdminClientProps) => {
return (
<AppContexts keycloak={keycloak} adminClient={adminClient}>
@ -90,15 +71,11 @@ export const App = ({ keycloak, adminClient }: AdminClientProps) => {
}
>
<ServerInfoProvider>
<Routes>
{routes.map((route, i) => (
<Route
key={i}
path={route.path}
element={<SecuredRoute route={route} />}
/>
))}
</Routes>
<Suspense fallback={<KeycloakSpinner />}>
<AuthWall>
<Outlet />
</AuthWall>
</Suspense>
</ServerInfoProvider>
</ErrorBoundary>
</Page>

View file

@ -1,24 +1,20 @@
import "@patternfly/patternfly/patternfly-addons.css";
import "@patternfly/react-core/dist/styles/base.css";
import "@patternfly/patternfly/patternfly-addons.css";
import { StrictMode } from "react";
import { render } from "react-dom";
import { createHashRouter, RouterProvider } from "react-router-dom";
import { App } from "./App";
import { initAdminClient } from "./context/auth/AdminClient";
import { initI18n } from "./i18n";
import { RootRoute } from "./routes";
import "./index.css";
const { keycloak, adminClient } = await initAdminClient();
await initI18n(adminClient);
const router = createHashRouter([RootRoute]);
const container = document.getElementById("app");
render(
<StrictMode>
<App keycloak={keycloak} adminClient={adminClient} />
<RouterProvider router={router} />
</StrictMode>,
container
);

View file

@ -0,0 +1,39 @@
import { AccessType } from "@keycloak/keycloak-admin-client/lib/defs/whoAmIRepresentation";
import { useMatches } from "react-router";
import { ForbiddenSection } from "../ForbiddenSection";
import { useAccess } from "../context/access/Access";
function hasProp<K extends PropertyKey>(
data: object,
prop: K
): data is Record<K, unknown> {
return prop in data;
}
export const AuthWall = ({ children }: any) => {
const matches = useMatches();
const { hasAccess } = useAccess();
const permissionNeeded = matches.flatMap(({ handle }) => {
if (
typeof handle !== "object" ||
handle === null ||
!hasProp(handle, "access")
) {
return [];
}
if (Array.isArray(handle.access)) {
return handle.access as AccessType[];
}
return [handle.access] as AccessType[];
});
return hasAccess(...permissionNeeded) ? (
children
) : (
<ForbiddenSection permissionNeeded={permissionNeeded} />
);
};

View file

@ -1,8 +1,12 @@
import type { AccessType } from "@keycloak/keycloak-admin-client/lib/defs/whoAmIRepresentation";
import type { TFunction } from "i18next";
import type { ComponentType } from "react";
import type { NonIndexRouteObject } from "react-router";
import type { NonIndexRouteObject, RouteObject } from "react-router";
import { initAdminClient } from "./context/auth/AdminClient";
import { initI18n } from "./i18n";
import { App } from "./App";
import { PageNotFoundSection } from "./PageNotFoundSection";
import authenticationRoutes from "./authentication/routes";
import clientScopesRoutes from "./client-scopes/routes";
import clientRoutes from "./clients/routes";
@ -10,7 +14,6 @@ import dashboardRoutes from "./dashboard/routes";
import eventRoutes from "./events/routes";
import groupsRoutes from "./groups/routes";
import identityProviders from "./identity-providers/routes";
import { PageNotFoundSection } from "./PageNotFoundSection";
import realmRoleRoutes from "./realm-roles/routes";
import realmSettingRoutes from "./realm-settings/routes";
import realmRoutes from "./realm/routes";
@ -28,7 +31,7 @@ export interface AppRouteObject extends NonIndexRouteObject {
handle: AppRouteObjectHandle;
}
const NotFoundRoute: AppRouteObject = {
export const NotFoundRoute: AppRouteObject = {
path: "*",
element: <PageNotFoundSection />,
handle: {
@ -52,3 +55,13 @@ export const routes: AppRouteObject[] = [
...dashboardRoutes,
NotFoundRoute,
];
const { keycloak, adminClient } = await initAdminClient();
await initI18n(adminClient);
export const RootRoute: RouteObject = {
path: "/",
element: <App keycloak={keycloak} adminClient={adminClient} />,
children: routes,
};