mirror of
https://github.com/microsoft/playwright.git
synced 2025-01-05 19:04:43 +03:00
chore(tracing): sync timeline and list highlight (#6235)
This commit is contained in:
parent
27e720f23a
commit
033bc9bfcc
@ -31,6 +31,7 @@
|
|||||||
--transparent-blue: #2196F355;
|
--transparent-blue: #2196F355;
|
||||||
--orange: #d24726;
|
--orange: #d24726;
|
||||||
--black: #1E1E1E;
|
--black: #1E1E1E;
|
||||||
|
--light-gray: #BBBBBB;
|
||||||
--gray: #888888;
|
--gray: #888888;
|
||||||
--separator: #80808059;
|
--separator: #80808059;
|
||||||
--focus-ring: #0E639CCC;
|
--focus-ring: #0E639CCC;
|
||||||
|
@ -14,7 +14,11 @@
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.action-list {
|
.action-list-title {
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-list-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: auto;
|
flex: auto;
|
||||||
@ -23,6 +27,7 @@
|
|||||||
color: #555;
|
color: #555;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
padding: 5px 0 0 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-entry {
|
.action-entry {
|
||||||
@ -35,15 +40,22 @@
|
|||||||
padding-left: 3px;
|
padding-left: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-entry.highlighted,
|
||||||
.action-entry.selected {
|
.action-entry.selected {
|
||||||
color: white;
|
color: white;
|
||||||
background-color: var(--gray);
|
background-color: var(--gray);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-list:focus .action-entry.selected {
|
.action-entry.highlighted {
|
||||||
|
color: white;
|
||||||
|
background-color: var(--light-gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-list-content:focus .action-entry.selected {
|
||||||
background-color: var(--blue);
|
background-color: var(--blue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-entry.highlighted > div,
|
||||||
.action-entry.selected > div {
|
.action-entry.selected > div {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
import { ActionEntry } from '../../../server/trace/viewer/traceModel';
|
import { ActionEntry } from '../../../server/trace/viewer/traceModel';
|
||||||
import './actionList.css';
|
import './actionList.css';
|
||||||
|
import './tabbedPane.css';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
export interface ActionListProps {
|
export interface ActionListProps {
|
||||||
@ -33,39 +34,57 @@ export const ActionList: React.FC<ActionListProps> = ({
|
|||||||
onSelected = () => {},
|
onSelected = () => {},
|
||||||
onHighlighted = () => {},
|
onHighlighted = () => {},
|
||||||
}) => {
|
}) => {
|
||||||
return <div
|
const actionListRef = React.createRef<HTMLDivElement>();
|
||||||
className='action-list'
|
|
||||||
tabIndex={0}
|
React.useEffect(() => {
|
||||||
onKeyDown={event => {
|
actionListRef.current?.focus();
|
||||||
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp')
|
}, [selectedAction]);
|
||||||
return;
|
|
||||||
const index = selectedAction ? actions.indexOf(selectedAction) : -1;
|
return <div className='action-list vbox'>
|
||||||
if (event.key === 'ArrowDown') {
|
<div className='.action-list-title tab-strip'>
|
||||||
if (index === -1)
|
<div className='tab-element'>
|
||||||
onSelected(actions[0]);
|
<div className='tab-label'>Actions</div>
|
||||||
else
|
</div>
|
||||||
onSelected(actions[Math.min(index + 1, actions.length - 1)]);
|
</div>
|
||||||
}
|
<div
|
||||||
if (event.key === 'ArrowUp') {
|
className='action-list-content'
|
||||||
if (index === -1)
|
tabIndex={0}
|
||||||
onSelected(actions[actions.length - 1]);
|
onKeyDown={event => {
|
||||||
else
|
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp')
|
||||||
onSelected(actions[Math.max(index - 1, 0)]);
|
return;
|
||||||
}
|
const index = selectedAction ? actions.indexOf(selectedAction) : -1;
|
||||||
}}
|
if (event.key === 'ArrowDown') {
|
||||||
>{actions.map((actionEntry, index) => {
|
if (index === -1)
|
||||||
const { metadata, actionId } = actionEntry;
|
onSelected(actions[0]);
|
||||||
return <div
|
else
|
||||||
className={'action-entry' + (actionEntry === selectedAction ? ' selected' : '')}
|
onSelected(actions[Math.min(index + 1, actions.length - 1)]);
|
||||||
key={actionId}
|
}
|
||||||
onClick={() => onSelected(actionEntry)}
|
if (event.key === 'ArrowUp') {
|
||||||
onMouseEnter={() => onHighlighted(actionEntry)}
|
if (index === -1)
|
||||||
onMouseLeave={() => (highlightedAction === actionEntry) && onHighlighted(undefined)}
|
onSelected(actions[actions.length - 1]);
|
||||||
|
else
|
||||||
|
onSelected(actions[Math.max(index - 1, 0)]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
ref={actionListRef}
|
||||||
>
|
>
|
||||||
<div className={'action-error codicon codicon-issues'} hidden={!metadata.error} />
|
{actions.map(actionEntry => {
|
||||||
<div className='action-title'>{metadata.apiName}</div>
|
const { metadata, actionId } = actionEntry;
|
||||||
{metadata.params.selector && <div className='action-selector' title={metadata.params.selector}>{metadata.params.selector}</div>}
|
const selectedSuffix = actionEntry === selectedAction ? ' selected' : '';
|
||||||
{metadata.method === 'goto' && metadata.params.url && <div className='action-url' title={metadata.params.url}>{metadata.params.url}</div>}
|
const highlightedSuffix = actionEntry === highlightedAction ? ' highlighted' : '';
|
||||||
</div>;
|
return <div
|
||||||
})}</div>;
|
className={'action-entry' + selectedSuffix + highlightedSuffix}
|
||||||
|
key={actionId}
|
||||||
|
onClick={() => onSelected(actionEntry)}
|
||||||
|
onMouseEnter={() => onHighlighted(actionEntry)}
|
||||||
|
onMouseLeave={() => (highlightedAction === actionEntry) && onHighlighted(undefined)}
|
||||||
|
>
|
||||||
|
<div className={'action-error codicon codicon-issues'} hidden={!metadata.error} />
|
||||||
|
<div className='action-title'>{metadata.apiName}</div>
|
||||||
|
{metadata.params.selector && <div className='action-selector' title={metadata.params.selector}>{metadata.params.selector}</div>}
|
||||||
|
{metadata.method === 'goto' && metadata.params.url && <div className='action-url' title={metadata.params.url}>{metadata.params.url}</div>}
|
||||||
|
</div>;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
};
|
};
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
white-space: pre;
|
white-space: pre;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding-top: 3px;
|
padding-top: 3px;
|
||||||
|
user-select: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-line {
|
.log-line {
|
||||||
|
@ -17,17 +17,20 @@
|
|||||||
.snapshot-tab {
|
.snapshot-tab {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: auto;
|
flex: auto;
|
||||||
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
outline: none;
|
outline: none;
|
||||||
padding-right: 60px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.snapshot-controls {
|
.snapshot-controls {
|
||||||
flex: 0 0 60px;
|
flex: none;
|
||||||
|
color: var(--toolbar-color);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
box-shadow: var(--box-shadow);
|
||||||
|
background-color: var(--toolbar-bg-color);
|
||||||
|
height: 32px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 5px 0 0 5px;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.snapshot-toggle {
|
.snapshot-toggle {
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
import { ActionEntry } from '../../../server/trace/viewer/traceModel';
|
import { ActionEntry } from '../../../server/trace/viewer/traceModel';
|
||||||
import { Size } from '../geometry';
|
import { Size } from '../geometry';
|
||||||
import './snapshotTab.css';
|
import './snapshotTab.css';
|
||||||
|
import './tabbedPane.css';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useMeasure } from './helpers';
|
import { useMeasure } from './helpers';
|
||||||
import type { Point } from '../../../common/types';
|
import type { Point } from '../../../common/types';
|
||||||
@ -26,9 +27,16 @@ export const SnapshotTab: React.FunctionComponent<{
|
|||||||
snapshotSize: Size,
|
snapshotSize: Size,
|
||||||
}> = ({ actionEntry, snapshotSize }) => {
|
}> = ({ actionEntry, snapshotSize }) => {
|
||||||
const [measure, ref] = useMeasure<HTMLDivElement>();
|
const [measure, ref] = useMeasure<HTMLDivElement>();
|
||||||
const [snapshotIndex, setSnapshotIndex] = React.useState(0);
|
let [snapshotIndex, setSnapshotIndex] = React.useState(0);
|
||||||
|
|
||||||
const snapshots = actionEntry ? (actionEntry.snapshots || []) : [];
|
const snapshotMap = new Map<string, { title: string, snapshotName: string }>();
|
||||||
|
for (const snapshot of actionEntry?.snapshots || [])
|
||||||
|
snapshotMap.set(snapshot.title, snapshot);
|
||||||
|
const actionSnapshot = snapshotMap.get('action') || snapshotMap.get('before');
|
||||||
|
const snapshots = [actionSnapshot ? { ...actionSnapshot, title: 'action' } : undefined, snapshotMap.get('before'), snapshotMap.get('after')].filter(Boolean) as { title: string, snapshotName: string }[];
|
||||||
|
|
||||||
|
if (snapshotIndex >= snapshots.length)
|
||||||
|
snapshotIndex = snapshots.length - 1;
|
||||||
|
|
||||||
const iframeRef = React.createRef<HTMLIFrameElement>();
|
const iframeRef = React.createRef<HTMLIFrameElement>();
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@ -60,21 +68,20 @@ export const SnapshotTab: React.FunctionComponent<{
|
|||||||
className='snapshot-tab'
|
className='snapshot-tab'
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onKeyDown={event => {
|
onKeyDown={event => {
|
||||||
if (event.key === 'ArrowDown')
|
if (event.key === 'ArrowRight')
|
||||||
setSnapshotIndex(Math.min(snapshotIndex + 1, snapshots.length - 1));
|
setSnapshotIndex(Math.min(snapshotIndex + 1, snapshots.length - 1));
|
||||||
if (event.key === 'ArrowUp')
|
if (event.key === 'ArrowLeft')
|
||||||
setSnapshotIndex(Math.max(snapshotIndex - 1, 0));
|
setSnapshotIndex(Math.max(snapshotIndex - 1, 0));
|
||||||
}}
|
}}
|
||||||
><div className='snapshot-controls'>
|
><div className='tab-strip'>
|
||||||
{snapshots.map((snapshot, index) => {
|
{snapshots.map((snapshot, index) => {
|
||||||
return <div
|
return <div className={'tab-element ' + (snapshotIndex === index ? ' selected' : '')}
|
||||||
key={snapshot.title}
|
onClick={() => setSnapshotIndex(index)}
|
||||||
className={'snapshot-toggle' + (snapshotIndex === index ? ' toggled' : '')}
|
key={snapshot.title}>
|
||||||
onClick={() => setSnapshotIndex(index)}>
|
<div className='tab-label'>{renderTitle(snapshot.title)}</div>
|
||||||
{snapshot.title}
|
|
||||||
</div>
|
</div>
|
||||||
})
|
})}
|
||||||
}</div>
|
</div>
|
||||||
<div ref={ref} className='snapshot-wrapper'>
|
<div ref={ref} className='snapshot-wrapper'>
|
||||||
<div className='snapshot-container' style={{
|
<div className='snapshot-container' style={{
|
||||||
width: snapshotSize.width + 'px',
|
width: snapshotSize.width + 'px',
|
||||||
@ -86,3 +93,13 @@ export const SnapshotTab: React.FunctionComponent<{
|
|||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function renderTitle(snapshotTitle: string): string {
|
||||||
|
if (snapshotTitle === 'before')
|
||||||
|
return 'Before';
|
||||||
|
if (snapshotTitle === 'after')
|
||||||
|
return 'After';
|
||||||
|
if (snapshotTitle === 'action')
|
||||||
|
return 'Action';
|
||||||
|
return snapshotTitle;
|
||||||
|
}
|
||||||
|
@ -78,7 +78,7 @@ export const SourceTab: React.FunctionComponent<{
|
|||||||
}
|
}
|
||||||
}, [needReveal, targetLineRef]);
|
}, [needReveal, targetLineRef]);
|
||||||
|
|
||||||
return <SplitView sidebarSize={250} orientation='horizontal'>
|
return <SplitView sidebarSize={100} orientation='vertical'>
|
||||||
<SourceView text={content} language='javascript' highlight={[{ line: targetLine, type: 'running' }]} revealLine={targetLine}></SourceView>
|
<SourceView text={content} language='javascript' highlight={[{ line: targetLine, type: 'running' }]} revealLine={targetLine}></SourceView>
|
||||||
<StackTraceView actionEntry={actionEntry} selectedFrame={selectedFrame} setSelectedFrame={setSelectedFrame}></StackTraceView>
|
<StackTraceView actionEntry={actionEntry} selectedFrame={selectedFrame} setSelectedFrame={setSelectedFrame}></StackTraceView>
|
||||||
</SplitView>;
|
</SplitView>;
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
box-shadow: var(--box-shadow);
|
box-shadow: var(--box-shadow);
|
||||||
background-color: var(--toolbar-bg-color);
|
background-color: var(--toolbar-bg-color);
|
||||||
height: 40px;
|
height: 32px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
flex: none;
|
flex: none;
|
||||||
|
@ -20,7 +20,6 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
border-bottom: 1px solid #ddd;
|
|
||||||
padding: 20px 0 5px;
|
padding: 20px 0 5px;
|
||||||
cursor: text;
|
cursor: text;
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,8 @@ export const Timeline: React.FunctionComponent<{
|
|||||||
selectedAction: ActionEntry | undefined,
|
selectedAction: ActionEntry | undefined,
|
||||||
highlightedAction: ActionEntry | undefined,
|
highlightedAction: ActionEntry | undefined,
|
||||||
onSelected: (action: ActionEntry) => void,
|
onSelected: (action: ActionEntry) => void,
|
||||||
}> = ({ context, boundaries, selectedAction, highlightedAction, onSelected }) => {
|
onHighlighted: (action: ActionEntry | undefined) => void,
|
||||||
|
}> = ({ context, boundaries, selectedAction, highlightedAction, onSelected, onHighlighted }) => {
|
||||||
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 [hoveredBarIndex, setHoveredBarIndex] = React.useState<number | undefined>();
|
const [hoveredBarIndex, setHoveredBarIndex] = React.useState<number | undefined>();
|
||||||
@ -144,13 +145,21 @@ export const Timeline: React.FunctionComponent<{
|
|||||||
if (!ref.current)
|
if (!ref.current)
|
||||||
return;
|
return;
|
||||||
const x = event.clientX - ref.current.getBoundingClientRect().left;
|
const x = event.clientX - ref.current.getBoundingClientRect().left;
|
||||||
|
const index = findHoveredBarIndex(x);
|
||||||
setPreviewX(x);
|
setPreviewX(x);
|
||||||
setHoveredBarIndex(findHoveredBarIndex(x));
|
setHoveredBarIndex(index);
|
||||||
|
if (typeof index === 'number')
|
||||||
|
onHighlighted(bars[index].entry);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onMouseLeave = () => {
|
const onMouseLeave = () => {
|
||||||
setPreviewX(undefined);
|
setPreviewX(undefined);
|
||||||
|
setHoveredBarIndex(undefined);
|
||||||
|
onHighlighted(undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onClick = (event: React.MouseEvent) => {
|
const onClick = (event: React.MouseEvent) => {
|
||||||
|
setPreviewX(undefined);
|
||||||
if (!ref.current)
|
if (!ref.current)
|
||||||
return;
|
return;
|
||||||
const x = event.clientX - ref.current.getBoundingClientRect().left;
|
const x = event.clientX - ref.current.getBoundingClientRect().left;
|
||||||
|
@ -64,17 +64,18 @@ export const Workbench: React.FunctionComponent<{
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ background: 'white', paddingLeft: '20px', flex: 'none' }}>
|
<div style={{ background: 'white', paddingLeft: '20px', flex: 'none', borderBottom: '1px solid #ddd' }}>
|
||||||
<Timeline
|
<Timeline
|
||||||
context={context}
|
context={context}
|
||||||
boundaries={boundaries}
|
boundaries={boundaries}
|
||||||
selectedAction={selectedAction}
|
selectedAction={selectedAction}
|
||||||
highlightedAction={highlightedAction}
|
highlightedAction={highlightedAction}
|
||||||
onSelected={action => setSelectedAction(action)}
|
onSelected={action => setSelectedAction(action)}
|
||||||
|
onHighlighted={action => setHighlightedAction(action)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<SplitView sidebarSize={250} orientation='horizontal' sidebarIsFirst={true}>
|
<SplitView sidebarSize={300} orientation='horizontal' sidebarIsFirst={true}>
|
||||||
<SplitView sidebarSize={250}>
|
<SplitView sidebarSize={300} orientation='horizontal'>
|
||||||
<SnapshotTab actionEntry={selectedAction} snapshotSize={snapshotSize} />
|
<SnapshotTab actionEntry={selectedAction} snapshotSize={snapshotSize} />
|
||||||
<TabbedPane tabs={[
|
<TabbedPane tabs={[
|
||||||
{ id: 'logs', title: 'Log', render: () => <LogsTab actionEntry={selectedAction} /> },
|
{ id: 'logs', title: 'Log', render: () => <LogsTab actionEntry={selectedAction} /> },
|
||||||
|
Loading…
Reference in New Issue
Block a user