Skip to content

Commit

Permalink
Refactor external routing and error handling components
Browse files Browse the repository at this point in the history
- Updated ExternalRoutes to use ExternalRedirectNotFound instead of Navigate for unmatched routes.
- Introduced RedirectErrorState component to handle redirect errors with customizable actions and fallback options.
- Enhanced PipelinesSdkRedirects to display RedirectErrorState on error, providing navigation options to users.
- Added ExternalRedirectNotFound component to show a user-friendly message for non-existent external redirects.

This improves user experience by providing clearer error states and navigation options during redirects.
  • Loading branch information
Gkrumbach07 committed Dec 11, 2024
1 parent bc1050e commit 89c8d87
Show file tree
Hide file tree
Showing 5 changed files with 204 additions and 25 deletions.
5 changes: 3 additions & 2 deletions frontend/src/pages/external/ExternalRoutes.tsx
Original file line number Diff line number Diff line change
@@ -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 = () => (
<Routes>
<Route path="/pipelinesSdk/:namespace/*" element={<PipelinesSdkRedirect />} />
<Route path="*" element={<Navigate to="/not-found" replace />} />
<Route path="*" element={<ExternalRedirectNotFound />} />
</Routes>
);

Expand Down
106 changes: 106 additions & 0 deletions frontend/src/pages/external/RedirectErrorState.tsx
Original file line number Diff line number Diff line change
@@ -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
* <RedirectErrorState
* title="Error redirecting to pipelines"
* errorMessage={error.message}
* fallbackUrl="/pipelines"
* />
*
* // With custom actions
* <RedirectErrorState
* title="Error redirecting to pipelines"
* errorMessage={error.message}
* actions={
* <>
* <Button variant="link" onClick={() => navigate('/pipelines')}>
* Go to pipelines
* </Button>
* <Button variant="link" onClick={() => navigate('/experiments')}>
* Go to experiments
* </Button>
* </>
* }
* />
* ```
*/

const RedirectErrorState: React.FC<RedirectErrorStateProps> = ({
title,
errorMessage,
fallbackUrl,
fallbackText,
actions,
}) => {
const navigate = useNavigate();

return (
<PageSection hasBodyWrapper={false} isFilled>
<EmptyState
headingLevel="h1"
icon={ExclamationCircleIcon}
titleText={title ?? 'Error redirecting'}
variant={EmptyStateVariant.lg}
data-id="redirect-error"
>
{errorMessage && <EmptyStateBody>{errorMessage}</EmptyStateBody>}
{fallbackUrl && (
<EmptyStateFooter>
<EmptyStateActions>
<Button variant="link" onClick={() => navigate(fallbackUrl)}>
{fallbackText}
</Button>
</EmptyStateActions>
</EmptyStateFooter>
)}
{actions && (
<EmptyStateFooter>
<EmptyStateActions>{actions}</EmptyStateActions>
</EmptyStateFooter>
)}
</EmptyState>
</PageSection>
);
};

export default RedirectErrorState;
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react';
import ApplicationsPage from '~/pages/ApplicationsPage';
import RedirectErrorState from '~/pages/external/RedirectErrorState';

const ExternalRedirectNotFound: React.FC = () => (
<ApplicationsPage
loaded
empty
emptyStatePage={
<RedirectErrorState
title="Not Found"
errorMessage="There is no external redirect for this URL"
fallbackText="Go to home"
fallbackUrl="/"
/>
}
/>
);

export default ExternalRedirectNotFound;
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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) {
Expand All @@ -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 (
<ApplicationsPage title="Redirecting..." description={null} loaded={loaded} empty={false} />
<ApplicationsPage
loaded={loaded}
empty={!!error}
emptyStatePage={
<RedirectErrorState
title="Error redirecting to pipelines"
errorMessage={error?.message}
actions={
<>
<Button variant="link" onClick={() => navigate('/pipelines')}>
Go to pipelines
</Button>
<Button variant="link" onClick={() => navigate('/experiments')}>
Go to experiments
</Button>
</>
}
/>
}
/>
);
};

Expand Down
70 changes: 50 additions & 20 deletions frontend/src/utilities/useRedirect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
* <ApplicationsPage
* loaded={loaded}
* empty={!!error}
* emptyStatePage={<RedirectErrorState fallbackUrl="/foo/bar"/>}
* />
* );
* ```
*/
export const useRedirect = (
createRedirectPath: () => string | Promise<string | undefined> | undefined,
Expand All @@ -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];
};

0 comments on commit 89c8d87

Please sign in to comment.