feat(trace viewer): small improvements (#5007)
- Show logs. - Show errors. - Highlight actions.
This commit is contained in:
parent
7701176b0a
commit
b45905ae3f
|
|
@ -66,6 +66,13 @@
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-header .action-error {
|
||||||
|
color: red;
|
||||||
|
top: 2px;
|
||||||
|
position: relative;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.action-selector {
|
.action-selector {
|
||||||
display: inline;
|
display: inline;
|
||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
|
|
|
||||||
|
|
@ -20,16 +20,23 @@ import * as React from 'react';
|
||||||
|
|
||||||
export const ActionList: React.FunctionComponent<{
|
export const ActionList: React.FunctionComponent<{
|
||||||
actions: ActionEntry[],
|
actions: ActionEntry[],
|
||||||
selectedAction?: ActionEntry,
|
selectedAction: ActionEntry | undefined,
|
||||||
|
highlightedAction: ActionEntry | undefined,
|
||||||
onSelected: (action: ActionEntry) => void,
|
onSelected: (action: ActionEntry) => void,
|
||||||
}> = ({ actions, selectedAction, onSelected }) => {
|
onHighlighted: (action: ActionEntry | undefined) => void,
|
||||||
|
}> = ({ actions, selectedAction, highlightedAction, onSelected, onHighlighted }) => {
|
||||||
|
const targetAction = highlightedAction || selectedAction;
|
||||||
return <div className='action-list'>{actions.map(actionEntry => {
|
return <div className='action-list'>{actions.map(actionEntry => {
|
||||||
const { action, actionId } = actionEntry;
|
const { action, actionId } = actionEntry;
|
||||||
return <div
|
return <div
|
||||||
className={'action-entry' + (actionEntry === selectedAction ? ' selected' : '')}
|
className={'action-entry' + (actionEntry === targetAction ? ' selected' : '')}
|
||||||
key={actionId}
|
key={actionId}
|
||||||
onClick={() => onSelected(actionEntry)}>
|
onClick={() => onSelected(actionEntry)}
|
||||||
|
onMouseEnter={() => onHighlighted(actionEntry)}
|
||||||
|
onMouseLeave={() => (highlightedAction === actionEntry) && onHighlighted(undefined)}
|
||||||
|
>
|
||||||
<div className='action-header'>
|
<div className='action-header'>
|
||||||
|
<div className={'action-error codicon codicon-issues'} hidden={!actionEntry.action.error} />
|
||||||
<div className='action-title'>{action.action}</div>
|
<div className='action-title'>{action.action}</div>
|
||||||
{action.selector && <div className='action-selector' title={action.selector}>{action.selector}</div>}
|
{action.selector && <div className='action-selector' title={action.selector}>{action.selector}</div>}
|
||||||
{action.action === 'goto' && action.value && <div className='action-url' title={action.value}>{action.value}</div>}
|
{action.action === 'goto' && action.value && <div className='action-url' title={action.value}>{action.value}</div>}
|
||||||
|
|
|
||||||
29
src/cli/traceViewer/web/ui/logsTab.css
Normal file
29
src/cli/traceViewer/web/ui/logsTab.css
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.logs-tab {
|
||||||
|
flex: auto;
|
||||||
|
position: relative;
|
||||||
|
overflow: auto;
|
||||||
|
background: #fdfcfc;
|
||||||
|
font-family: var(--monospace-font);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-line {
|
||||||
|
margin: 0 10px;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
37
src/cli/traceViewer/web/ui/logsTab.tsx
Normal file
37
src/cli/traceViewer/web/ui/logsTab.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
/**
|
||||||
|
* 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 '../../traceModel';
|
||||||
|
import * as React from 'react';
|
||||||
|
import './logsTab.css';
|
||||||
|
|
||||||
|
export const LogsTab: React.FunctionComponent<{
|
||||||
|
actionEntry: ActionEntry | undefined,
|
||||||
|
}> = ({ actionEntry }) => {
|
||||||
|
let logs: string[] = [];
|
||||||
|
if (actionEntry) {
|
||||||
|
logs = actionEntry.action.logs || [];
|
||||||
|
if (actionEntry.action.error)
|
||||||
|
logs = [actionEntry.action.error, ...logs];
|
||||||
|
}
|
||||||
|
return <div className='logs-tab'>{
|
||||||
|
logs.map((logLine, index) => {
|
||||||
|
return <div key={index} className='log-line'>
|
||||||
|
{logLine}
|
||||||
|
</div>;
|
||||||
|
})
|
||||||
|
}</div>;
|
||||||
|
};
|
||||||
|
|
@ -21,12 +21,13 @@ import { SourceTab } from './sourceTab';
|
||||||
import './propertiesTabbedPane.css';
|
import './propertiesTabbedPane.css';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useMeasure } from './helpers';
|
import { useMeasure } from './helpers';
|
||||||
|
import { LogsTab } from './logsTab';
|
||||||
|
|
||||||
export const PropertiesTabbedPane: React.FunctionComponent<{
|
export const PropertiesTabbedPane: React.FunctionComponent<{
|
||||||
actionEntry: ActionEntry | undefined,
|
actionEntry: ActionEntry | undefined,
|
||||||
snapshotSize: Size,
|
snapshotSize: Size,
|
||||||
}> = ({ actionEntry, snapshotSize }) => {
|
}> = ({ actionEntry, snapshotSize }) => {
|
||||||
const [selected, setSelected] = React.useState<'snapshot' | 'source' | 'network'>('snapshot');
|
const [selected, setSelected] = React.useState<'snapshot' | 'source' | 'network' | 'logs'>('snapshot');
|
||||||
return <div className='properties-tabbed-pane'>
|
return <div className='properties-tabbed-pane'>
|
||||||
<div className='vbox'>
|
<div className='vbox'>
|
||||||
<div className='hbox' style={{ flex: 'none' }}>
|
<div className='hbox' style={{ flex: 'none' }}>
|
||||||
|
|
@ -43,6 +44,10 @@ export const PropertiesTabbedPane: React.FunctionComponent<{
|
||||||
onClick={() => setSelected('network')}>
|
onClick={() => setSelected('network')}>
|
||||||
<div className='properties-tab-label'>Network</div>
|
<div className='properties-tab-label'>Network</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className={'properties-tab-element ' + (selected === 'logs' ? 'selected' : '')}
|
||||||
|
onClick={() => setSelected('logs')}>
|
||||||
|
<div className='properties-tab-label'>Logs</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='properties-tab-content' style={{ display: selected === 'snapshot' ? 'flex' : 'none' }}>
|
<div className='properties-tab-content' style={{ display: selected === 'snapshot' ? 'flex' : 'none' }}>
|
||||||
|
|
@ -54,6 +59,9 @@ export const PropertiesTabbedPane: React.FunctionComponent<{
|
||||||
<div className='properties-tab-content' style={{ display: selected === 'network' ? 'flex' : 'none' }}>
|
<div className='properties-tab-content' style={{ display: selected === 'network' ? 'flex' : 'none' }}>
|
||||||
<NetworkTab actionEntry={actionEntry} />
|
<NetworkTab actionEntry={actionEntry} />
|
||||||
</div>
|
</div>
|
||||||
|
<div className='properties-tab-content' style={{ display: selected === 'logs' ? 'flex' : 'none' }}>
|
||||||
|
<LogsTab actionEntry={actionEntry} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@
|
||||||
|
|
||||||
.timeline-lane.timeline-actions {
|
.timeline-lane.timeline-actions {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-action {
|
.timeline-action {
|
||||||
|
|
@ -72,19 +73,26 @@
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background-color: red;
|
background-color: red;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
--action-color: 'transparent';
|
||||||
|
background-color: var(--action-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-action.selected {
|
||||||
|
filter: brightness(70%);
|
||||||
|
box-shadow: 0 0 0 1px var(--action-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-action.click {
|
.timeline-action.click {
|
||||||
background-color: var(--green);
|
--action-color: var(--green);
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-action.fill,
|
.timeline-action.fill,
|
||||||
.timeline-action.press {
|
.timeline-action.press {
|
||||||
background-color: var(--orange);
|
--action-color: var(--orange);
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-action.goto {
|
.timeline-action.goto {
|
||||||
background-color: var(--blue);
|
--action-color: var(--blue);
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-action-label {
|
.timeline-action-label {
|
||||||
|
|
@ -93,6 +101,12 @@
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
background-color: #fffffff0;
|
background-color: #fffffff0;
|
||||||
|
justify-content: center;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-action-label.selected {
|
||||||
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-time-bar {
|
.timeline-time-bar {
|
||||||
|
|
|
||||||
|
|
@ -21,19 +21,25 @@ import { FilmStrip } from './filmStrip';
|
||||||
import { Boundaries } from '../geometry';
|
import { Boundaries } from '../geometry';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useMeasure } from './helpers';
|
import { useMeasure } from './helpers';
|
||||||
|
import { ActionEntry } from '../../traceModel';
|
||||||
|
|
||||||
export const Timeline: React.FunctionComponent<{
|
export const Timeline: React.FunctionComponent<{
|
||||||
context: ContextEntry,
|
context: ContextEntry,
|
||||||
boundaries: Boundaries,
|
boundaries: Boundaries,
|
||||||
}> = ({ context, boundaries }) => {
|
selectedAction: ActionEntry | undefined,
|
||||||
|
highlightedAction: ActionEntry | undefined,
|
||||||
|
onSelected: (action: ActionEntry) => void,
|
||||||
|
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 targetAction = highlightedAction || selectedAction;
|
||||||
|
|
||||||
const offsets = React.useMemo(() => {
|
const offsets = React.useMemo(() => {
|
||||||
return calculateDividerOffsets(measure.width, boundaries);
|
return calculateDividerOffsets(measure.width, boundaries);
|
||||||
}, [measure.width, boundaries]);
|
}, [measure.width, boundaries]);
|
||||||
const actionEntries = React.useMemo(() => {
|
const actionEntries = React.useMemo(() => {
|
||||||
const actions = [];
|
const actions: ActionEntry[] = [];
|
||||||
for (const page of context.pages)
|
for (const page of context.pages)
|
||||||
actions.push(...page.actions);
|
actions.push(...page.actions);
|
||||||
return actions;
|
return actions;
|
||||||
|
|
@ -41,23 +47,52 @@ export const Timeline: React.FunctionComponent<{
|
||||||
const actionTimes = React.useMemo(() => {
|
const actionTimes = React.useMemo(() => {
|
||||||
return actionEntries.map(entry => {
|
return actionEntries.map(entry => {
|
||||||
return {
|
return {
|
||||||
action: entry.action,
|
entry,
|
||||||
actionId: entry.actionId,
|
|
||||||
left: timeToPercent(measure.width, boundaries, entry.action.startTime!),
|
left: timeToPercent(measure.width, boundaries, entry.action.startTime!),
|
||||||
right: timeToPercent(measure.width, boundaries, entry.action.endTime!),
|
right: timeToPercent(measure.width, boundaries, entry.action.endTime!),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, [actionEntries, boundaries, measure.width]);
|
}, [actionEntries, boundaries, measure.width]);
|
||||||
|
|
||||||
|
const findHoveredAction = (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 entry: ActionEntry | undefined;
|
||||||
|
let distance: number | undefined;
|
||||||
|
for (const e of actionEntries) {
|
||||||
|
const left = Math.max(e.action.startTime!, time1);
|
||||||
|
const right = Math.min(e.action.endTime!, time2);
|
||||||
|
const middle = (e.action.startTime! + e.action.endTime!) / 2;
|
||||||
|
const d = Math.abs(time - middle);
|
||||||
|
if (left <= right && (!entry || d < distance!)) {
|
||||||
|
entry = e;
|
||||||
|
distance = d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
};
|
||||||
|
|
||||||
const onMouseMove = (event: React.MouseEvent) => {
|
const onMouseMove = (event: React.MouseEvent) => {
|
||||||
if (ref.current)
|
if (ref.current) {
|
||||||
setPreviewX(event.clientX - ref.current.getBoundingClientRect().left);
|
const x = event.clientX - ref.current.getBoundingClientRect().left;
|
||||||
|
setPreviewX(x);
|
||||||
|
onHighlighted(findHoveredAction(x));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const onMouseLeave = () => {
|
const onMouseLeave = () => {
|
||||||
setPreviewX(undefined);
|
setPreviewX(undefined);
|
||||||
};
|
};
|
||||||
|
const onClick = (event: React.MouseEvent) => {
|
||||||
|
if (ref.current) {
|
||||||
|
const x = event.clientX - ref.current.getBoundingClientRect().left;
|
||||||
|
const entry = findHoveredAction(x);
|
||||||
|
if (entry)
|
||||||
|
onSelected(entry);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return <div ref={ref} className='timeline-view' onMouseMove={onMouseMove} onMouseOver={onMouseMove} onMouseLeave={onMouseLeave}>
|
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.percent + '%' }}>
|
return <div key={index} className='timeline-divider' style={{ left: offset.percent + '%' }}>
|
||||||
|
|
@ -66,19 +101,22 @@ export const Timeline: React.FunctionComponent<{
|
||||||
})
|
})
|
||||||
}</div>
|
}</div>
|
||||||
<div className='timeline-lane timeline-action-labels'>{
|
<div className='timeline-lane timeline-action-labels'>{
|
||||||
actionTimes.map(({ action, actionId, left }) => {
|
actionTimes.map(({ entry, left, right }) => {
|
||||||
return <div key={actionId}
|
return <div key={entry.actionId}
|
||||||
className={'timeline-action-label ' + action.action}
|
className={'timeline-action-label ' + entry.action.action + (targetAction === entry ? ' selected' : '')}
|
||||||
style={{ left: left + '%' }}
|
style={{
|
||||||
|
left: left + '%',
|
||||||
|
width: (right - left) + '%',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{action.action}
|
{entry.action.action}
|
||||||
</div>;
|
</div>;
|
||||||
})
|
})
|
||||||
}</div>
|
}</div>
|
||||||
<div className='timeline-lane timeline-actions'>{
|
<div className='timeline-lane timeline-actions'>{
|
||||||
actionTimes.map(({ action, actionId, left, right }) => {
|
actionTimes.map(({ entry, left, right }) => {
|
||||||
return <div key={actionId}
|
return <div key={entry.actionId}
|
||||||
className={'timeline-action ' + action.action}
|
className={'timeline-action ' + entry.action.action + (targetAction === entry ? ' selected' : '')}
|
||||||
style={{
|
style={{
|
||||||
left: left + '%',
|
left: left + '%',
|
||||||
width: (right - left) + '%',
|
width: (right - left) + '%',
|
||||||
|
|
@ -129,6 +167,11 @@ function timeToPercent(clientWidth: number, boundaries: Boundaries, time: number
|
||||||
return 100 * position / clientWidth;
|
return 100 * position / clientWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function positionToTime(clientWidth: number, boundaries: Boundaries, x: number): number {
|
||||||
|
const percent = x / clientWidth;
|
||||||
|
return percent * (boundaries.maximum - boundaries.minimum) + boundaries.minimum;
|
||||||
|
}
|
||||||
|
|
||||||
function msToString(ms: number): string {
|
function msToString(ms: number): string {
|
||||||
if (!isFinite(ms))
|
if (!isFinite(ms))
|
||||||
return '-';
|
return '-';
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,8 @@ export const Workbench: React.FunctionComponent<{
|
||||||
traceModel: TraceModel,
|
traceModel: TraceModel,
|
||||||
}> = ({ traceModel }) => {
|
}> = ({ traceModel }) => {
|
||||||
const [context, setContext] = React.useState(traceModel.contexts[0]);
|
const [context, setContext] = React.useState(traceModel.contexts[0]);
|
||||||
const [action, setAction] = React.useState<ActionEntry | undefined>();
|
const [selectedAction, setSelectedAction] = React.useState<ActionEntry | undefined>();
|
||||||
|
const [highlightedAction, setHighlightedAction] = React.useState<ActionEntry | undefined>();
|
||||||
|
|
||||||
const actions = React.useMemo(() => {
|
const actions = React.useMemo(() => {
|
||||||
const actions: ActionEntry[] = [];
|
const actions: ActionEntry[] = [];
|
||||||
|
|
@ -47,7 +48,7 @@ export const Workbench: React.FunctionComponent<{
|
||||||
context={context}
|
context={context}
|
||||||
onChange={context => {
|
onChange={context => {
|
||||||
setContext(context);
|
setContext(context);
|
||||||
setAction(undefined);
|
setSelectedAction(undefined);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -55,13 +56,23 @@ export const Workbench: React.FunctionComponent<{
|
||||||
<Timeline
|
<Timeline
|
||||||
context={context}
|
context={context}
|
||||||
boundaries={{ minimum: context.startTime, maximum: context.endTime }}
|
boundaries={{ minimum: context.startTime, maximum: context.endTime }}
|
||||||
/>
|
selectedAction={selectedAction}
|
||||||
|
highlightedAction={highlightedAction}
|
||||||
|
onSelected={action => setSelectedAction(action)}
|
||||||
|
onHighlighted={action => setHighlightedAction(action)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='hbox'>
|
<div className='hbox'>
|
||||||
<div style={{ display: 'flex', flex: 'none' }}>
|
<div style={{ display: 'flex', flex: 'none' }}>
|
||||||
<ActionList actions={actions} selectedAction={action} onSelected={action => setAction(action)} />
|
<ActionList
|
||||||
|
actions={actions}
|
||||||
|
selectedAction={selectedAction}
|
||||||
|
highlightedAction={highlightedAction}
|
||||||
|
onSelected={action => setSelectedAction(action)}
|
||||||
|
onHighlighted={action => setHighlightedAction(action)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<PropertiesTabbedPane actionEntry={action} snapshotSize={snapshotSize} />
|
<PropertiesTabbedPane actionEntry={selectedAction} snapshotSize={snapshotSize} />
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ const path = require('path');
|
||||||
const HtmlWebPackPlugin = require('html-webpack-plugin');
|
const HtmlWebPackPlugin = require('html-webpack-plugin');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
mode: 'production',
|
mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
|
||||||
entry: {
|
entry: {
|
||||||
app: path.join(__dirname, 'index.tsx'),
|
app: path.join(__dirname, 'index.tsx'),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue