Skip to content

Commit c1c523b

Browse files
[devtools] panel ui issues tab content (#80729)
The UI shares the style with the error overlay. This PR added a loading state, which can be later shared with the error overlay as well. https://github.com/user-attachments/assets/d3759d19-7f70-48c3-8fee-5c6973c01d53 ### Success Criteria - [x] Does it have a loading state? - [x] Does it have its own scrollbar? - [x] Does it have an empty error state? - [x] Does it have the new feedback button on the toolbar? Closes NEXT-4545 Closes NEXT-4556 Closes NEXT-4554 --------- Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
1 parent 4e52916 commit c1c523b

File tree

11 files changed

+599
-55
lines changed

11 files changed

+599
-55
lines changed

packages/next/src/next-devtools/dev-overlay/components/code-frame/code-frame.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,6 @@ export const CODE_FRAME_STYLES = `
108108
--code-frame-padding: 12px;
109109
--code-frame-line-height: var(--size-16);
110110
background-color: var(--color-background-200);
111-
overflow: hidden;
112111
color: var(--color-gray-1000);
113112
text-overflow: ellipsis;
114113
border: 1px solid var(--color-gray-400);

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { DevToolsPanelTabType } from '../devtools-panel'
2-
import type { Corners } from '../../../shared'
2+
import type { Corners, OverlayState } from '../../../shared'
33
import type { DebugInfo } from '../../../../shared/types'
44
import type { ReadyRuntimeError } from '../../../utils/get-error-by-type'
55
import type { HydrationErrorState } from '../../../../shared/hydration-error'
@@ -16,6 +16,7 @@ export function DevToolsPanelTab({
1616
debugInfo,
1717
runtimeErrors,
1818
getSquashedHydrationErrorDetails,
19+
buildError,
1920
}: {
2021
activeTab: DevToolsPanelTabType
2122
devToolsPosition: Corners
@@ -25,6 +26,7 @@ export function DevToolsPanelTab({
2526
debugInfo: DebugInfo
2627
runtimeErrors: ReadyRuntimeError[]
2728
getSquashedHydrationErrorDetails: (error: Error) => HydrationErrorState | null
29+
buildError: OverlayState['buildError']
2830
}) {
2931
switch (activeTab) {
3032
case 'settings':
@@ -44,6 +46,7 @@ export function DevToolsPanelTab({
4446
debugInfo={debugInfo}
4547
runtimeErrors={runtimeErrors}
4648
getSquashedHydrationErrorDetails={getSquashedHydrationErrorDetails}
49+
buildError={buildError}
4750
/>
4851
)
4952
default:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import type { OverlayState } from '../../../../shared'
2+
import type { DebugInfo } from '../../../../../shared/types'
3+
import type { ReadyRuntimeError } from '../../../../utils/get-error-by-type'
4+
import type { ErrorType } from '../../../errors/error-type-label/error-type-label'
5+
6+
import { Suspense, useMemo, useState } from 'react'
7+
8+
import {
9+
GenericErrorDescription,
10+
HydrationErrorDescription,
11+
} from '../../../../container/errors'
12+
import { EnvironmentNameLabel } from '../../../errors/environment-name-label/environment-name-label'
13+
import { ErrorMessage } from '../../../errors/error-message/error-message'
14+
import { ErrorOverlayToolbar } from '../../../errors/error-overlay-toolbar/error-overlay-toolbar'
15+
import { ErrorTypeLabel } from '../../../errors/error-type-label/error-type-label'
16+
import { IssueFeedbackButton } from '../../../errors/error-overlay-toolbar/issue-feedback-button'
17+
import { Terminal } from '../../../terminal'
18+
import { HotlinkedText } from '../../../hot-linked-text'
19+
import { PseudoHtmlDiff } from '../../../../container/runtime-error/component-stack-pseudo-html'
20+
import { useFrames } from '../../../../utils/get-error-by-type'
21+
import { CodeFrame } from '../../../code-frame/code-frame'
22+
import { CallStack } from '../../../call-stack/call-stack'
23+
import { NEXTJS_HYDRATION_ERROR_LINK } from '../../../../../shared/react-19-hydration-error'
24+
import { ErrorContentSkeleton } from '../../../../container/runtime-error/error-content-skeleton'
25+
import { css } from '../../../../utils/css'
26+
27+
export function IssuesTabContent({
28+
notes,
29+
buildError,
30+
hydrationWarning,
31+
errorDetails,
32+
activeError,
33+
errorCode,
34+
errorType,
35+
debugInfo,
36+
}: {
37+
notes: string | null
38+
buildError: OverlayState['buildError']
39+
hydrationWarning: string | null
40+
errorDetails: {
41+
hydrationWarning: string | null
42+
notes: string | null
43+
reactOutputComponentDiff: string | null
44+
}
45+
activeError: ReadyRuntimeError
46+
errorCode: string | undefined
47+
errorType: ErrorType
48+
debugInfo: DebugInfo
49+
}) {
50+
if (buildError) {
51+
return <Terminal content={buildError} />
52+
}
53+
54+
const errorMessage = hydrationWarning ? (
55+
<HydrationErrorDescription message={hydrationWarning} />
56+
) : (
57+
<GenericErrorDescription error={activeError.error} />
58+
)
59+
60+
return (
61+
<div data-nextjs-devtools-panel-tab-issues-content-container>
62+
<div className="nextjs-container-errors-header">
63+
<div
64+
className="nextjs__container_errors__error_title"
65+
// allow assertion in tests before error rating is implemented
66+
data-nextjs-error-code={errorCode}
67+
>
68+
<span data-nextjs-error-label-group>
69+
<ErrorTypeLabel errorType={errorType} />
70+
{activeError.error.environmentName && (
71+
<EnvironmentNameLabel
72+
environmentName={activeError.error.environmentName}
73+
/>
74+
)}
75+
</span>
76+
<ErrorOverlayToolbar
77+
error={activeError.error}
78+
debugInfo={debugInfo}
79+
// TODO: Move the button inside and remove the feedback on the footer of the error overlay.
80+
feedbackButton={
81+
errorCode && <IssueFeedbackButton errorCode={errorCode} />
82+
}
83+
/>
84+
</div>
85+
<ErrorMessage errorMessage={errorMessage} />
86+
</div>
87+
<div className="error-overlay-notes-container">
88+
{notes ? (
89+
<>
90+
<p
91+
id="nextjs__container_errors__notes"
92+
className="nextjs__container_errors__notes"
93+
>
94+
{notes}
95+
</p>
96+
</>
97+
) : null}
98+
{hydrationWarning ? (
99+
<p
100+
id="nextjs__container_errors__link"
101+
className="nextjs__container_errors__link"
102+
>
103+
<HotlinkedText
104+
text={`See more info here: ${NEXTJS_HYDRATION_ERROR_LINK}`}
105+
/>
106+
</p>
107+
) : null}
108+
</div>
109+
{errorDetails.reactOutputComponentDiff ? (
110+
<PseudoHtmlDiff
111+
reactOutputComponentDiff={errorDetails.reactOutputComponentDiff || ''}
112+
/>
113+
) : null}
114+
<Suspense fallback={<ErrorContentSkeleton />}>
115+
<RuntimeError key={activeError.id.toString()} error={activeError} />
116+
</Suspense>
117+
</div>
118+
)
119+
}
120+
121+
/* Ported the content from container/runtime-error/index.tsx */
122+
function RuntimeError({ error }: { error: ReadyRuntimeError }) {
123+
const [isIgnoreListOpen, setIsIgnoreListOpen] = useState(false)
124+
const frames = useFrames(error)
125+
126+
const ignoredFramesTally = useMemo(() => {
127+
return frames.reduce((tally, frame) => tally + (frame.ignored ? 1 : 0), 0)
128+
}, [frames])
129+
130+
const firstFrame = useMemo(() => {
131+
const firstFirstPartyFrameIndex = frames.findIndex(
132+
(entry) =>
133+
!entry.ignored &&
134+
Boolean(entry.originalCodeFrame) &&
135+
Boolean(entry.originalStackFrame)
136+
)
137+
138+
return frames[firstFirstPartyFrameIndex] ?? null
139+
}, [frames])
140+
141+
return (
142+
<>
143+
{firstFrame &&
144+
firstFrame.originalStackFrame &&
145+
firstFrame.originalCodeFrame && (
146+
<CodeFrame
147+
stackFrame={firstFrame.originalStackFrame}
148+
codeFrame={firstFrame.originalCodeFrame}
149+
/>
150+
)}
151+
152+
{frames.length > 0 && (
153+
<CallStack
154+
frames={frames}
155+
isIgnoreListOpen={isIgnoreListOpen}
156+
onToggleIgnoreList={() => setIsIgnoreListOpen(!isIgnoreListOpen)}
157+
ignoredFramesTally={ignoredFramesTally}
158+
/>
159+
)}
160+
</>
161+
)
162+
}
163+
164+
// The components in this file shares the style with the Error Overlay.
165+
export const DEVTOOLS_PANEL_TAB_ISSUES_CONTENT_STYLES = css`
166+
[data-nextjs-devtools-panel-tab-issues-content-container] {
167+
flex: 1;
168+
display: flex;
169+
flex-direction: column;
170+
overflow-y: auto;
171+
min-height: 0;
172+
padding: 14px;
173+
}
174+
`
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,54 @@
1+
import type { OverlayState } from '../../../../shared'
12
import type { DebugInfo } from '../../../../../shared/types'
23
import type { ReadyRuntimeError } from '../../../../utils/get-error-by-type'
34
import type { HydrationErrorState } from '../../../../../shared/hydration-error'
45

56
import { IssuesTabSidebar } from './issues-tab-sidebar'
6-
import {
7-
GenericErrorDescription,
8-
HydrationErrorDescription,
9-
} from '../../../../container/errors'
10-
import { EnvironmentNameLabel } from '../../../errors/environment-name-label/environment-name-label'
11-
import { ErrorMessage } from '../../../errors/error-message/error-message'
12-
import { ErrorOverlayToolbar } from '../../../errors/error-overlay-toolbar/error-overlay-toolbar'
13-
import { ErrorTypeLabel } from '../../../errors/error-type-label/error-type-label'
7+
import { IssuesTabContent } from './issues-tab-content'
148
import { css } from '../../../../utils/css'
159
import { useActiveRuntimeError } from '../../../../hooks/use-active-runtime-error'
10+
import { Warning } from '../../../../icons/warning'
1611

1712
export function IssuesTab({
1813
debugInfo,
1914
runtimeErrors,
2015
getSquashedHydrationErrorDetails,
16+
buildError,
2117
}: {
2218
debugInfo: DebugInfo
2319
runtimeErrors: ReadyRuntimeError[]
2420
getSquashedHydrationErrorDetails: (error: Error) => HydrationErrorState | null
21+
buildError: OverlayState['buildError']
2522
}) {
2623
const {
27-
isLoading,
2824
errorCode,
2925
errorType,
3026
hydrationWarning,
3127
activeError,
3228
activeIdx,
3329
setActiveIndex,
30+
notes,
31+
errorDetails,
3432
} = useActiveRuntimeError({ runtimeErrors, getSquashedHydrationErrorDetails })
3533

36-
if (isLoading) {
37-
// TODO: better loading state
38-
return null
39-
}
40-
4134
if (!activeError) {
42-
return null
35+
return (
36+
<div data-nextjs-devtools-panel-tab-issues-empty>
37+
<div data-nextjs-devtools-panel-tab-issues-empty-content>
38+
<div data-nextjs-devtools-panel-tab-issues-empty-icon>
39+
<Warning width={16} height={16} />
40+
</div>
41+
<h3 data-nextjs-devtools-panel-tab-issues-empty-title>
42+
No Issues Found
43+
</h3>
44+
<p data-nextjs-devtools-panel-tab-issues-empty-subtitle>
45+
Issues will appear here when they occur.
46+
</p>
47+
</div>
48+
</div>
49+
)
4350
}
4451

45-
const errorMessage = hydrationWarning ? (
46-
<HydrationErrorDescription message={hydrationWarning} />
47-
) : (
48-
<GenericErrorDescription error={activeError.error} />
49-
)
50-
5152
return (
5253
<div data-nextjs-devtools-panel-tab-issues>
5354
<IssuesTabSidebar
@@ -56,32 +57,18 @@ export function IssuesTab({
5657
activeIdx={activeIdx}
5758
setActiveIndex={setActiveIndex}
5859
/>
59-
<div data-nextjs-devtools-panel-tab-issues-content>
60-
<div className="nextjs-container-errors-header">
61-
<div
62-
className="nextjs__container_errors__error_title"
63-
// allow assertion in tests before error rating is implemented
64-
data-nextjs-error-code={errorCode}
65-
>
66-
<span data-nextjs-error-label-group>
67-
<ErrorTypeLabel errorType={errorType} />
68-
{activeError.error.environmentName && (
69-
<EnvironmentNameLabel
70-
environmentName={activeError.error.environmentName}
71-
/>
72-
)}
73-
</span>
74-
<ErrorOverlayToolbar
75-
error={activeError.error}
76-
debugInfo={debugInfo}
77-
/>
78-
</div>
79-
<ErrorMessage errorMessage={errorMessage} />
80-
</div>
8160

82-
{/* TODO: Content */}
83-
<div>Content</div>
84-
</div>
61+
{/* This is the copy of the Error Overlay content. */}
62+
<IssuesTabContent
63+
buildError={buildError}
64+
notes={notes}
65+
hydrationWarning={hydrationWarning}
66+
errorDetails={errorDetails}
67+
activeError={activeError}
68+
debugInfo={debugInfo}
69+
errorCode={errorCode}
70+
errorType={errorType}
71+
/>
8572
</div>
8673
)
8774
}
@@ -93,11 +80,45 @@ export const DEVTOOLS_PANEL_TAB_ISSUES_STYLES = css`
9380
min-height: 0;
9481
}
9582
96-
[data-nextjs-devtools-panel-tab-issues-content] {
83+
[data-nextjs-devtools-panel-tab-issues-empty] {
84+
display: flex;
9785
flex: 1;
86+
padding: 12px;
87+
min-height: 0;
88+
}
89+
90+
[data-nextjs-devtools-panel-tab-issues-empty-content] {
9891
display: flex;
9992
flex-direction: column;
100-
overflow-y: auto;
101-
min-height: 0;
93+
align-items: center;
94+
justify-content: center;
95+
flex: 1;
96+
border: 1px dashed var(--color-gray-alpha-500);
97+
border-radius: 4px;
98+
}
99+
100+
[data-nextjs-devtools-panel-tab-issues-empty-icon] {
101+
margin-bottom: 16px;
102+
padding: 8px;
103+
border: 1px solid var(--color-gray-alpha-400);
104+
border-radius: 6px;
105+
106+
background-color: var(--color-background-100);
107+
display: flex;
108+
align-items: center;
109+
justify-content: center;
110+
}
111+
112+
[data-nextjs-devtools-panel-tab-issues-empty-title] {
113+
color: var(--color-gray-1000);
114+
font-size: 16px;
115+
font-weight: 500;
116+
line-height: var(--line-height-20);
117+
}
118+
119+
[data-nextjs-devtools-panel-tab-issues-empty-subtitle] {
120+
color: var(--color-gray-900);
121+
font-size: 14px;
122+
line-height: var(--line-height-21);
102123
}
103124
`

0 commit comments

Comments
 (0)