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:
parent
00b74d5e90
commit
2cd70efaba
11 changed files with 347 additions and 180 deletions
|
@ -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",
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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>) => {
|
||||
|
|
|
@ -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" />;
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
|
|
|
@ -507,9 +507,9 @@ const translation = {
|
|||
appearance_dark: '深色模式',
|
||||
saved: '已保存',
|
||||
},
|
||||
session_expire: {
|
||||
session_expired: {
|
||||
title: '会话已过期',
|
||||
subtitle: '由于会话过期,您已被登出. 请点击下方按钮重新登录到管理界面.',
|
||||
subtitle: '会话可能已过期,您已被登出. 请点击下方按钮重新登录到管理界面.',
|
||||
button: '重新登录',
|
||||
},
|
||||
},
|
||||
|
|
395
pnpm-lock.yaml
generated
395
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue