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:
Dmitry Gozman 2021-02-17 17:51:57 -08:00 committed by GitHub
parent 3248c2449c
commit da135c2abb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 252 additions and 226 deletions

View File

@ -64,6 +64,8 @@ export type VideoMetaInfo = {
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) {
const contextEntries = new Map<string, ContextEntry>();
const pageEntries = new Map<string, PageEntry>();
@ -108,6 +110,8 @@ export function readTraceFile(events: trace.TraceEvent[], traceModel: TraceModel
break;
}
case 'action': {
if (!kInterestingActions.includes(event.method))
break;
const pageEntry = pageEntries.get(event.pageId!)!;
const actionId = event.contextId + '/' + event.pageId + '/' + pageEntry.actions.length;
const action: ActionEntry = {

View File

@ -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>;
};

View 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;
}

View 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>;
};

View File

@ -14,19 +14,19 @@
limitations under the License.
*/
.properties-tabbed-pane {
.tabbed-pane {
display: flex;
flex: auto;
overflow: hidden;
}
.properties-tab-content {
.tab-content {
display: flex;
flex: auto;
overflow: hidden;
}
.properties-tab-strip {
.tab-strip {
flex: auto;
display: flex;
flex-direction: row;
@ -34,11 +34,11 @@
height: 34px;
}
.properties-tab-strip:focus {
.tab-strip:focus {
outline: none;
}
.properties-tab-element {
.tab-element {
padding: 2px 6px 0 6px;
margin-right: 4px;
cursor: pointer;
@ -52,7 +52,7 @@
outline: none;
}
.properties-tab-label {
.tab-label {
max-width: 250px;
white-space: pre;
overflow: hidden;
@ -60,49 +60,10 @@
display: inline-block;
}
.properties-tab-element.selected {
.tab-element.selected {
border-bottom-color: var(--color);
}
.properties-tab-element:hover {
.tab-element:hover {
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;
}

View 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>;
};

View File

@ -43,13 +43,12 @@ export const Timeline: React.FunctionComponent<{
}> = ({ context, boundaries, selectedAction, highlightedAction, onSelected, onTimeSelected }) => {
const [measure, ref] = useMeasure<HTMLDivElement>();
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(() => {
return calculateDividerOffsets(measure.width, boundaries);
}, [measure.width, boundaries]);
let targetBar: TimelineBar | undefined = hoveredBar;
const bars = React.useMemo(() => {
const bars: TimelineBar[] = [];
for (const page of context.pages) {
@ -67,8 +66,6 @@ export const Timeline: React.FunctionComponent<{
type: entry.action.method,
priority: 0,
});
if (entry === (highlightedAction || selectedAction))
targetBar = bars[bars.length - 1];
}
let lastDialogOpened: trace.DialogOpenedEvent | undefined;
for (const event of page.interestingEvents) {
@ -116,56 +113,55 @@ export const Timeline: React.FunctionComponent<{
return bars;
}, [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 time1 = 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;
for (const b of bars) {
const left = Math.max(b.leftTime, time1);
const right = Math.min(b.rightTime, time2);
const middle = (b.leftTime + b.rightTime) / 2;
for (let i = 0; i < bars.length; i++) {
const bar = bars[i];
const left = Math.max(bar.leftTime, time1);
const right = Math.min(bar.rightTime, time2);
const middle = (bar.leftTime + bar.rightTime) / 2;
const d = Math.abs(time - middle);
if (left <= right && (!bar || d < distance!)) {
bar = b;
if (left <= right && (index === undefined || d < distance!)) {
index = i;
distance = d;
}
}
return bar;
return index;
};
const onMouseMove = (event: React.MouseEvent) => {
if (ref.current) {
const x = event.clientX - ref.current.getBoundingClientRect().left;
setPreviewX(x);
onTimeSelected(positionToTime(measure.width, boundaries, x));
setHoveredBar(findHoveredBar(x));
}
if (!ref.current)
return;
const x = event.clientX - ref.current.getBoundingClientRect().left;
setPreviewX(x);
onTimeSelected(positionToTime(measure.width, boundaries, x));
setHoveredBarIndex(findHoveredBarIndex(x));
};
const onMouseLeave = () => {
setPreviewX(undefined);
onTimeSelected(undefined);
};
const onActionClick = (event: React.MouseEvent) => {
if (ref.current) {
const x = event.clientX - ref.current.getBoundingClientRect().left;
const bar = findHoveredBar(x);
if (bar && bar.entry)
onSelected(bar.entry);
event.stopPropagation();
}
};
const onTimeClick = (event: React.MouseEvent) => {
if (ref.current) {
const x = event.clientX - ref.current.getBoundingClientRect().left;
const time = positionToTime(measure.width, boundaries, x);
onTimeSelected(time);
event.stopPropagation();
}
const onClick = (event: React.MouseEvent) => {
if (!ref.current)
return;
const x = event.clientX - ref.current.getBoundingClientRect().left;
const index = findHoveredBarIndex(x);
if (index === undefined)
return;
const entry = bars[index].entry;
if (entry)
onSelected(entry);
};
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'>{
offsets.map((offset, index) => {
return <div key={index} className='timeline-divider' style={{ left: offset.position + 'px' }}>
@ -186,7 +182,7 @@ export const Timeline: React.FunctionComponent<{
</div>;
})
}</div>
<div className='timeline-lane timeline-bars' onClick={onActionClick}>{
<div className='timeline-lane timeline-bars'>{
bars.map((bar, index) => {
return <div key={index}
className={'timeline-bar ' + bar.type + (targetBar === bar ? ' selected' : '')}

View File

@ -16,11 +16,15 @@
import { ActionEntry, TraceModel } from '../../../cli/traceViewer/traceModel';
import { ActionList } from './actionList';
import { PropertiesTabbedPane } from './propertiesTabbedPane';
import { TabbedPane } from './tabbedPane';
import { Timeline } from './timeline';
import './workbench.css';
import * as React from 'react';
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<{
traceModel: TraceModel,
@ -39,6 +43,7 @@ export const Workbench: React.FunctionComponent<{
const snapshotSize = context.created.viewportSize || { width: 1280, height: 720 };
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'>
<div className='hbox header'>
@ -78,12 +83,12 @@ export const Workbench: React.FunctionComponent<{
onHighlighted={action => setHighlightedAction(action)}
/>
</div>
<PropertiesTabbedPane
actionEntry={selectedAction}
snapshotSize={snapshotSize}
selectedTime={selectedTime}
boundaries={boundaries}
/>
<TabbedPane tabs={[
{ id: 'snapshot', title: 'Snapshot', render: () => <SnapshotTab actionEntry={selectedAction} snapshotSize={snapshotSize} selection={snapshotSelection} boundaries={boundaries} /> },
{ id: 'source', title: 'Source', render: () => <SourceTab actionEntry={selectedAction} /> },
{ id: 'network', title: 'Network', render: () => <NetworkTab actionEntry={selectedAction} /> },
{ id: 'logs', title: 'Logs', render: () => <LogsTab actionEntry={selectedAction} /> },
]}/>
</div>
</div>;
};