0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-04-07 23:01:25 -05:00

fix(console,phrases): improve error handling when user associated app is removed ()

* fix(console,phrases): improve error handling when user associated app is removed

* chore: add changeset

* refactor(console): update per review comments
This commit is contained in:
Charles Zhao 2024-02-05 22:44:24 +08:00 committed by GitHub
parent 17fe38443c
commit 04ec78a917
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 94 additions and 29 deletions
.changeset
packages
console/src
components/ApplicationName
hooks
pages
ApplicationDetails/ApplicationDetailsContent/Branding
SignInExperience/PageContent/Content/LanguagesForm/ManageLanguage/LanguageEditor
utils
phrases/src/locales
de/translation/admin-console
en/translation/admin-console
es/translation/admin-console
fr/translation/admin-console
it/translation/admin-console
ja/translation/admin-console
ko/translation/admin-console
pl-pl/translation/admin-console
pt-br/translation/admin-console
pt-pt/translation/admin-console
ru/translation/admin-console
tr-tr/translation/admin-console
zh-cn/translation/admin-console
zh-hk/translation/admin-console
zh-tw/translation/admin-console

View file

@ -0,0 +1,6 @@
---
"@logto/console": patch
"@logto/phrases": patch
---
improve error handling when user associated application is removed

View file

@ -1,10 +1,14 @@
import type { Application } from '@logto/schemas';
import { adminConsoleApplicationId } from '@logto/schemas';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import useSWR from 'swr';
import useApi, { type RequestError } from '@/hooks/use-api';
import useSwrFetcher from '@/hooks/use-swr-fetcher';
import useTenantPathname from '@/hooks/use-tenant-pathname';
import { shouldRetryOnError } from '@/utils/request';
import * as styles from './index.module.scss';
@ -16,13 +20,32 @@ type Props = {
function ApplicationName({ applicationId, isLink = false }: Props) {
const isAdminConsole = applicationId === adminConsoleApplicationId;
const { data } = useSWR<Application>(!isAdminConsole && `api/applications/${applicationId}`);
const fetchApi = useApi({ hideErrorToast: true });
const fetcher = useSwrFetcher<Application>(fetchApi);
const { data, error } = useSWR<Application, RequestError>(
!isAdminConsole && `api/applications/${applicationId}`,
{
fetcher,
shouldRetryOnError: shouldRetryOnError({ ignore: [404] }),
}
);
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { getTo } = useTenantPathname();
const name = (isAdminConsole ? <>Admin Console ({t('system_app')})</> : data?.name) ?? '-';
const name = useMemo(() => {
if (isAdminConsole) {
return `Admin Console (${t('system_app')})`;
}
if (data?.name) {
return data.name;
}
if (error?.status === 404) {
return `${applicationId} (${t('general.deleted')})`;
}
return '-';
}, [applicationId, data?.name, error?.status, isAdminConsole, t]);
if (isLink && !isAdminConsole) {
if (isLink && !isAdminConsole && data?.name) {
return (
<Link className={styles.link} to={getTo(`/applications/${applicationId}`)}>
{name}

View file

@ -2,7 +2,9 @@ import type React from 'react';
import { useMemo } from 'react';
import type { SWRConfig } from 'swr';
import useApi, { RequestError } from './use-api';
import { shouldRetryOnError } from '@/utils/request';
import useApi from './use-api';
import useSwrFetcher from './use-swr-fetcher';
const useSwrOptions = (): Partial<React.ComponentProps<typeof SWRConfig>['value']> => {
@ -12,15 +14,7 @@ const useSwrOptions = (): Partial<React.ComponentProps<typeof SWRConfig>['value'
const config = useMemo(
() => ({
fetcher,
shouldRetryOnError: (error: unknown) => {
if (error instanceof RequestError) {
const { status } = error;
return status !== 401 && status !== 403;
}
return true;
},
shouldRetryOnError: shouldRetryOnError({ ignore: [401, 403] }),
}),
[fetcher]
);

View file

@ -1,8 +1,9 @@
import { type ApplicationSignInExperience } from '@logto/schemas';
import useSWR from 'swr';
import useApi, { RequestError } from '@/hooks/use-api';
import useApi, { type RequestError } from '@/hooks/use-api';
import useSwrFetcher from '@/hooks/use-swr-fetcher';
import { shouldRetryOnError } from '@/utils/request';
/**
* SWR fetcher for application sign-in experience
@ -18,13 +19,7 @@ const useApplicationSignInExperienceSWR = (applicationId: string) => {
`api/applications/${applicationId}/sign-in-experience`,
{
fetcher,
shouldRetryOnError: (error: unknown) => {
if (error instanceof RequestError) {
return error.status !== 404;
}
return true;
},
shouldRetryOnError: shouldRetryOnError({ ignore: [404] }),
}
);
};

View file

@ -24,11 +24,12 @@ import Table from '@/ds-components/Table';
import Tag from '@/ds-components/Tag';
import Textarea from '@/ds-components/Textarea';
import { Tooltip } from '@/ds-components/Tip';
import useApi, { RequestError } from '@/hooks/use-api';
import useApi, { type RequestError } from '@/hooks/use-api';
import useSwrFetcher from '@/hooks/use-swr-fetcher';
import useUiLanguages from '@/hooks/use-ui-languages';
import type { CustomPhraseResponse } from '@/types/custom-phrase';
import { trySubmitSafe } from '@/utils/form';
import { shouldRetryOnError } from '@/utils/request';
import * as styles from './LanguageDetails.module.scss';
import { hiddenLocalePhraseGroups, hiddenLocalePhrases } from './constants';
@ -79,13 +80,7 @@ function LanguageDetails() {
`api/custom-phrases/${selectedLanguage}`,
{
fetcher,
shouldRetryOnError: (error: unknown) => {
if (error instanceof RequestError) {
return error.status !== 404;
}
return true;
},
shouldRetryOnError: shouldRetryOnError({ ignore: [404] }),
}
);

View file

@ -0,0 +1,23 @@
import { RequestError } from '@/hooks/use-api';
type Options = {
ignore: number[];
};
/**
* Determines if a SWR request should be retried based on the error status code
* @param options.ignore - an array of status codes to exclude from retrying
* @returns An anonymous function that takes an error and returns a boolean. Returns `true` to retry the request, `false` otherwise.
*/
export const shouldRetryOnError = (options?: Options) => {
return (error: unknown): boolean => {
if (error instanceof RequestError) {
const { status } = error;
const { ignore } = options ?? {};
return !ignore?.includes(status);
}
return true;
};
};

View file

@ -26,6 +26,8 @@ const general = {
enable: 'Aktivieren',
reminder: 'Erinnerung',
delete: 'Löschen',
/** UNTRANSLATED */
deleted: 'Deleted',
more_options: 'MEHR OPTIONEN',
close: 'Schließen',
copy: 'Kopieren',

View file

@ -26,6 +26,7 @@ const general = {
enable: 'Enable',
reminder: 'Reminder',
delete: 'Delete',
deleted: 'Deleted',
more_options: 'MORE OPTIONS',
close: 'Close',
copy: 'Copy',

View file

@ -26,6 +26,8 @@ const general = {
enable: 'Habilitar',
reminder: 'Recordatorio',
delete: 'Eliminar',
/** UNTRANSLATED */
deleted: 'Deleted',
more_options: 'MÁS OPCIONES',
close: 'Cerrar',
copy: 'Copiar',

View file

@ -26,6 +26,8 @@ const general = {
enable: 'Activer',
reminder: 'Rappel',
delete: 'Supprimer',
/** UNTRANSLATED */
deleted: 'Deleted',
more_options: "PLUS D'OPTIONS",
close: 'Fermer',
copy: 'Copier',

View file

@ -26,6 +26,8 @@ const general = {
enable: 'Abilita',
reminder: 'Promemoria',
delete: 'Elimina',
/** UNTRANSLATED */
deleted: 'Deleted',
more_options: 'PIÙ OPZIONI',
close: 'Chiudi',
copy: 'Copia',

View file

@ -26,6 +26,8 @@ const general = {
enable: '有効にする',
reminder: 'リマインダー',
delete: '削除',
/** UNTRANSLATED */
deleted: 'Deleted',
more_options: 'その他のオプション',
close: '閉じる',
copy: 'コピーする',

View file

@ -26,6 +26,8 @@ const general = {
enable: '활성화',
reminder: '리마인더',
delete: '삭제',
/** UNTRANSLATED */
deleted: 'Deleted',
more_options: '더 많은 설정',
close: '닫기',
copy: '복사',

View file

@ -26,6 +26,8 @@ const general = {
enable: 'Włącz',
reminder: 'Przypomnienie',
delete: 'Usuń',
/** UNTRANSLATED */
deleted: 'Deleted',
more_options: 'WIĘCEJ OPCJI',
close: 'Zamknij',
copy: 'Kopiuj',

View file

@ -26,6 +26,8 @@ const general = {
enable: 'Habilitar',
reminder: 'Lembrete',
delete: 'Excluir',
/** UNTRANSLATED */
deleted: 'Deleted',
more_options: 'MAIS OPÇÕES',
close: 'Fechar',
copy: 'Copiar',

View file

@ -26,6 +26,8 @@ const general = {
enable: 'Ativar',
reminder: 'Lembrete',
delete: 'Eliminar',
/** UNTRANSLATED */
deleted: 'Deleted',
more_options: 'MAIS OPÇÕES',
close: 'Fechar',
copy: 'Copiar',

View file

@ -26,6 +26,8 @@ const general = {
enable: 'Включить',
reminder: 'Напоминание',
delete: 'Удалить',
/** UNTRANSLATED */
deleted: 'Deleted',
more_options: 'Дополнительные опции',
close: 'Закрыть',
copy: 'Копировать',

View file

@ -26,6 +26,8 @@ const general = {
enable: 'Etkinleştir',
reminder: 'Hatırlatıcı',
delete: 'Sil',
/** UNTRANSLATED */
deleted: 'Deleted',
more_options: 'DAHA FAZLA SEÇENEK',
close: 'Kapat',
copy: 'Kopyala',

View file

@ -26,6 +26,8 @@ const general = {
enable: '启用',
reminder: '提示',
delete: '删除',
/** UNTRANSLATED */
deleted: 'Deleted',
more_options: '更多选项',
close: '关闭',
copy: '复制',

View file

@ -26,6 +26,8 @@ const general = {
enable: '啟用',
reminder: '提示',
delete: '刪除',
/** UNTRANSLATED */
deleted: 'Deleted',
more_options: '更多選項',
close: '關閉',
copy: '複製',

View file

@ -26,6 +26,8 @@ const general = {
enable: '啟用',
reminder: '提示',
delete: '刪除',
/** UNTRANSLATED */
deleted: 'Deleted',
more_options: '更多選項',
close: '關閉',
copy: '複製',