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.
*/
.network-request-status .status-failure {
color: var(--vscode-statusBar-foreground);
background-color: var(--vscode-statusBarItem-errorBackground);
}
.network-request-status .status-route {
.network-request-status-route {
color: var(--vscode-statusBar-foreground);
background-color: var(--vscode-statusBar-background);
}
.network-request-status .status-route.api {
.network-request-status-route.api {
color: var(--vscode-statusBar-foreground);
background-color: var(--vscode-statusBarItem-remoteBackground);
}
.network-request-column {
overflow: hidden;
text-overflow: ellipsis;
flex: 0.5;
padding: 0 5px;
display: flex;
.network-grid-view .grid-view-column-method,
.network-grid-view .grid-view-column-status {
text-align: center;
}
.network-request-start {
flex: 0 0 65px;
justify-content: right;
padding-right: 10px;
}
.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);
.network-grid-view .grid-view-column-duration,
.network-grid-view .grid-view-column-size,
.network-grid-view .grid-view-column-start {
text-align: end;
}

View File

@ -22,7 +22,7 @@ import { NetworkResourceDetails } from './networkResourceDetails';
import { bytesToString, msToString } from '@web/uiUtils';
import { PlaceholderPanel } from './placeholderPanel';
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';
type NetworkTabModel = {
@ -32,7 +32,7 @@ type NetworkTabModel = {
type RenderedEntry = {
name: { name: string, url: string },
method: string,
status: { code: number, text: string, className: string },
status: { code: number, text: string },
contentType: string,
duration: number,
size: number,
@ -83,7 +83,9 @@ export const NetworkTab: React.FunctionComponent<{
onHighlighted={item => onEntryHovered(item?.resource)}
columns={selectedEntry ? ['name'] : ['name', 'method', 'status', 'contentType', 'duration', 'size', 'start', 'route']}
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)}
sorting={sorting}
setSorting={setSorting}
@ -117,23 +119,44 @@ const columnTitle = (column: ColumnName) => {
return '';
};
const renderCell = (entry: RenderedEntry, column: ColumnName) => {
const columnWidth = (column: ColumnName) => {
if (column === 'name')
return <span title={entry.name.url}>{entry.name.name}</span>;
return 200;
if (column === 'method')
return <span>{entry.method}</span>;
return 60;
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')
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')
return <span>{msToString(entry.duration)}</span>;
return { body: msToString(entry.duration) };
if (column === 'size')
return <span>{bytesToString(entry.size)}</span>;
return { body: bytesToString(entry.size) };
if (column === 'start')
return <span>{msToString(entry.start)}</span>;
return { body: msToString(entry.start) };
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 => {
@ -155,7 +178,7 @@ const renderEntry = (resource: Entry, boundaries: Boundaries): RenderedEntry =>
return {
name: { name: resourceName, url: resource.request.url },
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,
duration: resource.time,
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 {
if (request._wasAborted)
return 'aborted';

View File

@ -20,6 +20,10 @@
flex: auto;
}
.grid-view .list-view-entry {
padding-left: 0;
}
.grid-view-cell {
overflow: hidden;
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 RenderedGridCell = {
body: React.ReactNode;
title?: string;
};
export type GridViewProps<T> = Omit<ListViewProps<T>, 'render'> & {
columns: (keyof T)[],
columnTitle: (column: keyof T) => string,
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>,
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]);
return <div className='grid-view'>
return <div className={`grid-view ${model.name}-grid-view`}>
<ResizeView
orientation={'horizontal'}
offsets={offsets}
@ -75,10 +80,12 @@ export function GridView<T>(model: GridViewProps<T>) {
render={(item, index) => {
return <>
{model.columns.map((column, i) => {
const { body, title } = model.render(item, column, index);
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) }}>
{model.render(item, column, index)}
{body}
</div>;
})}
</>;
@ -87,6 +94,7 @@ export function GridView<T>(model: GridViewProps<T>) {
indent={model.indent}
isError={model.isError}
isWarning={model.isWarning}
isInfo={model.isInfo}
selectedItem={model.selectedItem}
onAccepted={model.onAccepted}
onSelected={model.onSelected}

View File

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

View File

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

View File

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

View File

@ -243,7 +243,9 @@ test('should have network requests', async ({ showTraceViewer }) => {
await traceViewer.showNetworkTab();
await expect(traceViewer.networkRequests).toContainText([/frame.htmlGET200text\/html/]);
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.filter({ hasText: '404' })).toHaveCSS('background-color', 'rgb(242, 222, 222)');
});
test('should have network request overrides', async ({ page, server, runAndTrace }) => {