Skip to content

Commit 1948d4f

Browse files
committed
[devtools] panel ui issues tab
1 parent 7d42ecb commit 1948d4f

File tree

8 files changed

+408
-15
lines changed

8 files changed

+408
-15
lines changed

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
import type { DevToolsPanelTabType } from '../devtools-panel'
22
import type { OverlayDispatch, OverlayState } from '../../../shared'
3+
import type { ReadyRuntimeError } from '../../../utils/get-error-by-type'
4+
import type { HydrationErrorState } from '../../../../shared/hydration-error'
35

46
import { SettingsTab } from './settings-tab'
7+
import { IssuesTab } from './issues-tab/issues-tab'
58

69
export function DevToolsPanelTab({
710
activeTab,
811
state,
912
dispatch,
13+
runtimeErrors,
14+
getSquashedHydrationErrorDetails,
1015
}: {
1116
activeTab: DevToolsPanelTabType
1217
state: OverlayState
1318
dispatch: OverlayDispatch
19+
runtimeErrors: ReadyRuntimeError[]
20+
getSquashedHydrationErrorDetails: (error: Error) => HydrationErrorState | null
1421
}) {
1522
if (activeTab === 'settings') {
1623
return <SettingsTab state={state} dispatch={dispatch} />
@@ -21,7 +28,13 @@ export function DevToolsPanelTab({
2128
}
2229

2330
if (activeTab === 'issues') {
24-
return <div>Issues</div>
31+
return (
32+
<IssuesTab
33+
state={state}
34+
runtimeErrors={runtimeErrors}
35+
getSquashedHydrationErrorDetails={getSquashedHydrationErrorDetails}
36+
/>
37+
)
2538
}
2639

2740
console.log(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import { Suspense, useMemo, useState } from 'react'
2+
import {
3+
GenericErrorDescription,
4+
getErrorTypeLabel,
5+
HydrationErrorDescription,
6+
useErrorDetails,
7+
} from '../../../../container/errors'
8+
import { EnvironmentNameLabel } from '../../../errors/environment-name-label/environment-name-label'
9+
import { ErrorMessage } from '../../../errors/error-message/error-message'
10+
import { ErrorOverlayToolbar } from '../../../errors/error-overlay-toolbar/error-overlay-toolbar'
11+
import { ErrorTypeLabel } from '../../../errors/error-type-label/error-type-label'
12+
import {
13+
useFrames,
14+
type ReadyRuntimeError,
15+
} from '../../../../utils/get-error-by-type'
16+
import type { HydrationErrorState } from '../../../../../shared/hydration-error'
17+
import type { OverlayState } from '../../../../shared'
18+
import { extractNextErrorCode } from '../../../../../../lib/error-telemetry-utils'
19+
import { css } from '../../../../utils/css'
20+
import { getFrameSource } from '../../../../../shared/stack-frame'
21+
22+
export function IssuesTab({
23+
state,
24+
runtimeErrors,
25+
getSquashedHydrationErrorDetails,
26+
}: {
27+
state: OverlayState
28+
runtimeErrors: ReadyRuntimeError[]
29+
getSquashedHydrationErrorDetails: (error: Error) => HydrationErrorState | null
30+
}) {
31+
const [activeIdx, setActiveIndex] = useState<number>(0)
32+
const activeError = useMemo<ReadyRuntimeError | null>(
33+
() =>
34+
// TODO: correct fallback
35+
runtimeErrors[activeIdx] ?? {
36+
error: new Error('No error'),
37+
type: 'runtime',
38+
},
39+
[activeIdx, runtimeErrors]
40+
)
41+
const error = activeError?.error
42+
43+
const errorCode = extractNextErrorCode(error)
44+
const errorType = getErrorTypeLabel(error, activeError.type)
45+
const errorDetails = useErrorDetails(
46+
activeError?.error,
47+
getSquashedHydrationErrorDetails
48+
)
49+
const hydrationWarning = errorDetails.hydrationWarning
50+
const errorMessage = hydrationWarning ? (
51+
<HydrationErrorDescription message={hydrationWarning} />
52+
) : (
53+
<GenericErrorDescription error={error} />
54+
)
55+
56+
return (
57+
<div data-nextjs-devtools-panel-tab-issues>
58+
<aside data-nextjs-devtools-panel-tab-issues-sidebar>
59+
<Suspense fallback={<div>Loading...</div>}>
60+
{runtimeErrors.map((runtimeError, idx) => {
61+
return (
62+
<Foo
63+
key={idx}
64+
runtimeError={runtimeError}
65+
errorType={errorType}
66+
idx={idx}
67+
activeIdx={activeIdx}
68+
setActiveIndex={setActiveIndex}
69+
/>
70+
)
71+
})}
72+
</Suspense>
73+
</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} />
87+
</div>
88+
<ErrorMessage errorMessage={errorMessage} />
89+
</div>
90+
</div>
91+
)
92+
}
93+
94+
function Foo({
95+
runtimeError,
96+
errorType,
97+
idx,
98+
activeIdx,
99+
setActiveIndex,
100+
}: {
101+
runtimeError: ReadyRuntimeError
102+
errorType: string
103+
idx: number
104+
activeIdx: number
105+
setActiveIndex: (idx: number) => void
106+
}) {
107+
const frames = useFrames(runtimeError)
108+
109+
const firstFrame = useMemo(() => {
110+
const firstFirstPartyFrameIndex = frames.findIndex(
111+
(entry) =>
112+
!entry.ignored &&
113+
Boolean(entry.originalCodeFrame) &&
114+
Boolean(entry.originalStackFrame)
115+
)
116+
117+
return frames[firstFirstPartyFrameIndex] ?? null
118+
}, [frames])
119+
120+
const frameSource = getFrameSource(firstFrame.originalStackFrame!)
121+
return (
122+
<div
123+
data-nextjs-devtools-panel-tab-issues-sidebar-frame
124+
data-nextjs-devtools-panel-tab-issues-sidebar-frame-active={
125+
idx === activeIdx
126+
}
127+
onClick={() => setActiveIndex(idx)}
128+
>
129+
<div data-nextjs-devtools-panel-tab-issues-sidebar-frame-error-type>
130+
{errorType}
131+
</div>
132+
<div data-nextjs-devtools-panel-tab-issues-sidebar-frame-source>
133+
{frameSource}
134+
</div>
135+
</div>
136+
)
137+
}
138+
139+
export const DEVTOOLS_PANEL_TAB_ISSUES_STYLES = css`
140+
[data-nextjs-devtools-panel-tab-issues] {
141+
display: flex;
142+
}
143+
144+
[data-nextjs-devtools-panel-tab-issues-sidebar] {
145+
display: flex;
146+
flex-direction: column;
147+
gap: 4px;
148+
padding: 8px;
149+
border-right: 1px solid var(--color-gray-400);
150+
151+
min-width: 128px;
152+
153+
@media (min-width: 576px) {
154+
max-width: 138px;
155+
width: 100%;
156+
}
157+
158+
@media (min-width: 768px) {
159+
max-width: 172.5px;
160+
width: 100%;
161+
}
162+
163+
@media (min-width: 992px) {
164+
max-width: 230px;
165+
width: 100%;
166+
}
167+
}
168+
169+
[data-nextjs-devtools-panel-tab-issues-sidebar-frame] {
170+
padding: 10px 8px;
171+
border-radius: var(--rounded-lg);
172+
cursor: pointer;
173+
transition: background-color 0.2s ease-in-out;
174+
175+
&:hover {
176+
background-color: var(--color-gray-200);
177+
}
178+
179+
&:active {
180+
background-color: var(--color-gray-300);
181+
}
182+
}
183+
184+
[data-nextjs-devtools-panel-tab-issues-sidebar-frame-active='true'] {
185+
background-color: var(--color-gray-100);
186+
}
187+
188+
[data-nextjs-devtools-panel-tab-issues-sidebar-frame-error-type] {
189+
color: var(--color-gray-1000);
190+
font-size: var(--size-14);
191+
font-weight: 500;
192+
line-height: var(--size-20);
193+
}
194+
195+
[data-nextjs-devtools-panel-tab-issues-sidebar-frame-source] {
196+
color: var(--color-gray-900);
197+
font-size: var(--size-13);
198+
line-height: var(--size-18);
199+
}
200+
201+
.nextjs-container-errors-header {
202+
position: relative;
203+
}
204+
.nextjs-container-errors-header > h1 {
205+
font-size: var(--size-20);
206+
line-height: var(--size-24);
207+
font-weight: bold;
208+
margin: calc(16px * 1.5) 0;
209+
color: var(--color-title-h1);
210+
}
211+
.nextjs-container-errors-header small {
212+
font-size: var(--size-14);
213+
color: var(--color-accents-1);
214+
margin-left: 16px;
215+
}
216+
.nextjs-container-errors-header small > span {
217+
font-family: var(--font-stack-monospace);
218+
}
219+
.nextjs-container-errors-header > div > small {
220+
margin: 0;
221+
margin-top: 4px;
222+
}
223+
.nextjs-container-errors-header > p > a {
224+
color: inherit;
225+
font-weight: bold;
226+
}
227+
.nextjs-container-errors-header
228+
> .nextjs-container-build-error-version-status {
229+
position: absolute;
230+
top: 16px;
231+
right: 16px;
232+
}
233+
`

0 commit comments

Comments
 (0)