Skip to content

Commit b45905a

Browse files
authored
feat(trace viewer): small improvements (#5007)
- Show logs. - Show errors. - Highlight actions.
1 parent 7701176 commit b45905a

File tree

9 files changed

+185
-29
lines changed

9 files changed

+185
-29
lines changed

src/cli/traceViewer/web/ui/actionList.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,13 @@
6666
white-space: nowrap;
6767
}
6868

69+
.action-header .action-error {
70+
color: red;
71+
top: 2px;
72+
position: relative;
73+
margin-right: 2px;
74+
}
75+
6976
.action-selector {
7077
display: inline;
7178
padding-left: 5px;

src/cli/traceViewer/web/ui/actionList.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,23 @@ import * as React from 'react';
2020

2121
export const ActionList: React.FunctionComponent<{
2222
actions: ActionEntry[],
23-
selectedAction?: ActionEntry,
23+
selectedAction: ActionEntry | undefined,
24+
highlightedAction: ActionEntry | undefined,
2425
onSelected: (action: ActionEntry) => void,
25-
}> = ({ actions, selectedAction, onSelected }) => {
26+
onHighlighted: (action: ActionEntry | undefined) => void,
27+
}> = ({ actions, selectedAction, highlightedAction, onSelected, onHighlighted }) => {
28+
const targetAction = highlightedAction || selectedAction;
2629
return <div className='action-list'>{actions.map(actionEntry => {
2730
const { action, actionId } = actionEntry;
2831
return <div
29-
className={'action-entry' + (actionEntry === selectedAction ? ' selected' : '')}
32+
className={'action-entry' + (actionEntry === targetAction ? ' selected' : '')}
3033
key={actionId}
31-
onClick={() => onSelected(actionEntry)}>
34+
onClick={() => onSelected(actionEntry)}
35+
onMouseEnter={() => onHighlighted(actionEntry)}
36+
onMouseLeave={() => (highlightedAction === actionEntry) && onHighlighted(undefined)}
37+
>
3238
<div className='action-header'>
39+
<div className={'action-error codicon codicon-issues'} hidden={!actionEntry.action.error} />
3340
<div className='action-title'>{action.action}</div>
3441
{action.selector && <div className='action-selector' title={action.selector}>{action.selector}</div>}
3542
{action.action === 'goto' && action.value && <div className='action-url' title={action.value}>{action.value}</div>}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
Copyright (c) Microsoft Corporation.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
.logs-tab {
18+
flex: auto;
19+
position: relative;
20+
overflow: auto;
21+
background: #fdfcfc;
22+
font-family: var(--monospace-font);
23+
white-space: nowrap;
24+
}
25+
26+
.log-line {
27+
margin: 0 10px;
28+
white-space: pre;
29+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { ActionEntry } from '../../traceModel';
18+
import * as React from 'react';
19+
import './logsTab.css';
20+
21+
export const LogsTab: React.FunctionComponent<{
22+
actionEntry: ActionEntry | undefined,
23+
}> = ({ actionEntry }) => {
24+
let logs: string[] = [];
25+
if (actionEntry) {
26+
logs = actionEntry.action.logs || [];
27+
if (actionEntry.action.error)
28+
logs = [actionEntry.action.error, ...logs];
29+
}
30+
return <div className='logs-tab'>{
31+
logs.map((logLine, index) => {
32+
return <div key={index} className='log-line'>
33+
{logLine}
34+
</div>;
35+
})
36+
}</div>;
37+
};

src/cli/traceViewer/web/ui/propertiesTabbedPane.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,13 @@ import { SourceTab } from './sourceTab';
2121
import './propertiesTabbedPane.css';
2222
import * as React from 'react';
2323
import { useMeasure } from './helpers';
24+
import { LogsTab } from './logsTab';
2425

2526
export const PropertiesTabbedPane: React.FunctionComponent<{
2627
actionEntry: ActionEntry | undefined,
2728
snapshotSize: Size,
2829
}> = ({ actionEntry, snapshotSize }) => {
29-
const [selected, setSelected] = React.useState<'snapshot' | 'source' | 'network'>('snapshot');
30+
const [selected, setSelected] = React.useState<'snapshot' | 'source' | 'network' | 'logs'>('snapshot');
3031
return <div className='properties-tabbed-pane'>
3132
<div className='vbox'>
3233
<div className='hbox' style={{ flex: 'none' }}>
@@ -43,6 +44,10 @@ export const PropertiesTabbedPane: React.FunctionComponent<{
4344
onClick={() => setSelected('network')}>
4445
<div className='properties-tab-label'>Network</div>
4546
</div>
47+
<div className={'properties-tab-element ' + (selected === 'logs' ? 'selected' : '')}
48+
onClick={() => setSelected('logs')}>
49+
<div className='properties-tab-label'>Logs</div>
50+
</div>
4651
</div>
4752
</div>
4853
<div className='properties-tab-content' style={{ display: selected === 'snapshot' ? 'flex' : 'none' }}>
@@ -54,6 +59,9 @@ export const PropertiesTabbedPane: React.FunctionComponent<{
5459
<div className='properties-tab-content' style={{ display: selected === 'network' ? 'flex' : 'none' }}>
5560
<NetworkTab actionEntry={actionEntry} />
5661
</div>
62+
<div className='properties-tab-content' style={{ display: selected === 'logs' ? 'flex' : 'none' }}>
63+
<LogsTab actionEntry={actionEntry} />
64+
</div>
5765
</div>
5866
</div>;
5967
};

src/cli/traceViewer/web/ui/timeline.css

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464

6565
.timeline-lane.timeline-actions {
6666
margin-bottom: 10px;
67+
overflow: visible;
6768
}
6869

6970
.timeline-action {
@@ -72,19 +73,26 @@
7273
bottom: 0;
7374
background-color: red;
7475
border-radius: 3px;
76+
--action-color: 'transparent';
77+
background-color: var(--action-color);
78+
}
79+
80+
.timeline-action.selected {
81+
filter: brightness(70%);
82+
box-shadow: 0 0 0 1px var(--action-color);
7583
}
7684

7785
.timeline-action.click {
78-
background-color: var(--green);
86+
--action-color: var(--green);
7987
}
8088

8189
.timeline-action.fill,
8290
.timeline-action.press {
83-
background-color: var(--orange);
91+
--action-color: var(--orange);
8492
}
8593

8694
.timeline-action.goto {
87-
background-color: var(--blue);
95+
--action-color: var(--blue);
8896
}
8997

9098
.timeline-action-label {
@@ -93,6 +101,12 @@
93101
bottom: 0;
94102
margin-left: 2px;
95103
background-color: #fffffff0;
104+
justify-content: center;
105+
display: none;
106+
}
107+
108+
.timeline-action-label.selected {
109+
display: flex;
96110
}
97111

98112
.timeline-time-bar {

src/cli/traceViewer/web/ui/timeline.tsx

Lines changed: 58 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,43 +21,78 @@ import { FilmStrip } from './filmStrip';
2121
import { Boundaries } from '../geometry';
2222
import * as React from 'react';
2323
import { useMeasure } from './helpers';
24+
import { ActionEntry } from '../../traceModel';
2425

2526
export const Timeline: React.FunctionComponent<{
2627
context: ContextEntry,
2728
boundaries: Boundaries,
28-
}> = ({ context, boundaries }) => {
29+
selectedAction: ActionEntry | undefined,
30+
highlightedAction: ActionEntry | undefined,
31+
onSelected: (action: ActionEntry) => void,
32+
onHighlighted: (action: ActionEntry | undefined) => void,
33+
}> = ({ context, boundaries, selectedAction, highlightedAction, onSelected, onHighlighted }) => {
2934
const [measure, ref] = useMeasure<HTMLDivElement>();
3035
const [previewX, setPreviewX] = React.useState<number | undefined>();
36+
const targetAction = highlightedAction || selectedAction;
3137

3238
const offsets = React.useMemo(() => {
3339
return calculateDividerOffsets(measure.width, boundaries);
3440
}, [measure.width, boundaries]);
3541
const actionEntries = React.useMemo(() => {
36-
const actions = [];
42+
const actions: ActionEntry[] = [];
3743
for (const page of context.pages)
3844
actions.push(...page.actions);
3945
return actions;
4046
}, [context]);
4147
const actionTimes = React.useMemo(() => {
4248
return actionEntries.map(entry => {
4349
return {
44-
action: entry.action,
45-
actionId: entry.actionId,
50+
entry,
4651
left: timeToPercent(measure.width, boundaries, entry.action.startTime!),
4752
right: timeToPercent(measure.width, boundaries, entry.action.endTime!),
4853
};
4954
});
5055
}, [actionEntries, boundaries, measure.width]);
5156

57+
const findHoveredAction = (x: number) => {
58+
const time = positionToTime(measure.width, boundaries, x);
59+
const time1 = positionToTime(measure.width, boundaries, x - 5);
60+
const time2 = positionToTime(measure.width, boundaries, x + 5);
61+
let entry: ActionEntry | undefined;
62+
let distance: number | undefined;
63+
for (const e of actionEntries) {
64+
const left = Math.max(e.action.startTime!, time1);
65+
const right = Math.min(e.action.endTime!, time2);
66+
const middle = (e.action.startTime! + e.action.endTime!) / 2;
67+
const d = Math.abs(time - middle);
68+
if (left <= right && (!entry || d < distance!)) {
69+
entry = e;
70+
distance = d;
71+
}
72+
}
73+
return entry;
74+
};
75+
5276
const onMouseMove = (event: React.MouseEvent) => {
53-
if (ref.current)
54-
setPreviewX(event.clientX - ref.current.getBoundingClientRect().left);
77+
if (ref.current) {
78+
const x = event.clientX - ref.current.getBoundingClientRect().left;
79+
setPreviewX(x);
80+
onHighlighted(findHoveredAction(x));
81+
}
5582
};
5683
const onMouseLeave = () => {
5784
setPreviewX(undefined);
5885
};
86+
const onClick = (event: React.MouseEvent) => {
87+
if (ref.current) {
88+
const x = event.clientX - ref.current.getBoundingClientRect().left;
89+
const entry = findHoveredAction(x);
90+
if (entry)
91+
onSelected(entry);
92+
}
93+
};
5994

60-
return <div ref={ref} className='timeline-view' onMouseMove={onMouseMove} onMouseOver={onMouseMove} onMouseLeave={onMouseLeave}>
95+
return <div ref={ref} className='timeline-view' onMouseMove={onMouseMove} onMouseOver={onMouseMove} onMouseLeave={onMouseLeave} onClick={onClick}>
6196
<div className='timeline-grid'>{
6297
offsets.map((offset, index) => {
6398
return <div key={index} className='timeline-divider' style={{ left: offset.percent + '%' }}>
@@ -66,19 +101,22 @@ export const Timeline: React.FunctionComponent<{
66101
})
67102
}</div>
68103
<div className='timeline-lane timeline-action-labels'>{
69-
actionTimes.map(({ action, actionId, left }) => {
70-
return <div key={actionId}
71-
className={'timeline-action-label ' + action.action}
72-
style={{ left: left + '%' }}
104+
actionTimes.map(({ entry, left, right }) => {
105+
return <div key={entry.actionId}
106+
className={'timeline-action-label ' + entry.action.action + (targetAction === entry ? ' selected' : '')}
107+
style={{
108+
left: left + '%',
109+
width: (right - left) + '%',
110+
}}
73111
>
74-
{action.action}
112+
{entry.action.action}
75113
</div>;
76114
})
77115
}</div>
78116
<div className='timeline-lane timeline-actions'>{
79-
actionTimes.map(({ action, actionId, left, right }) => {
80-
return <div key={actionId}
81-
className={'timeline-action ' + action.action}
117+
actionTimes.map(({ entry, left, right }) => {
118+
return <div key={entry.actionId}
119+
className={'timeline-action ' + entry.action.action + (targetAction === entry ? ' selected' : '')}
82120
style={{
83121
left: left + '%',
84122
width: (right - left) + '%',
@@ -129,6 +167,11 @@ function timeToPercent(clientWidth: number, boundaries: Boundaries, time: number
129167
return 100 * position / clientWidth;
130168
}
131169

170+
function positionToTime(clientWidth: number, boundaries: Boundaries, x: number): number {
171+
const percent = x / clientWidth;
172+
return percent * (boundaries.maximum - boundaries.minimum) + boundaries.minimum;
173+
}
174+
132175
function msToString(ms: number): string {
133176
if (!isFinite(ms))
134177
return '-';

src/cli/traceViewer/web/ui/workbench.tsx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ export const Workbench: React.FunctionComponent<{
2626
traceModel: TraceModel,
2727
}> = ({ traceModel }) => {
2828
const [context, setContext] = React.useState(traceModel.contexts[0]);
29-
const [action, setAction] = React.useState<ActionEntry | undefined>();
29+
const [selectedAction, setSelectedAction] = React.useState<ActionEntry | undefined>();
30+
const [highlightedAction, setHighlightedAction] = React.useState<ActionEntry | undefined>();
3031

3132
const actions = React.useMemo(() => {
3233
const actions: ActionEntry[] = [];
@@ -47,21 +48,31 @@ export const Workbench: React.FunctionComponent<{
4748
context={context}
4849
onChange={context => {
4950
setContext(context);
50-
setAction(undefined);
51+
setSelectedAction(undefined);
5152
}}
5253
/>
5354
</div>
5455
<div style={{ background: 'white', paddingLeft: '20px', flex: 'none' }}>
5556
<Timeline
5657
context={context}
5758
boundaries={{ minimum: context.startTime, maximum: context.endTime }}
58-
/>
59+
selectedAction={selectedAction}
60+
highlightedAction={highlightedAction}
61+
onSelected={action => setSelectedAction(action)}
62+
onHighlighted={action => setHighlightedAction(action)}
63+
/>
5964
</div>
6065
<div className='hbox'>
6166
<div style={{ display: 'flex', flex: 'none' }}>
62-
<ActionList actions={actions} selectedAction={action} onSelected={action => setAction(action)} />
67+
<ActionList
68+
actions={actions}
69+
selectedAction={selectedAction}
70+
highlightedAction={highlightedAction}
71+
onSelected={action => setSelectedAction(action)}
72+
onHighlighted={action => setHighlightedAction(action)}
73+
/>
6374
</div>
64-
<PropertiesTabbedPane actionEntry={action} snapshotSize={snapshotSize} />
75+
<PropertiesTabbedPane actionEntry={selectedAction} snapshotSize={snapshotSize} />
6576
</div>
6677
</div>;
6778
};

src/cli/traceViewer/web/web.webpack.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ const path = require('path');
22
const HtmlWebPackPlugin = require('html-webpack-plugin');
33

44
module.exports = {
5-
mode: 'production',
5+
mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
66
entry: {
77
app: path.join(__dirname, 'index.tsx'),
88
},

0 commit comments

Comments
 (0)