mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
Persist Request Insights display settings (#96277)
## Summary - Replaces the per-request verbose checkbox with a global Request Insights settings menu. - Persists `requestInsights.verbose` through the existing DevTools configuration path. - Restores the setting across reloads. - Does not include the unrelated global `pagehide` flush behavior. ## Verification ```bash pnpm build-all pnpm --filter=next types pnpm --filter=next build pnpm test-dev-turbo test/development/app-dir/request-insights/request-insights.test.ts pnpm test-dev-webpack test/development/app-dir/request-insights/request-insights.test.ts ``` <!-- NEXT_JS_LLM -->
This commit is contained in:
+89
-21
@@ -18,6 +18,95 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.request-insights-list-toolbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
min-height: 36px;
|
||||
padding: 4px 10px;
|
||||
border-bottom: 1px solid var(--color-gray-400);
|
||||
background: var(--color-background-100);
|
||||
font-size: var(--size-12);
|
||||
}
|
||||
|
||||
.request-insights-settings {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.request-insights-settings-trigger {
|
||||
display: inline-flex;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: var(--color-gray-900);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.request-insights-settings-trigger:hover,
|
||||
.request-insights-settings-trigger[aria-expanded='true'] {
|
||||
background: var(--color-gray-300);
|
||||
color: var(--color-gray-1000);
|
||||
}
|
||||
|
||||
.request-insights-settings-positioner {
|
||||
z-index: var(--top-z-index);
|
||||
}
|
||||
|
||||
.request-insights-settings-menu {
|
||||
min-width: 160px;
|
||||
border: 1px solid var(--color-gray-400);
|
||||
border-radius: 8px;
|
||||
background: var(--color-background-100);
|
||||
box-shadow: 0px 4px 8px -4px
|
||||
color-mix(in srgb, var(--color-gray-900) 4%, transparent);
|
||||
padding: 4px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.request-insights-settings-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border-radius: 6px;
|
||||
padding: 6px 8px;
|
||||
cursor: pointer;
|
||||
font-size: var(--size-12);
|
||||
color: var(--color-gray-1000);
|
||||
}
|
||||
|
||||
.request-insights-settings-item[data-highlighted] {
|
||||
background: var(--color-gray-200);
|
||||
}
|
||||
|
||||
.request-insights-settings-checkbox {
|
||||
display: inline-flex;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--color-gray-500);
|
||||
border-radius: 4px;
|
||||
background: var(--color-background-100);
|
||||
color: var(--color-background-100);
|
||||
}
|
||||
|
||||
.request-insights-settings-checkbox[data-checked] {
|
||||
border-color: var(--color-gray-1000);
|
||||
background: var(--color-gray-1000);
|
||||
}
|
||||
|
||||
.request-insights-row {
|
||||
display: grid;
|
||||
grid-template-columns: 8px minmax(0, 1fr) auto;
|
||||
@@ -158,27 +247,6 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.request-insights-verbose-toggle {
|
||||
display: inline-flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
height: var(--size-20);
|
||||
margin: 0;
|
||||
color: var(--color-gray-900);
|
||||
font-size: var(--size-12);
|
||||
line-height: var(--size-16);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.request-insights-verbose-toggle input {
|
||||
display: block;
|
||||
width: var(--size-14);
|
||||
height: var(--size-14);
|
||||
flex: 0 0 var(--size-14);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.request-insights-total {
|
||||
flex-shrink: 0;
|
||||
color: var(--color-gray-1000);
|
||||
|
||||
+76
-12
@@ -1,4 +1,5 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { Menu } from '@base-ui-components/react/menu'
|
||||
import {
|
||||
getRequestInsightKey,
|
||||
getRequestInsightKind,
|
||||
@@ -6,7 +7,10 @@ import {
|
||||
type RequestInsightFetch,
|
||||
} from '../../../shared/request-insights'
|
||||
import { useDevOverlayContext } from '../../../dev-overlay.browser'
|
||||
import { ACTION_DEVTOOLS_CONFIG } from '../../shared'
|
||||
import { saveDevToolsConfig } from '../../utils/save-devtools-config'
|
||||
import { CopyButton } from '../copy-button'
|
||||
import GearIcon from '../../icons/gear-icon'
|
||||
import { formatDuration } from './format-duration'
|
||||
import { getActiveRequestKey, isPageLoadRequest } from './request-list'
|
||||
import {
|
||||
@@ -20,11 +24,20 @@ import './request-insights-panel.css'
|
||||
const TRACE_TICK_COUNT = 5
|
||||
|
||||
export function RequestInsightsPanel() {
|
||||
const { state } = useDevOverlayContext()
|
||||
const { dispatch, state, shadowRoot } = useDevOverlayContext()
|
||||
const requests = useMemo(
|
||||
() => [...state.requestInsights].reverse(),
|
||||
[state.requestInsights]
|
||||
)
|
||||
const { verbose } = state.requestInsightsConfig
|
||||
const setVerbose = (checked: boolean) => {
|
||||
const requestInsights = { verbose: checked }
|
||||
dispatch({
|
||||
type: ACTION_DEVTOOLS_CONFIG,
|
||||
devToolsConfig: { requestInsights },
|
||||
})
|
||||
saveDevToolsConfig({ requestInsights })
|
||||
}
|
||||
const [selectedRequestKey, setSelectedRequestKey] = useState<string | null>(
|
||||
() => getActiveRequestKey(requests, null)
|
||||
)
|
||||
@@ -46,6 +59,44 @@ export function RequestInsightsPanel() {
|
||||
return (
|
||||
<div className="request-insights-panel">
|
||||
<div className="request-insights-list">
|
||||
<div className="request-insights-list-toolbar">
|
||||
<strong>Requests</strong>
|
||||
<div className="request-insights-settings">
|
||||
<Menu.Root delay={0} modal={false}>
|
||||
<Menu.Trigger
|
||||
aria-label="Request list settings"
|
||||
className="request-insights-settings-trigger"
|
||||
>
|
||||
<GearIcon />
|
||||
</Menu.Trigger>
|
||||
<Menu.Portal container={shadowRoot}>
|
||||
<Menu.Positioner
|
||||
align="end"
|
||||
className="request-insights-settings-positioner"
|
||||
side="bottom"
|
||||
sideOffset={4}
|
||||
>
|
||||
<Menu.Popup className="request-insights-settings-menu">
|
||||
<Menu.CheckboxItem
|
||||
checked={verbose}
|
||||
className="request-insights-settings-item"
|
||||
closeOnClick={false}
|
||||
onCheckedChange={setVerbose}
|
||||
>
|
||||
<span
|
||||
className="request-insights-settings-checkbox"
|
||||
data-checked={verbose || undefined}
|
||||
>
|
||||
{verbose ? <CheckIcon /> : null}
|
||||
</span>
|
||||
Verbose traces
|
||||
</Menu.CheckboxItem>
|
||||
</Menu.Popup>
|
||||
</Menu.Positioner>
|
||||
</Menu.Portal>
|
||||
</Menu.Root>
|
||||
</div>
|
||||
</div>
|
||||
{requests.map((request) => {
|
||||
const requestKey = getRequestInsightKey(request)
|
||||
return (
|
||||
@@ -60,7 +111,9 @@ export function RequestInsightsPanel() {
|
||||
})}
|
||||
</div>
|
||||
|
||||
{selectedRequest && <RequestDetails request={selectedRequest} />}
|
||||
{selectedRequest && (
|
||||
<RequestDetails request={selectedRequest} verbose={verbose} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -114,8 +167,27 @@ function RequestRow({
|
||||
)
|
||||
}
|
||||
|
||||
function RequestDetails({ request }: { request: RequestInsight }) {
|
||||
const [verbose, setVerbose] = useState(false)
|
||||
function CheckIcon() {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="12"
|
||||
viewBox="0 0 16 16"
|
||||
width="12"
|
||||
>
|
||||
<path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function RequestDetails({
|
||||
request,
|
||||
verbose,
|
||||
}: {
|
||||
request: RequestInsight
|
||||
verbose: boolean
|
||||
}) {
|
||||
const traceItems = useMemo(
|
||||
() => getTraceItems(request, verbose),
|
||||
[request, verbose]
|
||||
@@ -139,14 +211,6 @@ function RequestDetails({ request }: { request: RequestInsight }) {
|
||||
content={JSON.stringify(request, null, 2)}
|
||||
successLabel="Copied request JSON"
|
||||
/>
|
||||
<label className="request-insights-verbose-toggle">
|
||||
<input
|
||||
checked={verbose}
|
||||
onChange={(event) => setVerbose(event.currentTarget.checked)}
|
||||
type="checkbox"
|
||||
/>
|
||||
<span>Verbose</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="request-insights-total">
|
||||
|
||||
@@ -29,6 +29,9 @@ export type DevToolsConfig = {
|
||||
devToolsPanelSize?: Record<string, { width: number; height: number }>
|
||||
scale?: number
|
||||
hideShortcut?: string | null
|
||||
requestInsights?: {
|
||||
verbose?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export type Corners = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
|
||||
@@ -83,6 +86,9 @@ export interface OverlayState {
|
||||
readonly hideShortcut: string | null
|
||||
readonly instantNavs: boolean
|
||||
readonly requestInsights: readonly RequestInsight[]
|
||||
readonly requestInsightsConfig: Readonly<{
|
||||
verbose: boolean
|
||||
}>
|
||||
}
|
||||
type DevtoolsPanelName = string
|
||||
export type OverlayDispatch = React.Dispatch<DispatcherEvent>
|
||||
@@ -386,6 +392,7 @@ export const INITIAL_OVERLAY_STATE: Omit<
|
||||
hideShortcut: null,
|
||||
instantNavs: hasInstantNavsCookie,
|
||||
requestInsights: [],
|
||||
requestInsightsConfig: { verbose: false },
|
||||
}
|
||||
|
||||
function getInitialState(
|
||||
@@ -582,6 +589,7 @@ export function useErrorOverlayReducer(
|
||||
devToolsPanelSize,
|
||||
scale,
|
||||
hideShortcut,
|
||||
requestInsights: requestInsightsConfig,
|
||||
} = action.devToolsConfig
|
||||
|
||||
return {
|
||||
@@ -597,6 +605,13 @@ export function useErrorOverlayReducer(
|
||||
hideShortcut:
|
||||
// hideShortcut can be null.
|
||||
hideShortcut !== undefined ? hideShortcut : state.hideShortcut,
|
||||
requestInsightsConfig: requestInsightsConfig
|
||||
? {
|
||||
verbose:
|
||||
requestInsightsConfig.verbose ??
|
||||
state.requestInsightsConfig.verbose,
|
||||
}
|
||||
: state.requestInsightsConfig,
|
||||
}
|
||||
}
|
||||
case ACTION_INSTANT_NAVS_TOGGLE: {
|
||||
|
||||
@@ -18,4 +18,9 @@ export const devToolsConfigSchema: z.ZodType<DevToolsConfig> = z.object({
|
||||
.optional(),
|
||||
scale: z.number().optional(),
|
||||
hideShortcut: z.string().nullable().optional(),
|
||||
requestInsights: z
|
||||
.object({
|
||||
verbose: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { nextTestSetup } from 'e2e-utils'
|
||||
import { createServer } from 'http'
|
||||
import type { AddressInfo } from 'net'
|
||||
import { retry, waitForNoRedbox } from 'next-test-utils'
|
||||
import {
|
||||
retry,
|
||||
toggleDevToolsIndicatorPopover,
|
||||
waitForNoRedbox,
|
||||
} from 'next-test-utils'
|
||||
|
||||
type RequestInsight = {
|
||||
requestId: string
|
||||
@@ -223,6 +227,130 @@ describe('request insights', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('persists verbose trace settings', async () => {
|
||||
const browser = await next.browser('/instant-insights')
|
||||
|
||||
async function openRequestInsightsPanel() {
|
||||
await toggleDevToolsIndicatorPopover(browser)
|
||||
await browser.elementByCss('[data-request-insights]').click()
|
||||
await browser.waitForElementByCss('.request-insights-list-toolbar')
|
||||
// The panel selector menu stays mounted for its exit animation and its
|
||||
// click-outside handler would close the freshly opened panel. Wait for
|
||||
// it to fully unmount before interacting with the panel.
|
||||
await retry(async () => {
|
||||
const selectorMenuGone = await browser.eval(() => {
|
||||
const root = document.querySelector('nextjs-portal')?.shadowRoot
|
||||
return !root?.querySelector('#nextjs-dev-tools-menu')
|
||||
})
|
||||
expect(selectorMenuGone).toBe(true)
|
||||
})
|
||||
}
|
||||
|
||||
function getSettingsMenuState(): Promise<{
|
||||
open: boolean
|
||||
checked: string | null
|
||||
}> {
|
||||
return browser.eval(() => {
|
||||
const root = document.querySelector('nextjs-portal')?.shadowRoot
|
||||
const item = root?.querySelector('.request-insights-settings-item')
|
||||
return {
|
||||
open: !!root?.querySelector('.request-insights-settings-menu'),
|
||||
checked:
|
||||
item
|
||||
?.querySelector('.request-insights-settings-checkbox')
|
||||
?.getAttribute('data-checked') ?? null,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getSpanRowCount(): Promise<number> {
|
||||
return browser.eval(() => {
|
||||
const root = document.querySelector('nextjs-portal')?.shadowRoot
|
||||
return root?.querySelectorAll('.request-insights-span-row').length ?? 0
|
||||
})
|
||||
}
|
||||
|
||||
await openRequestInsightsPanel()
|
||||
await retry(async () => {
|
||||
const selectedRegularRequest = await browser.eval(() => {
|
||||
const root = document.querySelector('nextjs-portal')?.shadowRoot
|
||||
const row = Array.from(
|
||||
root?.querySelectorAll<HTMLButtonElement>('.request-insights-row') ??
|
||||
[]
|
||||
).find(
|
||||
(candidate) => !candidate.textContent?.includes('Instant Insights')
|
||||
)
|
||||
row?.click()
|
||||
return row !== undefined
|
||||
})
|
||||
expect(selectedRegularRequest).toBe(true)
|
||||
})
|
||||
await browser.elementByCss('.request-insights-settings-trigger').click()
|
||||
|
||||
await retry(async () => {
|
||||
expect(await getSettingsMenuState()).toEqual({
|
||||
open: true,
|
||||
checked: null,
|
||||
})
|
||||
})
|
||||
|
||||
let defaultSpanRowCount = 0
|
||||
await retry(async () => {
|
||||
defaultSpanRowCount = await getSpanRowCount()
|
||||
expect(defaultSpanRowCount).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
await browser
|
||||
.elementByCss(
|
||||
'.request-insights-settings-item:has-text("Verbose traces")'
|
||||
)
|
||||
.click()
|
||||
|
||||
await retry(async () => {
|
||||
expect(await getSettingsMenuState()).toEqual({
|
||||
open: true,
|
||||
checked: 'true',
|
||||
})
|
||||
expect(await getSpanRowCount()).toBeGreaterThan(defaultSpanRowCount)
|
||||
})
|
||||
|
||||
await retry(async () => {
|
||||
const config = JSON.parse(
|
||||
await next.readFile('build/dev/cache/next-devtools-config.json')
|
||||
)
|
||||
expect(config.requestInsights?.verbose).toBe(true)
|
||||
})
|
||||
|
||||
await browser.elementByCss('.request-insights-details').click()
|
||||
await retry(async () => {
|
||||
expect((await getSettingsMenuState()).open).toBe(false)
|
||||
})
|
||||
|
||||
await browser.refresh()
|
||||
await openRequestInsightsPanel()
|
||||
await browser.elementByCss('.request-insights-settings-trigger').click()
|
||||
|
||||
await retry(async () => {
|
||||
expect(await getSettingsMenuState()).toEqual({
|
||||
open: true,
|
||||
checked: 'true',
|
||||
})
|
||||
})
|
||||
|
||||
// Restore the default so this test does not affect later assertions.
|
||||
await browser
|
||||
.elementByCss(
|
||||
'.request-insights-settings-item:has-text("Verbose traces")'
|
||||
)
|
||||
.click()
|
||||
await retry(async () => {
|
||||
const config = JSON.parse(
|
||||
await next.readFile('build/dev/cache/next-devtools-config.json')
|
||||
)
|
||||
expect(config.requestInsights?.verbose).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
it('uses the development endpoint and reports truncated output', async () => {
|
||||
const { result, requestedPaths } = await runWithResponse(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user