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]; };