chore: network panel polish (#28924)
This commit is contained in:
parent
3851d9b897
commit
a0750b7854
|
|
@ -44,7 +44,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-network .toolbar {
|
.tab-network .toolbar {
|
||||||
margin-top: 3px !important;
|
|
||||||
min-height: 30px !important;
|
min-height: 30px !important;
|
||||||
background-color: initial !important;
|
background-color: initial !important;
|
||||||
border-bottom: 1px solid var(--vscode-panel-border);
|
border-bottom: 1px solid var(--vscode-panel-border);
|
||||||
|
|
|
||||||
|
|
@ -30,11 +30,7 @@ export const NetworkResourceDetails: React.FunctionComponent<{
|
||||||
|
|
||||||
return <TabbedPane
|
return <TabbedPane
|
||||||
dataTestId='network-request-details'
|
dataTestId='network-request-details'
|
||||||
leftToolbar={[
|
leftToolbar={[<ToolbarButton icon='close' title='Close' onClick={onClose}></ToolbarButton>]}
|
||||||
<ToolbarButton icon='arrow-left' title='Back' onClick={onClose}></ToolbarButton>,
|
|
||||||
<div style={{ width: 30 }}></div>
|
|
||||||
]}
|
|
||||||
rightToolbar={[<ToolbarButton icon='close' title='Close' onClick={onClose}></ToolbarButton>]}
|
|
||||||
tabs={[
|
tabs={[
|
||||||
{
|
{
|
||||||
id: 'request',
|
id: 'request',
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,14 @@
|
||||||
background-color: var(--vscode-statusBarItem-remoteBackground);
|
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 {
|
.network-request-start {
|
||||||
flex: 0 0 65px;
|
flex: 0 0 65px;
|
||||||
justify-content: right;
|
justify-content: right;
|
||||||
|
|
@ -36,7 +44,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-request-status {
|
.network-request-status {
|
||||||
flex: 0 0 65px;
|
flex: 0 0 75px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-request-method {
|
.network-request-method {
|
||||||
|
|
@ -54,21 +62,15 @@
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-request-content-type,
|
.network-request-body .network-request-start,
|
||||||
.network-request-duration,
|
.network-request-body .network-request-status,
|
||||||
.network-request-route,
|
.network-request-body .network-request-duration,
|
||||||
.network-request-size {
|
.network-request-body .network-request-size {
|
||||||
overflow: hidden;
|
justify-content: end;
|
||||||
text-overflow: ellipsis;
|
|
||||||
flex: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.network-request-route {
|
|
||||||
flex: 0.25;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-request-header {
|
.network-request-header {
|
||||||
margin-top: 3px;
|
margin: 3px 14px 0 5px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
border-bottom: 1px solid var(--vscode-panel-border);
|
border-bottom: 1px solid var(--vscode-panel-border);
|
||||||
flex: none;
|
flex: none;
|
||||||
|
|
@ -90,53 +92,18 @@
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
padding: 0 5px;
|
||||||
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-request-header.filter-start.positive .network-request-start .codicon-triangle-down {
|
.network-request-header > .filter-positive .codicon-triangle-down {
|
||||||
display: initial !important;
|
|
||||||
}
|
|
||||||
.network-request-header.filter-start.negative .network-request-start .codicon-triangle-up {
|
|
||||||
display: initial !important;
|
display: initial !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-request-header.filter-status.positive .network-request-status .codicon-triangle-down {
|
.network-request-header > .filter-negative .codicon-triangle-up {
|
||||||
display: initial !important;
|
|
||||||
}
|
|
||||||
.network-request-header.filter-status.negative .network-request-status .codicon-triangle-up {
|
|
||||||
display: initial !important;
|
display: initial !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-request-header.filter-method.positive .network-request-method .codicon-triangle-down {
|
.network-request-header .network-request-column {
|
||||||
display: initial !important;
|
border-right: 1px solid var(--vscode-panel-border);
|
||||||
}
|
|
||||||
.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;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Entry } from '@trace/har';
|
import type { Entry } from '@trace/har';
|
||||||
import { ListView } from '@web/components/listView';
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import type { Boundaries } from '../geometry';
|
import type { Boundaries } from '../geometry';
|
||||||
import './networkTab.css';
|
import './networkTab.css';
|
||||||
|
|
@ -23,15 +22,28 @@ 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 { SplitView } from '@web/components/splitView';
|
||||||
|
|
||||||
const NetworkListView = ListView<Entry>;
|
|
||||||
|
|
||||||
type SortBy = 'start' | 'status' | 'method' | 'file' | 'duration' | 'size' | 'content-type';
|
|
||||||
type Sorting = { by: SortBy, negate: boolean};
|
|
||||||
type NetworkTabModel = {
|
type NetworkTabModel = {
|
||||||
resources: Entry[],
|
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<RenderedEntry>;
|
||||||
|
|
||||||
export function useNetworkTabModel(model: MultiTraceModel | undefined, selectedTime: Boundaries | undefined): NetworkTabModel {
|
export function useNetworkTabModel(model: MultiTraceModel | undefined, selectedTime: Boundaries | undefined): NetworkTabModel {
|
||||||
const resources = React.useMemo(() => {
|
const resources = React.useMemo(() => {
|
||||||
const resources = model?.resources || [];
|
const resources = model?.resources || [];
|
||||||
|
|
@ -50,122 +62,110 @@ export const NetworkTab: React.FunctionComponent<{
|
||||||
networkModel: NetworkTabModel,
|
networkModel: NetworkTabModel,
|
||||||
onEntryHovered: (entry: Entry | undefined) => void,
|
onEntryHovered: (entry: Entry | undefined) => void,
|
||||||
}> = ({ boundaries, networkModel, onEntryHovered }) => {
|
}> = ({ boundaries, networkModel, onEntryHovered }) => {
|
||||||
const [resource, setResource] = React.useState<Entry | undefined>();
|
|
||||||
const [sorting, setSorting] = React.useState<Sorting | undefined>(undefined);
|
const [sorting, setSorting] = React.useState<Sorting | undefined>(undefined);
|
||||||
|
const [selectedEntry, setSelectedEntry] = React.useState<RenderedEntry | undefined>(undefined);
|
||||||
|
|
||||||
React.useMemo(() => {
|
const { renderedEntries } = React.useMemo(() => {
|
||||||
|
const renderedEntries = networkModel.resources.map(entry => renderEntry(entry, boundaries));
|
||||||
if (sorting)
|
if (sorting)
|
||||||
sort(networkModel.resources, sorting);
|
sort(renderedEntries, sorting);
|
||||||
}, [networkModel.resources, sorting]);
|
return { renderedEntries };
|
||||||
|
}, [networkModel.resources, sorting, boundaries]);
|
||||||
const toggleSorting = React.useCallback((f: SortBy) => {
|
|
||||||
setSorting({ by: f, negate: sorting?.by === f ? !sorting.negate : false });
|
|
||||||
}, [sorting]);
|
|
||||||
|
|
||||||
if (!networkModel.resources.length)
|
if (!networkModel.resources.length)
|
||||||
return <PlaceholderPanel text='No network calls' />;
|
return <PlaceholderPanel text='No network calls' />;
|
||||||
|
|
||||||
|
const grid = <NetworkGridView
|
||||||
|
name='network'
|
||||||
|
items={renderedEntries}
|
||||||
|
selectedItem={selectedEntry}
|
||||||
|
onSelected={item => 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 <>
|
return <>
|
||||||
{!resource && <div className='vbox'>
|
{!selectedEntry && grid}
|
||||||
<NetworkHeader sorting={sorting} toggleSorting={toggleSorting} />
|
{selectedEntry && <SplitView sidebarSize={200} sidebarIsFirst={true} orientation='horizontal'>
|
||||||
<NetworkListView
|
<NetworkResourceDetails resource={selectedEntry.resource} onClose={() => setSelectedEntry(undefined)} />
|
||||||
name='network'
|
{grid}
|
||||||
items={networkModel.resources}
|
</SplitView>}
|
||||||
render={entry => <NetworkResource boundaries={boundaries} resource={entry}></NetworkResource>}
|
|
||||||
onSelected={setResource}
|
|
||||||
onHighlighted={onEntryHovered}
|
|
||||||
/>
|
|
||||||
</div>}
|
|
||||||
{resource && <NetworkResourceDetails resource={resource} onClose={() => setResource(undefined)} />}
|
|
||||||
</>;
|
</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const NetworkHeader: React.FunctionComponent<{
|
const columnTitle = (column: ColumnName) => {
|
||||||
sorting: Sorting | undefined,
|
if (column === 'name')
|
||||||
toggleSorting: (sortBy: SortBy) => void,
|
return 'Name';
|
||||||
}> = ({ toggleSorting: toggleSortBy, sorting }) => {
|
if (column === 'method')
|
||||||
return <div className={'hbox network-request-header' + (sorting ? ' filter-' + sorting.by + (sorting.negate ? ' negative' : ' positive') : '')}>
|
return 'Method';
|
||||||
<div className='network-request-start' onClick={() => toggleSortBy('start') }>
|
if (column === 'status')
|
||||||
<span className='codicon codicon-triangle-up' />
|
return 'Status';
|
||||||
<span className='codicon codicon-triangle-down' />
|
if (column === 'contentType')
|
||||||
</div>
|
return 'Content Type';
|
||||||
<div className='network-request-status' onClick={() => toggleSortBy('status') }>
|
if (column === 'duration')
|
||||||
Status
|
return 'Duration';
|
||||||
<span className='codicon codicon-triangle-up' />
|
if (column === 'size')
|
||||||
<span className='codicon codicon-triangle-down' />
|
return 'Size';
|
||||||
</div>
|
if (column === 'start')
|
||||||
<div className='network-request-method' onClick={() => toggleSortBy('method') }>
|
return 'Start';
|
||||||
Method
|
if (column === 'route')
|
||||||
<span className='codicon codicon-triangle-up' />
|
return 'Route';
|
||||||
<span className='codicon codicon-triangle-down' />
|
return '';
|
||||||
</div>
|
|
||||||
<div className='network-request-file' onClick={() => toggleSortBy('file') }>
|
|
||||||
Request
|
|
||||||
<span className='codicon codicon-triangle-up' />
|
|
||||||
<span className='codicon codicon-triangle-down' />
|
|
||||||
</div>
|
|
||||||
<div className='network-request-content-type' onClick={() => toggleSortBy('content-type') }>
|
|
||||||
Content Type
|
|
||||||
<span className='codicon codicon-triangle-up' />
|
|
||||||
<span className='codicon codicon-triangle-down' />
|
|
||||||
</div>
|
|
||||||
<div className='network-request-duration' onClick={() => toggleSortBy('duration') }>
|
|
||||||
Duration
|
|
||||||
<span className='codicon codicon-triangle-up' />
|
|
||||||
<span className='codicon codicon-triangle-down' />
|
|
||||||
</div>
|
|
||||||
<div className='network-request-size' onClick={() => toggleSortBy('size') }>
|
|
||||||
Size
|
|
||||||
<span className='codicon codicon-triangle-up' />
|
|
||||||
<span className='codicon codicon-triangle-down' />
|
|
||||||
</div>
|
|
||||||
<div className='network-request-route'>Route</div>
|
|
||||||
</div>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const NetworkResource: React.FunctionComponent<{
|
const renderCell = (entry: RenderedEntry, column: ColumnName) => {
|
||||||
resource: Entry,
|
if (column === 'name')
|
||||||
boundaries: Boundaries,
|
return <span title={entry.name.url}>{entry.name.name}</span>;
|
||||||
}> = ({ resource, boundaries }) => {
|
if (column === 'method')
|
||||||
const { routeStatus, resourceName, contentType } = React.useMemo(() => {
|
return <span>{entry.method}</span>;
|
||||||
const routeStatus = formatRouteStatus(resource);
|
if (column === 'status')
|
||||||
let resourceName: string;
|
return <span className={entry.status.className} title={entry.status.text}>{entry.status.code > 0 ? entry.status.code : ''}</span>;
|
||||||
try {
|
if (column === 'contentType')
|
||||||
const url = new URL(resource.request.url);
|
return <span>{entry.contentType}</span>;
|
||||||
resourceName = url.pathname;
|
if (column === 'duration')
|
||||||
} catch {
|
return <span>{msToString(entry.duration)}</span>;
|
||||||
resourceName = resource.request.url;
|
if (column === 'size')
|
||||||
}
|
return <span>{bytesToString(entry.size)}</span>;
|
||||||
let contentType = resource.response.content.mimeType;
|
if (column === 'start')
|
||||||
const charset = contentType.match(/^(.*);\s*charset=.*$/);
|
return <span>{msToString(entry.start)}</span>;
|
||||||
if (charset)
|
if (column === 'route')
|
||||||
contentType = charset[1];
|
return entry.route && <span className={`status-route ${entry.route}`}>{entry.route}</span>;
|
||||||
return { routeStatus, resourceName, contentType };
|
|
||||||
}, [resource]);
|
|
||||||
|
|
||||||
return <div className='hbox'>
|
|
||||||
<div className='hbox network-request-start'>
|
|
||||||
<div>{msToString(resource._monotonicTime! - boundaries.minimum)}</div>
|
|
||||||
</div>
|
|
||||||
<div className='hbox network-request-status'>
|
|
||||||
<div className={formatStatus(resource.response.status)} title={resource.response.statusText}>{resource.response.status}</div>
|
|
||||||
</div>
|
|
||||||
<div className='hbox network-request-method'>
|
|
||||||
<div>{resource.request.method}</div>
|
|
||||||
</div>
|
|
||||||
<div className='network-request-file'>
|
|
||||||
<div className='network-request-file-url' title={resource.request.url}>{resourceName}</div>
|
|
||||||
</div>
|
|
||||||
<div className='network-request-content-type' title={contentType}>{contentType}</div>
|
|
||||||
<div className='network-request-duration'>{msToString(resource.time)}</div>
|
|
||||||
<div className='network-request-size'>{bytesToString(resource.response._transferSize! > 0 ? resource.response._transferSize! : resource.response.bodySize)}</div>
|
|
||||||
<div className='network-request-route'>
|
|
||||||
{routeStatus && <div className={`status-route ${routeStatus}`}>{routeStatus}</div>}
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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)
|
if (status >= 200 && status < 400)
|
||||||
return 'status-success';
|
return 'status-success';
|
||||||
if (status >= 400)
|
if (status >= 400)
|
||||||
|
|
@ -185,7 +185,7 @@ function formatRouteStatus(request: Entry): string {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function sort(resources: Entry[], sorting: Sorting) {
|
function sort(resources: RenderedEntry[], sorting: Sorting) {
|
||||||
const c = comparator(sorting?.by);
|
const c = comparator(sorting?.by);
|
||||||
if (c)
|
if (c)
|
||||||
resources.sort(c);
|
resources.sort(c);
|
||||||
|
|
@ -193,45 +193,45 @@ function sort(resources: Entry[], sorting: Sorting) {
|
||||||
resources.reverse();
|
resources.reverse();
|
||||||
}
|
}
|
||||||
|
|
||||||
function comparator(sortBy: SortBy) {
|
function comparator(sortBy: ColumnName) {
|
||||||
if (sortBy === 'start')
|
if (sortBy === 'start')
|
||||||
return (a: Entry, b: Entry) => a._monotonicTime! - b._monotonicTime!;
|
return (a: RenderedEntry, b: RenderedEntry) => a.start - b.start;
|
||||||
|
|
||||||
if (sortBy === 'duration')
|
if (sortBy === 'duration')
|
||||||
return (a: Entry, b: Entry) => a.time - b.time;
|
return (a: RenderedEntry, b: RenderedEntry) => a.duration - b.duration;
|
||||||
|
|
||||||
if (sortBy === 'status')
|
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') {
|
if (sortBy === 'method') {
|
||||||
return (a: Entry, b: Entry) => {
|
return (a: RenderedEntry, b: RenderedEntry) => {
|
||||||
const valueA = a.request.method;
|
const valueA = a.method;
|
||||||
const valueB = b.request.method;
|
const valueB = b.method;
|
||||||
return valueA.localeCompare(valueB);
|
return valueA.localeCompare(valueB);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sortBy === 'size') {
|
if (sortBy === 'size') {
|
||||||
return (a: Entry, b: Entry) => {
|
return (a: RenderedEntry, b: RenderedEntry) => {
|
||||||
const sizeA = a.response._transferSize! > 0 ? a.response._transferSize! : a.response.bodySize;
|
return a.size - b.size;
|
||||||
const sizeB = b.response._transferSize! > 0 ? b.response._transferSize! : b.response.bodySize;
|
|
||||||
return sizeA - sizeB;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sortBy === 'content-type') {
|
if (sortBy === 'contentType') {
|
||||||
return (a: Entry, b: Entry) => {
|
return (a: RenderedEntry, b: RenderedEntry) => {
|
||||||
const valueA = a.response.content.mimeType;
|
return a.contentType.localeCompare(b.contentType);
|
||||||
const valueB = b.response.content.mimeType;
|
|
||||||
return valueA.localeCompare(valueB);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sortBy === 'file') {
|
if (sortBy === 'name') {
|
||||||
return (a: Entry, b: Entry) => {
|
return (a: RenderedEntry, b: RenderedEntry) => {
|
||||||
const nameA = a.request.url.substring(a.request.url.lastIndexOf('/'));
|
return a.name.name.localeCompare(b.name.name);
|
||||||
const nameB = b.request.url.substring(b.request.url.lastIndexOf('/'));
|
};
|
||||||
return nameA.localeCompare(nameB);
|
}
|
||||||
|
|
||||||
|
if (sortBy === 'route') {
|
||||||
|
return (a: RenderedEntry, b: RenderedEntry) => {
|
||||||
|
return a.route.localeCompare(b.route);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -205,12 +205,11 @@ export const Timeline: React.FunctionComponent<{
|
||||||
}, [setSelectedTime]);
|
}, [setSelectedTime]);
|
||||||
|
|
||||||
return <div style={{ flex: 'none', borderBottom: '1px solid var(--vscode-panel-border)' }}>
|
return <div style={{ flex: 'none', borderBottom: '1px solid var(--vscode-panel-border)' }}>
|
||||||
<GlassPane
|
{!!dragWindow && <GlassPane
|
||||||
enabled={!!dragWindow}
|
|
||||||
cursor={dragWindow?.type === 'resize' ? 'ew-resize' : 'grab'}
|
cursor={dragWindow?.type === 'resize' ? 'ew-resize' : 'grab'}
|
||||||
onPaneMouseUp={onGlassPaneMouseUp}
|
onPaneMouseUp={onGlassPaneMouseUp}
|
||||||
onPaneMouseMove={onGlassPaneMouseMove}
|
onPaneMouseMove={onGlassPaneMouseMove}
|
||||||
onPaneDoubleClick={onPaneDoubleClick} />
|
onPaneDoubleClick={onPaneDoubleClick} />}
|
||||||
<div ref={ref}
|
<div ref={ref}
|
||||||
className='timeline-view'
|
className='timeline-view'
|
||||||
onMouseDown={onMouseDown}
|
onMouseDown={onMouseDown}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
../third_party/vscode/codicon.css
|
../third_party/vscode/codicon.css
|
||||||
../uiUtils.ts
|
../uiUtils.ts
|
||||||
../ansi2html.ts
|
../ansi2html.ts
|
||||||
|
../shared
|
||||||
|
|
||||||
[expandable.spec.tsx]
|
[expandable.spec.tsx]
|
||||||
***
|
***
|
||||||
|
|
|
||||||
69
packages/web/src/components/gridView.css
Normal file
69
packages/web/src/components/gridView.css
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.grid-view {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
flex: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-view-cell {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
padding: 0 5px;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-view-header {
|
||||||
|
user-select: none;
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 30px;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
border-bottom: 1px solid var(--vscode-panel-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-view-header .codicon-triangle-up {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.grid-view-header .codicon-triangle-down {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-view-header > .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;
|
||||||
|
}
|
||||||
107
packages/web/src/components/gridView.tsx
Normal file
107
packages/web/src/components/gridView.tsx
Normal file
|
|
@ -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<T> = { by: keyof T, negate: boolean };
|
||||||
|
|
||||||
|
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,
|
||||||
|
sorting?: Sorting<T>,
|
||||||
|
setSorting?: (sorting: Sorting<T> | undefined) => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function GridView<T>(model: GridViewProps<T>) {
|
||||||
|
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<number[]>(initialOffsets);
|
||||||
|
|
||||||
|
const toggleSorting = React.useCallback((f: keyof T) => {
|
||||||
|
model.setSorting?.({ by: f, negate: model.sorting?.by === f ? !model.sorting.negate : false });
|
||||||
|
}, [model]);
|
||||||
|
|
||||||
|
return <div className='grid-view'>
|
||||||
|
<ResizeView
|
||||||
|
orientation={'horizontal'}
|
||||||
|
offsets={offsets}
|
||||||
|
setOffsets={setOffsets}
|
||||||
|
resizerColor='var(--vscode-panel-border)'
|
||||||
|
resizerWidth={1}
|
||||||
|
minColumnWidth={25}>
|
||||||
|
</ResizeView>
|
||||||
|
<div className='vbox'>
|
||||||
|
<div className='grid-view-header'>
|
||||||
|
{model.columns.map((column, i) => {
|
||||||
|
return <div
|
||||||
|
className={'grid-view-header-cell ' + sortingHeader(column, model.sorting)}
|
||||||
|
style={{
|
||||||
|
width: offsets[i] - (offsets[i - 1] || 0),
|
||||||
|
}}
|
||||||
|
onClick={() => model.setSorting && toggleSorting(column)}
|
||||||
|
>
|
||||||
|
<span className='grid-view-header-cell-title'>{model.columnTitle(column)}</span>
|
||||||
|
<span className='codicon codicon-triangle-up' />
|
||||||
|
<span className='codicon codicon-triangle-down' />
|
||||||
|
</div>;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<ListView
|
||||||
|
name={model.name}
|
||||||
|
items={model.items}
|
||||||
|
id={model.id}
|
||||||
|
render={(item, index) => {
|
||||||
|
return <>
|
||||||
|
{model.columns.map((column, i) => {
|
||||||
|
return <div
|
||||||
|
className='grid-view-cell'
|
||||||
|
style={{ width: offsets[i] - (offsets[i - 1] || 0) }}>
|
||||||
|
{model.render(item, column, index)}
|
||||||
|
</div>;
|
||||||
|
})}
|
||||||
|
</>;
|
||||||
|
}}
|
||||||
|
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}
|
||||||
|
></ListView>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortingHeader<T>(column: keyof T, sorting: Sorting<T> | undefined) {
|
||||||
|
return column === sorting?.by ? ' filter-' + (sorting.negate ? 'negative' : 'positive') : '';
|
||||||
|
}
|
||||||
|
|
@ -25,12 +25,11 @@ export type SplitViewProps = {
|
||||||
orientation?: 'vertical' | 'horizontal';
|
orientation?: 'vertical' | 'horizontal';
|
||||||
minSidebarSize?: number;
|
minSidebarSize?: number;
|
||||||
settingName?: string;
|
settingName?: string;
|
||||||
children: JSX.Element | JSX.Element[] | string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const kMinSize = 50;
|
const kMinSize = 50;
|
||||||
|
|
||||||
export const SplitView: React.FC<SplitViewProps> = ({
|
export const SplitView: React.FC<React.PropsWithChildren<SplitViewProps>> = ({
|
||||||
sidebarSize,
|
sidebarSize,
|
||||||
sidebarHidden = false,
|
sidebarHidden = false,
|
||||||
sidebarIsFirst = false,
|
sidebarIsFirst = false,
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@
|
||||||
flex: auto;
|
flex: auto;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabbed-pane-tab {
|
.tabbed-pane-tab {
|
||||||
|
|
|
||||||
|
|
@ -17,16 +17,12 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export const GlassPane: React.FC<{
|
export const GlassPane: React.FC<{
|
||||||
enabled: boolean;
|
|
||||||
cursor: string;
|
cursor: string;
|
||||||
onPaneMouseMove?: (e: MouseEvent) => void;
|
onPaneMouseMove?: (e: MouseEvent) => void;
|
||||||
onPaneMouseUp?: (e: MouseEvent) => void;
|
onPaneMouseUp?: (e: MouseEvent) => void;
|
||||||
onPaneDoubleClick?: (e: MouseEvent) => void;
|
onPaneDoubleClick?: (e: MouseEvent) => void;
|
||||||
}> = ({ enabled, cursor, onPaneMouseMove, onPaneMouseUp, onPaneDoubleClick }) => {
|
}> = ({ cursor, onPaneMouseMove, onPaneMouseUp, onPaneDoubleClick }) => {
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!enabled)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const glassPaneDiv = document.createElement('div');
|
const glassPaneDiv = document.createElement('div');
|
||||||
glassPaneDiv.style.position = 'fixed';
|
glassPaneDiv.style.position = 'fixed';
|
||||||
glassPaneDiv.style.top = '0';
|
glassPaneDiv.style.top = '0';
|
||||||
|
|
@ -54,7 +50,7 @@ export const GlassPane: React.FC<{
|
||||||
document.body.removeEventListener('dblclick', onPaneDoubleClick);
|
document.body.removeEventListener('dblclick', onPaneDoubleClick);
|
||||||
document.body.removeChild(glassPaneDiv);
|
document.body.removeChild(glassPaneDiv);
|
||||||
};
|
};
|
||||||
}, [enabled, cursor, onPaneMouseMove, onPaneMouseUp, onPaneDoubleClick]);
|
}, [cursor, onPaneMouseMove, onPaneMouseUp, onPaneDoubleClick]);
|
||||||
|
|
||||||
return <></>;
|
return <></>;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { GlassPane } from './glassPane';
|
|
||||||
import { useMeasure } from '../uiUtils';
|
import { useMeasure } from '../uiUtils';
|
||||||
|
import { ResizeView } from './resizeView';
|
||||||
|
|
||||||
type TestAttachment = {
|
type TestAttachment = {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -141,25 +141,8 @@ export const ImageDiffSlider: React.FC<{
|
||||||
|
|
||||||
const [slider, setSlider] = React.useState<number>(canvasWidth / 2);
|
const [slider, setSlider] = React.useState<number>(canvasWidth / 2);
|
||||||
const sameSize = expectedImage.naturalWidth === actualImage.naturalWidth && expectedImage.naturalHeight === actualImage.naturalHeight;
|
const sameSize = expectedImage.naturalWidth === actualImage.naturalWidth && expectedImage.naturalHeight === actualImage.naturalHeight;
|
||||||
const [resizing, setResizing] = React.useState<{ offset: number, slider: number } | null>(null);
|
|
||||||
|
|
||||||
return <div style={{ flex: 'none', display: 'flex', alignItems: 'center', flexDirection: 'column', userSelect: 'none' }}>
|
return <div style={{ flex: 'none', display: 'flex', alignItems: 'center', flexDirection: 'column', userSelect: 'none' }}>
|
||||||
<GlassPane
|
|
||||||
enabled={!!resizing}
|
|
||||||
cursor={'ew-resize'}
|
|
||||||
onPaneMouseUp={() => 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);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div style={{ margin: 5 }}>
|
<div style={{ margin: 5 }}>
|
||||||
{!sameSize && <span style={{ flex: 'none', margin: '0 5px' }}>Expected </span>}
|
{!sameSize && <span style={{ flex: 'none', margin: '0 5px' }}>Expected </span>}
|
||||||
<span>{expectedImage.naturalWidth}</span>
|
<span>{expectedImage.naturalWidth}</span>
|
||||||
|
|
@ -170,8 +153,13 @@ export const ImageDiffSlider: React.FC<{
|
||||||
{!sameSize && <span style={{ flex: 'none', margin: '0 5px' }}>x</span>}
|
{!sameSize && <span style={{ flex: 'none', margin: '0 5px' }}>x</span>}
|
||||||
{!sameSize && <span>{actualImage.naturalHeight}</span>}
|
{!sameSize && <span>{actualImage.naturalHeight}</span>}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ position: 'relative', width: canvasWidth, height: canvasHeight, margin: 15, ...checkerboardStyle }}
|
<div style={{ position: 'relative', width: canvasWidth, height: canvasHeight, margin: 15, ...checkerboardStyle }}>
|
||||||
onMouseDown={event => setResizing({ offset: event.clientX, slider: slider })}>
|
<ResizeView
|
||||||
|
orientation={'horizontal'}
|
||||||
|
offsets={[slider]}
|
||||||
|
setOffsets={offsets => setSlider(offsets[0])}
|
||||||
|
resizerColor={'#57606a80'}
|
||||||
|
resizerWidth={6}></ResizeView>
|
||||||
<img alt='Expected' style={{
|
<img alt='Expected' style={{
|
||||||
width: expectedImage.naturalWidth * scale,
|
width: expectedImage.naturalWidth * scale,
|
||||||
height: expectedImage.naturalHeight * scale,
|
height: expectedImage.naturalHeight * scale,
|
||||||
|
|
@ -182,9 +170,6 @@ export const ImageDiffSlider: React.FC<{
|
||||||
height: actualImage.naturalHeight * scale,
|
height: actualImage.naturalHeight * scale,
|
||||||
}} draggable='false' src={actualImage.src} />
|
}} draggable='false' src={actualImage.src} />
|
||||||
</div>
|
</div>
|
||||||
<div style={{ position: 'absolute', top: 0, bottom: 0, left: slider, width: 6, background: '#57606a80', cursor: 'ew-resize', overflow: 'visible', display: 'flex', alignItems: 'center' }}>
|
|
||||||
<svg style={{ fill: '#57606a80', width: 30, flex: 'none', marginLeft: -12, pointerEvents: 'none' }} viewBox="0 0 27 20"><path d="M9.6 0L0 9.6l9.6 9.6z"></path><path d="M17 19.2l9.5-9.6L16.9 0z"></path></svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
99
packages/web/src/shared/resizeView.tsx
Normal file
99
packages/web/src/shared/resizeView.tsx
Normal file
|
|
@ -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<HTMLDivElement>();
|
||||||
|
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 <div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
zIndex: 1000,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
}}
|
||||||
|
ref={ref}>
|
||||||
|
{!!resizing && <GlassPane
|
||||||
|
cursor={orientation === 'horizontal' ? 'ew-resize' : 'ns-resize'}
|
||||||
|
onPaneMouseUp={() => 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 <div
|
||||||
|
style={{
|
||||||
|
...dividerStyle,
|
||||||
|
top: orientation === 'horizontal' ? 0 : offset,
|
||||||
|
left: orientation === 'horizontal' ? offset : 0,
|
||||||
|
pointerEvents: 'initial',
|
||||||
|
}}
|
||||||
|
onMouseDown={event => setResizing({ clientX: event.clientX, clientY: event.clientY, offset, index })}>
|
||||||
|
<div style={{
|
||||||
|
...fillStyle,
|
||||||
|
background: resizerColor,
|
||||||
|
}}></div>
|
||||||
|
</div>;
|
||||||
|
})}
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
@ -241,9 +241,9 @@ test('should have network requests', async ({ showTraceViewer }) => {
|
||||||
const traceViewer = await showTraceViewer([traceFile]);
|
const traceViewer = await showTraceViewer([traceFile]);
|
||||||
await traceViewer.selectAction('http://localhost');
|
await traceViewer.selectAction('http://localhost');
|
||||||
await traceViewer.showNetworkTab();
|
await traceViewer.showNetworkTab();
|
||||||
await expect(traceViewer.networkRequests).toContainText([/200GET\/frames\/frame.htmltext\/html/]);
|
await expect(traceViewer.networkRequests).toContainText([/frame.htmlGET200text\/html/]);
|
||||||
await expect(traceViewer.networkRequests).toContainText([/200GET\/frames\/style.csstext\/css/]);
|
await expect(traceViewer.networkRequests).toContainText([/style.cssGET200text\/css/]);
|
||||||
await expect(traceViewer.networkRequests).toContainText([/200GET\/frames\/script.jsapplication\/javascript/]);
|
await expect(traceViewer.networkRequests).toContainText([/script.jsGET200application\/javascript/]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should have network request overrides', async ({ page, server, runAndTrace }) => {
|
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.selectAction('http://localhost');
|
||||||
await traceViewer.showNetworkTab();
|
await traceViewer.showNetworkTab();
|
||||||
await expect(traceViewer.networkRequests).toContainText([/200GET\/frames\/frame.htmltext\/html/]);
|
await expect(traceViewer.networkRequests).toContainText([/frame.htmlGET200text\/html/]);
|
||||||
await expect(traceViewer.networkRequests).toContainText([/GET\/frames\/style.cssx-unknown.*aborted/]);
|
await expect(traceViewer.networkRequests).toContainText([/style.cssGETx-unknown.*aborted/]);
|
||||||
await expect(traceViewer.networkRequests).not.toContainText([/continued/]);
|
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.selectAction('http://localhost');
|
||||||
await traceViewer.showNetworkTab();
|
await traceViewer.showNetworkTab();
|
||||||
await expect.soft(traceViewer.networkRequests).toContainText([/200GET\/frames\/frame.htmltext\/html.*/]);
|
await expect.soft(traceViewer.networkRequests).toContainText([/frame.htmlGET200text\/html.*/]);
|
||||||
await expect.soft(traceViewer.networkRequests).toContainText([/200GET\/frames\/script.jsapplication\/javascript.*continued/]);
|
await expect.soft(traceViewer.networkRequests).toContainText([/script.jsGET200application\/javascript.*continued/]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should show snapshot URL', async ({ page, runAndTrace, server }) => {
|
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 expect(traceViewer.consoleLineMessages).toHaveText(['hello {foo: bar}']);
|
||||||
|
|
||||||
await traceViewer.showNetworkTab();
|
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 }) => {
|
test('should prefer later resource request with the same method', async ({ page, server, runAndTrace }) => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue