Skip to content

Commit 7671e2b

Browse files
authored
feat(trace-viewer): Collapse sections inside network request (#38086)
1 parent acabbaa commit 7671e2b

File tree

5 files changed

+122
-37
lines changed

5 files changed

+122
-37
lines changed

packages/trace-viewer/src/ui/networkResourceDetails.css

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616

1717
.network-request-details-tab {
1818
user-select: text;
19-
line-height: 24px;
20-
margin-left: 10px;
2119
overflow: auto;
2220
}
2321

@@ -37,8 +35,18 @@
3735
margin-left: 10px;
3836
}
3937

38+
.network-request-details-tab .expandable-title {
39+
padding-left: 3px;
40+
}
41+
42+
.network-request-details-tab .expandable-content {
43+
margin-left: 0;
44+
padding-left: 28px;
45+
line-height: 24px;
46+
}
47+
4048
.network-request-details-header {
41-
margin: 3px 0;
49+
margin: 3px 0 3px 14px;
4250
font-weight: bold;
4351
}
4452

@@ -51,6 +59,14 @@
5159
overflow: hidden;
5260
}
5361

62+
.network-request-request-body {
63+
max-height: 100%;
64+
}
65+
66+
.network-request-request-body .expandable-content {
67+
height: 100%;
68+
}
69+
5470
.network-font-preview {
5571
font-family: font-preview;
5672
font-size: 30px;

packages/trace-viewer/src/ui/networkResourceDetails.tsx

Lines changed: 51 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@ import { generateCurlCommand, generateFetchCall } from '../third_party/devtools'
2424
import { CopyToClipboardTextButton } from './copyToClipboard';
2525
import { getAPIRequestCodeGen } from './codegen';
2626
import type { Language } from '@isomorphic/locatorGenerators';
27-
import { msToString, useAsyncMemo } from '@web/uiUtils';
27+
import { msToString, useAsyncMemo, useSetting } from '@web/uiUtils';
2828
import type { Entry } from '@trace/har';
2929
import { useTraceModel } from './traceModelContext';
30+
import { Expandable } from '@web/components/expandable';
3031

3132
type RequestBody = { text: string, mimeType?: string } | null;
3233

@@ -105,42 +106,68 @@ const CopyDropdown: React.FC<{
105106
);
106107
};
107108

109+
const ExpandableSection: React.FC<{
110+
title: string;
111+
children?: React.ReactNode
112+
className?: string;
113+
}> = ({ title, children, className }) => {
114+
const [expanded, setExpanded] = useSetting(`trace-viewer-network-details-${title.replaceAll(' ', '-')}`, true);
115+
return <Expandable
116+
expanded={expanded}
117+
setExpanded={setExpanded}
118+
expandOnTitleClick
119+
title={<span className='network-request-details-header'>{title}</span>}
120+
className={className}
121+
>
122+
{children}
123+
</Expandable>;
124+
};
125+
108126
const RequestTab: React.FunctionComponent<{
109127
resource: ResourceSnapshot;
110128
startTimeOffset: number;
111129
requestBody: RequestBody,
112130
}> = ({ resource, startTimeOffset, requestBody }) => {
113131
return <div className='vbox network-request-details-tab'>
114-
<div className='network-request-details-header'>General</div>
115-
<div className='network-request-details-url'>{`URL: ${resource.request.url}`}</div>
116-
<div className='network-request-details-general'>{`Method: ${resource.request.method}`}</div>
117-
{resource.response.status !== -1 && <div className='network-request-details-general' style={{ display: 'flex' }}>
118-
Status Code: <span className={statusClass(resource.response.status)} style={{ display: 'inline-flex' }}>
119-
{`${resource.response.status} ${resource.response.statusText}`}
120-
</span></div>}
121-
{resource.request.queryString.length ? <>
122-
<div className='network-request-details-header'>Query String Parameters</div>
123-
<div className='network-request-details-headers'>
124-
{resource.request.queryString.map(param => `${param.name}: ${param.value}`).join('\n')}
125-
</div>
126-
</> : null}
127-
<div className='network-request-details-header'>Request Headers</div>
128-
<div className='network-request-details-headers'>{resource.request.headers.map(pair => `${pair.name}: ${pair.value}`).join('\n')}</div>
129-
<div className='network-request-details-header'>Time</div>
130-
<div className='network-request-details-general'>{`Start: ${msToString(startTimeOffset)}`}</div>
131-
<div className='network-request-details-general'>{`Duration: ${msToString(resource.time)}`}</div>
132-
133-
{requestBody && <div className='network-request-details-header'>Request Body</div>}
134-
{requestBody && <CodeMirrorWrapper text={requestBody.text} mimeType={requestBody.mimeType} readOnly lineNumbers={true}/>}
132+
<ExpandableSection title='General'>
133+
<div className='network-request-details-url'>{`URL: ${resource.request.url}`}</div>
134+
<div className='network-request-details-general'>{`Method: ${resource.request.method}`}</div>
135+
{resource.response.status !== -1 && <div className='network-request-details-general' style={{ display: 'flex' }}>
136+
Status Code: <span className={statusClass(resource.response.status)} style={{ display: 'inline-flex' }}>
137+
{`${resource.response.status} ${resource.response.statusText}`}
138+
</span></div>}
139+
</ExpandableSection>
140+
141+
{resource.request.queryString.length ?
142+
<ExpandableSection title='Query String Parameters'>
143+
<div className='network-request-details-headers'>
144+
{resource.request.queryString.map(param => `${param.name}: ${param.value}`).join('\n')}
145+
</div>
146+
</ExpandableSection>
147+
: null}
148+
149+
<ExpandableSection title='Request Headers'>
150+
<div className='network-request-details-headers'>{resource.request.headers.map(pair => `${pair.name}: ${pair.value}`).join('\n')}</div>
151+
</ExpandableSection>
152+
153+
<ExpandableSection title='Time'>
154+
<div className='network-request-details-general'>{`Start: ${msToString(startTimeOffset)}`}</div>
155+
<div className='network-request-details-general'>{`Duration: ${msToString(resource.time)}`}</div>
156+
</ExpandableSection>
157+
158+
{requestBody && <ExpandableSection title='Request Body' className='network-request-request-body'>
159+
<CodeMirrorWrapper text={requestBody.text} mimeType={requestBody.mimeType} readOnly lineNumbers={true}/>
160+
</ExpandableSection>}
135161
</div>;
136162
};
137163

138164
const ResponseTab: React.FunctionComponent<{
139165
resource: ResourceSnapshot;
140166
}> = ({ resource }) => {
141167
return <div className='vbox network-request-details-tab'>
142-
<div className='network-request-details-header'>Response Headers</div>
143-
<div className='network-request-details-headers'>{resource.response.headers.map(pair => `${pair.name}: ${pair.value}`).join('\n')}</div>
168+
<ExpandableSection title='Response Headers'>
169+
<div className='network-request-details-headers'>{resource.response.headers.map(pair => `${pair.name}: ${pair.value}`).join('\n')}</div>
170+
</ExpandableSection>
144171
</div>;
145172
};
146173

packages/web/src/components/expandable.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,7 @@
2929
user-select: none;
3030
cursor: pointer;
3131
}
32+
33+
.expandable-content {
34+
margin-left: 25px;
35+
}

packages/web/src/components/expandable.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@ export const Expandable: React.FunctionComponent<React.PropsWithChildren<{
2323
setExpanded: (expanded: boolean) => void,
2424
expanded: boolean,
2525
expandOnTitleClick?: boolean,
26-
}>> = ({ title, children, setExpanded, expanded, expandOnTitleClick }) => {
27-
const id = React.useId();
26+
className?: string;
27+
}>> = ({ title, children, setExpanded, expanded, expandOnTitleClick, className }) => {
28+
const titleId = React.useId();
29+
const regionId = React.useId();
2830

2931
const onClick = React.useCallback(() => setExpanded(!expanded), [expanded, setExpanded]);
3032

@@ -33,12 +35,13 @@ export const Expandable: React.FunctionComponent<React.PropsWithChildren<{
3335
style={{ cursor: 'pointer', color: 'var(--vscode-foreground)', marginLeft: '5px' }}
3436
onClick={!expandOnTitleClick ? onClick : undefined} />;
3537

36-
return <div className={clsx('expandable', expanded && 'expanded')}>
38+
return <div className={clsx('expandable', expanded && 'expanded', className)}>
3739
{expandOnTitleClick ?
3840
<div
41+
id={titleId}
3942
role='button'
4043
aria-expanded={expanded}
41-
aria-controls={id}
44+
aria-controls={regionId}
4245
className='expandable-title'
4346
onClick={onClick}>
4447
{chevron}
@@ -48,6 +51,6 @@ export const Expandable: React.FunctionComponent<React.PropsWithChildren<{
4851
{chevron}
4952
{title}
5053
</div>}
51-
{expanded && <div id={id} role='region' style={{ marginLeft: 25 }}>{children}</div>}
54+
{expanded && <div id={regionId} aria-labelledby={titleId} role='region' className='expandable-content'>{children}</div>}
5255
</div>;
5356
};

tests/playwright-test/ui-mode-test-network-tab.spec.ts

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -196,14 +196,14 @@ test('should display list of query parameters (only if present)', async ({ runUI
196196

197197
await page.getByText('call-with-query-params').click();
198198

199-
await expect(page.getByText('Query String Parameters')).toBeVisible();
200-
await expect(page.getByText('param1: value1')).toBeVisible();
201-
await expect(page.getByText('param1: value2')).toBeVisible();
202-
await expect(page.getByText('param2: value2')).toBeVisible();
199+
const region = page.getByRole('region', { name: 'Query String Parameters' });
200+
await expect(region.getByText('param1: value1')).toBeVisible();
201+
await expect(region.getByText('param1: value2')).toBeVisible();
202+
await expect(region.getByText('param2: value2')).toBeVisible();
203203

204204
await page.getByText('endpoint').click();
205205

206-
await expect(page.getByText('Query String Parameters')).not.toBeVisible();
206+
await expect(region).toBeHidden();
207207
});
208208

209209
test('should not duplicate network entries from beforeAll', {
@@ -241,3 +241,38 @@ test('should not duplicate network entries from beforeAll', {
241241
await page.getByText('Network', { exact: true }).click();
242242
await expect(page.getByRole('list', { name: 'Network requests' }).getByText('empty.html')).toHaveCount(1);
243243
});
244+
245+
test('should toggle sections inside network details', async ({ runUITest, server }) => {
246+
const { page } = await runUITest({
247+
'network-tab.test.ts': `
248+
import { test, expect } from '@playwright/test';
249+
test('network tab test', async ({ page }) => {
250+
await page.goto('${server.PREFIX}/network-tab/network.html');
251+
await page.evaluate(() => (window as any).donePromise);
252+
});
253+
`,
254+
});
255+
256+
await page.getByRole('treeitem', { name: 'network tab test' }).dblclick();
257+
await page.getByRole('tab', { name: 'Network' }).click();
258+
await page.getByRole('listitem').filter({ hasText: 'post-data-1' }).click();
259+
const requestPanel = page.getByRole('tabpanel', { name: 'Request' });
260+
261+
await requestPanel.getByRole('button', { name: 'Request Headers' }).click();
262+
await expect(requestPanel.getByRole('region', { name: 'Request Headers' })).toBeHidden();
263+
await expect(requestPanel.getByRole('region', { name: 'Time' })).toHaveText(/Start: .+Duration: \d+ms/);
264+
265+
await requestPanel.getByRole('button', { name: 'Time' }).click();
266+
await expect(requestPanel.getByRole('region', { name: 'Request Headers' })).toBeHidden();
267+
await expect(requestPanel.getByRole('region', { name: 'Time' })).toBeHidden();
268+
269+
await requestPanel.getByRole('button', { name: 'Time' }).click();
270+
await expect(requestPanel.getByRole('region', { name: 'Request Headers' })).toBeHidden();
271+
await expect(requestPanel.getByRole('region', { name: 'Time' })).toHaveText(/Start: .+Duration: \d+ms/);
272+
273+
// Re-opening should preserve open state
274+
await page.getByRole('tabpanel', { name: 'Network' }).getByRole('button', { name: 'Close' }).click();
275+
await page.getByRole('listitem').filter({ hasText: 'post-data-1' }).click();
276+
await expect(requestPanel.getByRole('region', { name: 'Request Headers' })).toBeHidden();
277+
await expect(requestPanel.getByRole('region', { name: 'Time' })).toHaveText(/Start: .+Duration: \d+ms/);
278+
});

0 commit comments

Comments
 (0)