Skip to content

Commit

Permalink
Refactor ErrorBoundary
Browse files Browse the repository at this point in the history
  • Loading branch information
haishanh committed Oct 5, 2023
1 parent 3bf2f67 commit def689d
Show file tree
Hide file tree
Showing 23 changed files with 238 additions and 115 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,13 @@
"i18next-browser-languagedetector": "7.1.0",
"immer": "10.0.2",
"invariant": "^2.2.4",
"is-network-error": "1.0.0",
"jotai": "^2.4.3",
"lodash-es": "^4.17.21",
"modern-normalize": "2.0.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-error-boundary": "4.0.11",
"react-feather": "^2.0.10",
"react-i18next": "13.2.2",
"react-icons": "4.11.0",
Expand Down
20 changes: 20 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/api/configs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { getURLAndInit } from 'src/misc/request-helper';
import { ClashGeneralConfig } from 'src/store/types';
import { ClashAPIConfig } from 'src/types';

import { req } from './fetch';

const endpoint = '/configs';
Expand Down
15 changes: 15 additions & 0 deletions src/api/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
import isNetworkError from 'is-network-error';

import { YacdBackendUnauthorizedError, YacdFetchNetworkError } from '$src/misc/errors';
import { FetchCtx } from '$src/types';

export function req(url: string, init: RequestInit) {
if (import.meta.env.DEV) {
return import('./mock').then((mod) => mod.mock(url, init));
}
return fetch(url, init);
}

export function handleFetchError(err: unknown, ctx: FetchCtx) {
if (isNetworkError(err)) throw new YacdFetchNetworkError('', ctx);
throw err;
}

export function validateFetchResponse(res: Response, ctx: FetchCtx) {
if (res.status === 401) throw new YacdBackendUnauthorizedError('', ctx);
return res;
}
5 changes: 3 additions & 2 deletions src/api/rule-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ function normalizeAPIResponse(data: RuleProviderAPIData) {
return { byName, names };
}

export async function fetchRuleProviders(endpoint: string, apiConfig: ClashAPIConfig) {
export async function fetchRuleProviders(ctx: { queryKey: readonly [string, ClashAPIConfig] }) {
const endpoint = ctx.queryKey[0];
const apiConfig = ctx.queryKey[1];
const { url, init } = getURLAndInit(apiConfig);

let data = { providers: {} };
try {
const res = await fetch(url + endpoint, init);
Expand Down
21 changes: 12 additions & 9 deletions src/api/rules.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import invariant from 'invariant';
import { getURLAndInit } from 'src/misc/request-helper';
import { ClashAPIConfig } from 'src/types';
import { handleFetchError, req, validateFetchResponse } from './fetch';

// const endpoint = '/rules';

Expand All @@ -22,18 +23,20 @@ function normalizeAPIResponse(json: { rules: Array<RuleAPIItem> }): Array<RuleIt
return json.rules.map((r: RuleAPIItem, i: number) => ({ ...r, id: i }));
}

export async function fetchRules(endpoint: string, apiConfig: ClashAPIConfig) {
export async function fetchRules(ctx: { queryKey: readonly [string, ClashAPIConfig] }) {
const endpoint = ctx.queryKey[0];
const apiConfig = ctx.queryKey[1];
const { url, init } = getURLAndInit(apiConfig);
let json = { rules: [] };
let res: Response;
try {
const { url, init } = getURLAndInit(apiConfig);
const res = await fetch(url + endpoint, init);
if (res.ok) {
json = await res.json();
}
res = await req(url + endpoint, init);
} catch (err) {
// log and ignore
// eslint-disable-next-line no-console
console.log('failed to fetch rules', err);
handleFetchError(err, { endpoint, apiConfig });
}
validateFetchResponse(res, { endpoint, apiConfig });
if (res.ok) {
json = await res.json();
}
return normalizeAPIResponse(json);
}
32 changes: 0 additions & 32 deletions src/components/ErrorBoundary.tsx

This file was deleted.

19 changes: 10 additions & 9 deletions src/components/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { QueryClientProvider } from '@tanstack/react-query';
import cx from 'clsx';
import { useAtom } from 'jotai';
import * as React from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { RouteObject } from 'react-router';
import { HashRouter as Router, useRoutes } from 'react-router-dom';
import { Toaster } from 'sonner';
Expand All @@ -18,7 +19,7 @@ import { darkModePureBlackToggleAtom } from '$src/store/app';
import { actions, initialState } from '../store';
import { Backend } from './backend/Backend';
import { MutableConnRefCtx } from './conns/ConnCtx';
import ErrorBoundary from './ErrorBoundary';
import { ErrorFallback } from './error/ErrorFallback';
import Home from './Home';
import Loading2 from './Loading2';
import s0 from './Root.module.scss';
Expand All @@ -32,7 +33,7 @@ const Config = lazy(() => import('./Config'));
const Logs = lazy(() => import('./Logs'));
const Proxies = lazy(() => import('./proxies/Proxies'));
const Rules = lazy(() => import('./Rules'));
const StyleGuide = lazy(() => import('$src/components/style/StyleGuide'))
const StyleGuide = lazy(() => import('$src/components/style/StyleGuide'));

const routes = [
{ path: '/', element: <Home /> },
Expand Down Expand Up @@ -88,21 +89,21 @@ function AppShell({ children }: { children: React.ReactNode }) {
}

const Root = () => (
<ErrorBoundary>
<Router>
<StateProvider initialState={initialState} actions={actions}>
<QueryClientProvider client={queryClient}>
<Router>
<AppConfigSideEffect />
<AppShell>
<AppConfigSideEffect />
<AppShell>
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Head />
<Suspense fallback={<Loading />}>
<App />
</Suspense>
</AppShell>
</Router>
</ErrorBoundary>
</AppShell>
</QueryClientProvider>
</StateProvider>
</ErrorBoundary>
</Router>
);

export default Root;
2 changes: 2 additions & 0 deletions src/components/Rules.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import { areEqual, VariableSizeList } from 'react-window';

Check failure on line 3 in src/components/Rules.tsx

View workflow job for this annotation

GitHub Actions / install (20.x)

'useNavigate' is defined but never used
import { toast } from 'sonner';

Check failure on line 5 in src/components/Rules.tsx

View workflow job for this annotation

GitHub Actions / install (20.x)

'toast' is defined but never used
import { RuleProviderItem } from '$src/components/rules/RuleProviderItem';
import { useRuleAndProvider } from '$src/components/rules/rules.hooks';
Expand All @@ -14,6 +15,7 @@ import useRemainingViewPortHeight from '../hooks/useRemainingViewPortHeight';
import ContentHeader from './ContentHeader';
import Rule from './Rule';
import s from './Rules.module.scss';
import { useNavigate } from 'react-router';

const { memo } = React;

Expand Down
24 changes: 0 additions & 24 deletions src/components/SvgGithub.tsx

This file was deleted.

4 changes: 2 additions & 2 deletions src/components/about/About.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { useQuery } from '@tanstack/react-query';
import * as React from 'react';
import { GitHub } from 'react-feather';
import { fetchVersion } from 'src/api/version';
import ContentHeader from 'src/components/ContentHeader';
import { useApiConfig } from 'src/store/app';

import { GitHubIcon } from '../icon/GitHubIcon';
import s from './About.module.scss';

function Version({ name, link, version }: { name: string; link: string; version: string }) {
Expand All @@ -17,7 +17,7 @@ function Version({ name, link, version }: { name: string; link: string; version:
</p>
<p>
<a className={s.link} href={link} target="_blank" rel="noopener noreferrer">
<GitHub size={20} />
<GitHubIcon size={20} />
<span>Source</span>
</a>
</p>
Expand Down
5 changes: 1 addition & 4 deletions src/components/backend/Backend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';

import { BackendList } from '$src/components/backend/BackendList';

import { Sep } from '../shared/Basic';
import { ThemeSwitcher } from '../shared/ThemeSwitcher';
import { BackendForm } from './BackendForm';

Expand All @@ -17,7 +18,3 @@ export function Backend() {
</div>
);
}

function Sep() {
return <div style={{ height: 20 }} />;
}
59 changes: 59 additions & 0 deletions src/components/error/BackendErrorFallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React, { useCallback } from 'react';
import type { FallbackProps } from 'react-error-boundary';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';

import { FetchCtx } from '$src/types';

import Button from '../Button';
import { Sep } from '../shared/Basic';
import { ErrorFallbackLayout } from './ErrorFallbackLayout';

function useStuff(resetErrorBoundary: FallbackProps['resetErrorBoundary']) {
const { t } = useTranslation();
const navigate = useNavigate();
const onClick = useCallback(
(e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
resetErrorBoundary();
navigate('/backend');
},
[navigate, resetErrorBoundary],
);
return { t, onClick };
}

export function FetchNetworkErrorFallback(props: {
ctx: FetchCtx;
resetErrorBoundary: FallbackProps['resetErrorBoundary'];
}) {
const { resetErrorBoundary, ctx } = props;
const { t, onClick } = useStuff(resetErrorBoundary);
return (
<ErrorFallbackLayout>
<p>Failed to connect to the backend {ctx.apiConfig.baseURL}</p>
<Sep />
<Button onClick={onClick}>{t('switch_backend')}</Button>
</ErrorFallbackLayout>
);
}

export function BackendUnauthorizedErrorFallback(props: {
ctx: FetchCtx;
resetErrorBoundary: FallbackProps['resetErrorBoundary'];
}) {
const { resetErrorBoundary, ctx } = props;
const { t, onClick } = useStuff(resetErrorBoundary);
return (
<ErrorFallbackLayout>
<p>Unauthorized to connect to the backend {ctx.apiConfig.baseURL}</p>
{
ctx.apiConfig.secret ?
<p>You might using a wrong secret</p>
: <p>You probably need to provide a secret</p>
}
<Sep />
<Button onClick={onClick}>{t('switch_backend')}</Button>
</ErrorFallbackLayout>
);
}
14 changes: 14 additions & 0 deletions src/components/error/ErrorBoundaryFallback.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.link {
display: inline-flex;
align-items: center;

color: var(--color-text-secondary);
&:hover,
&:active {
color: #387cec;
}

svg {
margin-right: 5px;
}
}
Loading

0 comments on commit def689d

Please sign in to comment.