pl-fe: error handling improvements, some refactoring i forgot to commit before
Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
@ -1,37 +0,0 @@
|
||||
import { defineMessages } from 'react-intl';
|
||||
|
||||
import toast from 'pl-fe/toast';
|
||||
|
||||
import { getClient } from '../api';
|
||||
|
||||
import type { AppDispatch, RootState } from 'pl-fe/store';
|
||||
|
||||
const messages = defineMessages({
|
||||
blocksSuccess: { id: 'import_data.success.blocks', defaultMessage: 'Blocks imported successfully' },
|
||||
followersSuccess: { id: 'import_data.success.followers', defaultMessage: 'Followers imported successfully' },
|
||||
mutesSuccess: { id: 'import_data.success.mutes', defaultMessage: 'Mutes imported successfully' },
|
||||
});
|
||||
|
||||
const importFollows = (list: File | string, overwrite?: boolean) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
||||
getClient(getState).settings.importFollows(list, overwrite ? 'overwrite' : 'merge').then(response => {
|
||||
toast.success(messages.followersSuccess);
|
||||
});
|
||||
|
||||
const importBlocks = (list: File | string, overwrite?: boolean) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
||||
getClient(getState).settings.importBlocks(list, overwrite ? 'overwrite' : 'merge').then(response => {
|
||||
toast.success(messages.blocksSuccess);
|
||||
});
|
||||
|
||||
const importMutes = (list: File | string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) =>
|
||||
getClient(getState).settings.importMutes(list).then(response => {
|
||||
toast.success(messages.mutesSuccess);
|
||||
});
|
||||
|
||||
export {
|
||||
importFollows,
|
||||
importBlocks,
|
||||
importMutes,
|
||||
};
|
||||
@ -1,204 +0,0 @@
|
||||
import React, { type ErrorInfo, useRef, useState } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { NODE_ENV } from 'pl-fe/build-config';
|
||||
import HStack from 'pl-fe/components/ui/hstack';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import Textarea from 'pl-fe/components/ui/textarea';
|
||||
import { useLogo } from 'pl-fe/hooks/use-logo';
|
||||
import { usePlFeConfig } from 'pl-fe/hooks/use-pl-fe-config';
|
||||
import { captureSentryException } from 'pl-fe/sentry';
|
||||
import KVStore from 'pl-fe/storage/kv-store';
|
||||
import sourceCode from 'pl-fe/utils/code';
|
||||
import { unregisterSW } from 'pl-fe/utils/sw';
|
||||
|
||||
import SentryFeedbackForm from './sentry-feedback-form';
|
||||
import SiteLogo from './site-logo';
|
||||
|
||||
interface ISiteErrorBoundary {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/** Application-level error boundary. Fills the whole screen. */
|
||||
const SiteErrorBoundary: React.FC<ISiteErrorBoundary> = ({ children }) => {
|
||||
const { links, sentryDsn } = usePlFeConfig();
|
||||
const { src: logoSrc } = useLogo();
|
||||
const textarea = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const [error, setError] = useState<unknown>();
|
||||
const [componentStack, setComponentStack] = useState<string | null | undefined>();
|
||||
const [browser, setBrowser] = useState<Bowser.Parser.Parser>();
|
||||
const [sentryEventId, setSentryEventId] = useState<string>();
|
||||
|
||||
const sentryEnabled = Boolean(sentryDsn);
|
||||
const isProduction = NODE_ENV === 'production';
|
||||
const errorText = String(error) + componentStack;
|
||||
|
||||
const clearCookies: React.MouseEventHandler = (e) => {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
KVStore.clear();
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
e.preventDefault();
|
||||
unregisterSW().then(goHome).catch(goHome);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopy: React.MouseEventHandler = () => {
|
||||
if (!textarea.current) return;
|
||||
|
||||
textarea.current.select();
|
||||
textarea.current.setSelectionRange(0, 99999);
|
||||
|
||||
document.execCommand('copy');
|
||||
};
|
||||
|
||||
const handleError = (error: Error, info: ErrorInfo) => {
|
||||
setError(error);
|
||||
setComponentStack(info.componentStack);
|
||||
|
||||
captureSentryException(error, {
|
||||
tags: {
|
||||
// Allow page crashes to be easily searched in Sentry.
|
||||
ErrorBoundary: 'yes',
|
||||
},
|
||||
})
|
||||
.then((eventId) => setSentryEventId(eventId))
|
||||
.catch(console.error);
|
||||
|
||||
import('bowser')
|
||||
.then(({ default: Bowser }) => setBrowser(Bowser.getParser(window.navigator.userAgent)))
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
const goHome = () => {
|
||||
location.href = '/';
|
||||
};
|
||||
|
||||
const fallback = (
|
||||
<div className='flex h-screen flex-col bg-white pb-12 pt-16 black:bg-black dark:bg-primary-900'>
|
||||
<main className='mx-auto flex w-full max-w-7xl grow flex-col justify-center px-4 sm:px-6 lg:px-8'>
|
||||
{logoSrc && (
|
||||
<div className='flex shrink-0 justify-center'>
|
||||
<a href='/' className='inline-flex'>
|
||||
<SiteLogo className='h-12 w-auto cursor-pointer' />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='py-8'>
|
||||
<div className='mx-auto max-w-xl space-y-2 text-center'>
|
||||
<h1 className='text-3xl font-extrabold tracking-tight text-gray-900 dark:text-gray-500 sm:text-4xl'>
|
||||
<FormattedMessage id='alert.unexpected.message' defaultMessage='Something went wrong.' />
|
||||
</h1>
|
||||
<p className='text-lg text-gray-700 dark:text-gray-600'>
|
||||
<FormattedMessage
|
||||
id='alert.unexpected.body'
|
||||
defaultMessage="We're sorry for the interruption. If the problem persists, please reach out to our support team. You may also try to {clearCookies} (this will log you out)."
|
||||
values={{
|
||||
clearCookies: (
|
||||
<a href='/' onClick={clearCookies} className='text-primary-600 hover:underline dark:text-primary-400'>
|
||||
<FormattedMessage
|
||||
id='alert.unexpected.clear_cookies'
|
||||
defaultMessage='clear cookies and browser data'
|
||||
/>
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<Text theme='muted'>
|
||||
<Text weight='medium' tag='span' theme='muted'>{sourceCode.displayName}:</Text>
|
||||
|
||||
{' '}{sourceCode.version}
|
||||
</Text>
|
||||
|
||||
<div className='mt-10'>
|
||||
<a href='/' className='text-base font-medium text-primary-600 hover:underline dark:text-primary-400'>
|
||||
<FormattedMessage id='alert.unexpected.return_home' defaultMessage='Return Home' />
|
||||
{' '}
|
||||
<span className='inline-block rtl:rotate-180' aria-hidden='true'>→</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mx-auto max-w-lg space-y-4 py-16'>
|
||||
{(isProduction) ? (
|
||||
(sentryEnabled && sentryEventId) && (
|
||||
<SentryFeedbackForm eventId={sentryEventId} />
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
{errorText && (
|
||||
<Textarea
|
||||
ref={textarea}
|
||||
value={errorText}
|
||||
onClick={handleCopy}
|
||||
isCodeEditor
|
||||
rows={12}
|
||||
readOnly
|
||||
/>
|
||||
)}
|
||||
|
||||
{browser && (
|
||||
<Stack>
|
||||
<Text weight='semibold'><FormattedMessage id='alert.unexpected.browser' defaultMessage='Browser' /></Text>
|
||||
<Text theme='muted'>{browser.getBrowserName()} {browser.getBrowserVersion()}</Text>
|
||||
</Stack>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer className='mx-auto w-full max-w-7xl shrink-0 px-4 sm:px-6 lg:px-8'>
|
||||
<HStack justifyContent='center' space={4} element='nav'>
|
||||
{links.status && (
|
||||
<SiteErrorBoundaryLink href={links.status}>
|
||||
<FormattedMessage id='alert.unexpected.links.status' defaultMessage='Status' />
|
||||
</SiteErrorBoundaryLink>
|
||||
)}
|
||||
|
||||
{links.help && (
|
||||
<SiteErrorBoundaryLink href={links.help}>
|
||||
<FormattedMessage id='alert.unexpected.links.help' defaultMessage='Help Center' />
|
||||
</SiteErrorBoundaryLink>
|
||||
)}
|
||||
|
||||
{links.support && (
|
||||
<SiteErrorBoundaryLink href={links.support}>
|
||||
<FormattedMessage id='alert.unexpected.links.support' defaultMessage='Support' />
|
||||
</SiteErrorBoundaryLink>
|
||||
)}
|
||||
</HStack>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<ErrorBoundary fallback={fallback} onError={handleError}>
|
||||
{children}
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
interface ISiteErrorBoundaryLink {
|
||||
href: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const SiteErrorBoundaryLink = ({ href, children }: ISiteErrorBoundaryLink) => (
|
||||
<>
|
||||
<span className='inline-block border-l border-gray-300' aria-hidden='true' />
|
||||
<a href={href} className='text-sm font-medium text-gray-700 hover:underline dark:text-gray-600'>
|
||||
{children}
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
|
||||
export { SiteErrorBoundary as default };
|
||||
@ -1,45 +0,0 @@
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import Column from 'pl-fe/components/ui/column';
|
||||
import IconButton from 'pl-fe/components/ui/icon-button';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import { isNetworkError } from 'pl-fe/utils/errors';
|
||||
|
||||
import type { ErrorRouteComponent } from '@tanstack/react-router';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'bundle_column_error.title', defaultMessage: 'Network error' },
|
||||
body: { id: 'bundle_column_error.body', defaultMessage: 'Something went wrong while loading this page.' },
|
||||
retry: { id: 'bundle_column_error.retry', defaultMessage: 'Try again' },
|
||||
});
|
||||
|
||||
const ErrorColumn: ErrorRouteComponent = ({ error, reset }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const handleRetry = () => {
|
||||
reset();
|
||||
};
|
||||
|
||||
if (!isNetworkError(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.title)}>
|
||||
<Stack space={4} alignItems='center' justifyContent='center' className='min-h-[160px] rounded-lg p-10'>
|
||||
<IconButton
|
||||
iconClassName='h-10 w-10'
|
||||
title={intl.formatMessage(messages.retry)}
|
||||
src={require('@phosphor-icons/core/regular/arrows-clockwise.svg')}
|
||||
onClick={handleRetry}
|
||||
/>
|
||||
|
||||
<Text align='center' theme='muted'>{intl.formatMessage(messages.body)}</Text>
|
||||
</Stack>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export { ErrorColumn as default };
|
||||
@ -12,6 +12,7 @@ import React, { useMemo } from 'react';
|
||||
import * as v from 'valibot';
|
||||
|
||||
import { FE_SUBDIRECTORY, WITH_LANDING_PAGE } from 'pl-fe/build-config';
|
||||
import SiteError from 'pl-fe/components/site-error';
|
||||
import Layout from 'pl-fe/components/ui/layout';
|
||||
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
|
||||
import { useFeatures } from 'pl-fe/hooks/use-features';
|
||||
@ -147,8 +148,6 @@ import {
|
||||
AwaitingApproval,
|
||||
} from '../util/async-components';
|
||||
|
||||
import ErrorColumn from './error-column';
|
||||
|
||||
import type { Features } from 'pl-api';
|
||||
|
||||
interface RouterContext {
|
||||
@ -1432,7 +1431,7 @@ const router = createRouter({
|
||||
},
|
||||
defaultNotFoundComponent: GenericNotFound,
|
||||
defaultPendingComponent: PendingComponent,
|
||||
defaultErrorComponent: ErrorColumn,
|
||||
defaultErrorComponent: SiteError,
|
||||
scrollRestoration: true,
|
||||
pathParamsAllowedCharacters: ['@'],
|
||||
});
|
||||
|
||||
@ -1,18 +1,15 @@
|
||||
import React, { Suspense } from 'react';
|
||||
|
||||
import LoadingScreen from 'pl-fe/components/loading-screen';
|
||||
import SiteErrorBoundary from 'pl-fe/components/site-error-boundary';
|
||||
import { RouterWithContext } from 'pl-fe/features/ui/router';
|
||||
|
||||
/** Highest level node with the Redux store. */
|
||||
const PlFeMount = () => {
|
||||
|
||||
return (
|
||||
<SiteErrorBoundary>
|
||||
<Suspense fallback={<LoadingScreen />}>
|
||||
<RouterWithContext />
|
||||
</Suspense>
|
||||
</SiteErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -233,14 +233,14 @@
|
||||
"admin.users.user_unsuggested_message": "@{acct} was unsuggested",
|
||||
"admin.users.user_unverified_message": "@{acct} was unverified",
|
||||
"admin.users.user_verified_message": "@{acct} was verified",
|
||||
"alert.unexpected.body": "We're sorry for the interruption. If the problem persists, please reach out to our support team. You may also try to {clearCookies} (this will log you out).",
|
||||
"alert.unexpected.body": "We're sorry for the interruption. If the problem persists, please report it in our {issueTracker}. You may also try to {clearCookies} (this will log you out).",
|
||||
"alert.unexpected.browser": "Browser",
|
||||
"alert.unexpected.clear_cookies": "clear cookies and browser data",
|
||||
"alert.unexpected.issue_tracker": "issue tracker",
|
||||
"alert.unexpected.links.help": "Help Center",
|
||||
"alert.unexpected.links.status": "Status",
|
||||
"alert.unexpected.links.support": "Support",
|
||||
"alert.unexpected.message": "Something went wrong.",
|
||||
"alert.unexpected.return_home": "Return Home",
|
||||
"alert.unexpected.submit_feedback": "Submit Feedback",
|
||||
"alert.unexpected.thanks": "Thanks for your feedback!",
|
||||
"aliases.account.add": "Create alias",
|
||||
|
||||
@ -1,11 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl, type MessageDescriptor } from 'react-intl';
|
||||
|
||||
import {
|
||||
importFollows,
|
||||
importBlocks,
|
||||
importMutes,
|
||||
} from 'pl-fe/actions/import-data';
|
||||
import List, { ListItem } from 'pl-fe/components/list';
|
||||
import Button from 'pl-fe/components/ui/button';
|
||||
import Column from 'pl-fe/components/ui/column';
|
||||
@ -15,14 +10,16 @@ import FormActions from 'pl-fe/components/ui/form-actions';
|
||||
import FormGroup from 'pl-fe/components/ui/form-group';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import Toggle from 'pl-fe/components/ui/toggle';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
import { useClient } from 'pl-fe/hooks/use-client';
|
||||
import { useFeatures } from 'pl-fe/hooks/use-features';
|
||||
|
||||
import type { AppDispatch, RootState } from 'pl-fe/store';
|
||||
import toast from 'pl-fe/toast';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.import_data', defaultMessage: 'Import data' },
|
||||
submit: { id: 'import_data.actions.import', defaultMessage: 'Import' },
|
||||
blocksSuccess: { id: 'import_data.success.blocks', defaultMessage: 'Blocks imported successfully' },
|
||||
followersSuccess: { id: 'import_data.success.followers', defaultMessage: 'Followers imported successfully' },
|
||||
mutesSuccess: { id: 'import_data.success.mutes', defaultMessage: 'Mutes imported successfully' },
|
||||
});
|
||||
|
||||
const followMessages = defineMessages({
|
||||
@ -49,13 +46,12 @@ interface IDataImporter {
|
||||
input_hint: MessageDescriptor;
|
||||
submit: MessageDescriptor;
|
||||
};
|
||||
action: (list: File, overwrite?: boolean) => (dispatch: AppDispatch, getState: () => RootState) => Promise<void>;
|
||||
action: (list: File, overwrite?: boolean) => Promise<void>;
|
||||
accept?: string;
|
||||
allowOverwrite?: boolean;
|
||||
}
|
||||
|
||||
const DataImporter: React.FC<IDataImporter> = ({ messages, action, accept = '.csv,text/csv', allowOverwrite }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@ -64,7 +60,7 @@ const DataImporter: React.FC<IDataImporter> = ({ messages, action, accept = '.cs
|
||||
|
||||
const handleSubmit: React.FormEventHandler = (event) => {
|
||||
setIsLoading(true);
|
||||
dispatch(action(file!, overwrite)).then(() => {
|
||||
action(file!, overwrite).then(() => {
|
||||
setIsLoading(false);
|
||||
}).catch(() => {
|
||||
setIsLoading(false);
|
||||
@ -114,9 +110,25 @@ const DataImporter: React.FC<IDataImporter> = ({ messages, action, accept = '.cs
|
||||
};
|
||||
|
||||
const ImportDataPage = () => {
|
||||
const client = useClient();
|
||||
const intl = useIntl();
|
||||
const features = useFeatures();
|
||||
|
||||
const importFollows = (list: File | string, overwrite?: boolean) =>
|
||||
client.settings.importFollows(list, overwrite ? 'overwrite' : 'merge').then(response => {
|
||||
toast.success(messages.followersSuccess);
|
||||
});
|
||||
|
||||
const importBlocks = (list: File | string, overwrite?: boolean) =>
|
||||
client.settings.importBlocks(list, overwrite ? 'overwrite' : 'merge').then(response => {
|
||||
toast.success(messages.blocksSuccess);
|
||||
});
|
||||
|
||||
const importMutes = (list: File | string) =>
|
||||
client.settings.importMutes(list).then(response => {
|
||||
toast.success(messages.mutesSuccess);
|
||||
});
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.heading)}>
|
||||
{features.importFollows && <DataImporter action={importFollows} messages={followMessages} allowOverwrite={features.importOverwrite} />}
|
||||
|
||||
@ -423,3 +423,19 @@ div:has(.⁂-background-shapes), .dark {
|
||||
content: '·';
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.⁂-site-error {
|
||||
@apply flex h-screen flex-col bg-white pb-12 pt-16 black:bg-black dark:bg-primary-900 md:col-span-12 lg:col-span-9 xl:col-span-6;
|
||||
|
||||
main {
|
||||
@apply mx-auto flex w-full max-w-7xl grow flex-col justify-center px-4 sm:px-6 lg:px-8;
|
||||
}
|
||||
|
||||
footer {
|
||||
@apply mx-auto w-full max-w-7xl shrink-0 px-4 sm:px-6 lg:px-8;
|
||||
}
|
||||
}
|
||||
|
||||
.⁂-layout__content .⁂-site-error {
|
||||
height: fit-content;
|
||||
}
|
||||
Reference in New Issue
Block a user