diff --git a/src/app-routes.tsx b/src/app-routes.tsx
index fdd494b..dd21576 100644
--- a/src/app-routes.tsx
+++ b/src/app-routes.tsx
@@ -2,8 +2,8 @@ import { Trans } from '@lingui/react/macro';
import { Banner, Flex, FlexItem } from '@patternfly/react-core';
import WrenchIcon from '@patternfly/react-icons/dist/esm/icons/wrench-icon';
import { type ElementType } from 'react';
-import { Navigate, Route, Routes, useLocation } from 'react-router';
-import { ExternalLink, NotFound } from 'src/components';
+import { Navigate, redirect, useLocation } from 'react-router';
+import { ErrorBoundary, ExternalLink, NotFound } from 'src/components';
import {
AboutProject,
AnsibleRemoteDetail,
@@ -365,26 +365,47 @@ const AuthHandler = ({
);
};
-export const AppRoutes = () => (
-
- {routes.map(({ beta, component, noAuth, path }, index) => (
-
- }
- key={index}
+const appRoutes = () =>
+ routes.map(({ beta, component, noAuth, path, ...rest }) => ({
+ element: (
+
- ))}
- }
- />
- } />
-
-);
+ ),
+ path: path,
+ ...rest,
+ }));
+
+const convert = (m) => {
+ const {
+ default: Component,
+ clientLoader: loader,
+ clientAction: action,
+ ...rest
+ } = m;
+ return { ...rest, loader, action, Component };
+};
+
+export const dataRoutes = [
+ {
+ id: 'root',
+ lazy: () => import('src/routes/root').then((m) => convert(m)),
+ children: [
+ {
+ errorElement: ,
+ children: [
+ {
+ index: true,
+ loader: () => redirect(formatPath(Paths.core.status)),
+ },
+ ...appRoutes(),
+ // "No matching route" is not handled by the error boundary.
+ { path: '*', element: },
+ ],
+ },
+ ],
+ },
+];
diff --git a/src/components/error-boundary.tsx b/src/components/error-boundary.tsx
new file mode 100644
index 0000000..27b2262
--- /dev/null
+++ b/src/components/error-boundary.tsx
@@ -0,0 +1,27 @@
+import { isRouteErrorResponse, useRouteError } from 'react-router';
+
+export const ErrorBoundary = () => {
+ const error = useRouteError();
+
+ if (isRouteErrorResponse(error)) {
+ return (
+ <>
+
+ {error.status} {error.statusText}
+
+ {error.data.toString()}
+ >
+ );
+ } else if (error instanceof Error) {
+ return (
+
+
Error
+
{error.message}
+
The stack trace is:
+
{error.stack}
+
+ );
+ console.error(error);
+ return Something went horribly wrong!
;
+ }
+};
diff --git a/src/components/index.ts b/src/components/index.ts
index b953c46..d9626b0 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -42,6 +42,7 @@ export { EmptyStateNoData } from './empty-state-no-data';
export { EmptyStateUnauthorized } from './empty-state-unauthorized';
export { EmptyStateNotImplemented } from './empty-state-under-construction';
export { EmptyStateXs } from './empty-state-xs';
+export { ErrorBoundary } from './error-boundary';
export { ExecutionEnvironmentHeader } from './execution-environment-header';
export { ExternalLink } from './external-link';
export { FormFieldHelper } from './form-field-helper';
diff --git a/src/entrypoint.tsx b/src/entrypoint.tsx
index 9ad9b92..c06b7fb 100644
--- a/src/entrypoint.tsx
+++ b/src/entrypoint.tsx
@@ -6,15 +6,12 @@ import '@patternfly/patternfly/patternfly.scss';
import { Button } from '@patternfly/react-core';
import { StrictMode, useEffect, useState } from 'react';
import { createRoot } from 'react-dom/client';
-import { BrowserRouter } from 'react-router';
-import { Alert, LoadingSpinner, UIVersion } from 'src/components';
-import { AppContextProvider } from './app-context';
-import { AppRoutes } from './app-routes';
+import { RouterProvider, createBrowserRouter } from 'react-router';
+import { Alert, LoadingSpinner } from 'src/components';
+import { dataRoutes } from './app-routes';
import './darkmode';
import './l10n';
-import { StandaloneLayout } from './layout';
import { configFallback, configPromise } from './ui-config';
-import { UserContextProvider } from './user-context';
// App entrypoint
@@ -94,16 +91,9 @@ function LoadConfig(_props) {
);
}
- return (
-
-
-
-
-
-
-
-
-
-
- );
+ const router = createBrowserRouter(dataRoutes, {
+ basename: config.UI_BASE_PATH,
+ });
+
+ return ;
}
diff --git a/src/layout.tsx b/src/layout.tsx
index 43c4cb5..84f9728 100644
--- a/src/layout.tsx
+++ b/src/layout.tsx
@@ -27,7 +27,7 @@ import {
SmallLogo,
StatefulDropdown,
} from 'src/components';
-import { StandaloneMenu } from './menu';
+import { PulpMenu } from './menu';
import { Paths, formatPath } from './paths';
import { useUserContext } from './user-context';
@@ -84,7 +84,7 @@ const UserDropdown = ({
/>
);
-export const StandaloneLayout = ({ children }: { children: ReactNode }) => {
+export const Layout = ({ children }: { children: ReactNode }) => {
const [aboutModalVisible, setAboutModalVisible] = useState(false);
const { credentials, clearCredentials } = useUserContext();
@@ -127,7 +127,7 @@ export const StandaloneLayout = ({ children }: { children: ReactNode }) => {
const Sidebar = (
-
+
);
diff --git a/src/menu.tsx b/src/menu.tsx
index 880b30d..614e615 100644
--- a/src/menu.tsx
+++ b/src/menu.tsx
@@ -286,7 +286,7 @@ function usePlugins() {
return plugins;
}
-export const StandaloneMenu = () => {
+export const PulpMenu = () => {
const [expandedSections, setExpandedSections] = useState([]);
const location = useLocation();
diff --git a/src/routes/root.tsx b/src/routes/root.tsx
new file mode 100644
index 0000000..417fad1
--- /dev/null
+++ b/src/routes/root.tsx
@@ -0,0 +1,22 @@
+import { Outlet, useNavigation } from 'react-router';
+import { AppContextProvider } from 'src/app-context';
+import { LoadingSpinner, UIVersion } from 'src/components';
+import { Layout } from 'src/layout';
+import { UserContextProvider } from 'src/user-context';
+
+export default function Root() {
+ const navigation = useNavigation();
+ const isNavigating = Boolean(navigation.location);
+
+ return (
+
+
+
+ {isNavigating && }
+
+
+
+
+
+ );
+}
diff --git a/src/ui-config.ts b/src/ui-config.ts
index 26f5f62..872cc43 100644
--- a/src/ui-config.ts
+++ b/src/ui-config.ts
@@ -1,13 +1,13 @@
import defaults from '../pulp-ui-config.json';
-export const configPromise = fetch('/pulp-ui-config.json').then((data) =>
- data.status > 0 && data.status < 300
- ? data.json()
- : Promise.reject(`${data.status}: ${data.statusText}`),
-);
+export const configPromise = fetch('/pulp-ui-config.json')
+ .then((data) =>
+ data.status > 0 && data.status < 300
+ ? data.json()
+ : Promise.reject(`${data.status}: ${data.statusText}`),
+ )
+ .then((data) => (config = data));
export let config = null;
export const configFallback = () => (config = defaults);
-
-configPromise.then((data) => (config = data));