Skip to content

[devtools] panel ui issues tab content #80729

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jun 23, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@ export const CODE_FRAME_STYLES = `
--code-frame-padding: 12px;
--code-frame-line-height: var(--size-16);
background-color: var(--color-background-200);
overflow: hidden;
color: var(--color-gray-1000);
text-overflow: ellipsis;
border: 1px solid var(--color-gray-400);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { DevToolsPanelTabType } from '../devtools-panel'
import type { Corners } from '../../../shared'
import type { Corners, OverlayState } from '../../../shared'
import type { DebugInfo } from '../../../../shared/types'
import type { ReadyRuntimeError } from '../../../utils/get-error-by-type'
import type { HydrationErrorState } from '../../../../shared/hydration-error'
Expand All @@ -16,6 +16,7 @@ export function DevToolsPanelTab({
debugInfo,
runtimeErrors,
getSquashedHydrationErrorDetails,
buildError,
}: {
activeTab: DevToolsPanelTabType
devToolsPosition: Corners
Expand All @@ -25,6 +26,7 @@ export function DevToolsPanelTab({
debugInfo: DebugInfo
runtimeErrors: ReadyRuntimeError[]
getSquashedHydrationErrorDetails: (error: Error) => HydrationErrorState | null
buildError: OverlayState['buildError']
}) {
switch (activeTab) {
case 'settings':
Expand All @@ -44,6 +46,7 @@ export function DevToolsPanelTab({
debugInfo={debugInfo}
runtimeErrors={runtimeErrors}
getSquashedHydrationErrorDetails={getSquashedHydrationErrorDetails}
buildError={buildError}
/>
)
default:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import type { OverlayState } from '../../../../shared'
import type { DebugInfo } from '../../../../../shared/types'
import type { ReadyRuntimeError } from '../../../../utils/get-error-by-type'
import type { ErrorType } from '../../../errors/error-type-label/error-type-label'

import { Suspense, useMemo, useState } from 'react'

import {
GenericErrorDescription,
HydrationErrorDescription,
} from '../../../../container/errors'
import { EnvironmentNameLabel } from '../../../errors/environment-name-label/environment-name-label'
import { ErrorMessage } from '../../../errors/error-message/error-message'
import { ErrorOverlayToolbar } from '../../../errors/error-overlay-toolbar/error-overlay-toolbar'
import { ErrorTypeLabel } from '../../../errors/error-type-label/error-type-label'
import { IssueFeedbackButton } from '../../../errors/error-overlay-toolbar/issue-feedback-button'
import { Terminal } from '../../../terminal'
import { HotlinkedText } from '../../../hot-linked-text'
import { PseudoHtmlDiff } from '../../../../container/runtime-error/component-stack-pseudo-html'
import { useFrames } from '../../../../utils/get-error-by-type'
import { CodeFrame } from '../../../code-frame/code-frame'
import { CallStack } from '../../../call-stack/call-stack'
import { NEXTJS_HYDRATION_ERROR_LINK } from '../../../../../shared/react-19-hydration-error'
import { ErrorContentSkeleton } from '../../../../container/runtime-error/error-content-skeleton'
import { css } from '../../../../utils/css'

export function IssuesTabContent({
notes,
buildError,
hydrationWarning,
errorDetails,
activeError,
errorCode,
errorType,
debugInfo,
}: {
notes: string | null
buildError: OverlayState['buildError']
hydrationWarning: string | null
errorDetails: {
hydrationWarning: string | null
notes: string | null
reactOutputComponentDiff: string | null
}
activeError: ReadyRuntimeError
errorCode: string | undefined
errorType: ErrorType
debugInfo: DebugInfo
}) {
if (buildError) {
return <Terminal content={buildError} />
}

const errorMessage = hydrationWarning ? (
<HydrationErrorDescription message={hydrationWarning} />
) : (
<GenericErrorDescription error={activeError.error} />
)

return (
<div data-nextjs-devtools-panel-tab-issues-content-container>
<div className="nextjs-container-errors-header">
<div
className="nextjs__container_errors__error_title"
// allow assertion in tests before error rating is implemented
data-nextjs-error-code={errorCode}
>
<span data-nextjs-error-label-group>
<ErrorTypeLabel errorType={errorType} />
{activeError.error.environmentName && (
<EnvironmentNameLabel
environmentName={activeError.error.environmentName}
/>
)}
</span>
<ErrorOverlayToolbar
error={activeError.error}
debugInfo={debugInfo}
// TODO: Move the button inside and remove the feedback on the footer of the error overlay.
feedbackButton={
errorCode && <IssueFeedbackButton errorCode={errorCode} />
}
/>
</div>
<ErrorMessage errorMessage={errorMessage} />
</div>
<div className="error-overlay-notes-container">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we able to extract the content of Error's content from errors.tsx to a separate component and reuse here?

It looks like a error view that can be reused here, the difference between the previous dev-overlay and this tab is the outside layout are different.

This tab can be composed by a issue tab layout (with nextjs-devtools-panel-tab-issues-content attribute) and the Error overview. Prefer to refactor rather than duplicate everything

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicating once is fine. Especially for UI. Abstracting it based on two usages is likely to be incorrect. Abstracting would be counter to forking the UI between feature flagged and not feature flagged.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The abstraction here is confusing tho. the component is IssueTabContent, then it could be either build or runtime error. nextjs-devtools-panel-tab-issues-conten is not used here.

I'm fine with even just refactoring a bit here with keeping the duplication. At least we can make it more like

IssueTabContent = BuildError | OtherError

<IssueTab nextjs-devtools-panel-tab-issues-content>
  <IssueTabContent>
<IssueTab>

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I've moved all the Error Overlay contents inside the IssueTabContent and added comments to reduce confusion.

{notes ? (
<>
<p
id="nextjs__container_errors__notes"
className="nextjs__container_errors__notes"
>
{notes}
</p>
</>
) : null}
{hydrationWarning ? (
<p
id="nextjs__container_errors__link"
className="nextjs__container_errors__link"
>
<HotlinkedText
text={`See more info here: ${NEXTJS_HYDRATION_ERROR_LINK}`}
/>
</p>
) : null}
</div>
{errorDetails.reactOutputComponentDiff ? (
<PseudoHtmlDiff
reactOutputComponentDiff={errorDetails.reactOutputComponentDiff || ''}
/>
) : null}
<Suspense fallback={<ErrorContentSkeleton />}>
<RuntimeError key={activeError.id.toString()} error={activeError} />
</Suspense>
</div>
)
}

/* Ported the content from container/runtime-error/index.tsx */
function RuntimeError({ error }: { error: ReadyRuntimeError }) {
const [isIgnoreListOpen, setIsIgnoreListOpen] = useState(false)
const frames = useFrames(error)

const ignoredFramesTally = useMemo(() => {
return frames.reduce((tally, frame) => tally + (frame.ignored ? 1 : 0), 0)
}, [frames])

const firstFrame = useMemo(() => {
const firstFirstPartyFrameIndex = frames.findIndex(
(entry) =>
!entry.ignored &&
Boolean(entry.originalCodeFrame) &&
Boolean(entry.originalStackFrame)
)

return frames[firstFirstPartyFrameIndex] ?? null
}, [frames])

return (
<>
{firstFrame &&
firstFrame.originalStackFrame &&
firstFrame.originalCodeFrame && (
<CodeFrame
stackFrame={firstFrame.originalStackFrame}
codeFrame={firstFrame.originalCodeFrame}
/>
)}

{frames.length > 0 && (
<CallStack
frames={frames}
isIgnoreListOpen={isIgnoreListOpen}
onToggleIgnoreList={() => setIsIgnoreListOpen(!isIgnoreListOpen)}
ignoredFramesTally={ignoredFramesTally}
/>
)}
</>
)
}

// The components in this file shares the style with the Error Overlay.
export const DEVTOOLS_PANEL_TAB_ISSUES_CONTENT_STYLES = css`
[data-nextjs-devtools-panel-tab-issues-content-container] {
flex: 1;
display: flex;
flex-direction: column;
overflow-y: auto;
min-height: 0;
padding: 14px;
}
`
Original file line number Diff line number Diff line change
@@ -1,53 +1,54 @@
import type { OverlayState } from '../../../../shared'
import type { DebugInfo } from '../../../../../shared/types'
import type { ReadyRuntimeError } from '../../../../utils/get-error-by-type'
import type { HydrationErrorState } from '../../../../../shared/hydration-error'

import { IssuesTabSidebar } from './issues-tab-sidebar'
import {
GenericErrorDescription,
HydrationErrorDescription,
} from '../../../../container/errors'
import { EnvironmentNameLabel } from '../../../errors/environment-name-label/environment-name-label'
import { ErrorMessage } from '../../../errors/error-message/error-message'
import { ErrorOverlayToolbar } from '../../../errors/error-overlay-toolbar/error-overlay-toolbar'
import { ErrorTypeLabel } from '../../../errors/error-type-label/error-type-label'
import { IssuesTabContent } from './issues-tab-content'
import { css } from '../../../../utils/css'
import { useActiveRuntimeError } from '../../../../hooks/use-active-runtime-error'
import { Warning } from '../../../../icons/warning'

export function IssuesTab({
debugInfo,
runtimeErrors,
getSquashedHydrationErrorDetails,
buildError,
}: {
debugInfo: DebugInfo
runtimeErrors: ReadyRuntimeError[]
getSquashedHydrationErrorDetails: (error: Error) => HydrationErrorState | null
buildError: OverlayState['buildError']
}) {
const {
isLoading,
errorCode,
errorType,
hydrationWarning,
activeError,
activeIdx,
setActiveIndex,
notes,
errorDetails,
} = useActiveRuntimeError({ runtimeErrors, getSquashedHydrationErrorDetails })

if (isLoading) {
// TODO: better loading state
return null
}
Comment on lines -36 to -39
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this isLoading is relevant anymore because of #80322


if (!activeError) {
return null
return (
<div data-nextjs-devtools-panel-tab-issues-empty>
<div data-nextjs-devtools-panel-tab-issues-empty-content>
<div data-nextjs-devtools-panel-tab-issues-empty-icon>
<Warning width={16} height={16} />
</div>
<h3 data-nextjs-devtools-panel-tab-issues-empty-title>
No Issues Found
</h3>
<p data-nextjs-devtools-panel-tab-issues-empty-subtitle>
Issues will appear here when they occur.
</p>
</div>
</div>
)
}

const errorMessage = hydrationWarning ? (
<HydrationErrorDescription message={hydrationWarning} />
) : (
<GenericErrorDescription error={activeError.error} />
)

return (
<div data-nextjs-devtools-panel-tab-issues>
<IssuesTabSidebar
Expand All @@ -56,32 +57,18 @@ export function IssuesTab({
activeIdx={activeIdx}
setActiveIndex={setActiveIndex}
/>
<div data-nextjs-devtools-panel-tab-issues-content>
<div className="nextjs-container-errors-header">
<div
className="nextjs__container_errors__error_title"
// allow assertion in tests before error rating is implemented
data-nextjs-error-code={errorCode}
>
<span data-nextjs-error-label-group>
<ErrorTypeLabel errorType={errorType} />
{activeError.error.environmentName && (
<EnvironmentNameLabel
environmentName={activeError.error.environmentName}
/>
)}
</span>
<ErrorOverlayToolbar
error={activeError.error}
debugInfo={debugInfo}
/>
</div>
<ErrorMessage errorMessage={errorMessage} />
</div>

{/* TODO: Content */}
<div>Content</div>
</div>
{/* This is the copy of the Error Overlay content. */}
<IssuesTabContent
buildError={buildError}
notes={notes}
hydrationWarning={hydrationWarning}
errorDetails={errorDetails}
activeError={activeError}
debugInfo={debugInfo}
errorCode={errorCode}
errorType={errorType}
/>
</div>
)
}
Expand All @@ -93,11 +80,45 @@ export const DEVTOOLS_PANEL_TAB_ISSUES_STYLES = css`
min-height: 0;
}

[data-nextjs-devtools-panel-tab-issues-content] {
[data-nextjs-devtools-panel-tab-issues-empty] {
display: flex;
flex: 1;
padding: 12px;
min-height: 0;
}

[data-nextjs-devtools-panel-tab-issues-empty-content] {
display: flex;
flex-direction: column;
overflow-y: auto;
min-height: 0;
align-items: center;
justify-content: center;
flex: 1;
border: 1px dashed var(--color-gray-alpha-500);
border-radius: 4px;
}

[data-nextjs-devtools-panel-tab-issues-empty-icon] {
margin-bottom: 16px;
padding: 8px;
border: 1px solid var(--color-gray-alpha-400);
border-radius: 6px;

background-color: var(--color-background-100);
display: flex;
align-items: center;
justify-content: center;
}

[data-nextjs-devtools-panel-tab-issues-empty-title] {
color: var(--color-gray-1000);
font-size: 16px;
font-weight: 500;
line-height: var(--line-height-20);
}

[data-nextjs-devtools-panel-tab-issues-empty-subtitle] {
color: var(--color-gray-900);
font-size: 14px;
line-height: var(--line-height-21);
}
`
Loading
Loading