Skip to content

Commit 8ded927

Browse files
committed
wip: all good, need better composition, port hooks, and styling
1 parent 5fd940c commit 8ded927

File tree

1 file changed

+214
-24
lines changed
  • packages/next/src/next-devtools/dev-overlay/components/devtools-panel/devtools-panel-tab/issues-tab

1 file changed

+214
-24
lines changed

packages/next/src/next-devtools/dev-overlay/components/devtools-panel/devtools-panel-tab/issues-tab/issues-tab.tsx

Lines changed: 214 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ import type { OverlayState } from '../../../../shared'
1818
import { extractNextErrorCode } from '../../../../../../lib/error-telemetry-utils'
1919
import { css } from '../../../../utils/css'
2020
import { getFrameSource } from '../../../../../shared/stack-frame'
21+
import { Terminal } from '../../../terminal'
22+
import { HotlinkedText } from '../../../hot-linked-text'
23+
import { NEXTJS_HYDRATION_ERROR_LINK } from '../../../../../shared/react-19-hydration-error'
24+
import { PseudoHtmlDiff } from '../../../../container/runtime-error/component-stack-pseudo-html'
25+
import { CodeFrame } from '../../../code-frame/code-frame'
26+
import { CallStack } from '../../../call-stack/call-stack'
2127

2228
export function IssuesTab({
2329
state,
@@ -38,14 +44,24 @@ export function IssuesTab({
3844
},
3945
[activeIdx, runtimeErrors]
4046
)
41-
const error = activeError?.error
4247

43-
const errorCode = extractNextErrorCode(error)
44-
const errorType = getErrorTypeLabel(error, activeError.type)
48+
if (!activeError) {
49+
return null
50+
}
51+
52+
// eslint-disable-next-line react-hooks/rules-of-hooks
4553
const errorDetails = useErrorDetails(
4654
activeError?.error,
4755
getSquashedHydrationErrorDetails
4856
)
57+
58+
const error = activeError?.error
59+
60+
const errorCode = extractNextErrorCode(error)
61+
const errorType = getErrorTypeLabel(error, activeError.type)
62+
// TOOD: May be better to always treat everything past the first blank line as notes
63+
// We're currently only special casing hydration error messages.
64+
const notes = errorDetails.notes
4965
const hydrationWarning = errorDetails.hydrationWarning
5066
const errorMessage = hydrationWarning ? (
5167
<HydrationErrorDescription message={hydrationWarning} />
@@ -71,26 +87,129 @@ export function IssuesTab({
7187
})}
7288
</Suspense>
7389
</aside>
74-
<div className="nextjs-container-errors-header">
75-
<div
76-
className="nextjs__container_errors__error_title"
77-
// allow assertion in tests before error rating is implemented
78-
data-nextjs-error-code={errorCode}
79-
>
80-
<span data-nextjs-error-label-group>
81-
<ErrorTypeLabel errorType={errorType} />
82-
{error.environmentName && (
83-
<EnvironmentNameLabel environmentName={error.environmentName} />
84-
)}
85-
</span>
86-
<ErrorOverlayToolbar error={error} debugInfo={state.debugInfo} />
90+
<div data-nextjs-devtools-panel-tab-issues-content>
91+
<div className="nextjs-container-errors-header">
92+
<div
93+
className="nextjs__container_errors__error_title"
94+
// allow assertion in tests before error rating is implemented
95+
data-nextjs-error-code={errorCode}
96+
>
97+
<span data-nextjs-error-label-group>
98+
<ErrorTypeLabel errorType={errorType} />
99+
{error.environmentName && (
100+
<EnvironmentNameLabel environmentName={error.environmentName} />
101+
)}
102+
</span>
103+
<ErrorOverlayToolbar error={error} debugInfo={state.debugInfo} />
104+
</div>
105+
<ErrorMessage errorMessage={errorMessage} />
87106
</div>
88-
<ErrorMessage errorMessage={errorMessage} />
107+
108+
<ErrorContent
109+
buildError={state.buildError}
110+
notes={notes}
111+
hydrationWarning={hydrationWarning}
112+
errorDetails={errorDetails}
113+
activeError={activeError}
114+
/>
89115
</div>
90116
</div>
91117
)
92118
}
93119

120+
function ErrorContent({
121+
notes,
122+
buildError,
123+
hydrationWarning,
124+
errorDetails,
125+
activeError,
126+
}: {
127+
notes: string | null
128+
buildError: OverlayState['buildError']
129+
hydrationWarning: string | null
130+
errorDetails: {
131+
hydrationWarning: string | null
132+
notes: string | null
133+
reactOutputComponentDiff: string | null
134+
}
135+
activeError: ReadyRuntimeError
136+
}) {
137+
if (buildError) {
138+
return <Terminal content={buildError} />
139+
}
140+
141+
return (
142+
<>
143+
<div className="error-overlay-notes-container">
144+
{notes ? (
145+
<>
146+
<p
147+
id="nextjs__container_errors__notes"
148+
className="nextjs__container_errors__notes"
149+
>
150+
{notes}
151+
</p>
152+
</>
153+
) : null}
154+
{hydrationWarning ? (
155+
<p
156+
id="nextjs__container_errors__link"
157+
className="nextjs__container_errors__link"
158+
>
159+
<HotlinkedText
160+
text={`See more info here: ${NEXTJS_HYDRATION_ERROR_LINK}`}
161+
/>
162+
</p>
163+
) : null}
164+
</div>
165+
{errorDetails.reactOutputComponentDiff ? (
166+
<PseudoHtmlDiff
167+
reactOutputComponentDiff={errorDetails.reactOutputComponentDiff || ''}
168+
/>
169+
) : null}
170+
<Suspense fallback={<div data-nextjs-error-suspended />}>
171+
<RuntimeError key={activeError.id.toString()} error={activeError} />
172+
</Suspense>
173+
</>
174+
)
175+
}
176+
177+
function RuntimeError({ error }: { error: ReadyRuntimeError }) {
178+
const frames = useFrames(error)
179+
180+
const firstFrame = useMemo(() => {
181+
const firstFirstPartyFrameIndex = frames.findIndex(
182+
(entry) =>
183+
!entry.ignored &&
184+
Boolean(entry.originalCodeFrame) &&
185+
Boolean(entry.originalStackFrame)
186+
)
187+
188+
return frames[firstFirstPartyFrameIndex] ?? null
189+
}, [frames])
190+
191+
if (!firstFrame.originalStackFrame) {
192+
return null
193+
}
194+
195+
if (!firstFrame.originalCodeFrame) {
196+
return null
197+
}
198+
199+
return (
200+
<>
201+
{firstFrame && (
202+
<CodeFrame
203+
stackFrame={firstFrame.originalStackFrame}
204+
codeFrame={firstFrame.originalCodeFrame}
205+
/>
206+
)}
207+
208+
{frames.length > 0 && <CallStack frames={frames} />}
209+
</>
210+
)
211+
}
212+
94213
function Foo({
95214
runtimeError,
96215
errorType,
@@ -119,20 +238,20 @@ function Foo({
119238

120239
const frameSource = getFrameSource(firstFrame.originalStackFrame!)
121240
return (
122-
<div
241+
<button
123242
data-nextjs-devtools-panel-tab-issues-sidebar-frame
124243
data-nextjs-devtools-panel-tab-issues-sidebar-frame-active={
125244
idx === activeIdx
126245
}
127246
onClick={() => setActiveIndex(idx)}
128247
>
129-
<div data-nextjs-devtools-panel-tab-issues-sidebar-frame-error-type>
248+
<span data-nextjs-devtools-panel-tab-issues-sidebar-frame-error-type>
130249
{errorType}
131-
</div>
132-
<div data-nextjs-devtools-panel-tab-issues-sidebar-frame-source>
250+
</span>
251+
<span data-nextjs-devtools-panel-tab-issues-sidebar-frame-source>
133252
{frameSource}
134-
</div>
135-
</div>
253+
</span>
254+
</button>
136255
)
137256
}
138257

@@ -167,9 +286,10 @@ export const DEVTOOLS_PANEL_TAB_ISSUES_STYLES = css`
167286
}
168287
169288
[data-nextjs-devtools-panel-tab-issues-sidebar-frame] {
289+
display: flex;
290+
flex-direction: column;
170291
padding: 10px 8px;
171292
border-radius: var(--rounded-lg);
172-
cursor: pointer;
173293
transition: background-color 0.2s ease-in-out;
174294
175295
&:hover {
@@ -186,18 +306,28 @@ export const DEVTOOLS_PANEL_TAB_ISSUES_STYLES = css`
186306
}
187307
188308
[data-nextjs-devtools-panel-tab-issues-sidebar-frame-error-type] {
309+
display: inline-block;
310+
align-self: flex-start;
189311
color: var(--color-gray-1000);
190312
font-size: var(--size-14);
191313
font-weight: 500;
192314
line-height: var(--size-20);
193315
}
194316
195317
[data-nextjs-devtools-panel-tab-issues-sidebar-frame-source] {
318+
display: inline-block;
319+
align-self: flex-start;
196320
color: var(--color-gray-900);
197321
font-size: var(--size-13);
198322
line-height: var(--size-18);
199323
}
200324
325+
[data-nextjs-devtools-panel-tab-issues-content] {
326+
width: 100%;
327+
padding: 14px;
328+
}
329+
330+
/* errors/dialog/header.tsx */
201331
.nextjs-container-errors-header {
202332
position: relative;
203333
}
@@ -230,4 +360,64 @@ export const DEVTOOLS_PANEL_TAB_ISSUES_STYLES = css`
230360
top: 16px;
231361
right: 16px;
232362
}
363+
364+
/* errors.tsx */
365+
.nextjs-error-with-static {
366+
bottom: calc(16px * 4.5);
367+
}
368+
p.nextjs__container_errors__link {
369+
font-size: var(--size-14);
370+
}
371+
p.nextjs__container_errors__notes {
372+
color: var(--color-stack-notes);
373+
font-size: var(--size-14);
374+
line-height: 1.5;
375+
}
376+
.nextjs-container-errors-body > h2:not(:first-child) {
377+
margin-top: calc(16px + 8px);
378+
}
379+
.nextjs-container-errors-body > h2 {
380+
color: var(--color-title-color);
381+
margin-bottom: 8px;
382+
font-size: var(--size-20);
383+
}
384+
.nextjs-toast-errors-parent {
385+
cursor: pointer;
386+
transition: transform 0.2s ease;
387+
}
388+
.nextjs-toast-errors-parent:hover {
389+
transform: scale(1.1);
390+
}
391+
.nextjs-toast-errors {
392+
display: flex;
393+
align-items: center;
394+
justify-content: flex-start;
395+
}
396+
.nextjs-toast-errors > svg {
397+
margin-right: 8px;
398+
}
399+
.nextjs-toast-hide-button {
400+
margin-left: 24px;
401+
border: none;
402+
background: none;
403+
color: var(--color-ansi-bright-white);
404+
padding: 0;
405+
transition: opacity 0.25s ease;
406+
opacity: 0.7;
407+
}
408+
.nextjs-toast-hide-button:hover {
409+
opacity: 1;
410+
}
411+
.nextjs__container_errors__error_title {
412+
display: flex;
413+
align-items: center;
414+
justify-content: space-between;
415+
margin-bottom: 14px;
416+
}
417+
.error-overlay-notes-container {
418+
margin: 8px 2px;
419+
}
420+
.error-overlay-notes-container p {
421+
white-space: pre-wrap;
422+
}
233423
`

0 commit comments

Comments
 (0)