diff --git a/frontend/src/pages/external/ExternalRoutes.tsx b/frontend/src/pages/external/ExternalRoutes.tsx
index 6527c6a257..8e08cc5d63 100644
--- a/frontend/src/pages/external/ExternalRoutes.tsx
+++ b/frontend/src/pages/external/ExternalRoutes.tsx
@@ -1,11 +1,12 @@
import React from 'react';
-import { Navigate, Route, Routes } from 'react-router-dom';
+import { Route, Routes } from 'react-router-dom';
import PipelinesSdkRedirect from './redirectComponents/PipelinesSdkRedirects';
+import ExternalRedirectNotFound from './redirectComponents/ExternalRedirectNotFound';
const ExternalRoutes: React.FC = () => (
} />
- } />
+ } />
);
diff --git a/frontend/src/pages/external/RedirectErrorState.tsx b/frontend/src/pages/external/RedirectErrorState.tsx
new file mode 100644
index 0000000000..de5f10b1a2
--- /dev/null
+++ b/frontend/src/pages/external/RedirectErrorState.tsx
@@ -0,0 +1,106 @@
+import {
+ PageSection,
+ EmptyState,
+ EmptyStateVariant,
+ EmptyStateBody,
+ EmptyStateFooter,
+ EmptyStateActions,
+ Button,
+} from '@patternfly/react-core';
+import { ExclamationCircleIcon } from '@patternfly/react-icons';
+import React from 'react';
+import { useNavigate } from 'react-router-dom';
+import { EitherOrNone } from '~/typeHelpers';
+
+type RedirectErrorStateProps = {
+ title?: string;
+ errorMessage?: string;
+} & EitherOrNone<
+ {
+ fallbackUrl: string;
+ fallbackText: string;
+ },
+ {
+ actions: React.ReactNode | React.ReactNode[];
+ }
+>;
+
+/**
+ * A component that displays an error state with optional title, message and actions
+ * Used for showing redirect/navigation errors with fallback options
+ *
+ * Props for the RedirectErrorState component
+ * @property {string} [title] - Optional title text to display in the error state
+ * @property {string} [errorMessage] - Optional error message to display
+ * @property {string} [fallbackUrl] - URL to navigate to when fallback button is clicked
+ * @property {React.ReactNode | React.ReactNode[]} [actions] - Custom action buttons/elements to display
+ *
+ * Note: The component accepts either fallbackUrl OR actions prop, but not both.
+ * This is enforced by the EitherOrNone type helper.
+ *
+ * @example
+ * ```tsx
+ * // With fallback URL
+ *
+ *
+ * // With custom actions
+ *
+ *
+ *
+ * >
+ * }
+ * />
+ * ```
+ */
+
+const RedirectErrorState: React.FC = ({
+ title,
+ errorMessage,
+ fallbackUrl,
+ fallbackText,
+ actions,
+}) => {
+ const navigate = useNavigate();
+
+ return (
+
+
+ {errorMessage && {errorMessage}}
+ {fallbackUrl && (
+
+
+
+
+
+ )}
+ {actions && (
+
+ {actions}
+
+ )}
+
+
+ );
+};
+
+export default RedirectErrorState;
diff --git a/frontend/src/pages/external/redirectComponents/ExternalRedirectNotFound.tsx b/frontend/src/pages/external/redirectComponents/ExternalRedirectNotFound.tsx
new file mode 100644
index 0000000000..fb8840dfd4
--- /dev/null
+++ b/frontend/src/pages/external/redirectComponents/ExternalRedirectNotFound.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import ApplicationsPage from '~/pages/ApplicationsPage';
+import RedirectErrorState from '~/pages/external/RedirectErrorState';
+
+const ExternalRedirectNotFound: React.FC = () => (
+
+ }
+ />
+);
+
+export default ExternalRedirectNotFound;
diff --git a/frontend/src/pages/external/redirectComponents/PipelinesSdkRedirects.tsx b/frontend/src/pages/external/redirectComponents/PipelinesSdkRedirects.tsx
index d2fe882904..aa5fa02b03 100644
--- a/frontend/src/pages/external/redirectComponents/PipelinesSdkRedirects.tsx
+++ b/frontend/src/pages/external/redirectComponents/PipelinesSdkRedirects.tsx
@@ -1,8 +1,10 @@
import React from 'react';
-import { useParams, useLocation } from 'react-router-dom';
+import { useParams, useLocation, useNavigate } from 'react-router-dom';
+import { Button } from '@patternfly/react-core';
import { experimentRunsRoute, globalPipelineRunDetailsRoute } from '~/routes';
import ApplicationsPage from '~/pages/ApplicationsPage';
import { useRedirect } from '~/utilities/useRedirect';
+import RedirectErrorState from '~/pages/external/RedirectErrorState';
/**
* Handles redirects from Pipeline SDK URLs to internal routes.
@@ -14,6 +16,7 @@ import { useRedirect } from '~/utilities/useRedirect';
const PipelinesSdkRedirects: React.FC = () => {
const { namespace } = useParams<{ namespace: string }>();
const location = useLocation();
+ const navigate = useNavigate();
const createRedirectPath = React.useCallback(() => {
if (!namespace) {
@@ -37,14 +40,33 @@ const PipelinesSdkRedirects: React.FC = () => {
throw new Error('Invalid URL format');
}, [namespace, location.hash]);
- const [redirect, { loaded }] = useRedirect(createRedirectPath);
+ const [redirect, { loaded, error }] = useRedirect(createRedirectPath);
React.useEffect(() => {
redirect();
}, [redirect]);
return (
-
+
+
+
+ >
+ }
+ />
+ }
+ />
);
};
diff --git a/frontend/src/utilities/useRedirect.ts b/frontend/src/utilities/useRedirect.ts
index 9579a7eb7f..c180b646dd 100644
--- a/frontend/src/utilities/useRedirect.ts
+++ b/frontend/src/utilities/useRedirect.ts
@@ -20,6 +20,42 @@ export type RedirectOptions = {
* @param createRedirectPath Function that creates the redirect path, can be async for data fetching
* @param options Redirect options
* @returns Array of [redirect function, redirect state]
+ *
+ * @example
+ * ```tsx
+ * const [redirect, state] = useRedirect(() => '/foo');
+ *
+ * // With async path creation
+ * const [redirect, state] = useRedirect(async () => {
+ * const data = await fetchData();
+ * return `/bar/${data.id}`;
+ * });
+ *
+ * // With options
+ * const [redirect, state] = useRedirect(() => '/foobar', {
+ * navigateOptions: { replace: true },
+ * onComplete: () => console.log('Redirected'),
+ * onError: (error) => console.error(error)
+ * });
+ *
+ * // Usage
+ * const createRedirectPath = React.useCallback(() => '/some/path', []);
+ *
+ * const [redirect, { loaded, error }] = useRedirect(createRedirectPath);
+ *
+ * React.useEffect(() => {
+ * redirect();
+ * }, [redirect]);
+ *
+ *
+ * return (
+ * }
+ * />
+ * );
+ * ```
*/
export const useRedirect = (
createRedirectPath: () => string | Promise | undefined,
@@ -33,27 +69,21 @@ export const useRedirect = (
error: undefined,
});
- const redirect = React.useCallback(
- async (notFoundOnError = true) => {
- try {
- const path = await createRedirectPath();
- if (!path) {
- throw new Error('No redirect path available');
- }
- navigate(path, navigateOptions);
- setState({ loaded: true, error: undefined });
- onComplete?.();
- } catch (e) {
- const error = e instanceof Error ? e : new Error('Failed to redirect');
- setState({ loaded: true, error });
- onError?.(error);
- if (notFoundOnError) {
- navigate('/not-found', { replace: true });
- }
+ const redirect = React.useCallback(async () => {
+ try {
+ const path = await createRedirectPath();
+ if (!path) {
+ throw new Error('No redirect path available');
}
- },
- [createRedirectPath, navigate, navigateOptions, onComplete, onError],
- );
+ navigate(path, navigateOptions);
+ setState({ loaded: true, error: undefined });
+ onComplete?.();
+ } catch (e) {
+ const error = e instanceof Error ? e : new Error('Failed to redirect');
+ setState({ loaded: true, error });
+ onError?.(error);
+ }
+ }, [createRedirectPath, navigate, navigateOptions, onComplete, onError]);
return [redirect, state];
};