feat: add copy button for runtime error (#65921)

### What

Add copy stack trace button to copy the original stacktrace in error
overlay.
It will be a dimmed red button with disallowed cursor when it's found
not able to copy.

#### Video



https://github.com/vercel/next.js/assets/4800338/c44e0bf8-f340-41ba-8ee8-4b94fdc298e1


### Why

Makes error reporting easier, users can do one-click to copy the
original text
This commit is contained in:
Jiachi Liu 2024-05-30 09:58:01 +02:00 committed by GitHub
parent 0558f61c41
commit a95356a59d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 113 additions and 7 deletions

View file

@ -0,0 +1,79 @@
import { useState } from 'react'
enum CopyState {
Initial = 0,
Copied = 1,
Error = 2,
}
export function CopyButton({
label,
successLabel,
content,
...props
}: React.HTMLProps<HTMLSpanElement> & {
label: string
successLabel: string
content: string
}) {
const [copied, setCopied] = useState(CopyState.Initial)
const isDisabled = copied === CopyState.Error
const title = isDisabled ? '' : copied ? successLabel : label
return (
<span
{...props}
title={title}
aria-label={title}
aria-disabled={isDisabled}
role="button"
onClick={() => {
if (isDisabled) return
if (!navigator.clipboard) {
setCopied(CopyState.Error)
return
}
navigator.clipboard.writeText(content).then(() => {
if (copied) return
setCopied(CopyState.Copied)
setTimeout(() => setCopied(CopyState.Initial), 2000)
})
}}
>
{copied ? <CopySuccessIcon /> : <CopyIcon />}
</span>
)
}
function CopyIcon() {
return (
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="transparent"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
</svg>
)
}
function CopySuccessIcon() {
return (
<svg
height="16"
xlinkTitle="copied"
viewBox="0 0 16 16"
width="16"
stroke="currentColor"
fill="currentColor"
data-nextjs-data-runtime-error-copy-stack-success
>
<path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z" />
</svg>
)
}

View file

@ -27,12 +27,9 @@ function FrameworkGroup({
export function GroupedStackFrames({
groupedStackFrames,
show,
}: {
groupedStackFrames: StackFramesGroup[]
show: boolean
}) {
if (!show) return
return (
<>
{groupedStackFrames.map((stackFramesGroup, groupIndex) => {

View file

@ -4,6 +4,7 @@ import type { ReadyRuntimeError } from '../../helpers/getErrorByType'
import { noop as css } from '../../helpers/noop-template'
import { groupStackFramesByFramework } from '../../helpers/group-stack-frames-by-framework'
import { GroupedStackFrames } from './GroupedStackFrames'
import { CopyButton } from '../../components/copy-button'
export type RuntimeErrorProps = { error: ReadyRuntimeError }
@ -67,7 +68,6 @@ export function RuntimeError({ error }: RuntimeErrorProps) {
<h2>Source</h2>
<GroupedStackFrames
groupedStackFrames={leadingFramesGroupedByFramework}
show={all}
/>
<CodeFrame
stackFrame={firstFrame.originalStackFrame!}
@ -78,10 +78,20 @@ export function RuntimeError({ error }: RuntimeErrorProps) {
{stackFramesGroupedByFramework.length ? (
<React.Fragment>
<h2>Call Stack</h2>
<h2>
Call Stack
{error.error.stack && (
<CopyButton
data-nextjs-data-runtime-error-copy-stack
label="Copy error stack"
successLabel="Copied"
content={error.error.stack}
/>
)}
</h2>
<GroupedStackFrames
groupedStackFrames={stackFramesGroupedByFramework}
show={all}
/>
</React.Fragment>
) : undefined}
@ -116,6 +126,27 @@ export const styles = css`
margin-bottom: var(--size-gap-double);
}
[data-nextjs-data-runtime-error-copy-stack] {
position: relative;
margin-left: var(--size-gap);
}
[data-nextjs-data-runtime-error-copy-stack] > svg {
vertical-align: middle;
}
[data-nextjs-data-runtime-error-copy-stack][aria-disabled],
[data-nextjs-data-runtime-error-copy-stack][aria-disabled]:hover {
cursor: pointer;
color: var(--color-ansi-red);
opacity: 0.3;
cursor: not-allowed;
}
[data-nextjs-data-runtime-error-copy-stack-success] {
color: var(--color-ansi-green);
}
[data-nextjs-data-runtime-error-copy-stack]:hover {
cursor: pointer;
}
[data-nextjs-call-stack-frame] > h3,
[data-nextjs-component-stack-frame] > h3 {
margin-top: 0;
@ -141,7 +172,6 @@ export const styles = css`
height: var(--size-font-small);
margin-left: var(--size-gap);
flex-shrink: 0;
display: none;
}