Skip to content
Open
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
9 changes: 9 additions & 0 deletions packages/trace-viewer/src/ui/networkResourceDetails.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
overflow: auto;
}

.network-response-toolbar {
border-top: 1px solid var(--vscode-panel-border);
border-bottom: none !important;
}

.network-request-details-tab > * {
flex: none;
}
Expand Down Expand Up @@ -70,6 +75,10 @@
overflow: hidden;
}

.network-response-body {
flex: 1;
}

.network-request-request-body {
max-height: 100%;
}
Expand Down
111 changes: 74 additions & 37 deletions packages/trace-viewer/src/ui/networkResourceDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ import { msToString, useAsyncMemo, useSetting } from '@web/uiUtils';
import type { Entry } from '@trace/har';
import { useTraceModel } from './traceModelContext';
import { Expandable } from '@web/components/expandable';
import { Toolbar } from '@web/components/toolbar';

type RequestBody = { text: string, mimeType?: string } | null;
type ResponseBody = { dataUrl?: string, text?: string, mimeType?: string, font?: BufferSource } | null;


export const NetworkResourceDetails: React.FunctionComponent<{
Expand All @@ -48,9 +50,9 @@ export const NetworkResourceDetails: React.FunctionComponent<{
const requestContentType = requestContentTypeHeader ? requestContentTypeHeader.value : '';
if (resource.request.postData._sha1) {
const response = await fetch(model.createRelativeUrl(`sha1/${resource.request.postData._sha1}`));
return { text: formatBody(await response.text(), requestContentType), mimeType: requestContentType };
return { text: await response.text(), mimeType: requestContentType };
} else {
return { text: formatBody(resource.request.postData.text, requestContentType), mimeType: requestContentType };
return { text: resource.request.postData.text, mimeType: requestContentType };
}
} else {
return null;
Expand Down Expand Up @@ -107,22 +109,37 @@ const CopyDropdown: React.FC<{
);
};

const FormatToggleButton: React.FC<{
toggled: boolean;
error?: boolean;
onToggle: () => void;
}> = ({ toggled, error, onToggle }) => {
return <ToolbarButton icon='json' title='Pretty print' toggled={toggled} errorBadge={error ? 'Formatting failed' : undefined} onClick={e => {
e.stopPropagation();
onToggle();
}}/>;
};

const ExpandableSection: React.FC<{
title: string;
showCount?: boolean,
data?: { name: string, value: React.ReactNode }[],
children?: React.ReactNode
titleChildren?: React.ReactNode;
className?: string;
}> = ({ title, data, showCount, children, className }) => {
}> = ({ title, data, showCount, children, titleChildren, className }) => {
const [expanded, setExpanded] = useSetting(`trace-viewer-network-details-${title.replaceAll(' ', '-')}`, true);
return <Expandable
expanded={expanded}
setExpanded={setExpanded}
expandOnTitleClick
title={
<span className='network-request-details-header'>{title}
{showCount && <span className='network-request-details-header-count'> × {data?.length ?? 0}</span>}
</span>
<>
<span className='network-request-details-header'>{title}
{showCount && <span className='network-request-details-header-count'> × {data?.length ?? 0}</span>}
</span>
{ titleChildren }
</>
}
className={className}
>
Expand Down Expand Up @@ -166,11 +183,21 @@ const PayloadTab: React.FunctionComponent<{
resource: ResourceSnapshot;
requestBody: RequestBody,
}> = ({ resource, requestBody }) => {
const [showFormatted, setShowFormatted] = useSetting('trace-viewer-network-details-show-formatted-payload', true);
const hasQueryString = resource.request.queryString.length > 0;
const hasRequestBody = !!(requestBody || resource.request.postData);
const formatResult = useFormattedBody(requestBody, showFormatted);

return <div className='vbox network-request-details-tab'>
{resource.request.queryString.length === 0 && !requestBody && <em className='network-request-no-payload'>No payload for this request.</em>}
{resource.request.queryString.length > 0 && <ExpandableSection title='Query String Parameters' showCount data={resource.request.queryString}/>}
{requestBody && <ExpandableSection title='Request Body' className='network-request-request-body'>
<CodeMirrorWrapper text={requestBody.text} mimeType={requestBody.mimeType} readOnly lineNumbers={true}/>
{!hasQueryString && !hasRequestBody && <em className='network-request-no-payload'>No payload for this request.</em>}
{hasQueryString && <ExpandableSection title='Query String Parameters' showCount data={resource.request.queryString}/>}
{requestBody && <ExpandableSection title='Request Body' className='network-request-request-body' titleChildren={
<>
<div style={{ margin: 'auto' }}></div>
<FormatToggleButton toggled={showFormatted} error={formatResult.error} onToggle={() => setShowFormatted(!showFormatted)} />
</>
Comment on lines +195 to +198
}>
<CodeMirrorWrapper text={formatResult.text} mimeType={requestBody.mimeType} readOnly lineNumbers={true}/>
</ExpandableSection>}
</div>;
};
Expand All @@ -179,7 +206,7 @@ const ResponseTab: React.FunctionComponent<{
resource: ResourceSnapshot;
}> = ({ resource }) => {
const model = useTraceModel();
const [responseBody, setResponseBody] = React.useState<{ dataUrl?: string, text?: string, mimeType?: string, font?: BufferSource } | null>(null);
const [responseBody, setResponseBody] = React.useState<ResponseBody>(null);

React.useEffect(() => {
const readResources = async () => {
Expand All @@ -197,8 +224,7 @@ const ResponseTab: React.FunctionComponent<{
const font = await response.arrayBuffer();
setResponseBody({ font });
} else {
const formattedBody = formatBody(await response.text(), resource.response.content.mimeType);
setResponseBody({ text: formattedBody, mimeType: resource.response.content.mimeType });
setResponseBody({ text: await response.text(), mimeType: resource.response.content.mimeType });
}
} else {
setResponseBody(null);
Expand All @@ -208,11 +234,20 @@ const ResponseTab: React.FunctionComponent<{
readResources();
}, [resource, model]);

const [showFormattedResponse, setShowFormattedResponse] = useSetting('trace-viewer-network-details-show-formatted-response', true);
const formatResult = useFormattedBody(responseBody, showFormattedResponse);

return <div className='vbox network-request-details-tab'>
{!resource.response.content._sha1 && <div>Response body is not available for this request.</div>}
{responseBody && responseBody.font && <FontPreview font={responseBody.font} />}
{responseBody && responseBody.dataUrl && <div><img draggable='false' src={responseBody.dataUrl} /></div>}
{responseBody && responseBody.text && <CodeMirrorWrapper text={responseBody.text} mimeType={responseBody.mimeType} readOnly lineNumbers={true}/>}
{responseBody && responseBody.text !== undefined && <div className='vbox network-response-body'>
<CodeMirrorWrapper text={formatResult.text} mimeType={responseBody.mimeType} readOnly lineNumbers={true}/>
<Toolbar noShadow={true} noMinHeight={true} className='network-response-toolbar'>
<div style={{ margin: 'auto' }}></div>
Comment on lines +246 to +247
<FormatToggleButton toggled={showFormattedResponse} error={formatResult.error} onToggle={() => setShowFormattedResponse(!showFormattedResponse)} />
</Toolbar>
</div>}
</div>;
};

Expand Down Expand Up @@ -288,32 +323,34 @@ function formatXml(xml: string, indent = ' ') {
return lines.join('\n');
}

function formatBody(body: string | null, contentType: string): string {
if (body === null)
return 'Loading...';
function formatBody(body: string, contentType?: string): string {
if (!body.trim() || !contentType)
return body;

const bodyStr = body;
if (bodyStr === '')
return '<Empty>';
if (isJsonMimeType(contentType))
return JSON.stringify(JSON.parse(body), null, 2);

if (isJsonMimeType(contentType)) {
try {
return JSON.stringify(JSON.parse(bodyStr), null, 2);
} catch (err) {
return bodyStr;
}
}

if (isXmlMimeType(contentType)) {
try {
return formatXml(bodyStr);
} catch {
return bodyStr;
}
}
if (isXmlMimeType(contentType))
return formatXml(body);

if (contentType.includes('application/x-www-form-urlencoded'))
return decodeURIComponent(bodyStr);
return decodeURIComponent(body);

return bodyStr;
return body;
}

const useFormattedBody = (body: RequestBody | ResponseBody, showFormatted: boolean) => {
return React.useMemo(() => {
if (body?.text === undefined)
return { text: '' };

if (!showFormatted)
return { text: body.text };

try {
return { text: formatBody(body.text, body.mimeType) };
} catch {
return { text: body.text, error: true };
}
}, [body, showFormatted]);
};
3 changes: 1 addition & 2 deletions packages/trace-viewer/src/ui/uiModeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -469,8 +469,7 @@ export const UIModeView: React.FC<{}> = ({
<div className='section-title'>Playwright</div>
<ToolbarButton icon='refresh' title='Reload' onClick={() => reloadTests()} disabled={isRunningTest || isLoading}></ToolbarButton>
<div style={{ position: 'relative' }}>
<ToolbarButton icon={'terminal'} title={'Toggle output — ' + (isMac ? '⌃`' : 'Ctrl + `')} toggled={isShowingOutput} onClick={() => { setIsShowingOutput(!isShowingOutput); }} />
{outputContainsError && <div title='Output contains error' style={{ position: 'absolute', top: 2, right: 2, width: 7, height: 7, borderRadius: '50%', backgroundColor: 'var(--vscode-notificationsErrorIcon-foreground)' }} />}
<ToolbarButton icon={'terminal'} title={'Toggle output — ' + (isMac ? '⌃`' : 'Ctrl + `')} toggled={isShowingOutput} errorBadge={outputContainsError ? 'Output contains error' : undefined} onClick={() => { setIsShowingOutput(!isShowingOutput); }} />
</div>
{!hasBrowsers && <ToolbarButton icon='lightbulb-autofix' style={{ color: 'var(--vscode-list-warningForeground)' }} title='Playwright browsers are missing' onClick={openInstallDialog} />}
</Toolbar>
Expand Down
6 changes: 1 addition & 5 deletions packages/web/src/components/toolbar.css
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
background-color: var(--vscode-sideBar-background);
}

.toolbar:after {
.toolbar:not(.no-shadow):after {
content: '';
display: block;
position: absolute;
Expand All @@ -41,10 +41,6 @@
z-index: 100;
}

.toolbar.no-shadow:after {
box-shadow: none;
}

.toolbar.no-min-height {
min-height: 0;
}
Expand Down
11 changes: 11 additions & 0 deletions packages/web/src/components/toolbarButton.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

.toolbar-button {
position: relative;
flex: none;
border: none;
outline: none;
Expand Down Expand Up @@ -43,6 +44,16 @@
color: var(--vscode-notificationLink-foreground);
}

.toolbar-button-error-badge {
position: absolute;
top: 2px;
right: 2px;
width: 7px;
height: 7px;
border-radius: 50%;
background-color: var(--vscode-notificationsErrorIcon-foreground);
}

.toolbar-separator {
flex: none;
background-color: var(--vscode-menu-separatorBackground);
Expand Down
5 changes: 5 additions & 0 deletions packages/web/src/components/toolbarButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export interface ToolbarButtonProps {
testId?: string,
className?: string,
ariaLabel?: string,
errorBadge?: string,
}

export const ToolbarButton = React.forwardRef<HTMLButtonElement, React.PropsWithChildren<ToolbarButtonProps>>(function ToolbarButton({
Expand All @@ -42,7 +43,9 @@ export const ToolbarButton = React.forwardRef<HTMLButtonElement, React.PropsWith
testId,
className,
ariaLabel,
errorBadge,
}, ref) {
const errorId = React.useId();
return <button
ref={ref}
className={clsx(className, 'toolbar-button', icon, toggled && 'toggled')}
Expand All @@ -54,9 +57,11 @@ export const ToolbarButton = React.forwardRef<HTMLButtonElement, React.PropsWith
style={style}
data-testid={testId}
aria-label={ariaLabel || title}
aria-describedby={errorBadge ? errorId : undefined}
>
{icon && <span className={`codicon codicon-${icon}`} style={children ? { marginRight: 5 } : {}}></span>}
{children}
{errorBadge && <span id={errorId} className='toolbar-button-error-badge' title={errorBadge} aria-label={errorBadge}></span>}
</button>;
});

Expand Down
Loading
Loading