mirror of
https://github.com/suitenumerique/docs
synced 2026-04-21 13:37:20 +00:00
♿️(frontend) redirect unmanaged 5xx to dedicated /500 page
Add /500 with coffee illustration; replace inline TextErrors for API 5xx
This commit is contained in:
parent
31fea43729
commit
9a5d81f983
11 changed files with 234 additions and 171 deletions
|
|
@ -33,6 +33,48 @@ test.describe('Doc Routing', () => {
|
||||||
await expect(page).toHaveURL(/\/docs\/$/);
|
await expect(page).toHaveURL(/\/docs\/$/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('checks 500 refresh retries original document request', async ({
|
||||||
|
page,
|
||||||
|
browserName,
|
||||||
|
}) => {
|
||||||
|
const [docTitle] = await createDoc(page, 'doc-routing-500', browserName, 1);
|
||||||
|
await verifyDocName(page, docTitle);
|
||||||
|
|
||||||
|
const docId = page.url().split('/docs/')[1]?.split('/')[0];
|
||||||
|
// While true, every doc GET fails (including React Query retries) so we
|
||||||
|
// reliably land on /500. Set to false before refresh so the doc loads again.
|
||||||
|
let failDocumentGet = true;
|
||||||
|
|
||||||
|
await page.route(/\**\/documents\/\**/, async (route) => {
|
||||||
|
const request = route.request();
|
||||||
|
if (
|
||||||
|
failDocumentGet &&
|
||||||
|
request.method().includes('GET') &&
|
||||||
|
docId &&
|
||||||
|
request.url().includes(`/documents/${docId}/`)
|
||||||
|
) {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 500,
|
||||||
|
json: { detail: 'Internal Server Error' },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await route.continue();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.reload();
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(/\/500\/?\?from=/, { timeout: 15000 });
|
||||||
|
|
||||||
|
const refreshButton = page.getByRole('button', { name: 'Refresh page' });
|
||||||
|
await expect(refreshButton).toBeVisible();
|
||||||
|
|
||||||
|
failDocumentGet = false;
|
||||||
|
await refreshButton.click();
|
||||||
|
|
||||||
|
await verifyDocName(page, docTitle);
|
||||||
|
});
|
||||||
|
|
||||||
test('checks 404 on docs/[id] page', async ({ page }) => {
|
test('checks 404 on docs/[id] page', async ({ page }) => {
|
||||||
await page.waitForTimeout(300);
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
|
|
|
||||||
134
src/frontend/apps/impress/src/components/ErrorPage.tsx
Normal file
134
src/frontend/apps/impress/src/components/ErrorPage.tsx
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
import { Button } from '@gouvfr-lasuite/cunningham-react';
|
||||||
|
import Head from 'next/head';
|
||||||
|
import Image, { StaticImageData } from 'next/image';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import { Box, Icon, StyledLink, Text } from '@/components';
|
||||||
|
|
||||||
|
const StyledButton = styled(Button)`
|
||||||
|
width: fit-content;
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface ErrorPageProps {
|
||||||
|
image: StaticImageData;
|
||||||
|
description: string;
|
||||||
|
refreshTarget?: string;
|
||||||
|
showReload?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSafeRefreshUrl = (target?: string): string | undefined => {
|
||||||
|
if (!target) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return target.startsWith('/') && !target.startsWith('//')
|
||||||
|
? target
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = new URL(target, window.location.origin);
|
||||||
|
if (url.origin !== window.location.origin) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return url.pathname + url.search + url.hash;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ErrorPage = ({
|
||||||
|
image,
|
||||||
|
description,
|
||||||
|
refreshTarget,
|
||||||
|
showReload,
|
||||||
|
}: ErrorPageProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const errorTitle = t('An unexpected error occurred.');
|
||||||
|
const safeTarget = getSafeRefreshUrl(refreshTarget);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>
|
||||||
|
{errorTitle} - {t('Docs')}
|
||||||
|
</title>
|
||||||
|
<meta
|
||||||
|
property="og:title"
|
||||||
|
content={`${errorTitle} - ${t('Docs')}`}
|
||||||
|
key="title"
|
||||||
|
/>
|
||||||
|
</Head>
|
||||||
|
<Box
|
||||||
|
$align="center"
|
||||||
|
$margin="auto"
|
||||||
|
$gap="md"
|
||||||
|
$padding={{ bottom: '2rem' }}
|
||||||
|
>
|
||||||
|
<Text as="h1" $textAlign="center" className="sr-only">
|
||||||
|
{errorTitle} - {t('Docs')}
|
||||||
|
</Text>
|
||||||
|
<Image
|
||||||
|
src={image}
|
||||||
|
alt=""
|
||||||
|
width={300}
|
||||||
|
style={{
|
||||||
|
maxWidth: '100%',
|
||||||
|
height: 'auto',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text
|
||||||
|
as="p"
|
||||||
|
$textAlign="center"
|
||||||
|
$maxWidth="350px"
|
||||||
|
$theme="neutral"
|
||||||
|
$margin="0"
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Box $direction="row" $gap="sm">
|
||||||
|
<StyledLink href="/">
|
||||||
|
<StyledButton
|
||||||
|
color="neutral"
|
||||||
|
icon={
|
||||||
|
<Icon
|
||||||
|
iconName="house"
|
||||||
|
variant="symbols-outlined"
|
||||||
|
$withThemeInherited
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t('Home')}
|
||||||
|
</StyledButton>
|
||||||
|
</StyledLink>
|
||||||
|
|
||||||
|
{(safeTarget || showReload) && (
|
||||||
|
<StyledButton
|
||||||
|
color="neutral"
|
||||||
|
variant="bordered"
|
||||||
|
icon={
|
||||||
|
<Icon
|
||||||
|
iconName="refresh"
|
||||||
|
variant="symbols-outlined"
|
||||||
|
$withThemeInherited
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onClick={() =>
|
||||||
|
safeTarget
|
||||||
|
? window.location.assign(safeTarget)
|
||||||
|
: window.location.reload()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t('Refresh page')}
|
||||||
|
</StyledButton>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -4,7 +4,6 @@ import { useTranslation } from 'react-i18next';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import { Box, Text, TextType } from '@/components';
|
import { Box, Text, TextType } from '@/components';
|
||||||
import { useHttpErrorMessages } from '@/hooks';
|
|
||||||
|
|
||||||
const AlertStyled = styled(Alert)`
|
const AlertStyled = styled(Alert)`
|
||||||
& .c__button--tertiary:hover {
|
& .c__button--tertiary:hover {
|
||||||
|
|
@ -17,7 +16,6 @@ interface TextErrorsProps extends TextType {
|
||||||
defaultMessage?: string;
|
defaultMessage?: string;
|
||||||
icon?: ReactNode;
|
icon?: ReactNode;
|
||||||
canClose?: boolean;
|
canClose?: boolean;
|
||||||
status?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TextErrors = ({
|
export const TextErrors = ({
|
||||||
|
|
@ -25,7 +23,6 @@ export const TextErrors = ({
|
||||||
defaultMessage,
|
defaultMessage,
|
||||||
icon,
|
icon,
|
||||||
canClose = false,
|
canClose = false,
|
||||||
status,
|
|
||||||
...textProps
|
...textProps
|
||||||
}: TextErrorsProps) => {
|
}: TextErrorsProps) => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -38,7 +35,6 @@ export const TextErrors = ({
|
||||||
<TextOnlyErrors
|
<TextOnlyErrors
|
||||||
causes={causes}
|
causes={causes}
|
||||||
defaultMessage={defaultMessage}
|
defaultMessage={defaultMessage}
|
||||||
status={status}
|
|
||||||
{...textProps}
|
{...textProps}
|
||||||
/>
|
/>
|
||||||
</AlertStyled>
|
</AlertStyled>
|
||||||
|
|
@ -48,39 +44,9 @@ export const TextErrors = ({
|
||||||
export const TextOnlyErrors = ({
|
export const TextOnlyErrors = ({
|
||||||
causes,
|
causes,
|
||||||
defaultMessage,
|
defaultMessage,
|
||||||
status,
|
|
||||||
...textProps
|
...textProps
|
||||||
}: TextErrorsProps) => {
|
}: TextErrorsProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const httpError = useHttpErrorMessages(status);
|
|
||||||
|
|
||||||
if (httpError) {
|
|
||||||
return (
|
|
||||||
<Box $direction="column" $gap="0.2rem">
|
|
||||||
<Text
|
|
||||||
as="h1"
|
|
||||||
$theme="error"
|
|
||||||
$textAlign="center"
|
|
||||||
$margin="0"
|
|
||||||
$size="1rem"
|
|
||||||
$weight="unset"
|
|
||||||
{...textProps}
|
|
||||||
>
|
|
||||||
{httpError.title}
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
as="p"
|
|
||||||
$theme="error"
|
|
||||||
$textAlign="center"
|
|
||||||
$margin="0"
|
|
||||||
$size="0.875rem"
|
|
||||||
{...textProps}
|
|
||||||
>
|
|
||||||
{httpError.detail}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box $direction="column" $gap="0.2rem">
|
<Box $direction="column" $gap="0.2rem">
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ export * from './Card';
|
||||||
export * from './DropButton';
|
export * from './DropButton';
|
||||||
export * from './dropdown-menu/DropdownMenu';
|
export * from './dropdown-menu/DropdownMenu';
|
||||||
export * from './Emoji/EmojiPicker';
|
export * from './Emoji/EmojiPicker';
|
||||||
|
export * from './ErrorPage';
|
||||||
export * from './quick-search';
|
export * from './quick-search';
|
||||||
export * from './Icon';
|
export * from './Icon';
|
||||||
export * from './InfiniteScroll';
|
export * from './InfiniteScroll';
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,6 @@ export const DocVersionEditor = ({
|
||||||
<Box $margin="large" className="--docs--doc-version-editor-error">
|
<Box $margin="large" className="--docs--doc-version-editor-error">
|
||||||
<TextErrors
|
<TextErrors
|
||||||
causes={error.cause}
|
causes={error.cause}
|
||||||
status={error.status}
|
|
||||||
icon={
|
icon={
|
||||||
error.status === 502 ? (
|
error.status === 502 ? (
|
||||||
<Text
|
<Text
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,6 @@ const VersionListState = ({
|
||||||
>
|
>
|
||||||
<TextErrors
|
<TextErrors
|
||||||
causes={error.cause}
|
causes={error.cause}
|
||||||
status={error.status}
|
|
||||||
icon={
|
icon={
|
||||||
error.status === 502 ? (
|
error.status === 502 ? (
|
||||||
<Icon iconName="wifi_off" $theme="danger" />
|
<Icon iconName="wifi_off" $theme="danger" />
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
export * from './useClipboard';
|
export * from './useClipboard';
|
||||||
export * from './useCmdK';
|
export * from './useCmdK';
|
||||||
export * from './useDate';
|
export * from './useDate';
|
||||||
export * from './useHttpErrorMessages';
|
|
||||||
export * from './useKeyboardAction';
|
export * from './useKeyboardAction';
|
||||||
|
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
export const useHttpErrorMessages = (status?: number) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const messages: Record<number, { title: string; detail: string }> = {
|
|
||||||
500: {
|
|
||||||
title: t('500 - Internal Server Error'),
|
|
||||||
detail: t('The server met an unexpected condition.'),
|
|
||||||
},
|
|
||||||
502: {
|
|
||||||
title: t('502 - Bad Gateway'),
|
|
||||||
detail: t(
|
|
||||||
'The server received an invalid response. Please check your connection and try again.',
|
|
||||||
),
|
|
||||||
},
|
|
||||||
503: {
|
|
||||||
title: t('503 - Service Unavailable'),
|
|
||||||
detail: t(
|
|
||||||
'The service is temporarily unavailable. Please try again later.',
|
|
||||||
),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return status ? messages[status] : undefined;
|
|
||||||
};
|
|
||||||
32
src/frontend/apps/impress/src/pages/500.tsx
Normal file
32
src/frontend/apps/impress/src/pages/500.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { ReactElement } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import error_img from '@/assets/icons/error-coffee.png';
|
||||||
|
import { ErrorPage } from '@/components';
|
||||||
|
import { PageLayout } from '@/layouts';
|
||||||
|
import { NextPageWithLayout } from '@/types/next';
|
||||||
|
|
||||||
|
const Page: NextPageWithLayout = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { query } = useRouter();
|
||||||
|
const from = Array.isArray(query.from) ? query.from[0] : query.from;
|
||||||
|
const refreshTarget =
|
||||||
|
from?.startsWith('/') && !from.startsWith('//') ? from : undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorPage
|
||||||
|
image={error_img}
|
||||||
|
description={t(
|
||||||
|
'An unexpected error occurred. Go grab a coffee or try to refresh the page.',
|
||||||
|
)}
|
||||||
|
refreshTarget={refreshTarget}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Page.getLayout = function getLayout(page: ReactElement) {
|
||||||
|
return <PageLayout withFooter={false}>{page}</PageLayout>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
||||||
|
|
@ -1,100 +1,22 @@
|
||||||
import { Button } from '@gouvfr-lasuite/cunningham-react';
|
|
||||||
import * as Sentry from '@sentry/nextjs';
|
import * as Sentry from '@sentry/nextjs';
|
||||||
import { NextPageContext } from 'next';
|
import { NextPageContext } from 'next';
|
||||||
import NextError from 'next/error';
|
import NextError from 'next/error';
|
||||||
import Head from 'next/head';
|
|
||||||
import Image from 'next/image';
|
|
||||||
import { ReactElement } from 'react';
|
import { ReactElement } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
import error_img from '@/assets/icons/error-planetes.png';
|
import error_img from '@/assets/icons/error-planetes.png';
|
||||||
import { Box, Icon, StyledLink, Text } from '@/components';
|
import { ErrorPage } from '@/components';
|
||||||
import { PageLayout } from '@/layouts';
|
import { PageLayout } from '@/layouts';
|
||||||
|
|
||||||
const StyledButton = styled(Button)`
|
|
||||||
width: fit-content;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Error = () => {
|
const Error = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const errorTitle = t('An unexpected error occurred.');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ErrorPage
|
||||||
<Head>
|
image={error_img}
|
||||||
<title>
|
description={t('An unexpected error occurred.')}
|
||||||
{errorTitle} - {t('Docs')}
|
showReload
|
||||||
</title>
|
/>
|
||||||
<meta
|
|
||||||
property="og:title"
|
|
||||||
content={`${errorTitle} - ${t('Docs')}`}
|
|
||||||
key="title"
|
|
||||||
/>
|
|
||||||
</Head>
|
|
||||||
<Box
|
|
||||||
$align="center"
|
|
||||||
$margin="auto"
|
|
||||||
$gap="md"
|
|
||||||
$padding={{ bottom: '2rem' }}
|
|
||||||
>
|
|
||||||
<Text as="h2" $textAlign="center" className="sr-only">
|
|
||||||
{errorTitle} - {t('Docs')}
|
|
||||||
</Text>
|
|
||||||
<Image
|
|
||||||
src={error_img}
|
|
||||||
alt=""
|
|
||||||
width={300}
|
|
||||||
style={{
|
|
||||||
maxWidth: '100%',
|
|
||||||
height: 'auto',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Text
|
|
||||||
as="p"
|
|
||||||
$textAlign="center"
|
|
||||||
$maxWidth="350px"
|
|
||||||
$theme="neutral"
|
|
||||||
$margin="0"
|
|
||||||
>
|
|
||||||
{errorTitle}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Box $direction="row" $gap="sm">
|
|
||||||
<StyledLink href="/">
|
|
||||||
<StyledButton
|
|
||||||
color="neutral"
|
|
||||||
icon={
|
|
||||||
<Icon
|
|
||||||
iconName="house"
|
|
||||||
variant="symbols-outlined"
|
|
||||||
$withThemeInherited
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t('Home')}
|
|
||||||
</StyledButton>
|
|
||||||
</StyledLink>
|
|
||||||
|
|
||||||
<StyledButton
|
|
||||||
color="neutral"
|
|
||||||
variant="bordered"
|
|
||||||
icon={
|
|
||||||
<Icon
|
|
||||||
iconName="refresh"
|
|
||||||
variant="symbols-outlined"
|
|
||||||
$withThemeInherited
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onClick={() => window.location.reload()}
|
|
||||||
>
|
|
||||||
{t('Refresh page')}
|
|
||||||
</StyledButton>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { useRouter } from 'next/router';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Box, Icon, Loading, TextErrors } from '@/components';
|
import { Loading } from '@/components';
|
||||||
import { DEFAULT_QUERY_RETRY } from '@/core';
|
import { DEFAULT_QUERY_RETRY } from '@/core';
|
||||||
import {
|
import {
|
||||||
Doc,
|
Doc,
|
||||||
|
|
@ -105,7 +105,7 @@ const DocPage = ({ id }: DocProps) => {
|
||||||
const { setCurrentDoc } = useDocStore();
|
const { setCurrentDoc } = useDocStore();
|
||||||
const { addTask } = useBroadcastStore();
|
const { addTask } = useBroadcastStore();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { replace } = useRouter();
|
const { replace, asPath } = useRouter();
|
||||||
useCollaboration(doc?.id, doc?.content);
|
useCollaboration(doc?.id, doc?.content);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { authenticated } = useAuth();
|
const { authenticated } = useAuth();
|
||||||
|
|
@ -191,44 +191,39 @@ const DocPage = ({ id }: DocProps) => {
|
||||||
}, [addTask, doc?.id, queryClient]);
|
}, [addTask, doc?.id, queryClient]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isError || !error?.status || ![404, 401].includes(error.status)) {
|
if (!isError || !error?.status || [403].includes(error.status)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let replacePath = `/${error.status}`;
|
|
||||||
|
|
||||||
if (error.status === 401) {
|
if (error.status === 401) {
|
||||||
if (authenticated) {
|
if (authenticated) {
|
||||||
queryClient.setQueryData([KEY_AUTH], null);
|
queryClient.setQueryData([KEY_AUTH], null);
|
||||||
}
|
}
|
||||||
setAuthUrl();
|
setAuthUrl();
|
||||||
|
void replace('/401');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void replace(replacePath);
|
if (error.status === 404) {
|
||||||
}, [isError, error?.status, replace, authenticated, queryClient]);
|
void replace('/404');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.status === 502) {
|
||||||
|
void replace('/offline');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fromPath = encodeURIComponent(asPath);
|
||||||
|
void replace(`/500?from=${fromPath}`);
|
||||||
|
}, [isError, error?.status, replace, authenticated, queryClient, asPath]);
|
||||||
|
|
||||||
if (isError && error?.status) {
|
if (isError && error?.status) {
|
||||||
if ([404, 401].includes(error.status)) {
|
|
||||||
return <Loading />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error.status === 403) {
|
if (error.status === 403) {
|
||||||
return <DocPage403 id={id} />;
|
return <DocPage403 id={id} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <Loading />;
|
||||||
<Box $margin="large">
|
|
||||||
<TextErrors
|
|
||||||
causes={error.cause}
|
|
||||||
status={error.status}
|
|
||||||
icon={
|
|
||||||
error.status === 502 ? (
|
|
||||||
<Icon iconName="wifi_off" $theme="danger" $withThemeInherited />
|
|
||||||
) : undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!doc) {
|
if (!doc) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue