From a0750b7854280f539d2b79773026b373eac0ae44 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Wed, 10 Jan 2024 15:28:33 -0800 Subject: [PATCH] chore: network panel polish (#28924) --- .../src/ui/networkResourceDetails.css | 1 - .../src/ui/networkResourceDetails.tsx | 6 +- packages/trace-viewer/src/ui/networkTab.css | 75 ++--- packages/trace-viewer/src/ui/networkTab.tsx | 256 +++++++++--------- packages/trace-viewer/src/ui/timeline.tsx | 5 +- packages/web/src/components/DEPS.list | 1 + packages/web/src/components/gridView.css | 69 +++++ packages/web/src/components/gridView.tsx | 107 ++++++++ packages/web/src/components/splitView.tsx | 3 +- packages/web/src/components/tabbedPane.css | 1 + packages/web/src/shared/glassPane.tsx | 8 +- packages/web/src/shared/imageDiffView.tsx | 31 +-- packages/web/src/shared/resizeView.tsx | 99 +++++++ tests/library/trace-viewer.spec.ts | 19 +- 14 files changed, 451 insertions(+), 230 deletions(-) create mode 100644 packages/web/src/components/gridView.css create mode 100644 packages/web/src/components/gridView.tsx create mode 100644 packages/web/src/shared/resizeView.tsx diff --git a/packages/trace-viewer/src/ui/networkResourceDetails.css b/packages/trace-viewer/src/ui/networkResourceDetails.css index 308fe4cb81..fce4e8b2c3 100644 --- a/packages/trace-viewer/src/ui/networkResourceDetails.css +++ b/packages/trace-viewer/src/ui/networkResourceDetails.css @@ -44,7 +44,6 @@ } .tab-network .toolbar { - margin-top: 3px !important; min-height: 30px !important; background-color: initial !important; border-bottom: 1px solid var(--vscode-panel-border); diff --git a/packages/trace-viewer/src/ui/networkResourceDetails.tsx b/packages/trace-viewer/src/ui/networkResourceDetails.tsx index ed42c32160..bfbeacd57b 100644 --- a/packages/trace-viewer/src/ui/networkResourceDetails.tsx +++ b/packages/trace-viewer/src/ui/networkResourceDetails.tsx @@ -30,11 +30,7 @@ export const NetworkResourceDetails: React.FunctionComponent<{ return , -
- ]} - rightToolbar={[]} + leftToolbar={[]} tabs={[ { id: 'request', diff --git a/packages/trace-viewer/src/ui/networkTab.css b/packages/trace-viewer/src/ui/networkTab.css index 809697915c..99c4d45d81 100644 --- a/packages/trace-viewer/src/ui/networkTab.css +++ b/packages/trace-viewer/src/ui/networkTab.css @@ -29,6 +29,14 @@ background-color: var(--vscode-statusBarItem-remoteBackground); } +.network-request-column { + overflow: hidden; + text-overflow: ellipsis; + flex: 0.5; + padding: 0 5px; + display: flex; +} + .network-request-start { flex: 0 0 65px; justify-content: right; @@ -36,7 +44,7 @@ } .network-request-status { - flex: 0 0 65px; + flex: 0 0 75px; } .network-request-method { @@ -54,21 +62,15 @@ flex: 1; } -.network-request-content-type, -.network-request-duration, -.network-request-route, -.network-request-size { - overflow: hidden; - text-overflow: ellipsis; - flex: 0.5; -} - -.network-request-route { - flex: 0.25; +.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-top: 3px; + margin: 3px 14px 0 5px; height: 30px; border-bottom: 1px solid var(--vscode-panel-border); flex: none; @@ -90,53 +92,18 @@ white-space: nowrap; height: 100%; overflow: hidden; + padding: 0 5px; + justify-content: space-between; } -.network-request-header.filter-start.positive .network-request-start .codicon-triangle-down { - display: initial !important; -} -.network-request-header.filter-start.negative .network-request-start .codicon-triangle-up { +.network-request-header > .filter-positive .codicon-triangle-down { display: initial !important; } -.network-request-header.filter-status.positive .network-request-status .codicon-triangle-down { - display: initial !important; -} -.network-request-header.filter-status.negative .network-request-status .codicon-triangle-up { +.network-request-header > .filter-negative .codicon-triangle-up { display: initial !important; } -.network-request-header.filter-method.positive .network-request-method .codicon-triangle-down { - display: initial !important; -} -.network-request-header.filter-method.negative .network-request-method .codicon-triangle-up { - display: initial !important; -} - -.network-request-header.filter-file.positive .network-request-file .codicon-triangle-down { - display: initial !important; -} -.network-request-header.filter-file.negative .network-request-file .codicon-triangle-up { - display: initial !important; -} - -.network-request-header.filter-content-type.positive .network-request-content-type .codicon-triangle-down { - display: initial !important; -} -.network-request-header.filter-content-type.negative .network-request-content-type .codicon-triangle-up { - display: initial !important; -} - -.network-request-header.filter-duration.positive .network-request-duration .codicon-triangle-down { - display: initial !important; -} -.network-request-header.filter-duration.negative .network-request-duration .codicon-triangle-up { - display: initial !important; -} - -.network-request-header.filter-size.positive .network-request-size .codicon-triangle-down { - display: initial !important; -} -.network-request-header.filter-size.negative .network-request-size .codicon-triangle-up { - display: initial !important; +.network-request-header .network-request-column { + border-right: 1px solid var(--vscode-panel-border); } diff --git a/packages/trace-viewer/src/ui/networkTab.tsx b/packages/trace-viewer/src/ui/networkTab.tsx index c2cd760f60..c2b27fb023 100644 --- a/packages/trace-viewer/src/ui/networkTab.tsx +++ b/packages/trace-viewer/src/ui/networkTab.tsx @@ -15,7 +15,6 @@ */ import type { Entry } from '@trace/har'; -import { ListView } from '@web/components/listView'; import * as React from 'react'; import type { Boundaries } from '../geometry'; import './networkTab.css'; @@ -23,15 +22,28 @@ 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 { SplitView } from '@web/components/splitView'; -const NetworkListView = ListView; - -type SortBy = 'start' | 'status' | 'method' | 'file' | 'duration' | 'size' | 'content-type'; -type Sorting = { by: SortBy, negate: boolean}; type NetworkTabModel = { resources: Entry[], }; +type RenderedEntry = { + name: { name: string, url: string }, + method: string, + status: { code: number, text: string, className: string }, + contentType: string, + duration: number, + size: number, + start: number, + route: string, + resource: Entry, +}; +type ColumnName = keyof RenderedEntry; +type Sorting = { by: ColumnName, negate: boolean}; +const NetworkGridView = GridView; + export function useNetworkTabModel(model: MultiTraceModel | undefined, selectedTime: Boundaries | undefined): NetworkTabModel { const resources = React.useMemo(() => { const resources = model?.resources || []; @@ -50,122 +62,110 @@ export const NetworkTab: React.FunctionComponent<{ networkModel: NetworkTabModel, onEntryHovered: (entry: Entry | undefined) => void, }> = ({ boundaries, networkModel, onEntryHovered }) => { - const [resource, setResource] = React.useState(); const [sorting, setSorting] = React.useState(undefined); + const [selectedEntry, setSelectedEntry] = React.useState(undefined); - React.useMemo(() => { + const { renderedEntries } = React.useMemo(() => { + const renderedEntries = networkModel.resources.map(entry => renderEntry(entry, boundaries)); if (sorting) - sort(networkModel.resources, sorting); - }, [networkModel.resources, sorting]); - - const toggleSorting = React.useCallback((f: SortBy) => { - setSorting({ by: f, negate: sorting?.by === f ? !sorting.negate : false }); - }, [sorting]); + sort(renderedEntries, sorting); + return { renderedEntries }; + }, [networkModel.resources, sorting, boundaries]); if (!networkModel.resources.length) return ; + const grid = setSelectedEntry(item)} + onHighlighted={item => onEntryHovered(item?.resource)} + columns={selectedEntry ? ['name'] : ['name', 'method', 'status', 'contentType', 'duration', 'size', 'start', 'route']} + columnTitle={columnTitle} + columnWidth={column => column === 'name' ? 200 : 100} + render={(item, column) => renderCell(item, column)} + sorting={sorting} + setSorting={setSorting} + />; return <> - {!resource &&
- - } - onSelected={setResource} - onHighlighted={onEntryHovered} - /> -
} - {resource && setResource(undefined)} />} + {!selectedEntry && grid} + {selectedEntry && + setSelectedEntry(undefined)} /> + {grid} + } ; }; -const NetworkHeader: React.FunctionComponent<{ - sorting: Sorting | undefined, - toggleSorting: (sortBy: SortBy) => void, -}> = ({ toggleSorting: toggleSortBy, sorting }) => { - return
-
toggleSortBy('start') }> - - -
-
toggleSortBy('status') }> -  Status - - -
-
toggleSortBy('method') }> - Method - - -
-
toggleSortBy('file') }> - Request - - -
-
toggleSortBy('content-type') }> - Content Type - - -
-
toggleSortBy('duration') }> - Duration - - -
-
toggleSortBy('size') }> - Size - - -
-
Route
-
; +const columnTitle = (column: ColumnName) => { + if (column === 'name') + return 'Name'; + if (column === 'method') + return 'Method'; + if (column === 'status') + return 'Status'; + if (column === 'contentType') + return 'Content Type'; + if (column === 'duration') + return 'Duration'; + if (column === 'size') + return 'Size'; + if (column === 'start') + return 'Start'; + if (column === 'route') + return 'Route'; + return ''; }; -const NetworkResource: React.FunctionComponent<{ - resource: Entry, - boundaries: Boundaries, -}> = ({ resource, boundaries }) => { - const { routeStatus, resourceName, contentType } = React.useMemo(() => { - const routeStatus = formatRouteStatus(resource); - let resourceName: string; - try { - const url = new URL(resource.request.url); - resourceName = url.pathname; - } catch { - resourceName = resource.request.url; - } - let contentType = resource.response.content.mimeType; - const charset = contentType.match(/^(.*);\s*charset=.*$/); - if (charset) - contentType = charset[1]; - return { routeStatus, resourceName, contentType }; - }, [resource]); - - return
-
-
{msToString(resource._monotonicTime! - boundaries.minimum)}
-
-
-
{resource.response.status}
-
-
-
{resource.request.method}
-
-
-
{resourceName}
-
-
{contentType}
-
{msToString(resource.time)}
-
{bytesToString(resource.response._transferSize! > 0 ? resource.response._transferSize! : resource.response.bodySize)}
-
- {routeStatus &&
{routeStatus}
} -
-
; +const renderCell = (entry: RenderedEntry, column: ColumnName) => { + if (column === 'name') + return {entry.name.name}; + if (column === 'method') + return {entry.method}; + if (column === 'status') + return {entry.status.code > 0 ? entry.status.code : ''}; + if (column === 'contentType') + return {entry.contentType}; + if (column === 'duration') + return {msToString(entry.duration)}; + if (column === 'size') + return {bytesToString(entry.size)}; + if (column === 'start') + return {msToString(entry.start)}; + if (column === 'route') + return entry.route && {entry.route}; }; -function formatStatus(status: number): string { +const renderEntry = (resource: Entry, boundaries: Boundaries): RenderedEntry => { + const routeStatus = formatRouteStatus(resource); + let resourceName: string; + try { + const url = new URL(resource.request.url); + resourceName = url.pathname.substring(url.pathname.lastIndexOf('/') + 1); + if (!resourceName) + resourceName = url.host; + } catch { + resourceName = resource.request.url; + } + let contentType = resource.response.content.mimeType; + const charset = contentType.match(/^(.*);\s*charset=.*$/); + if (charset) + contentType = charset[1]; + + 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) }, + contentType: contentType, + duration: resource.time, + size: resource.response._transferSize! > 0 ? resource.response._transferSize! : resource.response.bodySize, + start: resource._monotonicTime! - boundaries.minimum, + route: routeStatus, + resource + }; +}; + +function statusClassName(status: number): string { if (status >= 200 && status < 400) return 'status-success'; if (status >= 400) @@ -185,7 +185,7 @@ function formatRouteStatus(request: Entry): string { return ''; } -function sort(resources: Entry[], sorting: Sorting) { +function sort(resources: RenderedEntry[], sorting: Sorting) { const c = comparator(sorting?.by); if (c) resources.sort(c); @@ -193,45 +193,45 @@ function sort(resources: Entry[], sorting: Sorting) { resources.reverse(); } -function comparator(sortBy: SortBy) { +function comparator(sortBy: ColumnName) { if (sortBy === 'start') - return (a: Entry, b: Entry) => a._monotonicTime! - b._monotonicTime!; + return (a: RenderedEntry, b: RenderedEntry) => a.start - b.start; if (sortBy === 'duration') - return (a: Entry, b: Entry) => a.time - b.time; + return (a: RenderedEntry, b: RenderedEntry) => a.duration - b.duration; if (sortBy === 'status') - return (a: Entry, b: Entry) => a.response.status - b.response.status; + return (a: RenderedEntry, b: RenderedEntry) => a.status.code - b.status.code; if (sortBy === 'method') { - return (a: Entry, b: Entry) => { - const valueA = a.request.method; - const valueB = b.request.method; + return (a: RenderedEntry, b: RenderedEntry) => { + const valueA = a.method; + const valueB = b.method; return valueA.localeCompare(valueB); }; } if (sortBy === 'size') { - return (a: Entry, b: Entry) => { - const sizeA = a.response._transferSize! > 0 ? a.response._transferSize! : a.response.bodySize; - const sizeB = b.response._transferSize! > 0 ? b.response._transferSize! : b.response.bodySize; - return sizeA - sizeB; + return (a: RenderedEntry, b: RenderedEntry) => { + return a.size - b.size; }; } - if (sortBy === 'content-type') { - return (a: Entry, b: Entry) => { - const valueA = a.response.content.mimeType; - const valueB = b.response.content.mimeType; - return valueA.localeCompare(valueB); + if (sortBy === 'contentType') { + return (a: RenderedEntry, b: RenderedEntry) => { + return a.contentType.localeCompare(b.contentType); }; } - if (sortBy === 'file') { - return (a: Entry, b: Entry) => { - const nameA = a.request.url.substring(a.request.url.lastIndexOf('/')); - const nameB = b.request.url.substring(b.request.url.lastIndexOf('/')); - return nameA.localeCompare(nameB); + if (sortBy === 'name') { + return (a: RenderedEntry, b: RenderedEntry) => { + return a.name.name.localeCompare(b.name.name); + }; + } + + if (sortBy === 'route') { + return (a: RenderedEntry, b: RenderedEntry) => { + return a.route.localeCompare(b.route); }; } } diff --git a/packages/trace-viewer/src/ui/timeline.tsx b/packages/trace-viewer/src/ui/timeline.tsx index 69d45ba7ba..67f4e3b071 100644 --- a/packages/trace-viewer/src/ui/timeline.tsx +++ b/packages/trace-viewer/src/ui/timeline.tsx @@ -205,12 +205,11 @@ export const Timeline: React.FunctionComponent<{ }, [setSelectedTime]); return
- + onPaneDoubleClick={onPaneDoubleClick} />}
.filter-positive .codicon-triangle-down { + display: initial !important; +} + +.grid-view-header > .filter-negative .codicon-triangle-up { + display: initial !important; +} + +.grid-view-header-cell { + flex: none; + align-items: center; + overflow: hidden; + text-overflow: ellipsis; + padding-left: 10px; + cursor: pointer; + display: flex; + white-space: nowrap; +} + +.grid-view-header-cell-title { + overflow: hidden; + text-overflow: ellipsis; + flex: auto; +} diff --git a/packages/web/src/components/gridView.tsx b/packages/web/src/components/gridView.tsx new file mode 100644 index 0000000000..0e6e74ed5b --- /dev/null +++ b/packages/web/src/components/gridView.tsx @@ -0,0 +1,107 @@ +/* + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the 'License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import * as React from 'react'; +import { ListView } from './listView'; +import type { ListViewProps } from './listView'; +import './gridView.css'; +import { ResizeView } from '@web/shared/resizeView'; + +export type Sorting = { by: keyof T, negate: boolean }; + +export type GridViewProps = Omit, 'render'> & { + columns: (keyof T)[], + columnTitle: (column: keyof T) => string, + columnWidth: (column: keyof T) => number, + render: (item: T, column: keyof T, index: number) => React.ReactNode, + sorting?: Sorting, + setSorting?: (sorting: Sorting | undefined) => void, +}; + +export function GridView(model: GridViewProps) { + const initialOffsets: number[] = []; + for (let i = 0; i < model.columns.length - 1; ++i) { + const column = model.columns[i]; + initialOffsets[i] = (initialOffsets[i - 1] || 0) + model.columnWidth(column); + } + const [offsets, setOffsets] = React.useState(initialOffsets); + + const toggleSorting = React.useCallback((f: keyof T) => { + model.setSorting?.({ by: f, negate: model.sorting?.by === f ? !model.sorting.negate : false }); + }, [model]); + + return
+ + +
+
+ {model.columns.map((column, i) => { + return
model.setSorting && toggleSorting(column)} + > + {model.columnTitle(column)} + + +
; + })} +
+ { + return <> + {model.columns.map((column, i) => { + return
+ {model.render(item, column, index)} +
; + })} + ; + }} + icon={model.icon} + indent={model.indent} + isError={model.isError} + isWarning={model.isWarning} + selectedItem={model.selectedItem} + onAccepted={model.onAccepted} + onSelected={model.onSelected} + onLeftArrow={model.onLeftArrow} + onRightArrow={model.onRightArrow} + onHighlighted={model.onHighlighted} + onIconClicked={model.onIconClicked} + noItemsMessage={model.noItemsMessage} + dataTestId={model.dataTestId} + noHighlightOnHover={model.noHighlightOnHover} + >
+
+
; +} + +function sortingHeader(column: keyof T, sorting: Sorting | undefined) { + return column === sorting?.by ? ' filter-' + (sorting.negate ? 'negative' : 'positive') : ''; +} diff --git a/packages/web/src/components/splitView.tsx b/packages/web/src/components/splitView.tsx index 67f9f1c78c..a252996e73 100644 --- a/packages/web/src/components/splitView.tsx +++ b/packages/web/src/components/splitView.tsx @@ -25,12 +25,11 @@ export type SplitViewProps = { orientation?: 'vertical' | 'horizontal'; minSidebarSize?: number; settingName?: string; - children: JSX.Element | JSX.Element[] | string; }; const kMinSize = 50; -export const SplitView: React.FC = ({ +export const SplitView: React.FC> = ({ sidebarSize, sidebarHidden = false, sidebarIsFirst = false, diff --git a/packages/web/src/components/tabbedPane.css b/packages/web/src/components/tabbedPane.css index 3a8a694ef3..d3291e02bf 100644 --- a/packages/web/src/components/tabbedPane.css +++ b/packages/web/src/components/tabbedPane.css @@ -29,6 +29,7 @@ flex: auto; overflow: hidden; position: relative; + flex-direction: column; } .tabbed-pane-tab { diff --git a/packages/web/src/shared/glassPane.tsx b/packages/web/src/shared/glassPane.tsx index c9430826c2..77e7fd5a5f 100644 --- a/packages/web/src/shared/glassPane.tsx +++ b/packages/web/src/shared/glassPane.tsx @@ -17,16 +17,12 @@ import React from 'react'; export const GlassPane: React.FC<{ - enabled: boolean; cursor: string; onPaneMouseMove?: (e: MouseEvent) => void; onPaneMouseUp?: (e: MouseEvent) => void; onPaneDoubleClick?: (e: MouseEvent) => void; -}> = ({ enabled, cursor, onPaneMouseMove, onPaneMouseUp, onPaneDoubleClick }) => { +}> = ({ cursor, onPaneMouseMove, onPaneMouseUp, onPaneDoubleClick }) => { React.useEffect(() => { - if (!enabled) - return; - const glassPaneDiv = document.createElement('div'); glassPaneDiv.style.position = 'fixed'; glassPaneDiv.style.top = '0'; @@ -54,7 +50,7 @@ export const GlassPane: React.FC<{ document.body.removeEventListener('dblclick', onPaneDoubleClick); document.body.removeChild(glassPaneDiv); }; - }, [enabled, cursor, onPaneMouseMove, onPaneMouseUp, onPaneDoubleClick]); + }, [cursor, onPaneMouseMove, onPaneMouseUp, onPaneDoubleClick]); return <>; }; diff --git a/packages/web/src/shared/imageDiffView.tsx b/packages/web/src/shared/imageDiffView.tsx index 52e7afc2fe..f6721876f9 100644 --- a/packages/web/src/shared/imageDiffView.tsx +++ b/packages/web/src/shared/imageDiffView.tsx @@ -15,8 +15,8 @@ */ import * as React from 'react'; -import { GlassPane } from './glassPane'; import { useMeasure } from '../uiUtils'; +import { ResizeView } from './resizeView'; type TestAttachment = { name: string; @@ -141,25 +141,8 @@ export const ImageDiffSlider: React.FC<{ const [slider, setSlider] = React.useState(canvasWidth / 2); const sameSize = expectedImage.naturalWidth === actualImage.naturalWidth && expectedImage.naturalHeight === actualImage.naturalHeight; - const [resizing, setResizing] = React.useState<{ offset: number, slider: number } | null>(null); return
- setResizing(null)} - onPaneMouseMove={event => { - if (!event.buttons) { - setResizing(null); - } else if (resizing) { - const offset = event.clientX; - const delta = offset - resizing.offset; - const newSlider = resizing.slider + delta; - const slider = Math.min(Math.max(0, newSlider), canvasWidth); - setSlider(slider); - } - }} - />
{!sameSize && Expected } {expectedImage.naturalWidth} @@ -170,8 +153,13 @@ export const ImageDiffSlider: React.FC<{ {!sameSize && x} {!sameSize && {actualImage.naturalHeight}}
-
setResizing({ offset: event.clientX, slider: slider })}> +
+ setSlider(offsets[0])} + resizerColor={'#57606a80'} + resizerWidth={6}> Expected
-
- -
; }; diff --git a/packages/web/src/shared/resizeView.tsx b/packages/web/src/shared/resizeView.tsx new file mode 100644 index 0000000000..b915ce95d4 --- /dev/null +++ b/packages/web/src/shared/resizeView.tsx @@ -0,0 +1,99 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { GlassPane } from './glassPane'; +import { useMeasure } from '../uiUtils'; + +const fillStyle: React.CSSProperties = { + position: 'absolute', + top: 0, + right: 0, + bottom: 0, + left: 0, +}; + +export const ResizeView: React.FC<{ + orientation: 'horizontal' | 'vertical', + offsets: number[], + setOffsets: (offsets: number[]) => void, + resizerColor: string, + resizerWidth: number, + minColumnWidth?: number, +}> = ({ orientation, offsets, setOffsets, resizerColor, resizerWidth, minColumnWidth }) => { + const minGap = minColumnWidth || 0; + const [resizing, setResizing] = React.useState<{ clientX: number, clientY: number, offset: number, index: number } | null>(null); + const [measure, ref] = useMeasure(); + const dividerStyle: React.CSSProperties = { + position: 'absolute', + right: orientation === 'horizontal' ? undefined : 0, + bottom: orientation === 'horizontal' ? 0 : undefined, + width: orientation === 'horizontal' ? 7 : undefined, + height: orientation === 'horizontal' ? undefined : 7, + borderTopWidth: orientation === 'horizontal' ? undefined : (7 - resizerWidth) / 2, + borderRightWidth: orientation === 'horizontal' ? (7 - resizerWidth) / 2 : undefined, + borderBottomWidth: orientation === 'horizontal' ? undefined : (7 - resizerWidth) / 2, + borderLeftWidth: orientation === 'horizontal' ? (7 - resizerWidth) / 2 : undefined, + borderColor: 'transparent', + borderStyle: 'solid', + cursor: orientation === 'horizontal' ? 'ew-resize' : 'ns-resize', + }; + return
+ {!!resizing && setResizing(null)} + onPaneMouseMove={event => { + if (!event.buttons) { + setResizing(null); + } else if (resizing) { + const delta = orientation === 'horizontal' ? event.clientX - resizing.clientX : event.clientY - resizing.clientY; + const newOffset = resizing.offset + delta; + const previous = resizing.index > 0 ? offsets[resizing.index - 1] : 0; + const next = orientation === 'horizontal' ? measure.width : measure.height; + const constrainedDelta = Math.min(Math.max(previous + minGap, newOffset), next - minGap) - offsets[resizing.index]; + for (let i = resizing.index; i < offsets.length; ++i) + offsets[i] = offsets[i] + constrainedDelta; + setOffsets([...offsets]); + } + }} + />} + {offsets.map((offset, index) => { + return
setResizing({ clientX: event.clientX, clientY: event.clientY, offset, index })}> +
+
; + })} +
; +}; diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index 1d126060f4..6bfd299bee 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -241,9 +241,9 @@ test('should have network requests', async ({ showTraceViewer }) => { const traceViewer = await showTraceViewer([traceFile]); await traceViewer.selectAction('http://localhost'); await traceViewer.showNetworkTab(); - await expect(traceViewer.networkRequests).toContainText([/200GET\/frames\/frame.htmltext\/html/]); - await expect(traceViewer.networkRequests).toContainText([/200GET\/frames\/style.csstext\/css/]); - await expect(traceViewer.networkRequests).toContainText([/200GET\/frames\/script.jsapplication\/javascript/]); + await expect(traceViewer.networkRequests).toContainText([/frame.htmlGET200text\/html/]); + await expect(traceViewer.networkRequests).toContainText([/style.cssGET200text\/css/]); + await expect(traceViewer.networkRequests).toContainText([/script.jsGET200application\/javascript/]); }); test('should have network request overrides', async ({ page, server, runAndTrace }) => { @@ -253,8 +253,8 @@ test('should have network request overrides', async ({ page, server, runAndTrace }); await traceViewer.selectAction('http://localhost'); await traceViewer.showNetworkTab(); - await expect(traceViewer.networkRequests).toContainText([/200GET\/frames\/frame.htmltext\/html/]); - await expect(traceViewer.networkRequests).toContainText([/GET\/frames\/style.cssx-unknown.*aborted/]); + await expect(traceViewer.networkRequests).toContainText([/frame.htmlGET200text\/html/]); + await expect(traceViewer.networkRequests).toContainText([/style.cssGETx-unknown.*aborted/]); await expect(traceViewer.networkRequests).not.toContainText([/continued/]); }); @@ -265,8 +265,8 @@ test('should have network request overrides 2', async ({ page, server, runAndTra }); await traceViewer.selectAction('http://localhost'); await traceViewer.showNetworkTab(); - await expect.soft(traceViewer.networkRequests).toContainText([/200GET\/frames\/frame.htmltext\/html.*/]); - await expect.soft(traceViewer.networkRequests).toContainText([/200GET\/frames\/script.jsapplication\/javascript.*continued/]); + await expect.soft(traceViewer.networkRequests).toContainText([/frame.htmlGET200text\/html.*/]); + await expect.soft(traceViewer.networkRequests).toContainText([/script.jsGET200application\/javascript.*continued/]); }); test('should show snapshot URL', async ({ page, runAndTrace, server }) => { @@ -937,7 +937,10 @@ test('should open trace-1.37', async ({ showTraceViewer }) => { await expect(traceViewer.consoleLineMessages).toHaveText(['hello {foo: bar}']); await traceViewer.showNetworkTab(); - await expect(traceViewer.networkRequests).toContainText([/200GET\/index.htmltext\/html/, /200GET\/style.cssx-unknown/]); + await expect(traceViewer.networkRequests).toContainText([ + /index.htmlGET200text\/html/, + /style.cssGET200x-unknown/ + ]); }); test('should prefer later resource request with the same method', async ({ page, server, runAndTrace }) => {