mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-15 06:02:57 +03:00
fix(trace viewer): follow up with recent instrumentation changes (#5488)
- List all actions we are interested in - Fix timeline hover flicker - Extract tabbed pane component - Preview snapshots without clicking on the action
This commit is contained in:
parent
3248c2449c
commit
da135c2abb
@ -64,6 +64,8 @@ export type VideoMetaInfo = {
|
|||||||
endTime: number;
|
endTime: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const kInterestingActions = ['click', 'dblclick', 'hover', 'check', 'uncheck', 'tap', 'fill', 'press', 'type', 'selectOption', 'setInputFiles', 'goto', 'setContent', 'goBack', 'goForward', 'reload'];
|
||||||
|
|
||||||
export function readTraceFile(events: trace.TraceEvent[], traceModel: TraceModel, filePath: string) {
|
export function readTraceFile(events: trace.TraceEvent[], traceModel: TraceModel, filePath: string) {
|
||||||
const contextEntries = new Map<string, ContextEntry>();
|
const contextEntries = new Map<string, ContextEntry>();
|
||||||
const pageEntries = new Map<string, PageEntry>();
|
const pageEntries = new Map<string, PageEntry>();
|
||||||
@ -108,6 +110,8 @@ export function readTraceFile(events: trace.TraceEvent[], traceModel: TraceModel
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'action': {
|
case 'action': {
|
||||||
|
if (!kInterestingActions.includes(event.method))
|
||||||
|
break;
|
||||||
const pageEntry = pageEntries.get(event.pageId!)!;
|
const pageEntry = pageEntries.get(event.pageId!)!;
|
||||||
const actionId = event.contextId + '/' + event.pageId + '/' + pageEntry.actions.length;
|
const actionId = event.contextId + '/' + event.pageId + '/' + pageEntry.actions.length;
|
||||||
const action: ActionEntry = {
|
const action: ActionEntry = {
|
||||||
|
@ -1,135 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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 { ActionEntry } from '../../../cli/traceViewer/traceModel';
|
|
||||||
import { Boundaries, Size } from '../geometry';
|
|
||||||
import { NetworkTab } from './networkTab';
|
|
||||||
import { SourceTab } from './sourceTab';
|
|
||||||
import './propertiesTabbedPane.css';
|
|
||||||
import * as React from 'react';
|
|
||||||
import { msToString, useMeasure } from './helpers';
|
|
||||||
import { LogsTab } from './logsTab';
|
|
||||||
|
|
||||||
export const PropertiesTabbedPane: React.FunctionComponent<{
|
|
||||||
actionEntry: ActionEntry | undefined,
|
|
||||||
snapshotSize: Size,
|
|
||||||
selectedTime: number | undefined,
|
|
||||||
boundaries: Boundaries,
|
|
||||||
}> = ({ actionEntry, snapshotSize, selectedTime, boundaries }) => {
|
|
||||||
const [selected, setSelected] = React.useState<'snapshot' | 'source' | 'network' | 'logs'>('snapshot');
|
|
||||||
return <div className='properties-tabbed-pane'>
|
|
||||||
<div className='vbox'>
|
|
||||||
<div className='hbox' style={{ flex: 'none' }}>
|
|
||||||
<div className='properties-tab-strip'>
|
|
||||||
<div className={'properties-tab-element ' + (selected === 'snapshot' ? 'selected' : '')}
|
|
||||||
onClick={() => setSelected('snapshot')}>
|
|
||||||
<div className='properties-tab-label'>Snapshot</div>
|
|
||||||
</div>
|
|
||||||
<div className={'properties-tab-element ' + (selected === 'source' ? 'selected' : '')}
|
|
||||||
onClick={() => setSelected('source')}>
|
|
||||||
<div className='properties-tab-label'>Source</div>
|
|
||||||
</div>
|
|
||||||
<div className={'properties-tab-element ' + (selected === 'network' ? 'selected' : '')}
|
|
||||||
onClick={() => setSelected('network')}>
|
|
||||||
<div className='properties-tab-label'>Network</div>
|
|
||||||
</div>
|
|
||||||
<div className={'properties-tab-element ' + (selected === 'logs' ? 'selected' : '')}
|
|
||||||
onClick={() => setSelected('logs')}>
|
|
||||||
<div className='properties-tab-label'>Logs</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{ selected === 'snapshot' && <div className='properties-tab-content'>
|
|
||||||
<SnapshotTab actionEntry={actionEntry} snapshotSize={snapshotSize} selectedTime={selectedTime} boundaries={boundaries} />
|
|
||||||
</div> }
|
|
||||||
{ selected === 'source' && <div className='properties-tab-content'>
|
|
||||||
<SourceTab actionEntry={actionEntry} />
|
|
||||||
</div> }
|
|
||||||
{ selected === 'network' && <div className='properties-tab-content'>
|
|
||||||
<NetworkTab actionEntry={actionEntry} />
|
|
||||||
</div> }
|
|
||||||
{ selected === 'logs' && <div className='properties-tab-content'>
|
|
||||||
<LogsTab actionEntry={actionEntry} />
|
|
||||||
</div> }
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
const SnapshotTab: React.FunctionComponent<{
|
|
||||||
actionEntry: ActionEntry | undefined,
|
|
||||||
snapshotSize: Size,
|
|
||||||
selectedTime: number | undefined,
|
|
||||||
boundaries: Boundaries,
|
|
||||||
}> = ({ actionEntry, snapshotSize, selectedTime, boundaries }) => {
|
|
||||||
const [measure, ref] = useMeasure<HTMLDivElement>();
|
|
||||||
const [snapshotIndex, setSnapshotIndex] = React.useState(0);
|
|
||||||
|
|
||||||
let snapshots: { name: string, snapshotId?: string, snapshotTime?: number }[] = [];
|
|
||||||
snapshots = (actionEntry ? (actionEntry.action.snapshots || []) : []).slice();
|
|
||||||
if (!snapshots.length || snapshots[0].name !== 'before')
|
|
||||||
snapshots.unshift({ name: 'before', snapshotTime: actionEntry ? actionEntry.action.startTime : 0 });
|
|
||||||
if (snapshots[snapshots.length - 1].name !== 'after')
|
|
||||||
snapshots.push({ name: 'after', snapshotTime: actionEntry ? actionEntry.action.endTime : 0 });
|
|
||||||
|
|
||||||
const iframeRef = React.createRef<HTMLIFrameElement>();
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!actionEntry || !iframeRef.current)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// TODO: this logic is copied from SnapshotServer. Find a way to share.
|
|
||||||
let snapshotUrl = 'data:text/html,Snapshot is not available';
|
|
||||||
if (selectedTime) {
|
|
||||||
snapshotUrl = `/snapshot/pageId/${actionEntry.action.pageId!}/timestamp/${selectedTime}/main`;
|
|
||||||
} else {
|
|
||||||
const snapshot = snapshots[snapshotIndex];
|
|
||||||
if (snapshot && snapshot.snapshotTime)
|
|
||||||
snapshotUrl = `/snapshot/pageId/${actionEntry.action.pageId!}/timestamp/${snapshot.snapshotTime}/main`;
|
|
||||||
else if (snapshot && snapshot.snapshotId)
|
|
||||||
snapshotUrl = `/snapshot/pageId/${actionEntry.action.pageId!}/snapshotId/${snapshot.snapshotId}/main`;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
(iframeRef.current.contentWindow as any).showSnapshot(snapshotUrl);
|
|
||||||
} catch (e) {
|
|
||||||
}
|
|
||||||
}, [actionEntry, snapshotIndex, selectedTime]);
|
|
||||||
|
|
||||||
const scale = Math.min(measure.width / snapshotSize.width, measure.height / snapshotSize.height);
|
|
||||||
return <div className='snapshot-tab'>
|
|
||||||
<div className='snapshot-controls'>{
|
|
||||||
selectedTime && <div key='selectedTime' className='snapshot-toggle'>
|
|
||||||
{msToString(selectedTime - boundaries.minimum)}
|
|
||||||
</div>
|
|
||||||
}{!selectedTime && snapshots.map((snapshot, index) => {
|
|
||||||
return <div
|
|
||||||
key={snapshot.name}
|
|
||||||
className={'snapshot-toggle' + (snapshotIndex === index ? ' toggled' : '')}
|
|
||||||
onClick={() => setSnapshotIndex(index)}>
|
|
||||||
{snapshot.name}
|
|
||||||
</div>
|
|
||||||
})
|
|
||||||
}</div>
|
|
||||||
<div ref={ref} className='snapshot-wrapper'>
|
|
||||||
<div className='snapshot-container' style={{
|
|
||||||
width: snapshotSize.width + 'px',
|
|
||||||
height: snapshotSize.height + 'px',
|
|
||||||
transform: `translate(${-snapshotSize.width * (1 - scale) / 2}px, ${-snapshotSize.height * (1 - scale) / 2}px) scale(${scale})`,
|
|
||||||
}}>
|
|
||||||
<iframe ref={iframeRef} id='snapshot' name='snapshot' src='/snapshot/'></iframe>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
};
|
|
54
src/web/traceViewer/ui/snapshotTab.css
Normal file
54
src/web/traceViewer/ui/snapshotTab.css
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.snapshot-tab {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.snapshot-controls {
|
||||||
|
flex: 0 0 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.snapshot-toggle {
|
||||||
|
padding: 5px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.snapshot-toggle.toggled {
|
||||||
|
background: var(--inactive-focus-ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
.snapshot-wrapper {
|
||||||
|
flex: auto;
|
||||||
|
margin: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.snapshot-container {
|
||||||
|
display: block;
|
||||||
|
background: white;
|
||||||
|
outline: 1px solid #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
iframe#snapshot {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
}
|
90
src/web/traceViewer/ui/snapshotTab.tsx
Normal file
90
src/web/traceViewer/ui/snapshotTab.tsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
/**
|
||||||
|
* 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 { ActionEntry } from '../../../cli/traceViewer/traceModel';
|
||||||
|
import { Boundaries, Size } from '../geometry';
|
||||||
|
import './snapshotTab.css';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { msToString, useMeasure } from './helpers';
|
||||||
|
|
||||||
|
export const SnapshotTab: React.FunctionComponent<{
|
||||||
|
actionEntry: ActionEntry | undefined,
|
||||||
|
snapshotSize: Size,
|
||||||
|
selection: { pageId: string, time: number } | undefined,
|
||||||
|
boundaries: Boundaries,
|
||||||
|
}> = ({ actionEntry, snapshotSize, selection, boundaries }) => {
|
||||||
|
const [measure, ref] = useMeasure<HTMLDivElement>();
|
||||||
|
const [snapshotIndex, setSnapshotIndex] = React.useState(0);
|
||||||
|
|
||||||
|
let snapshots: { name: string, snapshotId?: string, snapshotTime?: number }[] = [];
|
||||||
|
snapshots = (actionEntry ? (actionEntry.action.snapshots || []) : []).slice();
|
||||||
|
if (actionEntry) {
|
||||||
|
if (!snapshots.length || snapshots[0].name !== 'before')
|
||||||
|
snapshots.unshift({ name: 'before', snapshotTime: actionEntry ? actionEntry.action.startTime : 0 });
|
||||||
|
if (snapshots[snapshots.length - 1].name !== 'after')
|
||||||
|
snapshots.push({ name: 'after', snapshotTime: actionEntry ? actionEntry.action.endTime : 0 });
|
||||||
|
}
|
||||||
|
const { pageId, time } = selection || { pageId: undefined, time: 0 };
|
||||||
|
|
||||||
|
const iframeRef = React.createRef<HTMLIFrameElement>();
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!iframeRef.current)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// TODO: this logic is copied from SnapshotServer. Find a way to share.
|
||||||
|
let snapshotUrl = 'data:text/html,Snapshot is not available';
|
||||||
|
if (pageId) {
|
||||||
|
snapshotUrl = `/snapshot/pageId/${pageId}/timestamp/${time}/main`;
|
||||||
|
} else if (actionEntry) {
|
||||||
|
const snapshot = snapshots[snapshotIndex];
|
||||||
|
if (snapshot && snapshot.snapshotTime)
|
||||||
|
snapshotUrl = `/snapshot/pageId/${actionEntry.action.pageId!}/timestamp/${snapshot.snapshotTime}/main`;
|
||||||
|
else if (snapshot && snapshot.snapshotId)
|
||||||
|
snapshotUrl = `/snapshot/pageId/${actionEntry.action.pageId!}/snapshotId/${snapshot.snapshotId}/main`;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
(iframeRef.current.contentWindow as any).showSnapshot(snapshotUrl);
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
}, [actionEntry, snapshotIndex, pageId, time]);
|
||||||
|
|
||||||
|
const scale = Math.min(measure.width / snapshotSize.width, measure.height / snapshotSize.height);
|
||||||
|
return <div className='snapshot-tab'>
|
||||||
|
<div className='snapshot-controls'>{
|
||||||
|
selection && <div key='selectedTime' className='snapshot-toggle'>
|
||||||
|
{msToString(selection.time - boundaries.minimum)}
|
||||||
|
</div>
|
||||||
|
}{!selection && snapshots.map((snapshot, index) => {
|
||||||
|
return <div
|
||||||
|
key={snapshot.name}
|
||||||
|
className={'snapshot-toggle' + (snapshotIndex === index ? ' toggled' : '')}
|
||||||
|
onClick={() => setSnapshotIndex(index)}>
|
||||||
|
{snapshot.name}
|
||||||
|
</div>
|
||||||
|
})
|
||||||
|
}</div>
|
||||||
|
<div ref={ref} className='snapshot-wrapper'>
|
||||||
|
<div className='snapshot-container' style={{
|
||||||
|
width: snapshotSize.width + 'px',
|
||||||
|
height: snapshotSize.height + 'px',
|
||||||
|
transform: `translate(${-snapshotSize.width * (1 - scale) / 2}px, ${-snapshotSize.height * (1 - scale) / 2}px) scale(${scale})`,
|
||||||
|
}}>
|
||||||
|
<iframe ref={iframeRef} id='snapshot' name='snapshot' src='/snapshot/'></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
};
|
@ -14,19 +14,19 @@
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.properties-tabbed-pane {
|
.tabbed-pane {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: auto;
|
flex: auto;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.properties-tab-content {
|
.tab-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: auto;
|
flex: auto;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.properties-tab-strip {
|
.tab-strip {
|
||||||
flex: auto;
|
flex: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@ -34,11 +34,11 @@
|
|||||||
height: 34px;
|
height: 34px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.properties-tab-strip:focus {
|
.tab-strip:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.properties-tab-element {
|
.tab-element {
|
||||||
padding: 2px 6px 0 6px;
|
padding: 2px 6px 0 6px;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -52,7 +52,7 @@
|
|||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.properties-tab-label {
|
.tab-label {
|
||||||
max-width: 250px;
|
max-width: 250px;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -60,49 +60,10 @@
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.properties-tab-element.selected {
|
.tab-element.selected {
|
||||||
border-bottom-color: var(--color);
|
border-bottom-color: var(--color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.properties-tab-element:hover {
|
.tab-element:hover {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.snapshot-tab {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.snapshot-controls {
|
|
||||||
flex: 0 0 24px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.snapshot-toggle {
|
|
||||||
padding: 5px 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.snapshot-toggle.toggled {
|
|
||||||
background: var(--inactive-focus-ring);
|
|
||||||
}
|
|
||||||
|
|
||||||
.snapshot-wrapper {
|
|
||||||
flex: auto;
|
|
||||||
margin: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.snapshot-container {
|
|
||||||
display: block;
|
|
||||||
background: white;
|
|
||||||
outline: 1px solid #aaa;
|
|
||||||
}
|
|
||||||
|
|
||||||
iframe#snapshot {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border: none;
|
|
||||||
}
|
|
51
src/web/traceViewer/ui/tabbedPane.tsx
Normal file
51
src/web/traceViewer/ui/tabbedPane.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* 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 './tabbedPane.css';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
export interface TabbedPaneTab {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
render: () => React.ReactElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TabbedPane: React.FunctionComponent<{
|
||||||
|
tabs: TabbedPaneTab[],
|
||||||
|
}> = ({ tabs }) => {
|
||||||
|
const [selected, setSelected] = React.useState<string>(tabs.length ? tabs[0].id : '');
|
||||||
|
return <div className='tabbed-pane'>
|
||||||
|
<div className='vbox'>
|
||||||
|
<div className='hbox' style={{ flex: 'none' }}>
|
||||||
|
<div className='tab-strip'>{
|
||||||
|
tabs.map(tab => {
|
||||||
|
return <div className={'tab-element ' + (selected === tab.id ? 'selected' : '')}
|
||||||
|
onClick={() => setSelected(tab.id)}
|
||||||
|
key={tab.id}>
|
||||||
|
<div className='tab-label'>{tab.title}</div>
|
||||||
|
</div>
|
||||||
|
})
|
||||||
|
}</div>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
tabs.map(tab => {
|
||||||
|
if (selected === tab.id)
|
||||||
|
return <div key={tab.id} className='tab-content'>{tab.render()}</div>;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
};
|
@ -43,13 +43,12 @@ export const Timeline: React.FunctionComponent<{
|
|||||||
}> = ({ context, boundaries, selectedAction, highlightedAction, onSelected, onTimeSelected }) => {
|
}> = ({ context, boundaries, selectedAction, highlightedAction, onSelected, onTimeSelected }) => {
|
||||||
const [measure, ref] = useMeasure<HTMLDivElement>();
|
const [measure, ref] = useMeasure<HTMLDivElement>();
|
||||||
const [previewX, setPreviewX] = React.useState<number | undefined>();
|
const [previewX, setPreviewX] = React.useState<number | undefined>();
|
||||||
const [hoveredBar, setHoveredBar] = React.useState<TimelineBar | undefined>();
|
const [hoveredBarIndex, setHoveredBarIndex] = React.useState<number | undefined>();
|
||||||
|
|
||||||
const offsets = React.useMemo(() => {
|
const offsets = React.useMemo(() => {
|
||||||
return calculateDividerOffsets(measure.width, boundaries);
|
return calculateDividerOffsets(measure.width, boundaries);
|
||||||
}, [measure.width, boundaries]);
|
}, [measure.width, boundaries]);
|
||||||
|
|
||||||
let targetBar: TimelineBar | undefined = hoveredBar;
|
|
||||||
const bars = React.useMemo(() => {
|
const bars = React.useMemo(() => {
|
||||||
const bars: TimelineBar[] = [];
|
const bars: TimelineBar[] = [];
|
||||||
for (const page of context.pages) {
|
for (const page of context.pages) {
|
||||||
@ -67,8 +66,6 @@ export const Timeline: React.FunctionComponent<{
|
|||||||
type: entry.action.method,
|
type: entry.action.method,
|
||||||
priority: 0,
|
priority: 0,
|
||||||
});
|
});
|
||||||
if (entry === (highlightedAction || selectedAction))
|
|
||||||
targetBar = bars[bars.length - 1];
|
|
||||||
}
|
}
|
||||||
let lastDialogOpened: trace.DialogOpenedEvent | undefined;
|
let lastDialogOpened: trace.DialogOpenedEvent | undefined;
|
||||||
for (const event of page.interestingEvents) {
|
for (const event of page.interestingEvents) {
|
||||||
@ -116,56 +113,55 @@ export const Timeline: React.FunctionComponent<{
|
|||||||
return bars;
|
return bars;
|
||||||
}, [context, boundaries, measure.width]);
|
}, [context, boundaries, measure.width]);
|
||||||
|
|
||||||
const findHoveredBar = (x: number) => {
|
const hoveredBar = hoveredBarIndex !== undefined ? bars[hoveredBarIndex] : undefined;
|
||||||
|
let targetBar: TimelineBar | undefined = bars.find(bar => bar.entry === (highlightedAction || selectedAction));
|
||||||
|
targetBar = hoveredBar || targetBar;
|
||||||
|
|
||||||
|
const findHoveredBarIndex = (x: number) => {
|
||||||
const time = positionToTime(measure.width, boundaries, x);
|
const time = positionToTime(measure.width, boundaries, x);
|
||||||
const time1 = positionToTime(measure.width, boundaries, x - 5);
|
const time1 = positionToTime(measure.width, boundaries, x - 5);
|
||||||
const time2 = positionToTime(measure.width, boundaries, x + 5);
|
const time2 = positionToTime(measure.width, boundaries, x + 5);
|
||||||
let bar: TimelineBar | undefined;
|
let index: number | undefined;
|
||||||
let distance: number | undefined;
|
let distance: number | undefined;
|
||||||
for (const b of bars) {
|
for (let i = 0; i < bars.length; i++) {
|
||||||
const left = Math.max(b.leftTime, time1);
|
const bar = bars[i];
|
||||||
const right = Math.min(b.rightTime, time2);
|
const left = Math.max(bar.leftTime, time1);
|
||||||
const middle = (b.leftTime + b.rightTime) / 2;
|
const right = Math.min(bar.rightTime, time2);
|
||||||
|
const middle = (bar.leftTime + bar.rightTime) / 2;
|
||||||
const d = Math.abs(time - middle);
|
const d = Math.abs(time - middle);
|
||||||
if (left <= right && (!bar || d < distance!)) {
|
if (left <= right && (index === undefined || d < distance!)) {
|
||||||
bar = b;
|
index = i;
|
||||||
distance = d;
|
distance = d;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return bar;
|
return index;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onMouseMove = (event: React.MouseEvent) => {
|
const onMouseMove = (event: React.MouseEvent) => {
|
||||||
if (ref.current) {
|
if (!ref.current)
|
||||||
const x = event.clientX - ref.current.getBoundingClientRect().left;
|
return;
|
||||||
setPreviewX(x);
|
const x = event.clientX - ref.current.getBoundingClientRect().left;
|
||||||
onTimeSelected(positionToTime(measure.width, boundaries, x));
|
setPreviewX(x);
|
||||||
setHoveredBar(findHoveredBar(x));
|
onTimeSelected(positionToTime(measure.width, boundaries, x));
|
||||||
}
|
setHoveredBarIndex(findHoveredBarIndex(x));
|
||||||
};
|
};
|
||||||
const onMouseLeave = () => {
|
const onMouseLeave = () => {
|
||||||
setPreviewX(undefined);
|
setPreviewX(undefined);
|
||||||
onTimeSelected(undefined);
|
onTimeSelected(undefined);
|
||||||
};
|
};
|
||||||
const onActionClick = (event: React.MouseEvent) => {
|
const onClick = (event: React.MouseEvent) => {
|
||||||
if (ref.current) {
|
if (!ref.current)
|
||||||
const x = event.clientX - ref.current.getBoundingClientRect().left;
|
return;
|
||||||
const bar = findHoveredBar(x);
|
const x = event.clientX - ref.current.getBoundingClientRect().left;
|
||||||
if (bar && bar.entry)
|
const index = findHoveredBarIndex(x);
|
||||||
onSelected(bar.entry);
|
if (index === undefined)
|
||||||
event.stopPropagation();
|
return;
|
||||||
}
|
const entry = bars[index].entry;
|
||||||
};
|
if (entry)
|
||||||
const onTimeClick = (event: React.MouseEvent) => {
|
onSelected(entry);
|
||||||
if (ref.current) {
|
|
||||||
const x = event.clientX - ref.current.getBoundingClientRect().left;
|
|
||||||
const time = positionToTime(measure.width, boundaries, x);
|
|
||||||
onTimeSelected(time);
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return <div ref={ref} className='timeline-view' onMouseMove={onMouseMove} onMouseOver={onMouseMove} onMouseLeave={onMouseLeave} onClick={onTimeClick}>
|
return <div ref={ref} className='timeline-view' onMouseMove={onMouseMove} onMouseOver={onMouseMove} onMouseLeave={onMouseLeave} onClick={onClick}>
|
||||||
<div className='timeline-grid'>{
|
<div className='timeline-grid'>{
|
||||||
offsets.map((offset, index) => {
|
offsets.map((offset, index) => {
|
||||||
return <div key={index} className='timeline-divider' style={{ left: offset.position + 'px' }}>
|
return <div key={index} className='timeline-divider' style={{ left: offset.position + 'px' }}>
|
||||||
@ -186,7 +182,7 @@ export const Timeline: React.FunctionComponent<{
|
|||||||
</div>;
|
</div>;
|
||||||
})
|
})
|
||||||
}</div>
|
}</div>
|
||||||
<div className='timeline-lane timeline-bars' onClick={onActionClick}>{
|
<div className='timeline-lane timeline-bars'>{
|
||||||
bars.map((bar, index) => {
|
bars.map((bar, index) => {
|
||||||
return <div key={index}
|
return <div key={index}
|
||||||
className={'timeline-bar ' + bar.type + (targetBar === bar ? ' selected' : '')}
|
className={'timeline-bar ' + bar.type + (targetBar === bar ? ' selected' : '')}
|
||||||
|
@ -16,11 +16,15 @@
|
|||||||
|
|
||||||
import { ActionEntry, TraceModel } from '../../../cli/traceViewer/traceModel';
|
import { ActionEntry, TraceModel } from '../../../cli/traceViewer/traceModel';
|
||||||
import { ActionList } from './actionList';
|
import { ActionList } from './actionList';
|
||||||
import { PropertiesTabbedPane } from './propertiesTabbedPane';
|
import { TabbedPane } from './tabbedPane';
|
||||||
import { Timeline } from './timeline';
|
import { Timeline } from './timeline';
|
||||||
import './workbench.css';
|
import './workbench.css';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { ContextSelector } from './contextSelector';
|
import { ContextSelector } from './contextSelector';
|
||||||
|
import { NetworkTab } from './networkTab';
|
||||||
|
import { SourceTab } from './sourceTab';
|
||||||
|
import { SnapshotTab } from './snapshotTab';
|
||||||
|
import { LogsTab } from './logsTab';
|
||||||
|
|
||||||
export const Workbench: React.FunctionComponent<{
|
export const Workbench: React.FunctionComponent<{
|
||||||
traceModel: TraceModel,
|
traceModel: TraceModel,
|
||||||
@ -39,6 +43,7 @@ export const Workbench: React.FunctionComponent<{
|
|||||||
|
|
||||||
const snapshotSize = context.created.viewportSize || { width: 1280, height: 720 };
|
const snapshotSize = context.created.viewportSize || { width: 1280, height: 720 };
|
||||||
const boundaries = { minimum: context.startTime, maximum: context.endTime };
|
const boundaries = { minimum: context.startTime, maximum: context.endTime };
|
||||||
|
const snapshotSelection = context.pages.length && selectedTime !== undefined ? { pageId: context.pages[0].created.pageId, time: selectedTime } : undefined;
|
||||||
|
|
||||||
return <div className='vbox workbench'>
|
return <div className='vbox workbench'>
|
||||||
<div className='hbox header'>
|
<div className='hbox header'>
|
||||||
@ -78,12 +83,12 @@ export const Workbench: React.FunctionComponent<{
|
|||||||
onHighlighted={action => setHighlightedAction(action)}
|
onHighlighted={action => setHighlightedAction(action)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<PropertiesTabbedPane
|
<TabbedPane tabs={[
|
||||||
actionEntry={selectedAction}
|
{ id: 'snapshot', title: 'Snapshot', render: () => <SnapshotTab actionEntry={selectedAction} snapshotSize={snapshotSize} selection={snapshotSelection} boundaries={boundaries} /> },
|
||||||
snapshotSize={snapshotSize}
|
{ id: 'source', title: 'Source', render: () => <SourceTab actionEntry={selectedAction} /> },
|
||||||
selectedTime={selectedTime}
|
{ id: 'network', title: 'Network', render: () => <NetworkTab actionEntry={selectedAction} /> },
|
||||||
boundaries={boundaries}
|
{ id: 'logs', title: 'Logs', render: () => <LogsTab actionEntry={selectedAction} /> },
|
||||||
/>
|
]}/>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user