From b45905ae3f1a066a8ecb358035ce745ddd21cf3a Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 14 Jan 2021 20:16:02 -0800 Subject: [PATCH] feat(trace viewer): small improvements (#5007) - Show logs. - Show errors. - Highlight actions. --- src/cli/traceViewer/web/ui/actionList.css | 7 ++ src/cli/traceViewer/web/ui/actionList.tsx | 15 +++- src/cli/traceViewer/web/ui/logsTab.css | 29 ++++++++ src/cli/traceViewer/web/ui/logsTab.tsx | 37 ++++++++++ .../web/ui/propertiesTabbedPane.tsx | 10 ++- src/cli/traceViewer/web/ui/timeline.css | 20 ++++- src/cli/traceViewer/web/ui/timeline.tsx | 73 +++++++++++++++---- src/cli/traceViewer/web/ui/workbench.tsx | 21 ++++-- src/cli/traceViewer/web/web.webpack.config.js | 2 +- 9 files changed, 185 insertions(+), 29 deletions(-) create mode 100644 src/cli/traceViewer/web/ui/logsTab.css create mode 100644 src/cli/traceViewer/web/ui/logsTab.tsx diff --git a/src/cli/traceViewer/web/ui/actionList.css b/src/cli/traceViewer/web/ui/actionList.css index 970180556a..c58cb701d0 100644 --- a/src/cli/traceViewer/web/ui/actionList.css +++ b/src/cli/traceViewer/web/ui/actionList.css @@ -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; diff --git a/src/cli/traceViewer/web/ui/actionList.tsx b/src/cli/traceViewer/web/ui/actionList.tsx index 94b38c4542..283c758d26 100644 --- a/src/cli/traceViewer/web/ui/actionList.tsx +++ b/src/cli/traceViewer/web/ui/actionList.tsx @@ -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
{actions.map(actionEntry => { const { action, actionId } = actionEntry; return
onSelected(actionEntry)}> + onClick={() => onSelected(actionEntry)} + onMouseEnter={() => onHighlighted(actionEntry)} + onMouseLeave={() => (highlightedAction === actionEntry) && onHighlighted(undefined)} + >
+ ; }; diff --git a/src/cli/traceViewer/web/ui/timeline.css b/src/cli/traceViewer/web/ui/timeline.css index 24fa5b057b..497f0a2675 100644 --- a/src/cli/traceViewer/web/ui/timeline.css +++ b/src/cli/traceViewer/web/ui/timeline.css @@ -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 { diff --git a/src/cli/traceViewer/web/ui/timeline.tsx b/src/cli/traceViewer/web/ui/timeline.tsx index 25e48452f5..676834fa3c 100644 --- a/src/cli/traceViewer/web/ui/timeline.tsx +++ b/src/cli/traceViewer/web/ui/timeline.tsx @@ -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(); const [previewX, setPreviewX] = React.useState(); + 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
+ return
{ offsets.map((offset, index) => { return
@@ -66,19 +101,22 @@ export const Timeline: React.FunctionComponent<{ }) }
{ - actionTimes.map(({ action, actionId, left }) => { - return
{ + return
- {action.action} + {entry.action.action}
; }) }
{ - actionTimes.map(({ action, actionId, left, right }) => { - return
{ + return
= ({ traceModel }) => { const [context, setContext] = React.useState(traceModel.contexts[0]); - const [action, setAction] = React.useState(); + const [selectedAction, setSelectedAction] = React.useState(); + const [highlightedAction, setHighlightedAction] = React.useState(); 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); }} />
@@ -55,13 +56,23 @@ export const Workbench: React.FunctionComponent<{ + selectedAction={selectedAction} + highlightedAction={highlightedAction} + onSelected={action => setSelectedAction(action)} + onHighlighted={action => setHighlightedAction(action)} + />
- setAction(action)} /> + setSelectedAction(action)} + onHighlighted={action => setHighlightedAction(action)} + />
- +
; }; diff --git a/src/cli/traceViewer/web/web.webpack.config.js b/src/cli/traceViewer/web/web.webpack.config.js index 4fd608469e..dedec1cf60 100644 --- a/src/cli/traceViewer/web/web.webpack.config.js +++ b/src/cli/traceViewer/web/web.webpack.config.js @@ -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'), },