Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,20 +1,32 @@
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'

import { SettingsTab } from './settings-tab'
import { IssuesTab } from './issues-tab/issues-tab'

export function DevToolsPanelTab({
activeTab,
runtimeErrors,
devToolsPosition,
scale,
debugInfo,
buildError,
handlePositionChange,
handleScaleChange,
getSquashedHydrationErrorDetails,
}: {
activeTab: DevToolsPanelTabType
runtimeErrors: ReadyRuntimeError[]
devToolsPosition: Corners
scale: number
debugInfo: DebugInfo
buildError: OverlayState['buildError']
handlePositionChange: (e: React.ChangeEvent<HTMLSelectElement>) => void
handleScaleChange: (e: React.ChangeEvent<HTMLSelectElement>) => void
getSquashedHydrationErrorDetails: (error: Error) => HydrationErrorState | null
}) {
switch (activeTab) {
case 'settings':
Expand All @@ -29,7 +41,14 @@ export function DevToolsPanelTab({
case 'route':
return <div>Route</div>
case 'issues':
return <div>Issues</div>
return (
<IssuesTab
debugInfo={debugInfo}
buildError={buildError}
runtimeErrors={runtimeErrors}
getSquashedHydrationErrorDetails={getSquashedHydrationErrorDetails}
/>
)
default:
return null
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import type { ReadyRuntimeError } from '../../../../utils/get-error-by-type'
import type { OverlayState } from '../../../../shared'

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

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 { css } from '../../../../utils/css'

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

return (
<>
<div className="error-overlay-notes-container">
{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}
{/* TODO: Loading state */}
<Suspense fallback={<div data-nextjs-error-suspended />}>
<RuntimeError key={activeError.id.toString()} error={activeError} />
</Suspense>
</>
)
}

/* 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 && (
<CodeFrame
stackFrame={firstFrame.originalStackFrame!}
codeFrame={firstFrame.originalCodeFrame!}
/>
)}

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

/* Shares the style with container/errors.tsx and errors/dialog/header.tsx */
export const DEVTOOLS_PANEL_TAB_ISSUES_CONTENT_STYLES = css`
[data-nextjs-devtools-panel-tab-issues-content] {
width: 100%;
padding: 14px;
}
`
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import type { ReadyRuntimeError } from '../../../../utils/get-error-by-type'

import { Suspense, useMemo } from 'react'

import { css } from '../../../../utils/css'
import { getFrameSource } from '../../../../../shared/stack-frame'
import { useFrames } from '../../../../utils/get-error-by-type'

export function IssuesTabSidebar({
runtimeErrors,
errorType,
activeIdx,
setActiveIndex,
}: {
runtimeErrors: ReadyRuntimeError[]
errorType: string | null
activeIdx: number
setActiveIndex: (idx: number) => void
}) {
return (
<aside data-nextjs-devtools-panel-tab-issues-sidebar>
{runtimeErrors.map((runtimeError, idx) => {
// TODO: Loading state
return (
<Suspense fallback={<div>Loading...</div>}>
<IssuesTabSidebarFrame
key={idx}
runtimeError={runtimeError}
errorType={errorType}
idx={idx}
activeIdx={activeIdx}
setActiveIndex={setActiveIndex}
/>
</Suspense>
)
})}
</aside>
)
}

function IssuesTabSidebarFrame({
runtimeError,
errorType,
idx,
activeIdx,
setActiveIndex,
}: {
runtimeError: ReadyRuntimeError
errorType: string | null
idx: number
activeIdx: number
setActiveIndex: (idx: number) => void
}) {
const frames = useFrames(runtimeError)

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

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

const frameSource = getFrameSource(firstFrame.originalStackFrame!)
return (
<button
data-nextjs-devtools-panel-tab-issues-sidebar-frame
data-nextjs-devtools-panel-tab-issues-sidebar-frame-active={
idx === activeIdx
}
onClick={() => setActiveIndex(idx)}
>
<span data-nextjs-devtools-panel-tab-issues-sidebar-frame-error-type>
{errorType}
</span>
<span data-nextjs-devtools-panel-tab-issues-sidebar-frame-source>
{frameSource}
</span>
</button>
)
}

export const DEVTOOLS_PANEL_TAB_ISSUES_SIDEBAR_STYLES = css`
[data-nextjs-devtools-panel-tab-issues-sidebar] {
display: flex;
flex-direction: column;
gap: 4px;
padding: 8px;
border-right: 1px solid var(--color-gray-400);

min-width: 128px;

@media (min-width: 576px) {
max-width: 138px;
width: 100%;
}

@media (min-width: 768px) {
max-width: 172.5px;
width: 100%;
}

@media (min-width: 992px) {
max-width: 230px;
width: 100%;
}
}

[data-nextjs-devtools-panel-tab-issues-sidebar-frame] {
display: flex;
flex-direction: column;
padding: 10px 8px;
border-radius: var(--rounded-lg);
transition: background-color 0.2s ease-in-out;

&:hover {
background-color: var(--color-gray-200);
}

&:active {
background-color: var(--color-gray-300);
}
}

[data-nextjs-devtools-panel-tab-issues-sidebar-frame-active='true'] {
background-color: var(--color-gray-100);
}

[data-nextjs-devtools-panel-tab-issues-sidebar-frame-error-type] {
display: inline-block;
align-self: flex-start;
color: var(--color-gray-1000);
font-size: var(--size-14);
font-weight: 500;
line-height: var(--size-20);
}

[data-nextjs-devtools-panel-tab-issues-sidebar-frame-source] {
display: inline-block;
align-self: flex-start;
color: var(--color-gray-900);
font-size: var(--size-13);
line-height: var(--size-18);
}
`
Loading