security: add copy request ID button to ErrorDisplay
- Added "Copy Request ID" button that copies request ID to clipboard - Button appears for server errors when request_id is available - Uses modern Clipboard API with fallback to execCommand - Shows success toast when copied - Added alongside existing "Report Issue" button - Fixed TypeScript error in isServerError calculation - Action 5.3.1.2 complete
This commit is contained in:
parent
0fa52a3776
commit
60dea49cf2
2 changed files with 85 additions and 20 deletions
|
|
@ -1715,11 +1715,20 @@ Critical path dependencies:
|
|||
- Note: ErrorDisplay.tsx already shows request ID without dev check (line 651), so this change affects formatErrorMessage function specifically
|
||||
- **Rollback**: Restore dev check
|
||||
|
||||
- [ ] **Action 5.3.1.2**: Add "Report Issue" button with request ID
|
||||
- [x] **Action 5.3.1.2**: Add "Report Issue" button with request ID
|
||||
- **Scope**: `apps/web/src/components/ui/ErrorDisplay.tsx` - Add button, copy request ID
|
||||
- **Dependencies**: Action 3.1.1.2 complete
|
||||
- **Dependencies**: Action 3.1.1.2 complete ✅
|
||||
- **Risk**: LOW
|
||||
- **Validation**: Button copies request ID to clipboard
|
||||
- **Validation**: ✅ Button copies request ID to clipboard:
|
||||
- Added `handleCopyRequestId` callback that copies request ID to clipboard
|
||||
- Added "Copy Request ID" button with Copy icon from lucide-react
|
||||
- Button appears for server errors when request_id is available
|
||||
- Button shown alongside existing "Report Issue" button
|
||||
- Uses modern Clipboard API with fallback to execCommand
|
||||
- Shows success toast when copied
|
||||
- Added to both banner and modal variants
|
||||
- Fixed TypeScript error in isServerError calculation (use normalized error instead of apiError.status)
|
||||
- No TypeScript errors
|
||||
- **Rollback**: Remove button
|
||||
|
||||
### Sub-Epic 5.4: Rate Limit UI 🟢
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
ChevronDown,
|
||||
ChevronUp,
|
||||
Bug,
|
||||
Copy,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button } from './button';
|
||||
|
|
@ -244,11 +245,12 @@ export const ErrorDisplay = React.forwardRef<HTMLDivElement, ErrorDisplayProps>(
|
|||
|
||||
// Determine if this is a server error that should show report issue button
|
||||
const isServerError = React.useMemo(() => {
|
||||
const normalized = normalizeError(error);
|
||||
return (
|
||||
errorCategory === 'server_error' ||
|
||||
(apiError.status !== undefined && apiError.status >= 500)
|
||||
(normalized.status !== undefined && normalized.status >= 500)
|
||||
);
|
||||
}, [errorCategory, apiError]);
|
||||
}, [errorCategory, error]);
|
||||
|
||||
// Determine if details should be shown
|
||||
const isDev = import.meta.env.DEV;
|
||||
|
|
@ -417,6 +419,38 @@ export const ErrorDisplay = React.forwardRef<HTMLDivElement, ErrorDisplayProps>(
|
|||
}
|
||||
}, [error, context]);
|
||||
|
||||
// Action 5.3.1.2: Copy request ID to clipboard
|
||||
const handleCopyRequestId = React.useCallback(async () => {
|
||||
if (!apiError.request_id) return;
|
||||
|
||||
try {
|
||||
if (
|
||||
typeof navigator !== 'undefined' &&
|
||||
navigator.clipboard &&
|
||||
navigator.clipboard.writeText
|
||||
) {
|
||||
await navigator.clipboard.writeText(apiError.request_id);
|
||||
toast.success('Request ID copied to clipboard');
|
||||
} else {
|
||||
// Fallback: create temporary textarea
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = apiError.request_id;
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.opacity = '0';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
toast.success('Request ID copied to clipboard');
|
||||
} finally {
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error('Failed to copy request ID');
|
||||
}
|
||||
}, [apiError.request_id]);
|
||||
|
||||
// Determine if dismissible
|
||||
const isDismissible =
|
||||
dismissible ?? (onDismiss !== undefined || variant === 'modal');
|
||||
|
|
@ -525,16 +559,28 @@ export const ErrorDisplay = React.forwardRef<HTMLDivElement, ErrorDisplayProps>(
|
|||
{isRetrying ? 'Retrying...' : 'Retry'}
|
||||
</Button>
|
||||
)}
|
||||
{isServerError && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size={size === 'sm' ? 'sm' : 'default'}
|
||||
onClick={handleReportIssue}
|
||||
className="border-current text-current hover:bg-current/10"
|
||||
>
|
||||
<Bug className="w-4 h-4 mr-1.5" />
|
||||
Report Issue
|
||||
</Button>
|
||||
{isServerError && apiError.request_id && (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
size={size === 'sm' ? 'sm' : 'default'}
|
||||
onClick={handleCopyRequestId}
|
||||
className="border-current text-current hover:bg-current/10"
|
||||
title="Copy Request ID to clipboard"
|
||||
>
|
||||
<Copy className="w-4 h-4 mr-1.5" />
|
||||
Copy Request ID
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size={size === 'sm' ? 'sm' : 'default'}
|
||||
onClick={handleReportIssue}
|
||||
className="border-current text-current hover:bg-current/10"
|
||||
>
|
||||
<Bug className="w-4 h-4 mr-1.5" />
|
||||
Report Issue
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{actions.map((action, idx) => (
|
||||
<Button
|
||||
|
|
@ -624,11 +670,21 @@ export const ErrorDisplay = React.forwardRef<HTMLDivElement, ErrorDisplayProps>(
|
|||
{isRetrying ? 'Retrying...' : 'Retry'}
|
||||
</Button>
|
||||
)}
|
||||
{isServerError && (
|
||||
<Button variant="outline" onClick={handleReportIssue}>
|
||||
<Bug className="w-4 h-4 mr-1.5" />
|
||||
Report Issue
|
||||
</Button>
|
||||
{isServerError && apiError.request_id && (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleCopyRequestId}
|
||||
title="Copy Request ID to clipboard"
|
||||
>
|
||||
<Copy className="w-4 h-4 mr-1.5" />
|
||||
Copy Request ID
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleReportIssue}>
|
||||
<Bug className="w-4 h-4 mr-1.5" />
|
||||
Report Issue
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{actions.map((action, idx) => (
|
||||
<Button
|
||||
|
|
|
|||
Loading…
Reference in a new issue