0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-10 22:22:45 -05:00

refactor(console): improve error handling in admin console

This commit is contained in:
Charles Zhao 2022-05-28 18:08:24 +08:00
parent 00b74d5e90
commit 2cd70efaba
No known key found for this signature in database
GPG key ID: 4858774754C92DF2
11 changed files with 347 additions and 180 deletions

View file

@ -19,7 +19,7 @@
"devDependencies": {
"@fontsource/roboto-mono": "^4.5.7",
"@logto/phrases": "^0.1.0",
"@logto/react": "^0.1.7",
"@logto/react": "^0.1.11",
"@logto/shared": "^0.1.0",
"@logto/schemas": "^0.1.0",
"@mdx-js/react": "^1.6.22",

View file

@ -1,14 +1,15 @@
import { useLogto } from '@logto/react';
import { LogtoClientError, useLogto } from '@logto/react';
import { AppearanceMode } from '@logto/schemas';
import React, { useEffect } from 'react';
import { Outlet, useHref, useLocation, useNavigate } from 'react-router-dom';
import AppError from '@/components/AppError';
import LogtoLoading from '@/components/LogtoLoading';
import SessionExpired from '@/components/SessionExpired';
import { themeStorageKey } from '@/consts';
import useAdminConsoleConfigs from '@/hooks/use-configs';
import initI18n from '@/i18n/init';
import LogtoLoading from '../LogtoLoading';
import SessionExpired from '../SessionExpired';
import Sidebar, { getPath } from './components/Sidebar';
import { useSidebarMenuItems } from './components/Sidebar/hook';
import Topbar from './components/Topbar';
@ -61,12 +62,16 @@ const AppContent = () => {
}
}, [location.pathname, configs, sections, navigate]);
if (!isAuthenticated || isLoadingConfigs) {
return <LogtoLoading message="general.loading" />;
if (error) {
if (error instanceof LogtoClientError) {
return <SessionExpired />;
}
return <AppError errorMessage={error.message} callStack={error.stack} />;
}
if (error) {
return <SessionExpired />;
if (!isAuthenticated || isLoadingConfigs) {
return <LogtoLoading message="general.loading" />;
}
return (

View file

@ -28,13 +28,13 @@
display: flex;
align-items: center;
margin-top: _.unit(4);
span {
display: inline-block;
font: var(--font-body-medium);
}
max-width: 470px;
font: var(--font-body-medium);
text-align: center;
.expander {
display: inline-flex;
align-items: center;
margin-left: _.unit(2);
color: var(--color-primary);
cursor: pointer;

View file

@ -1,4 +1,4 @@
import React, { Fragment, useState } from 'react';
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import ErrorImage from '@/assets/images/warning.svg';
@ -7,22 +7,27 @@ import { ArrowDown, ArrowUp } from '@/icons/Arrow';
import * as styles from './index.module.scss';
type Props = {
title?: string;
errorCode?: string;
errorMessage?: string;
callStack?: string;
children?: React.ReactNode;
};
const AppError = ({ errorMessage, callStack }: Props) => {
const AppError = ({ title, errorCode, errorMessage, callStack, children }: Props) => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const [isDetailsOpen, setIsDetailsOpen] = useState(false);
return (
<div className={styles.container}>
<img src={ErrorImage} alt="oops" />
<label>{t('errors.something_went_wrong')}</label>
<label>{title ?? t('errors.something_went_wrong')}</label>
<div className={styles.summary}>
<span>{errorMessage}</span>
{callStack && (
<>
<span>
{errorCode}
{errorCode && errorMessage && ': '}
{errorMessage}
{callStack && (
<span
className={styles.expander}
onClick={() => {
@ -30,12 +35,13 @@ const AppError = ({ errorMessage, callStack }: Props) => {
}}
>
{t('errors.more_details')}
{isDetailsOpen ? <ArrowUp /> : <ArrowDown />}
</span>
{isDetailsOpen ? <ArrowUp /> : <ArrowDown />}
</>
)}
)}
</span>
</div>
{callStack && isDetailsOpen && <div className={styles.details}>{callStack}</div>}
{children}
</div>
);
};

View file

@ -1,28 +1,6 @@
@use '@/scss/underscore' as _;
.container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: var(--color-layer-1);
align-items: center;
overflow: hidden;
img {
height: 256px;
width: 256px;
margin: _.unit(42) 0 _.unit(6);
}
.title {
font: var(--font-title-large);
margin-bottom: _.unit(4);
}
.subtitle {
font: var(--font-body-medium);
margin-bottom: _.unit(6);
max-width: 470px;
text-align: center;
}
.retryButton {
flex-shrink: 0;
margin-top: _.unit(6);
}

View file

@ -3,30 +3,31 @@ import React from 'react';
import { useTranslation } from 'react-i18next';
import { useHref } from 'react-router-dom';
import WarningIcon from '@/assets/images/warning.svg';
import AppError from '../AppError';
import Button from '../Button';
import * as styles from './index.module.scss';
const SessionExpired = () => {
const { signIn } = useLogto();
const { error, signIn } = useLogto();
const href = useHref('/callback');
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
return (
<div className={styles.container}>
<img src={WarningIcon} />
<div className={styles.title}>{t('session_expire.title')}</div>
<div className={styles.subtitle}>{t('session_expire.subtitle')}</div>
<AppError
title={t('session_expired.title')}
errorMessage={t('session_expired.subtitle')}
callStack={error?.stack}
>
<Button
className={styles.retryButton}
size="large"
type="outline"
title="admin_console.session_expire.button"
title="admin_console.session_expired.button"
onClick={() => {
void signIn(new URL(href, window.location.origin).toString());
}}
/>
</div>
</AppError>
);
};

View file

@ -5,12 +5,12 @@ import useSWR from 'swr';
import useApi, { RequestError } from './use-api';
const useAdminConsoleConfigs = () => {
const { isAuthenticated } = useLogto();
const { isAuthenticated, error: authError } = useLogto();
const {
data: settings,
error,
mutate,
} = useSWR<Setting, RequestError>(isAuthenticated && '/api/settings');
} = useSWR<Setting, RequestError>(isAuthenticated && !authError && '/api/settings');
const api = useApi();
const updateConfigs = async (delta: Partial<AdminConsoleConfig>) => {

View file

@ -1,11 +1,25 @@
import { useHandleSignInCallback } from '@logto/react';
import { LogtoError, OidcError, useHandleSignInCallback } from '@logto/react';
import React from 'react';
import AppError from '@/components/AppError';
import LogtoLoading from '@/components/LogtoLoading';
import { getBasename } from '@/utilities/app';
const Callback = () => {
useHandleSignInCallback(getBasename());
const { error } = useHandleSignInCallback(getBasename());
if (error) {
const errorCode =
error instanceof LogtoError && error.data instanceof OidcError
? error.data.error
: error.name;
const errorMessage =
error instanceof LogtoError && error.data instanceof OidcError
? error.data.errorDescription
: error.message;
return <AppError errorCode={errorCode} errorMessage={errorMessage} callStack={error.stack} />;
}
return <LogtoLoading message="general.redirecting" />;
};

View file

@ -511,10 +511,10 @@ const translation = {
appearance_dark: 'Dark mode',
saved: 'Saved!',
},
session_expire: {
session_expired: {
title: 'Session Expired',
subtitle:
'Your session has expired and you have been disconnected. Click the button below to sign in admin console again.',
'Your session might have expired and you have been disconnected. Click the button below to sign in to admin console again.',
button: 'Sign in again',
},
},

View file

@ -507,9 +507,9 @@ const translation = {
appearance_dark: '深色模式',
saved: '已保存',
},
session_expire: {
session_expired: {
title: '会话已过期',
subtitle: '由于会话过期,您已被登出. 请点击下方按钮重新登录到管理界面.',
subtitle: '会话可能已过期,您已被登出. 请点击下方按钮重新登录到管理界面.',
button: '重新登录',
},
},

395
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff