Skip to content
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
Copy Markdown
Contributor

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
Copy Markdown
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
Copy Markdown
Contributor

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
Copy Markdown
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
Copy Markdown
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