[UI] Copy error button (#13168)

Adds button to copy UI error information
This commit is contained in:
Oliver authored and GitHub committed 2026-11-03 10:36:54 +11:00
1 parent 2310b54e26
commit c9ba1169f4
2 files changed
+74 -25

No files matched your search

+71 -6
View File
@@ -1,13 +1,57 @@
import { t } from '@lingui/core/macro';
import { Alert, Stack, Text } from '@mantine/core';
import { Alert, Group, Stack, Text } from '@mantine/core';
import { ErrorBoundary, type FallbackRender } from '@sentry/react';
import { IconExclamationCircle, IconInfoCircle } from '@tabler/icons-react';
import { type ReactNode, useCallback, useState } from 'react';
import { CopyButton } from './CopyButton';
/**
* Construct a plain-text error report, suitable for copying into a bug report
*/
export function formatErrorReport({
label,
error,
componentStack
}: {
label: string;
error: unknown;
componentStack?: string;
}): string {
const lines: string[] = [
`Error rendering component: ${label}`,
`Timestamp: ${new Date().toISOString()}`,
`URL: ${window.location.href}`,
`User Agent: ${navigator.userAgent}`,
''
];
if (error instanceof Error) {
lines.push(`${error.name}: ${error.message}`);
if (error.stack) {
lines.push('', 'Stack trace:', error.stack);
}
} else {
lines.push(String(error));
}
if (componentStack) {
lines.push('', 'Component stack:', componentStack.trim());
}
return lines.join('\n');
}
export function DefaultFallback({
title,
error
}: Readonly<{ title: string; error: string | null }>): ReactNode {
error,
details
}: Readonly<{
title: string;
error: string | null;
details?: string | null;
}>): ReactNode {
const copyValue = details ?? error;
return (
<>
<Alert
@@ -26,7 +70,20 @@ export function DefaultFallback({
</Alert>
{error && (
<Alert color='red' icon={<IconInfoCircle />} title={t`Error Details`}>
<Text size='sm'>{error}</Text>
<Stack gap='xs'>
<Text size='sm'>{error}</Text>
{copyValue && (
<Group justify='flex-start'>
<CopyButton
value={copyValue}
label={t`Copy error information`}
tooltip={t`Copy full error information to the clipboard`}
color='red'
variant='light'
/>
</Group>
)}
</Stack>
</Alert>
)}
</>
@@ -43,20 +100,28 @@ export function Boundary({
fallback?: React.ReactElement<any> | FallbackRender;
}>): ReactNode {
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [errorDetails, setErrorDetails] = useState<string | null>(null);
const onError = useCallback(
(error: unknown, componentStack: string | undefined, eventId: string) => {
console.error(`ERR: Error rendering component: ${label}`);
console.error(error);
setErrorMessage(error instanceof Error ? error.message : String(error));
setErrorDetails(formatErrorReport({ label, error, componentStack }));
},
[]
[label]
);
return (
<ErrorBoundary
fallback={
fallback ?? <DefaultFallback title={label} error={errorMessage} />
fallback ?? (
<DefaultFallback
title={label}
error={errorMessage}
details={errorDetails}
/>
)
}
onError={onError}
>
@@ -1,4 +1,4 @@
import { DefaultFallback } from '@lib/components/Boundary';
import { Boundary } from '@lib/components/Boundary';
import { StylishText } from '@lib/components/StylishText';
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
import { apiUrl } from '@lib/functions/Api';
@@ -22,7 +22,6 @@ import {
TextInput
} from '@mantine/core';
import { hideNotification, showNotification } from '@mantine/notifications';
import { ErrorBoundary } from '@sentry/react';
import {
IconAlertCircle,
IconAt,
@@ -45,16 +44,6 @@ export function SecurityContent() {
const user = useUserState();
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const onError = useCallback(
(error: unknown, componentStack: string | undefined, eventId: string) => {
console.error(`ERR: Error rendering component: ${error}`);
setErrorMessage(error instanceof Error ? error.message : String(error));
},
[]
);
return (
<Stack>
<Accordion multiple defaultValue={['email']}>
@@ -97,14 +86,9 @@ export function SecurityContent() {
<StylishText size='lg'>{t`Access Tokens`}</StylishText>
</Accordion.Control>
<Accordion.Panel>
<ErrorBoundary
fallback={
<DefaultFallback title={'API Table'} error={errorMessage} />
}
onError={onError}
>
<Boundary label='API Table'>
<ApiTokenTable only_myself />
</ErrorBoundary>
</Boundary>
</Accordion.Panel>
</Accordion.Item>
{user.isSuperuser() && (