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;
|
||||
}
|
||||
|
||||
.action-header .action-error {
|
||||
color: red;
|
||||
top: 2px;
|
||||
position: relative;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.action-selector {
|
||||
display: inline;
|
||||
padding-left: 5px;
|
||||
|
|
|
|||
|
|
@ -20,16 +20,23 @@ import * as React from 'react';
|
|||
|
||||
export const ActionList: React.FunctionComponent<{
|
||||
actions: ActionEntry[],
|
||||
selectedAction?: ActionEntry,
|
||||
selectedAction: ActionEntry | undefined,
|
||||
highlightedAction: ActionEntry | undefined,
|
||||
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 => {
|
||||
const { action, actionId } = actionEntry;
|
||||
return <div
|
||||
className={'action-entry' + (actionEntry === selectedAction ? ' selected' : '')}
|
||||
className={'action-entry' + (actionEntry === targetAction ? ' selected' : '')}
|
||||
key={actionId}
|
||||
onClick={() => onSelected(actionEntry)}>
|
||||
onClick={() => onSelected(actionEntry)}
|
||||
onMouseEnter={() => onHighlighted(actionEntry)}
|
||||
onMouseLeave={() => (highlightedAction === actionEntry) && onHighlighted(undefined)}
|
||||
>
|
||||
<div className='action-header'>
|
||||
<div className={'action-error codicon codicon-issues'} hidden={!actionEntry.action.error} />
|
||||
<div className='action-title'>{action.action}</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>}
|
||||
|
|
|
|||
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 * as React from 'react';
|
||||
import { useMeasure } from './helpers';
|
||||
import { LogsTab } from './logsTab';
|
||||
|
||||
export const PropertiesTabbedPane: React.FunctionComponent<{
|
||||
actionEntry: ActionEntry | undefined,
|
||||
snapshotSize: Size,
|
||||
}> = ({ 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'>
|
||||
<div className='vbox'>
|
||||
<div className='hbox' style={{ flex: 'none' }}>
|
||||
|
|
@ -43,6 +44,10 @@ export const PropertiesTabbedPane: React.FunctionComponent<{
|
|||
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>
|
||||
<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' }}>
|
||||
<NetworkTab actionEntry={actionEntry} />
|
||||
</div>
|
||||
<div className='properties-tab-content' style={{ display: selected === 'logs' ? 'flex' : 'none' }}>
|
||||
<LogsTab actionEntry={actionEntry} />
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@
|
|||
|
||||
.timeline-lane.timeline-actions {
|
||||
margin-bottom: 10px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.timeline-action {
|
||||
|
|
@ -72,19 +73,26 @@
|
|||
bottom: 0;
|
||||
background-color: red;
|
||||
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 {
|
||||
background-color: var(--green);
|
||||
--action-color: var(--green);
|
||||
}
|
||||
|
||||
.timeline-action.fill,
|
||||
.timeline-action.press {
|
||||
background-color: var(--orange);
|
||||
--action-color: var(--orange);
|
||||
}
|
||||
|
||||
.timeline-action.goto {
|
||||
background-color: var(--blue);
|
||||
--action-color: var(--blue);
|
||||
}
|
||||
|
||||
.timeline-action-label {
|
||||
|
|
@ -93,6 +101,12 @@
|
|||
bottom: 0;
|
||||
margin-left: 2px;
|
||||
background-color: #fffffff0;
|
||||
justify-content: center;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.timeline-action-label.selected {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.timeline-time-bar {
|
||||
|
|
|
|||
|
|
@ -21,19 +21,25 @@ import { FilmStrip } from './filmStrip';
|
|||
import { Boundaries } from '../geometry';
|
||||
import * as React from 'react';
|
||||
import { useMeasure } from './helpers';
|
||||
import { ActionEntry } from '../../traceModel';
|
||||
|
||||
export const Timeline: React.FunctionComponent<{
|
||||
context: ContextEntry,
|
||||
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 [previewX, setPreviewX] = React.useState<number | undefined>();
|
||||
const targetAction = highlightedAction || selectedAction;
|
||||
|
||||
const offsets = React.useMemo(() => {
|
||||
return calculateDividerOffsets(measure.width, boundaries);
|
||||
}, [measure.width, boundaries]);
|
||||
const actionEntries = React.useMemo(() => {
|
||||
const actions = [];
|
||||
const actions: ActionEntry[] = [];
|
||||
for (const page of context.pages)
|
||||
actions.push(...page.actions);
|
||||
return actions;
|
||||
|
|
@ -41,23 +47,52 @@ export const Timeline: React.FunctionComponent<{
|
|||
const actionTimes = React.useMemo(() => {
|
||||
return actionEntries.map(entry => {
|
||||
return {
|
||||
action: entry.action,
|
||||
actionId: entry.actionId,
|
||||
entry,
|
||||
left: timeToPercent(measure.width, boundaries, entry.action.startTime!),
|
||||
right: timeToPercent(measure.width, boundaries, entry.action.endTime!),
|
||||
};
|
||||
});
|
||||
}, [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) => {
|
||||
if (ref.current)
|
||||
setPreviewX(event.clientX - ref.current.getBoundingClientRect().left);
|
||||
if (ref.current) {
|
||||
const x = event.clientX - ref.current.getBoundingClientRect().left;
|
||||
setPreviewX(x);
|
||||
onHighlighted(findHoveredAction(x));
|
||||
}
|
||||
};
|
||||
const onMouseLeave = () => {
|
||||
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'>{
|
||||
offsets.map((offset, index) => {
|
||||
return <div key={index} className='timeline-divider' style={{ left: offset.percent + '%' }}>
|
||||
|
|
@ -66,19 +101,22 @@ export const Timeline: React.FunctionComponent<{
|
|||
})
|
||||
}</div>
|
||||
<div className='timeline-lane timeline-action-labels'>{
|
||||
actionTimes.map(({ action, actionId, left }) => {
|
||||
return <div key={actionId}
|
||||
className={'timeline-action-label ' + action.action}
|
||||
style={{ left: left + '%' }}
|
||||
actionTimes.map(({ entry, left, right }) => {
|
||||
return <div key={entry.actionId}
|
||||
className={'timeline-action-label ' + entry.action.action + (targetAction === entry ? ' selected' : '')}
|
||||
style={{
|
||||
left: left + '%',
|
||||
width: (right - left) + '%',
|
||||
}}
|
||||
>
|
||||
{action.action}
|
||||
{entry.action.action}
|
||||
</div>;
|
||||
})
|
||||
}</div>
|
||||
<div className='timeline-lane timeline-actions'>{
|
||||
actionTimes.map(({ action, actionId, left, right }) => {
|
||||
return <div key={actionId}
|
||||
className={'timeline-action ' + action.action}
|
||||
actionTimes.map(({ entry, left, right }) => {
|
||||
return <div key={entry.actionId}
|
||||
className={'timeline-action ' + entry.action.action + (targetAction === entry ? ' selected' : '')}
|
||||
style={{
|
||||
left: left + '%',
|
||||
width: (right - left) + '%',
|
||||
|
|
@ -129,6 +167,11 @@ function timeToPercent(clientWidth: number, boundaries: Boundaries, time: number
|
|||
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 {
|
||||
if (!isFinite(ms))
|
||||
return '-';
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ export const Workbench: React.FunctionComponent<{
|
|||
traceModel: TraceModel,
|
||||
}> = ({ traceModel }) => {
|
||||
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: ActionEntry[] = [];
|
||||
|
|
@ -47,7 +48,7 @@ export const Workbench: React.FunctionComponent<{
|
|||
context={context}
|
||||
onChange={context => {
|
||||
setContext(context);
|
||||
setAction(undefined);
|
||||
setSelectedAction(undefined);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -55,13 +56,23 @@ export const Workbench: React.FunctionComponent<{
|
|||
<Timeline
|
||||
context={context}
|
||||
boundaries={{ minimum: context.startTime, maximum: context.endTime }}
|
||||
/>
|
||||
selectedAction={selectedAction}
|
||||
highlightedAction={highlightedAction}
|
||||
onSelected={action => setSelectedAction(action)}
|
||||
onHighlighted={action => setHighlightedAction(action)}
|
||||
/>
|
||||
</div>
|
||||
<div className='hbox'>
|
||||
<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>
|
||||
<PropertiesTabbedPane actionEntry={action} snapshotSize={snapshotSize} />
|
||||
<PropertiesTabbedPane actionEntry={selectedAction} snapshotSize={snapshotSize} />
|
||||
</div>
|
||||
</div>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ const path = require('path');
|
|||
const HtmlWebPackPlugin = require('html-webpack-plugin');
|
||||
|
||||
module.exports = {
|
||||
mode: 'production',
|
||||
mode: process.env.NODE_ENV === 'production' ? 'production' : 'development',
|
||||
entry: {
|
||||
app: path.join(__dirname, 'index.tsx'),
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue