diff --git a/packages/next/src/next-devtools/dev-overlay/components/devtools-panel/devtools-panel-footer.tsx b/packages/next/src/next-devtools/dev-overlay/components/devtools-panel/devtools-panel-footer.tsx index 7812924af4661..95c840a1329dd 100644 --- a/packages/next/src/next-devtools/dev-overlay/components/devtools-panel/devtools-panel-footer.tsx +++ b/packages/next/src/next-devtools/dev-overlay/components/devtools-panel/devtools-panel-footer.tsx @@ -49,7 +49,6 @@ export const DEVTOOLS_PANEL_FOOTER_STYLES = css` display: flex; justify-content: space-between; align-items: center; - margin-top: auto; border-top: 1px solid var(--color-gray-400); border-radius: 0 0 var(--rounded-xl) var(--rounded-xl); } diff --git a/packages/next/src/next-devtools/dev-overlay/components/devtools-panel/devtools-panel-tab/issues-tab/issues-tab-sidebar-frame-skeleton.tsx b/packages/next/src/next-devtools/dev-overlay/components/devtools-panel/devtools-panel-tab/issues-tab/issues-tab-sidebar-frame-skeleton.tsx new file mode 100644 index 0000000000000..f75e8d69cf9bb --- /dev/null +++ b/packages/next/src/next-devtools/dev-overlay/components/devtools-panel/devtools-panel-tab/issues-tab/issues-tab-sidebar-frame-skeleton.tsx @@ -0,0 +1,52 @@ +import { css } from '../../../../utils/css' + +export function IssuesTabSidebarFrameSkeleton() { + return ( + <> +
+
+ + ) +} + +export const DEVTOOLS_PANEL_TAB_ISSUES_SIDEBAR_FRAME_SKELETON_STYLES = css` + [data-nextjs-devtools-panel-tab-issues-sidebar-frame-skeleton-bar] { + height: var(--size-12); + border-radius: 100px; + background: linear-gradient( + 90deg, + var(--color-gray-200) 25%, + var(--color-gray-100) 50%, + var(--color-gray-200) 75% + ); + background-size: 200% 100%; + animation: skeleton-shimmer 1.5s ease-in-out infinite; + } + + [data-nextjs-devtools-panel-tab-issues-sidebar-frame-skeleton-bar='1'] { + width: 75%; + margin-bottom: 8px; + } + + [data-nextjs-devtools-panel-tab-issues-sidebar-frame-skeleton-bar='2'] { + width: 36.5%; + } + + @keyframes skeleton-shimmer { + 0% { + background-position: -200% 0; + } + 100% { + background-position: 200% 0; + } + } + + /* Respect user's motion preferences */ + @media (prefers-reduced-motion: reduce) { + [data-nextjs-devtools-panel-tab-issues-sidebar-frame-skeleton-bar='1'], + [data-nextjs-devtools-panel-tab-issues-sidebar-frame-skeleton-bar='2'] { + animation: none; + background: var(--color-gray-200); + } + } +` diff --git a/packages/next/src/next-devtools/dev-overlay/components/devtools-panel/devtools-panel-tab/issues-tab/issues-tab-sidebar.stories.tsx b/packages/next/src/next-devtools/dev-overlay/components/devtools-panel/devtools-panel-tab/issues-tab/issues-tab-sidebar.stories.tsx new file mode 100644 index 0000000000000..b03cb56494c6a --- /dev/null +++ b/packages/next/src/next-devtools/dev-overlay/components/devtools-panel/devtools-panel-tab/issues-tab/issues-tab-sidebar.stories.tsx @@ -0,0 +1,41 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { useState } from 'react' +import { IssuesTabSidebar } from './issues-tab-sidebar' +import { withShadowPortal } from '../../../../storybook/with-shadow-portal' +import { runtimeErrors } from '../../../../storybook/errors' + +const meta: Meta = { + component: IssuesTabSidebar, + parameters: { + layout: 'centered', + }, + decorators: [withShadowPortal], +} + +export default meta +type Story = StoryObj + +const SidebarWrapper = ({ + runtimeErrors: errors, + errorType, +}: { + runtimeErrors: any[] + errorType: string | null +}) => { + const [activeIdx, setActiveIdx] = useState(0) + + return ( + + ) +} + +export const Default: Story = { + render: () => ( + + ), +} diff --git a/packages/next/src/next-devtools/dev-overlay/components/devtools-panel/devtools-panel-tab/issues-tab/issues-tab-sidebar.tsx b/packages/next/src/next-devtools/dev-overlay/components/devtools-panel/devtools-panel-tab/issues-tab/issues-tab-sidebar.tsx new file mode 100644 index 0000000000000..ee8669bcbbf97 --- /dev/null +++ b/packages/next/src/next-devtools/dev-overlay/components/devtools-panel/devtools-panel-tab/issues-tab/issues-tab-sidebar.tsx @@ -0,0 +1,175 @@ +import type { ReadyRuntimeError } from '../../../../utils/get-error-by-type' + +import { Suspense, useMemo, memo } from 'react' + +import { css } from '../../../../utils/css' +import { getFrameSource } from '../../../../../shared/stack-frame' +import { useFrames } from '../../../../utils/get-error-by-type' +import { getErrorTypeLabel } from '../../../../container/errors' +import { IssuesTabSidebarFrameSkeleton } from './issues-tab-sidebar-frame-skeleton' + +export function IssuesTabSidebar({ + runtimeErrors, + activeIdx, + setActiveIndex, +}: { + runtimeErrors: ReadyRuntimeError[] + errorType: string | null + activeIdx: number + setActiveIndex: (idx: number) => void +}) { + return ( + + ) +} + +const IssuesTabSidebarFrameItem = memo(function IssuesTabSidebarFrameItem({ + runtimeError, +}: { + runtimeError: ReadyRuntimeError +}) { + 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]) + + if (!firstFrame?.originalStackFrame) { + return null + } + + const errorType = getErrorTypeLabel(runtimeError.error, runtimeError.type) + const frameSource = getFrameSource(firstFrame.originalStackFrame) + + return ( + <> + + {errorType} + + + {frameSource} + + + ) +}) + +const IssuesTabSidebarFrame = memo(function IssuesTabSidebarFrame({ + runtimeError, + idx, + isActive, + setActiveIndex, +}: { + runtimeError: ReadyRuntimeError + idx: number + isActive: boolean + setActiveIndex: (idx: number) => void +}) { + return ( + + ) +}) + +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); + overflow-y: auto; + min-height: 0; + + @media (max-width: 575px) { + max-width: 112px; + } + + @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); + } + + /* Ellipsis for long stack frame source or small devices. */ + [data-nextjs-devtools-panel-tab-issues-sidebar-frame-error-type], + [data-nextjs-devtools-panel-tab-issues-sidebar-frame-source] { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 100%; + } +` diff --git a/packages/next/src/next-devtools/dev-overlay/components/devtools-panel/devtools-panel-tab/issues-tab/issues-tab.tsx b/packages/next/src/next-devtools/dev-overlay/components/devtools-panel/devtools-panel-tab/issues-tab/issues-tab.tsx index 1500394d0ed75..d88484deba0fc 100644 --- a/packages/next/src/next-devtools/dev-overlay/components/devtools-panel/devtools-panel-tab/issues-tab/issues-tab.tsx +++ b/packages/next/src/next-devtools/dev-overlay/components/devtools-panel/devtools-panel-tab/issues-tab/issues-tab.tsx @@ -2,6 +2,7 @@ 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, @@ -22,8 +23,15 @@ export function IssuesTab({ runtimeErrors: ReadyRuntimeError[] getSquashedHydrationErrorDetails: (error: Error) => HydrationErrorState | null }) { - const { isLoading, errorCode, errorType, hydrationWarning, activeError } = - useActiveRuntimeError({ runtimeErrors, getSquashedHydrationErrorDetails }) + const { + isLoading, + errorCode, + errorType, + hydrationWarning, + activeError, + activeIdx, + setActiveIndex, + } = useActiveRuntimeError({ runtimeErrors, getSquashedHydrationErrorDetails }) if (isLoading) { // TODO: better loading state @@ -42,8 +50,12 @@ export function IssuesTab({ return (
- {/* TODO: Sidebar */} - +
{}, - issueCount: 3, + issueCount: runtimeErrors.length, runtimeErrors, getSquashedHydrationErrorDetails: () => null, }, diff --git a/packages/next/src/next-devtools/dev-overlay/storybook/errors.ts b/packages/next/src/next-devtools/dev-overlay/storybook/errors.ts index 703a3f84b41d5..c8c885daabe01 100644 --- a/packages/next/src/next-devtools/dev-overlay/storybook/errors.ts +++ b/packages/next/src/next-devtools/dev-overlay/storybook/errors.ts @@ -1,5 +1,6 @@ import type { SupportedErrorEvent } from '../container/runtime-error/render-error' import type { ReadyRuntimeError } from '../utils/get-error-by-type' +import { lorem } from '../utils/lorem' const originalCodeFrame = (message: string) => { return `\u001b[0m \u001b[90m 1 \u001b[39m \u001b[36mexport\u001b[39m \u001b[36mdefault\u001b[39m \u001b[36mfunction\u001b[39m \u001b[33mHome\u001b[39m() {\u001b[0m @@ -103,7 +104,7 @@ export const runtimeErrors: ReadyRuntimeError[] = [ { id: 1, runtime: true, - error: new Error('First error message'), + error: new Error(lorem), frames: () => Promise.resolve([ frame, @@ -181,4 +182,46 @@ export const runtimeErrors: ReadyRuntimeError[] = [ ]), type: 'runtime', }, + { + id: 5, + runtime: true, + error: new Error('Very long stack frame file name.'), + frames: () => + Promise.resolve([ + { + error: true, + reason: 'Fifth error message', + external: false, + ignored: false, + sourceStackFrame: { + ...sourceStackFrame, + file: 'foo/bar/baz/qux/quux/quuz/corge/grault/garply/waldo/fred/plugh/xyzzy/thud.tsx', + }, + originalStackFrame: { + ...originalStackFrame, + file: 'foo/bar/baz/qux/quux/quuz/corge/grault/garply/waldo/fred/plugh/xyzzy/thud.tsx (0:0)', + }, + originalCodeFrame: originalCodeFrame('Fifth error message'), + }, + ]), + type: 'console', + }, + { + id: 6, + runtime: true, + error: new Error('Sixth error message'), + frames: () => + Promise.resolve([ + { + error: true, + reason: 'Sixth error message', + external: false, + ignored: false, + sourceStackFrame, + originalStackFrame, + originalCodeFrame: originalCodeFrame('Sixth error message'), + }, + ]), + type: 'recoverable', + }, ] diff --git a/packages/next/src/next-devtools/dev-overlay/styles/component-styles.tsx b/packages/next/src/next-devtools/dev-overlay/styles/component-styles.tsx index aad9a7e5fe20a..3a7117693c239 100644 --- a/packages/next/src/next-devtools/dev-overlay/styles/component-styles.tsx +++ b/packages/next/src/next-devtools/dev-overlay/styles/component-styles.tsx @@ -30,6 +30,8 @@ import { DEVTOOLS_PANEL_VERSION_INFO_STYLES } from '../components/devtools-panel import { DEVTOOLS_PANEL_TAB_SETTINGS_STYLES } from '../components/devtools-panel/devtools-panel-tab/settings-tab' import { CALL_STACK_STYLES } from '../components/call-stack/call-stack' import { DEVTOOLS_PANEL_TAB_ISSUES_STYLES } from '../components/devtools-panel/devtools-panel-tab/issues-tab/issues-tab' +import { DEVTOOLS_PANEL_TAB_ISSUES_SIDEBAR_STYLES } from '../components/devtools-panel/devtools-panel-tab/issues-tab/issues-tab-sidebar' +import { DEVTOOLS_PANEL_TAB_ISSUES_SIDEBAR_FRAME_SKELETON_STYLES } from '../components/devtools-panel/devtools-panel-tab/issues-tab/issues-tab-sidebar-frame-skeleton' export function ComponentStyles() { return ( @@ -66,6 +68,8 @@ export function ComponentStyles() { ${DEVTOOLS_PANEL_VERSION_INFO_STYLES} ${DEVTOOLS_PANEL_TAB_SETTINGS_STYLES} ${DEVTOOLS_PANEL_TAB_ISSUES_STYLES} + ${DEVTOOLS_PANEL_TAB_ISSUES_SIDEBAR_STYLES} + ${DEVTOOLS_PANEL_TAB_ISSUES_SIDEBAR_FRAME_SKELETON_STYLES} `} )