mirror of
https://github.com/urbit/shrub.git
synced 2024-12-01 06:35:32 +03:00
errors: more robust error handling
This commit is contained in:
parent
a9917cf2ec
commit
b0ccb67adc
1796
pkg/grid/package-lock.json
generated
1796
pkg/grid/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -36,6 +36,7 @@
|
||||
"query-string": "^7.0.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-error-boundary": "^3.1.3",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"slugify": "^1.6.0",
|
||||
"zustand": "^3.5.7"
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import Mousetrap from 'mousetrap';
|
||||
import { BrowserRouter, Switch, Route, useHistory, useLocation } from 'react-router-dom';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { Grid } from './pages/Grid';
|
||||
import useDocketState from './state/docket';
|
||||
import { PermalinkRoutes } from './pages/PermalinkRoutes';
|
||||
@ -11,6 +12,8 @@ import { useMedia } from './logic/useMedia';
|
||||
import { useHarkStore } from './state/hark';
|
||||
import { useTheme } from './state/settings';
|
||||
import { useLocalState } from './state/local';
|
||||
import { ErrorAlert } from './components/ErrorAlert';
|
||||
import { useErrorHandler } from './logic/useErrorHandler';
|
||||
|
||||
const getNoteRedirect = (path: string) => {
|
||||
if (path.startsWith('/desk/')) {
|
||||
@ -23,6 +26,7 @@ const getNoteRedirect = (path: string) => {
|
||||
const AppRoutes = () => {
|
||||
const { push } = useHistory();
|
||||
const { search } = useLocation();
|
||||
const handleError = useErrorHandler();
|
||||
|
||||
useEffect(() => {
|
||||
const query = new URLSearchParams(search);
|
||||
@ -31,6 +35,7 @@ const AppRoutes = () => {
|
||||
push(redir);
|
||||
}
|
||||
}, [search]);
|
||||
|
||||
const theme = useTheme();
|
||||
const isDarkMode = useMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
@ -46,23 +51,28 @@ const AppRoutes = () => {
|
||||
|
||||
useEffect(() => {}, []);
|
||||
|
||||
useEffect(() => {
|
||||
useEffect(
|
||||
handleError(() => {
|
||||
window.name = 'grid';
|
||||
|
||||
const { fetchDefaultAlly, fetchAllies, fetchCharges } = useDocketState.getState();
|
||||
fetchDefaultAlly();
|
||||
fetchCharges();
|
||||
fetchAllies();
|
||||
|
||||
const { fetchVats, fetchLag } = useKilnState.getState();
|
||||
fetchVats();
|
||||
fetchLag();
|
||||
|
||||
useContactState.getState().initialize(api);
|
||||
useHarkStore.getState().initialize(api);
|
||||
|
||||
Mousetrap.bind(['command+/', 'ctrl+/'], () => {
|
||||
push('/leap/search');
|
||||
});
|
||||
}, []);
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
@ -76,8 +86,10 @@ export function App() {
|
||||
const base = import.meta.env.MODE === 'mock' ? undefined : '/apps/grid';
|
||||
|
||||
return (
|
||||
<ErrorBoundary FallbackComponent={ErrorAlert} onReset={() => window.location.reload()}>
|
||||
<BrowserRouter basename={base}>
|
||||
<AppRoutes />
|
||||
</BrowserRouter>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
55
pkg/grid/src/components/ErrorAlert.tsx
Normal file
55
pkg/grid/src/components/ErrorAlert.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import { Dialog, DialogClose, DialogContent } from './Dialog';
|
||||
import { Button } from './Button';
|
||||
|
||||
interface ErrorAlertProps {
|
||||
error: Error;
|
||||
resetErrorBoundary: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const SubmitIssue = ({ error }: { error: Error }) => {
|
||||
const title = error.message;
|
||||
const body = `\`\`\`%0A${error.stack?.replaceAll('\n', '%0A')}%0A\`\`\``;
|
||||
|
||||
return (
|
||||
<Button
|
||||
as="a"
|
||||
variant="caution"
|
||||
href={`https://github.com/urbit/landscape/issues/new?assignees=&labels=bug&title=${title}&body=${body}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Submit Issue
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export const ErrorAlert = ({ error, resetErrorBoundary, className }: ErrorAlertProps) => {
|
||||
return (
|
||||
<Dialog defaultOpen modal onOpenChange={() => resetErrorBoundary()}>
|
||||
<DialogContent
|
||||
showClose={false}
|
||||
className={cn('pr-8 space-y-6', className)}
|
||||
containerClass="w-full max-w-3xl"
|
||||
>
|
||||
<h2 className="h4">
|
||||
<span className="mr-3 text-orange-500">Encountered error:</span>
|
||||
<span className="font-mono">{error.message}</span>
|
||||
</h2>
|
||||
{error.stack && (
|
||||
<div className="w-full p-2 bg-gray-50 overflow-x-auto rounded">
|
||||
<pre>{error.stack}</pre>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex space-x-6">
|
||||
<DialogClose as={Button} variant="secondary">
|
||||
Try Again
|
||||
</DialogClose>
|
||||
<DialogClose as={SubmitIssue} error={error} />
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
17
pkg/grid/src/logic/useErrorHandler.ts
Normal file
17
pkg/grid/src/logic/useErrorHandler.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { useErrorHandler as useBoundaryHandler } from 'react-error-boundary';
|
||||
|
||||
export function useErrorHandler() {
|
||||
const handle = useBoundaryHandler();
|
||||
|
||||
function handleError(cb: (...args: any[]) => any) {
|
||||
return (...args: any[]) => {
|
||||
try {
|
||||
cb(...args);
|
||||
} catch (error) {
|
||||
handle(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return handleError;
|
||||
}
|
@ -13,6 +13,7 @@ import React, {
|
||||
import { Link, useHistory, useRouteMatch } from 'react-router-dom';
|
||||
import { Cross } from '../components/icons/Cross';
|
||||
import { useDebounce } from '../logic/useDebounce';
|
||||
import { useErrorHandler } from '../logic/useErrorHandler';
|
||||
import { MenuState, useLeapStore } from './Nav';
|
||||
|
||||
function normalizePathEnding(path: string) {
|
||||
@ -58,6 +59,7 @@ export const Leap = React.forwardRef(
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
useImperativeHandle(ref, () => inputRef.current);
|
||||
const { rawInput, selectedMatch, matches, selection, select } = useLeapStore();
|
||||
const handleError = useErrorHandler();
|
||||
|
||||
useEffect(() => {
|
||||
if (selection && rawInput === '') {
|
||||
@ -123,7 +125,7 @@ export const Leap = React.forwardRef(
|
||||
const handleSearch = useCallback(debouncedSearch, [match]);
|
||||
|
||||
const onChange = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
handleError((e: ChangeEvent<HTMLInputElement>) => {
|
||||
const input = e.target as HTMLInputElement;
|
||||
const value = input.value.trim();
|
||||
const isDeletion = (e.nativeEvent as InputEvent).inputType === 'deleteContentBackward';
|
||||
@ -148,12 +150,12 @@ export const Leap = React.forwardRef(
|
||||
}
|
||||
|
||||
handleSearch(value);
|
||||
},
|
||||
}),
|
||||
[matches]
|
||||
);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(e: FormEvent<HTMLFormElement>) => {
|
||||
handleError((e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
const value = inputRef.current?.value.trim();
|
||||
@ -170,12 +172,12 @@ export const Leap = React.forwardRef(
|
||||
|
||||
push(currentMatch.url);
|
||||
useLeapStore.setState({ rawInput: '' });
|
||||
},
|
||||
}),
|
||||
[match, selectedMatch]
|
||||
);
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
(e: KeyboardEvent<HTMLDivElement>) => {
|
||||
handleError((e: KeyboardEvent<HTMLDivElement>) => {
|
||||
const deletion = e.key === 'Backspace' || e.key === 'Delete';
|
||||
const arrow = e.key === 'ArrowDown' || e.key === 'ArrowUp';
|
||||
|
||||
@ -207,7 +209,7 @@ export const Leap = React.forwardRef(
|
||||
selectedMatch: newMatch
|
||||
});
|
||||
}
|
||||
},
|
||||
}),
|
||||
[selection, rawInput, match, matches, selectedMatch]
|
||||
);
|
||||
|
||||
|
@ -2,9 +2,11 @@ import { DialogContent } from '@radix-ui/react-dialog';
|
||||
import * as Portal from '@radix-ui/react-portal';
|
||||
import classNames from 'classnames';
|
||||
import React, { FunctionComponent, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { Route, Switch, useHistory, useRouteMatch } from 'react-router-dom';
|
||||
import create from 'zustand';
|
||||
import { Dialog } from '../components/Dialog';
|
||||
import { ErrorAlert } from '../components/ErrorAlert';
|
||||
import { Help } from './Help';
|
||||
import { Leap } from './Leap';
|
||||
import { Notifications } from './Notifications';
|
||||
@ -106,7 +108,7 @@ export const Nav: FunctionComponent<NavProps> = ({ menu }) => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ErrorBoundary FallbackComponent={ErrorAlert} onReset={() => push('/')}>
|
||||
{/* Using portal so that we can retain the same nav items both in the dialog and in the base header */}
|
||||
<Portal.Root
|
||||
containerRef={dialogContentOpen ? dialogNavRef : navRef}
|
||||
@ -172,6 +174,6 @@ export const Nav: FunctionComponent<NavProps> = ({ menu }) => {
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
@ -1,10 +1,12 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Link, NavLink, Route, Switch } from 'react-router-dom';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { Link, NavLink, Route, RouteComponentProps, Switch } from 'react-router-dom';
|
||||
import { Button } from '../components/Button';
|
||||
import { ErrorAlert } from '../components/ErrorAlert';
|
||||
import { useHarkStore } from '../state/hark';
|
||||
import { Inbox } from './notifications/Inbox';
|
||||
|
||||
export const Notifications = () => {
|
||||
export const Notifications = ({ history }: RouteComponentProps) => {
|
||||
const markAllAsRead = () => {
|
||||
const { archiveAll } = useHarkStore.getState();
|
||||
archiveAll();
|
||||
@ -26,6 +28,10 @@ export const Notifications = () => {
|
||||
// const select = useLeapStore((s) => s.select);
|
||||
|
||||
return (
|
||||
<ErrorBoundary
|
||||
FallbackComponent={ErrorAlert}
|
||||
onReset={() => history.push('/leap/notifications')}
|
||||
>
|
||||
<div className="grid grid-rows-[auto,1fr] h-full p-4 md:p-8 overflow-hidden">
|
||||
<header className="space-x-2 mb-8">
|
||||
<NavLink
|
||||
@ -64,5 +70,6 @@ export const Notifications = () => {
|
||||
</Route>
|
||||
</Switch>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
@ -1,16 +1,19 @@
|
||||
import React from 'react';
|
||||
import { Route, RouteComponentProps, Switch } from 'react-router-dom';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { TreatyInfo } from './search/TreatyInfo';
|
||||
import { Apps } from './search/Apps';
|
||||
import { Home } from './search/Home';
|
||||
import { Providers } from './search/Providers';
|
||||
import { ErrorAlert } from '../components/ErrorAlert';
|
||||
|
||||
type SearchProps = RouteComponentProps<{
|
||||
query?: string;
|
||||
}>;
|
||||
|
||||
export const Search = ({ match }: SearchProps) => {
|
||||
export const Search = ({ match, history }: SearchProps) => {
|
||||
return (
|
||||
<ErrorBoundary FallbackComponent={ErrorAlert} onReset={() => history.push('/leap/search')}>
|
||||
<Switch>
|
||||
<Route
|
||||
path={[`${match.path}/direct/apps/:host/:desk`, `${match.path}/:ship/apps/:host/:desk`]}
|
||||
@ -20,5 +23,6 @@ export const Search = ({ match }: SearchProps) => {
|
||||
<Route path={`${match.path}/:ship`} component={Providers} />
|
||||
<Route path={`${match.path}`} component={Home} />
|
||||
</Switch>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React, { PropsWithChildren, useCallback } from 'react';
|
||||
import { Link, Route, RouteComponentProps, Switch, useRouteMatch } from 'react-router-dom';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import classNames from 'classnames';
|
||||
import { NotificationPrefs } from './preferences/NotificationPrefs';
|
||||
import { SystemUpdatePrefs } from './preferences/SystemUpdatePrefs';
|
||||
@ -9,6 +10,7 @@ import { InterfacePrefs } from './preferences/InterfacePrefs';
|
||||
import { useCharges } from '../state/docket';
|
||||
import { AppPrefs } from './preferences/AppPrefs';
|
||||
import { DocketImage } from '../components/DocketImage';
|
||||
import { ErrorAlert } from '../components/ErrorAlert';
|
||||
|
||||
interface SystemPreferencesSectionProps {
|
||||
url: string;
|
||||
@ -36,7 +38,7 @@ function SystemPreferencesSection({
|
||||
}
|
||||
|
||||
export const SystemPreferences = (props: RouteComponentProps<{ submenu: string }>) => {
|
||||
const { match } = props;
|
||||
const { match, history } = props;
|
||||
const subMatch = useRouteMatch<{ submenu: string; desk?: string }>(
|
||||
`${match.url}/:submenu/:desk?`
|
||||
);
|
||||
@ -60,6 +62,10 @@ export const SystemPreferences = (props: RouteComponentProps<{ submenu: string }
|
||||
const subUrl = useCallback((submenu: string) => `${match.url}/${submenu}`, [match]);
|
||||
|
||||
return (
|
||||
<ErrorBoundary
|
||||
FallbackComponent={ErrorAlert}
|
||||
onReset={() => history.push('/leap/system-preferences')}
|
||||
>
|
||||
<div className="flex h-full overflow-y-auto">
|
||||
<aside className="flex-none self-start min-w-60 py-8 font-semibold border-r-2 border-gray-50">
|
||||
<nav className="px-6">
|
||||
@ -109,5 +115,6 @@ export const SystemPreferences = (props: RouteComponentProps<{ submenu: string }
|
||||
</Switch>
|
||||
</section>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { map, omit } from 'lodash';
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { Route, RouteComponentProps } from 'react-router-dom';
|
||||
import { ErrorAlert } from '../components/ErrorAlert';
|
||||
import { MenuState, Nav } from '../nav/Nav';
|
||||
import { useCharges } from '../state/docket';
|
||||
import { RemoveApp } from '../tiles/RemoveApp';
|
||||
@ -12,7 +14,7 @@ type GridProps = RouteComponentProps<{
|
||||
menu?: MenuState;
|
||||
}>;
|
||||
|
||||
export const Grid: FunctionComponent<GridProps> = ({ match }) => {
|
||||
export const Grid: FunctionComponent<GridProps> = ({ match, history }) => {
|
||||
const charges = useCharges();
|
||||
const chargesLoaded = Object.keys(charges).length > 0;
|
||||
|
||||
@ -32,6 +34,7 @@ export const Grid: FunctionComponent<GridProps> = ({ match }) => {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<ErrorBoundary FallbackComponent={ErrorAlert} onReset={() => history.push('/')}>
|
||||
<Route exact path="/app/:desk">
|
||||
<TileInfo />
|
||||
</Route>
|
||||
@ -41,6 +44,7 @@ export const Grid: FunctionComponent<GridProps> = ({ match }) => {
|
||||
<Route exact path="/app/:desk/remove">
|
||||
<RemoveApp />
|
||||
</Route>
|
||||
</ErrorBoundary>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user