chore: polish network panel highlight (#29299)

Fixes https://github.com/microsoft/playwright/issues/29287
This commit is contained in:
Pavel Feldman 2024-02-01 13:44:26 -08:00 committed by GitHub
parent 4784139bb0
commit 020a39860d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 77 additions and 109 deletions

View file

@ -14,96 +14,23 @@
limitations under the License. limitations under the License.
*/ */
.network-request-status .status-failure { .network-request-status-route {
color: var(--vscode-statusBar-foreground);
background-color: var(--vscode-statusBarItem-errorBackground);
}
.network-request-status .status-route {
color: var(--vscode-statusBar-foreground); color: var(--vscode-statusBar-foreground);
background-color: var(--vscode-statusBar-background); background-color: var(--vscode-statusBar-background);
} }
.network-request-status .status-route.api { .network-request-status-route.api {
color: var(--vscode-statusBar-foreground); color: var(--vscode-statusBar-foreground);
background-color: var(--vscode-statusBarItem-remoteBackground); background-color: var(--vscode-statusBarItem-remoteBackground);
} }
.network-request-column { .network-grid-view .grid-view-column-method,
overflow: hidden; .network-grid-view .grid-view-column-status {
text-overflow: ellipsis; text-align: center;
flex: 0.5;
padding: 0 5px;
display: flex;
} }
.network-request-start { .network-grid-view .grid-view-column-duration,
flex: 0 0 65px; .network-grid-view .grid-view-column-size,
justify-content: right; .network-grid-view .grid-view-column-start {
padding-right: 10px; text-align: end;
}
.network-request-status {
flex: 0 0 75px;
}
.network-request-method {
flex: 0 0 65px;
}
.network-request-file {
display: flex;
flex: 1;
}
.network-request-file-url {
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
.network-request-body .network-request-start,
.network-request-body .network-request-status,
.network-request-body .network-request-duration,
.network-request-body .network-request-size {
justify-content: end;
}
.network-request-header {
margin: 3px 14px 0 5px;
height: 30px;
border-bottom: 1px solid var(--vscode-panel-border);
flex: none;
align-items: center;
cursor: pointer;
user-select: none;
}
.network-request-header .codicon-triangle-up {
display: none;
}
.network-request-header .codicon-triangle-down {
display: none;
}
.network-request-header > div {
display: flex;
align-items: center;
white-space: nowrap;
height: 100%;
overflow: hidden;
padding: 0 5px;
justify-content: space-between;
}
.network-request-header > .filter-positive .codicon-triangle-down {
display: initial !important;
}
.network-request-header > .filter-negative .codicon-triangle-up {
display: initial !important;
}
.network-request-header .network-request-column {
border-right: 1px solid var(--vscode-panel-border);
} }

View file

@ -22,7 +22,7 @@ import { NetworkResourceDetails } from './networkResourceDetails';
import { bytesToString, msToString } from '@web/uiUtils'; import { bytesToString, msToString } from '@web/uiUtils';
import { PlaceholderPanel } from './placeholderPanel'; import { PlaceholderPanel } from './placeholderPanel';
import type { MultiTraceModel } from './modelUtil'; import type { MultiTraceModel } from './modelUtil';
import { GridView } from '@web/components/gridView'; import { GridView, type RenderedGridCell } from '@web/components/gridView';
import { SplitView } from '@web/components/splitView'; import { SplitView } from '@web/components/splitView';
type NetworkTabModel = { type NetworkTabModel = {
@ -32,7 +32,7 @@ type NetworkTabModel = {
type RenderedEntry = { type RenderedEntry = {
name: { name: string, url: string }, name: { name: string, url: string },
method: string, method: string,
status: { code: number, text: string, className: string }, status: { code: number, text: string },
contentType: string, contentType: string,
duration: number, duration: number,
size: number, size: number,
@ -83,7 +83,9 @@ export const NetworkTab: React.FunctionComponent<{
onHighlighted={item => onEntryHovered(item?.resource)} onHighlighted={item => onEntryHovered(item?.resource)}
columns={selectedEntry ? ['name'] : ['name', 'method', 'status', 'contentType', 'duration', 'size', 'start', 'route']} columns={selectedEntry ? ['name'] : ['name', 'method', 'status', 'contentType', 'duration', 'size', 'start', 'route']}
columnTitle={columnTitle} columnTitle={columnTitle}
columnWidth={column => column === 'name' ? 200 : 100} columnWidth={columnWidth}
isError={item => item.status.code >= 400}
isInfo={item => !!item.route}
render={(item, column) => renderCell(item, column)} render={(item, column) => renderCell(item, column)}
sorting={sorting} sorting={sorting}
setSorting={setSorting} setSorting={setSorting}
@ -117,23 +119,44 @@ const columnTitle = (column: ColumnName) => {
return ''; return '';
}; };
const renderCell = (entry: RenderedEntry, column: ColumnName) => { const columnWidth = (column: ColumnName) => {
if (column === 'name') if (column === 'name')
return <span title={entry.name.url}>{entry.name.name}</span>; return 200;
if (column === 'method') if (column === 'method')
return <span>{entry.method}</span>; return 60;
if (column === 'status') if (column === 'status')
return <span className={entry.status.className} title={entry.status.text}>{entry.status.code > 0 ? entry.status.code : ''}</span>; return 60;
if (column === 'contentType') if (column === 'contentType')
return <span>{entry.contentType}</span>; return 200;
return 100;
};
const renderCell = (entry: RenderedEntry, column: ColumnName): RenderedGridCell => {
if (column === 'name') {
return {
body: entry.name.name,
title: entry.name.url,
};
}
if (column === 'method')
return { body: entry.method };
if (column === 'status') {
return {
body: entry.status.code > 0 ? entry.status.code : '',
title: entry.status.text
};
}
if (column === 'contentType')
return { body: entry.contentType };
if (column === 'duration') if (column === 'duration')
return <span>{msToString(entry.duration)}</span>; return { body: msToString(entry.duration) };
if (column === 'size') if (column === 'size')
return <span>{bytesToString(entry.size)}</span>; return { body: bytesToString(entry.size) };
if (column === 'start') if (column === 'start')
return <span>{msToString(entry.start)}</span>; return { body: msToString(entry.start) };
if (column === 'route') if (column === 'route')
return entry.route && <span className={`status-route ${entry.route}`}>{entry.route}</span>; return { body: entry.route };
return { body: '' };
}; };
const renderEntry = (resource: Entry, boundaries: Boundaries): RenderedEntry => { const renderEntry = (resource: Entry, boundaries: Boundaries): RenderedEntry => {
@ -155,7 +178,7 @@ const renderEntry = (resource: Entry, boundaries: Boundaries): RenderedEntry =>
return { return {
name: { name: resourceName, url: resource.request.url }, name: { name: resourceName, url: resource.request.url },
method: resource.request.method, method: resource.request.method,
status: { code: resource.response.status, text: resource.response.statusText, className: statusClassName(resource.response.status) }, status: { code: resource.response.status, text: resource.response.statusText },
contentType: contentType, contentType: contentType,
duration: resource.time, duration: resource.time,
size: resource.response._transferSize! > 0 ? resource.response._transferSize! : resource.response.bodySize, size: resource.response._transferSize! > 0 ? resource.response._transferSize! : resource.response.bodySize,
@ -165,14 +188,6 @@ const renderEntry = (resource: Entry, boundaries: Boundaries): RenderedEntry =>
}; };
}; };
function statusClassName(status: number): string {
if (status >= 200 && status < 400)
return 'status-success';
if (status >= 400)
return 'status-failure';
return '';
}
function formatRouteStatus(request: Entry): string { function formatRouteStatus(request: Entry): string {
if (request._wasAborted) if (request._wasAborted)
return 'aborted'; return 'aborted';

View file

@ -20,6 +20,10 @@
flex: auto; flex: auto;
} }
.grid-view .list-view-entry {
padding-left: 0;
}
.grid-view-cell { .grid-view-cell {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;

View file

@ -22,11 +22,16 @@ import { ResizeView } from '@web/shared/resizeView';
export type Sorting<T> = { by: keyof T, negate: boolean }; export type Sorting<T> = { by: keyof T, negate: boolean };
export type RenderedGridCell = {
body: React.ReactNode;
title?: string;
};
export type GridViewProps<T> = Omit<ListViewProps<T>, 'render'> & { export type GridViewProps<T> = Omit<ListViewProps<T>, 'render'> & {
columns: (keyof T)[], columns: (keyof T)[],
columnTitle: (column: keyof T) => string, columnTitle: (column: keyof T) => string,
columnWidth: (column: keyof T) => number, columnWidth: (column: keyof T) => number,
render: (item: T, column: keyof T, index: number) => React.ReactNode, render: (item: T, column: keyof T, index: number) => RenderedGridCell,
sorting?: Sorting<T>, sorting?: Sorting<T>,
setSorting?: (sorting: Sorting<T> | undefined) => void, setSorting?: (sorting: Sorting<T> | undefined) => void,
}; };
@ -43,7 +48,7 @@ export function GridView<T>(model: GridViewProps<T>) {
model.setSorting?.({ by: f, negate: model.sorting?.by === f ? !model.sorting.negate : false }); model.setSorting?.({ by: f, negate: model.sorting?.by === f ? !model.sorting.negate : false });
}, [model]); }, [model]);
return <div className='grid-view'> return <div className={`grid-view ${model.name}-grid-view`}>
<ResizeView <ResizeView
orientation={'horizontal'} orientation={'horizontal'}
offsets={offsets} offsets={offsets}
@ -75,10 +80,12 @@ export function GridView<T>(model: GridViewProps<T>) {
render={(item, index) => { render={(item, index) => {
return <> return <>
{model.columns.map((column, i) => { {model.columns.map((column, i) => {
const { body, title } = model.render(item, column, index);
return <div return <div
className='grid-view-cell' className={`grid-view-cell grid-view-column-${String(column)}`}
title={title}
style={{ width: offsets[i] - (offsets[i - 1] || 0) }}> style={{ width: offsets[i] - (offsets[i - 1] || 0) }}>
{model.render(item, column, index)} {body}
</div>; </div>;
})} })}
</>; </>;
@ -87,6 +94,7 @@ export function GridView<T>(model: GridViewProps<T>) {
indent={model.indent} indent={model.indent}
isError={model.isError} isError={model.isError}
isWarning={model.isWarning} isWarning={model.isWarning}
isInfo={model.isInfo}
selectedItem={model.selectedItem} selectedItem={model.selectedItem}
onAccepted={model.onAccepted} onAccepted={model.onAccepted}
onSelected={model.onSelected} onSelected={model.onSelected}

View file

@ -74,8 +74,14 @@
.list-view-entry.error { .list-view-entry.error {
color: var(--vscode-list-errorForeground); color: var(--vscode-list-errorForeground);
background-color: var(--vscode-inputValidation-errorBackground);
} }
.list-view-entry.warning { .list-view-entry.warning {
color: var(--vscode-list-warningForeground); color: var(--vscode-list-warningForeground);
background-color: var(--vscode-inputValidation-warningBackground);
}
.list-view-entry.info {
background-color: var(--vscode-inputValidation-infoBackground);
} }

View file

@ -26,6 +26,7 @@ export type ListViewProps<T> = {
indent?: (item: T, index: number) => number | undefined, indent?: (item: T, index: number) => number | undefined,
isError?: (item: T, index: number) => boolean, isError?: (item: T, index: number) => boolean,
isWarning?: (item: T, index: number) => boolean, isWarning?: (item: T, index: number) => boolean,
isInfo?: (item: T, index: number) => boolean,
selectedItem?: T, selectedItem?: T,
onAccepted?: (item: T, index: number) => void, onAccepted?: (item: T, index: number) => void,
onSelected?: (item: T, index: number) => void, onSelected?: (item: T, index: number) => void,
@ -48,6 +49,7 @@ export function ListView<T>({
icon, icon,
isError, isError,
isWarning, isWarning,
isInfo,
indent, indent,
selectedItem, selectedItem,
onAccepted, onAccepted,
@ -136,12 +138,13 @@ export function ListView<T>({
const highlightedSuffix = !noHighlightOnHover && highlightedItem === item ? ' highlighted' : ''; const highlightedSuffix = !noHighlightOnHover && highlightedItem === item ? ' highlighted' : '';
const errorSuffix = isError?.(item, index) ? ' error' : ''; const errorSuffix = isError?.(item, index) ? ' error' : '';
const warningSuffix = isWarning?.(item, index) ? ' warning' : ''; const warningSuffix = isWarning?.(item, index) ? ' warning' : '';
const infoSuffix = isInfo?.(item, index) ? ' info' : '';
const indentation = indent?.(item, index) || 0; const indentation = indent?.(item, index) || 0;
const rendered = render(item, index); const rendered = render(item, index);
return <div return <div
key={id?.(item, index) || index} key={id?.(item, index) || index}
role='listitem' role='listitem'
className={'list-view-entry' + selectedSuffix + highlightedSuffix + errorSuffix + warningSuffix} className={'list-view-entry' + selectedSuffix + highlightedSuffix + errorSuffix + warningSuffix + infoSuffix}
onClick={() => onSelected?.(item, index)} onClick={() => onSelected?.(item, index)}
onMouseEnter={() => setHighlightedItem(item)} onMouseEnter={() => setHighlightedItem(item)}
onMouseLeave={() => setHighlightedItem(undefined)} onMouseLeave={() => setHighlightedItem(undefined)}

View file

@ -57,7 +57,7 @@ export const ResizeView: React.FC<{
top: 0, top: 0,
right: 0, right: 0,
bottom: 0, bottom: 0,
left: 0, left: -(7 - resizerWidth) / 2,
zIndex: 1000, zIndex: 1000,
pointerEvents: 'none', pointerEvents: 'none',
}} }}

View file

@ -1,6 +1,9 @@
<!DOCTYPE html> <!DOCTYPE html>
<link rel='stylesheet' href='./style.css'> <link rel='stylesheet' href='./style.css'>
<script src='./script.js' type='text/javascript'></script> <script src='./script.js' type='text/javascript'></script>
<script>
fetch('./404');
</script>
<style> <style>
body { body {
height: 100px; height: 100px;

View file

@ -243,7 +243,9 @@ test('should have network requests', async ({ showTraceViewer }) => {
await traceViewer.showNetworkTab(); await traceViewer.showNetworkTab();
await expect(traceViewer.networkRequests).toContainText([/frame.htmlGET200text\/html/]); await expect(traceViewer.networkRequests).toContainText([/frame.htmlGET200text\/html/]);
await expect(traceViewer.networkRequests).toContainText([/style.cssGET200text\/css/]); await expect(traceViewer.networkRequests).toContainText([/style.cssGET200text\/css/]);
await expect(traceViewer.networkRequests).toContainText([/404GET404text\/plain/]);
await expect(traceViewer.networkRequests).toContainText([/script.jsGET200application\/javascript/]); await expect(traceViewer.networkRequests).toContainText([/script.jsGET200application\/javascript/]);
await expect(traceViewer.networkRequests.filter({ hasText: '404' })).toHaveCSS('background-color', 'rgb(242, 222, 222)');
}); });
test('should have network request overrides', async ({ page, server, runAndTrace }) => { test('should have network request overrides', async ({ page, server, runAndTrace }) => {