Skip to content

Commit

Permalink
fix: Refresh auth on 401, better error messaging (#874)
Browse files Browse the repository at this point in the history
* Refresh auth on 401, better error messaging

Signed-off-by: Carina Ursu <[email protected]>

* fix tests

Signed-off-by: Carina Ursu <[email protected]>

---------

Signed-off-by: Carina Ursu <[email protected]>
  • Loading branch information
ursucarina committed May 20, 2024
1 parent bb1db21 commit c636bf0
Show file tree
Hide file tree
Showing 15 changed files with 434 additions and 52 deletions.
2 changes: 1 addition & 1 deletion packages/flyte-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"tslib": "^2.4.1"
},
"dependencies": {
"axios": "^0.27.2",
"axios": "^1.7.1",
"camelcase-keys": "^7.0.2",
"snakecase-keys": "^5.4.2"
}
Expand Down
2 changes: 1 addition & 1 deletion packages/oss-console/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
"@tanstack/react-virtual": "^3.0.0-alpha.0",
"@types/d3-shape": "^1.2.6",
"@xstate/react": "^1.0.0",
"axios": "^0.27.2",
"axios": "^1.7.1",
"copy-to-clipboard": "^3.0.8",
"cronstrue": "^1.31.0",
"d3-dag": "^0.3.4",
Expand Down
58 changes: 38 additions & 20 deletions packages/oss-console/src/App/ApplicationRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,34 +13,52 @@ import { PrettyError } from '../components/Errors/PrettyError';
import { useDomainPathUpgrade } from '../routes/useDomainPathUpgrade';
import { Routes } from '../routes/routes';
import AnimateRoute from '../routes/AnimateRoute';
import { loadChunkWithAuth } from '../components/data/loadChunkWithAuth';

const SelectProject = lazy(
() => import(/* webpackChunkName: "SelectProject" */ '../components/SelectProject'),
const SelectProject = lazy(() =>
loadChunkWithAuth(
() => import(/* webpackChunkName: "SelectProject" */ '../components/SelectProject'),
),
);
const ListProjectEntities = lazy(
() => import(/* webpackChunkName: "ListProjectEntities" */ '../components/ListProjectEntities'),
const ListProjectEntities = lazy(() =>
loadChunkWithAuth(
() => import(/* webpackChunkName: "ListProjectEntities" */ '../components/ListProjectEntities'),
),
);

const ExecutionDetails = lazy(
() =>
import(/* webpackChunkName: "ExecutionDetails" */ '../components/Executions/ExecutionDetails'),
const ExecutionDetails = lazy(() =>
loadChunkWithAuth(
() =>
import(
/* webpackChunkName: "ExecutionDetails" */ '../components/Executions/ExecutionDetails'
),
),
);

const TaskDetails = lazy(() => import(/* webpackChunkName: "TaskDetails" */ '../components/Task'));
const WorkflowDetails = lazy(
() => import(/* webpackChunkName: "WorkflowDetails" */ '../components/Workflow/WorkflowDetails'),
const TaskDetails = lazy(() =>
loadChunkWithAuth(() => import(/* webpackChunkName: "TaskDetails" */ '../components/Task')),
);
const LaunchPlanDetails = lazy(
() =>
import(
/* webpackChunkName: "LaunchPlanDetails" */ '../components/LaunchPlan/LaunchPlanDetails'
),
const WorkflowDetails = lazy(() =>
loadChunkWithAuth(
() =>
import(/* webpackChunkName: "WorkflowDetails" */ '../components/Workflow/WorkflowDetails'),
),
);
const EntityVersionsDetailsContainer = lazy(
() =>
import(
/* webpackChunkName: "EntityVersionsDetailsContainer" */ '../components/Entities/VersionDetails/EntityVersionDetailsContainer'
),
const LaunchPlanDetails = lazy(() =>
loadChunkWithAuth(
() =>
import(
/* webpackChunkName: "LaunchPlanDetails" */ '../components/LaunchPlan/LaunchPlanDetails'
),
),
);
const EntityVersionsDetailsContainer = lazy(() =>
loadChunkWithAuth(
() =>
import(
/* webpackChunkName: "EntityVersionsDetailsContainer" */ '../components/Entities/VersionDetails/EntityVersionDetailsContainer'
),
),
);

export const ApplicationRouter: React.FC = () => {
Expand Down
6 changes: 2 additions & 4 deletions packages/oss-console/src/App/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import { FeatureFlagsProvider } from '../basics/FeatureFlags';
import { debug, debugPrefix } from '../common/log';
import { APIContext, useAPIState } from '../components/data/apiContext';
import { QueryAuthorizationObserver } from '../components/data/QueryAuthorizationObserver';
import { createQueryClient } from '../components/data/queryCache';
import { SystemStatusBanner } from '../components/Notifications/SystemStatusBanner';
import { history } from '../routes/history';
import { LocalCacheProvider } from '../basics/LocalCache/ContextProvider';
Expand All @@ -30,12 +29,11 @@ import TopLevelLayoutProvider from '../components/common/TopLevelLayout/TopLevel
import ApplicationRouter from './ApplicationRouter';
import { ErrorBoundary } from '../components/common/ErrorBoundary';
import DownForMaintenance from '../components/Errors/DownForMaintenance';
import { globalQueryClient } from '../components/data/globalQueryClient';

const QueryClientProvider: React.FC<PropsWithChildren<QueryClientProviderProps>> =
QueryClientProviderImport;

const queryClient = createQueryClient();

export const AppComponent: React.FC<unknown> = () => {
if (env.NODE_ENV === 'development') {
debug.enable(`${debugPrefix}*:*`);
Expand All @@ -61,7 +59,7 @@ export const AppComponent: React.FC<unknown> = () => {
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
TransitionComponent={Collapse}
>
<QueryClientProvider client={queryClient}>
<QueryClientProvider client={globalQueryClient}>
<FlyteApiProvider flyteApiDomain={env.ADMIN_API} disableAutomaticLogin>
<APIContext.Provider value={apiState}>
<QueryAuthorizationObserver />
Expand Down
142 changes: 142 additions & 0 deletions packages/oss-console/src/components/Errors/ErrorHandler.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import NotAuthorizedError from '@clients/common/Errors/NotAuthorizedError';
import NotFoundError from '@clients/common/Errors/NotFoundError';
import { AxiosError } from 'axios';
import React from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Link from '@mui/material/Link';
import Button from '@mui/material/Button';
import { useFlyteApi } from '@clients/flyte-api/ApiProvider';
import { GenericError } from './GenericError';

export interface ErrorHandlerProps {
error: NotFoundError | NotAuthorizedError | AxiosError | Error;
}

export const ErrorHandler: React.FC<ErrorHandlerProps> = ({ error }) => {
const { getLoginUrl } = useFlyteApi();

const contactSupport = (
<>
<Typography variant="label">
Please join the Slack community and explore its history on{' '}
<Link
color="inherit"
variant="label"
sx={{
fontSize: 'inherit',
}}
href="https://discuss.flyte.org/"
target="_blank"
>
discuss.flyte.org
</Link>
{', '}
or file a GitHub issue on{' '}
<Link
color="inherit"
variant="label"
sx={{
fontSize: 'inherit',
}}
href="https://github.com/flyteorg/flyte"
target="_blank"
>
flyteorg/flyte
</Link>{' '}
if the problem persists.
</Typography>
</>
);

if (
error instanceof NotFoundError ||
(error instanceof AxiosError && error.response?.status === 404)
) {
return (
<GenericError
title="404"
description="Not Found"
content={
<>
<Box py={1} />
<Typography variant="body2">The requested resource was not found</Typography>

<Box py={1} />

{contactSupport}
</>
}
/>
);
}

if (
error instanceof NotAuthorizedError ||
(error instanceof AxiosError && error.response?.status === 401)
) {
return (
<GenericError
title="401"
description="Unauthorized"
content={
<>
<Box py={1} />
<Typography variant="body2">
<strong>You do not have the proper authentication to access this page</strong>
</Typography>

<Box py={1} />

{contactSupport}
<Box py={1} />

<Button
variant="contained"
href={getLoginUrl()}
autoFocus
data-cy="login-button-overlay"
>
Log in
</Button>
</>
}
/>
);
}

if (error instanceof AxiosError && error.response?.status === 403) {
return (
<GenericError
title="403"
description="Forbidden"
content={
<>
<Box py={1} />
<Typography variant="body2">
<strong>You don't have permission to access this page</strong>
</Typography>

<Box py={1} />

{contactSupport}
</>
}
/>
);
}

return (
<GenericError
title={(error as AxiosError)?.response?.status || 'Oops!'}
description="Something went wrong."
content={
<>
<Box py={1} />

{contactSupport}
</>
}
/>
);
};
102 changes: 102 additions & 0 deletions packages/oss-console/src/components/Errors/GenericError.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import React from 'react';
import { type SvgIconProps } from '@mui/material/SvgIcon';
import PageMeta from '@clients/primitives/PageMeta';
import Container from '@mui/material/Container';
import NotFoundLogo from '@clients/ui-atoms/NotFoundLogo';
import Grid from '@mui/material/Grid';
import Typography from '@mui/material/Typography';

export interface GenericErrorProps {
title?: string | number;
description?: string;
content?: React.ReactNode;
Icon?: (props: SvgIconProps) => React.JSX.Element;
}

/**
* React prints the \n as "\n" in the DOM,
* so we need to split on that to make new lines
*/
const makeDescriptionSpans = (description: string = '') => {
const descriptionSpans = description.split('\\n').filter((span) => !!span);
return descriptionSpans.map((span) => (
<span key={span}>
{span}
<br />
</span>
));
};

export const GenericError: React.FC<GenericErrorProps> = ({
title,
description,
content,
Icon = NotFoundLogo,
}) => {
return (
<>
<PageMeta title={title?.toString()} />
<Container
sx={{
height: '100%',
flexGrow: 1,
display: 'flex',
alignItems: 'center',
}}
>
<Grid
container
gap={1}
sx={{
flexDirection: { xs: 'column-reverse', md: 'row' },
}}
>
<Grid item xs={12} md={2} />
<Grid
item
xs={12}
md={4}
sx={{
paddingRight: (t) => t.spacing(2),
paddingBottom: (t) => t.spacing(2),
}}
>
<Typography
variant="h3"
pb={1}
sx={{
fontSize: '22px',
}}
>
{title}
</Typography>
<Typography
variant="h1"
sx={{
fontSize: '42px',
}}
>
{makeDescriptionSpans(description)}
</Typography>

{content}
</Grid>
<Grid item xs={12} md={1} py={3} sx={{ display: 'flex' }} />
{Icon && (
<Grid item xs={4} md={4} sx={{ display: 'flex' }}>
<Icon
sx={{
width: '100%',
height: '100%',
maxWidth: { xs: '150px', md: '250px' },
maxHeight: { xs: '150px', md: '250px' },
color: (theme) => theme.palette.common.grays[20],
}}
/>
</Grid>
)}
</Grid>
</Container>
</>
);
};
12 changes: 9 additions & 3 deletions packages/oss-console/src/components/common/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import Typography from '@mui/material/Typography';
import Card from '@mui/material/Card';
import ErrorOutline from '@mui/icons-material/ErrorOutline';
import NotFoundError from '@clients/common/Errors/NotFoundError';
import NotAuthorizedError from '@clients/common/Errors/NotAuthorizedError';
import { AxiosError } from 'axios';
import { log } from '../../common/log';
import { useCommonStyles } from './styles';
import { PrettyError } from '../Errors/PrettyError';
import { NonIdealState } from './NonIdealState';
import { ErrorHandler } from '../Errors/ErrorHandler';

interface ErrorBoundaryState {
error?: Error;
Expand Down Expand Up @@ -58,8 +60,12 @@ export class ErrorBoundary extends React.Component<
render() {
const { fixed = false } = this.props;
if (this.state.error) {
if (this.state.error instanceof NotFoundError) {
return <PrettyError />;
if (
this.state.error instanceof NotFoundError ||
this.state.error instanceof NotAuthorizedError ||
(this.state.error as AxiosError).response?.status === 403
) {
return <ErrorHandler error={this.state.error} />;
}

return <RenderError error={this.state.error} fixed={fixed} />;
Expand Down
Loading

0 comments on commit c636bf0

Please sign in to comment.