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:
parent
0558f61c41
commit
a95356a59d
3 changed files with 113 additions and 7 deletions
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -27,12 +27,9 @@ function FrameworkGroup({
|
|||
|
||||
export function GroupedStackFrames({
|
||||
groupedStackFrames,
|
||||
show,
|
||||
}: {
|
||||
groupedStackFrames: StackFramesGroup[]
|
||||
show: boolean
|
||||
}) {
|
||||
if (!show) return
|
||||
return (
|
||||
<>
|
||||
{groupedStackFrames.map((stackFramesGroup, groupIndex) => {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue