From 04b35089be4c094e6f8dc254702c51502d9233aa Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Mon, 6 Mar 2023 12:25:00 -0800 Subject: [PATCH] chore: extract shared trace viewer components (#21433) --- packages/trace-viewer/src/index.tsx | 2 +- packages/trace-viewer/src/ui/filmStrip.tsx | 8 +- packages/trace-viewer/src/ui/metadataView.tsx | 43 ++++ packages/trace-viewer/src/ui/timeline.tsx | 22 +- packages/trace-viewer/src/ui/watchMode.tsx | 16 +- packages/trace-viewer/src/ui/workbench.tsx | 188 +----------------- .../trace-viewer/src/ui/workbenchLoader.tsx | 161 +++++++++++++++ 7 files changed, 243 insertions(+), 197 deletions(-) create mode 100644 packages/trace-viewer/src/ui/metadataView.tsx create mode 100644 packages/trace-viewer/src/ui/workbenchLoader.tsx diff --git a/packages/trace-viewer/src/index.tsx b/packages/trace-viewer/src/index.tsx index 28ee7876af..49b6edc591 100644 --- a/packages/trace-viewer/src/index.tsx +++ b/packages/trace-viewer/src/index.tsx @@ -19,7 +19,7 @@ import { applyTheme } from '@web/theme'; import '@web/third_party/vscode/codicon.css'; import React from 'react'; import * as ReactDOM from 'react-dom'; -import { WorkbenchLoader } from './ui/workbench'; +import { WorkbenchLoader } from './ui/workbenchLoader'; (async () => { applyTheme(); diff --git a/packages/trace-viewer/src/ui/filmStrip.tsx b/packages/trace-viewer/src/ui/filmStrip.tsx index f18a6d0f3d..0c7906905f 100644 --- a/packages/trace-viewer/src/ui/filmStrip.tsx +++ b/packages/trace-viewer/src/ui/filmStrip.tsx @@ -25,10 +25,10 @@ import type { MultiTraceModel } from './modelUtil'; const tileSize = { width: 200, height: 45 }; export const FilmStrip: React.FunctionComponent<{ - context: MultiTraceModel, + model: MultiTraceModel, boundaries: Boundaries, previewPoint?: { x: number, clientY: number }, -}> = ({ context, boundaries, previewPoint }) => { +}> = ({ model, boundaries, previewPoint }) => { const [measure, ref] = useMeasure(); let pageIndex = 0; @@ -37,7 +37,7 @@ export const FilmStrip: React.FunctionComponent<{ pageIndex = ((previewPoint.clientY - bounds.top) / tileSize.height) | 0; } - const screencastFrames = context.pages[pageIndex]?.screencastFrames; + const screencastFrames = model.pages[pageIndex]?.screencastFrames; let previewImage = undefined; let previewSize = undefined; if (previewPoint !== undefined && screencastFrames) { @@ -48,7 +48,7 @@ export const FilmStrip: React.FunctionComponent<{ } return
{ - context.pages.filter(p => p.screencastFrames.length).map((page, index) => p.screencastFrames.length).map((page, index) => = ({ model }) => { + return
+
Time
+ {model.wallTime &&
start time:{new Date(model.wallTime).toLocaleString()}
} +
duration:{msToString(model.endTime - model.startTime)}
+
Browser
+
engine:{model.browserName}
+ {model.platform &&
platform:{model.platform}
} + {model.options.userAgent &&
user agent:{model.options.userAgent}
} +
Viewport
+ {model.options.viewport &&
width:{model.options.viewport.width}
} + {model.options.viewport &&
height:{model.options.viewport.height}
} +
is mobile:{String(!!model.options.isMobile)}
+ {model.options.deviceScaleFactor &&
device scale:{String(model.options.deviceScaleFactor)}
} +
Counts
+
pages:{model.pages.length}
+
actions:{model.actions.length}
+
events:{model.events.length}
+
; +}; diff --git a/packages/trace-viewer/src/ui/timeline.tsx b/packages/trace-viewer/src/ui/timeline.tsx index d5ed285027..f1db9b8e6f 100644 --- a/packages/trace-viewer/src/ui/timeline.tsx +++ b/packages/trace-viewer/src/ui/timeline.tsx @@ -38,24 +38,26 @@ type TimelineBar = { }; export const Timeline: React.FunctionComponent<{ - context: MultiTraceModel, - boundaries: Boundaries, + model: MultiTraceModel, selectedAction: ActionTraceEvent | undefined, onSelected: (action: ActionTraceEvent) => void, -}> = ({ context, boundaries, selectedAction, onSelected }) => { +}> = ({ model, selectedAction, onSelected }) => { const [measure, ref] = useMeasure(); const barsRef = React.useRef(null); const [previewPoint, setPreviewPoint] = React.useState<{ x: number, clientY: number } | undefined>(); const [hoveredBarIndex, setHoveredBarIndex] = React.useState(); - const offsets = React.useMemo(() => { - return calculateDividerOffsets(measure.width, boundaries); - }, [measure.width, boundaries]); + const { boundaries, offsets } = React.useMemo(() => { + const boundaries = { minimum: model.startTime, maximum: model.endTime }; + // Leave some nice free space on the right hand side. + boundaries.maximum += (boundaries.maximum - boundaries.minimum) / 20; + return { boundaries, offsets: calculateDividerOffsets(measure.width, boundaries) }; + }, [measure.width, model]); const bars = React.useMemo(() => { const bars: TimelineBar[] = []; - for (const entry of context.actions) { + for (const entry of model.actions) { let detail = trimRight(entry.params.selector || '', 50); if (entry.method === 'goto') detail = trimRight(entry.params.url || '', 50); @@ -72,7 +74,7 @@ export const Timeline: React.FunctionComponent<{ }); } - for (const event of context.events) { + for (const event of model.events) { const startTime = event.time; bars.push({ event, @@ -87,7 +89,7 @@ export const Timeline: React.FunctionComponent<{ }); } return bars; - }, [context, boundaries, measure.width]); + }, [model, boundaries, measure.width]); const hoveredBar = hoveredBarIndex !== undefined ? bars[hoveredBarIndex] : undefined; let targetBar: TimelineBar | undefined = bars.find(bar => bar.action === selectedAction); @@ -185,7 +187,7 @@ export const Timeline: React.FunctionComponent<{ >
; }) } - +
void = () => {}; let updateStepsProgress: () => void = () => {}; @@ -277,8 +278,7 @@ export const TraceView: React.FC<{
; } - return ; - + return ; }; declare global { @@ -485,3 +485,11 @@ function flattenTree(fileItems: FileItem[], expandedItems: Map { + const params = new URLSearchParams(); + params.set('trace', url); + const response = await fetch(`contexts?${params.toString()}`); + const contextEntries = await response.json() as ContextEntry[]; + return new MultiTraceModel(contextEntries); +} diff --git a/packages/trace-viewer/src/ui/workbench.tsx b/packages/trace-viewer/src/ui/workbench.tsx index cf4bd5d5ae..764b97fd27 100644 --- a/packages/trace-viewer/src/ui/workbench.tsx +++ b/packages/trace-viewer/src/ui/workbench.tsx @@ -16,173 +16,29 @@ import type { ActionTraceEvent } from '@trace/trace'; import { SplitView } from '@web/components/splitView'; -import { msToString } from '@web/uiUtils'; -import { ToolbarButton } from '@web/components/toolbarButton'; import * as React from 'react'; -import type { ContextEntry } from '../entries'; import { ActionList } from './actionList'; import { CallTab } from './callTab'; import { ConsoleTab } from './consoleTab'; import * as modelUtil from './modelUtil'; -import { MultiTraceModel } from './modelUtil'; +import type { MultiTraceModel } from './modelUtil'; import { NetworkTab } from './networkTab'; import { SnapshotTab } from './snapshotTab'; import { SourceTab } from './sourceTab'; import { TabbedPane } from '@web/components/tabbedPane'; import { Timeline } from './timeline'; import './workbench.css'; -import { toggleTheme } from '@web/theme'; - -export const WorkbenchLoader: React.FunctionComponent<{ -}> = () => { - const [traceURLs, setTraceURLs] = React.useState([]); - const [uploadedTraceNames, setUploadedTraceNames] = React.useState([]); - const [model, setModel] = React.useState(emptyModel); - const [progress, setProgress] = React.useState<{ done: number, total: number }>({ done: 0, total: 0 }); - const [dragOver, setDragOver] = React.useState(false); - const [processingErrorMessage, setProcessingErrorMessage] = React.useState(null); - const [fileForLocalModeError, setFileForLocalModeError] = React.useState(null); - - const processTraceFiles = (files: FileList) => { - const blobUrls = []; - const fileNames = []; - const url = new URL(window.location.href); - for (let i = 0; i < files.length; i++) { - const file = files.item(i); - if (!file) - continue; - const blobTraceURL = URL.createObjectURL(file); - blobUrls.push(blobTraceURL); - fileNames.push(file.name); - url.searchParams.append('trace', blobTraceURL); - url.searchParams.append('traceFileName', file.name); - } - const href = url.toString(); - // Snapshot loaders will inherit the trace url from the query parameters, - // so set it here. - window.history.pushState({}, '', href); - setTraceURLs(blobUrls); - setUploadedTraceNames(fileNames); - setDragOver(false); - setProcessingErrorMessage(null); - }; - - const handleDropEvent = (event: React.DragEvent) => { - event.preventDefault(); - processTraceFiles(event.dataTransfer.files); - }; - - const handleFileInputChange = (event: any) => { - event.preventDefault(); - if (!event.target.files) - return; - processTraceFiles(event.target.files); - }; - - React.useEffect(() => { - const newTraceURLs = new URL(window.location.href).searchParams.getAll('trace'); - // Don't accept file:// URLs - this means we re opened locally. - for (const url of newTraceURLs) { - if (url.startsWith('file:')) { - setFileForLocalModeError(url || null); - return; - } - } - - // Don't re-use blob file URLs on page load (results in Fetch error) - if (!newTraceURLs.some(url => url.startsWith('blob:'))) - setTraceURLs(newTraceURLs); - }, [setTraceURLs]); - - React.useEffect(() => { - (async () => { - if (traceURLs.length) { - const swListener = (event: any) => { - if (event.data.method === 'progress') - setProgress(event.data.params); - }; - navigator.serviceWorker.addEventListener('message', swListener); - setProgress({ done: 0, total: 1 }); - const contextEntries: ContextEntry[] = []; - for (let i = 0; i < traceURLs.length; i++) { - const url = traceURLs[i]; - const params = new URLSearchParams(); - params.set('trace', url); - if (uploadedTraceNames.length) - params.set('traceFileName', uploadedTraceNames[i]); - const response = await fetch(`contexts?${params.toString()}`); - if (!response.ok) { - setTraceURLs([]); - setProcessingErrorMessage((await response.json()).error); - return; - } - contextEntries.push(...(await response.json())); - } - navigator.serviceWorker.removeEventListener('message', swListener); - const model = new MultiTraceModel(contextEntries); - setProgress({ done: 0, total: 0 }); - setModel(model); - } else { - setModel(emptyModel); - } - })(); - }, [traceURLs, uploadedTraceNames]); - - return
{ event.preventDefault(); setDragOver(true); }}> -
-
🎭
-
Playwright
- {model.title &&
{model.title}
} -
- toggleTheme()}> -
- - {!!progress.total &&
-
-
} - {fileForLocalModeError &&
-
Trace Viewer uses Service Workers to show traces. To view trace:
-
-
1. Click here to put your trace into the download shelf
- -
3. Drop the trace from the download shelf into the page
-
-
} - {!dragOver && !fileForLocalModeError && (!traceURLs.length || processingErrorMessage) &&
-
{processingErrorMessage}
-
Drop Playwright Trace to load
-
or
- -
Playwright Trace Viewer is a Progressive Web App, it does not send your trace anywhere, - it opens it locally.
-
} - {dragOver &&
{ setDragOver(false); }} - onDrop={event => handleDropEvent(event)}> -
Release to analyse the Playwright Trace
-
} -
; -}; +import { MetadataView } from './metadataView'; export const Workbench: React.FunctionComponent<{ model: MultiTraceModel, - view: 'embedded' | 'standalone' -}> = ({ model, view }) => { +}> = ({ model }) => { const [selectedAction, setSelectedAction] = React.useState(); const [highlightedAction, setHighlightedAction] = React.useState(); const [selectedNavigatorTab, setSelectedNavigatorTab] = React.useState('actions'); const [selectedPropertiesTab, setSelectedPropertiesTab] = React.useState('logs'); - const activeAction = highlightedAction || selectedAction; - const boundaries = { minimum: model.startTime, maximum: model.endTime }; - // Leave some nice free space on the right hand side. - boundaries.maximum += (boundaries.maximum - boundaries.minimum) / 20; const { errors, warnings } = activeAction ? modelUtil.stats(activeAction) : { errors: 0, warnings: 0 }; const consoleCount = errors + warnings; const networkCount = activeAction ? modelUtil.resourcesForAction(activeAction).length : 0; @@ -194,13 +50,12 @@ export const Workbench: React.FunctionComponent<{ ]; if (model.hasSource) - tabs.push({ id: 'source', title: 'Source', count: 0, render: () => }); + tabs.push({ id: 'source', title: 'Source', count: 0, render: () => }); return
setSelectedAction(action)} /> @@ -224,36 +79,13 @@ export const Workbench: React.FunctionComponent<{ }} setSelectedTab={setSelectedPropertiesTab} /> }, - { id: 'metadata', title: 'Metadata', count: 0, render: () =>
-
Time
- {model.wallTime &&
start time:{new Date(model.wallTime).toLocaleString()}
} -
duration:{msToString(model.endTime - model.startTime)}
-
Browser
-
engine:{model.browserName}
- {model.platform &&
platform:{model.platform}
} - {model.options.userAgent &&
user agent:{model.options.userAgent}
} -
Viewport
- {model.options.viewport &&
width:{model.options.viewport.width}
} - {model.options.viewport &&
height:{model.options.viewport.height}
} -
is mobile:{String(!!model.options.isMobile)}
- {model.options.deviceScaleFactor &&
device scale:{String(model.options.deviceScaleFactor)}
} -
Counts
-
pages:{model.pages.length}
-
actions:{model.actions.length}
-
events:{model.events.length}
-
}, + { id: 'metadata', + title: 'Metadata', + count: 0, + render: () => + }, ] } selectedTab={selectedNavigatorTab} setSelectedTab={setSelectedNavigatorTab}/>
; }; - -export const emptyModel = new MultiTraceModel([]); - -export async function loadSingleTraceFile(url: string): Promise { - const params = new URLSearchParams(); - params.set('trace', url); - const response = await fetch(`contexts?${params.toString()}`); - const contextEntries = await response.json() as ContextEntry[]; - return new MultiTraceModel(contextEntries); -} diff --git a/packages/trace-viewer/src/ui/workbenchLoader.tsx b/packages/trace-viewer/src/ui/workbenchLoader.tsx new file mode 100644 index 0000000000..83266cbfe1 --- /dev/null +++ b/packages/trace-viewer/src/ui/workbenchLoader.tsx @@ -0,0 +1,161 @@ +/* + 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 { ToolbarButton } from '@web/components/toolbarButton'; +import * as React from 'react'; +import type { ContextEntry } from '../entries'; +import { MultiTraceModel } from './modelUtil'; +import './workbench.css'; +import { toggleTheme } from '@web/theme'; +import { Workbench } from './workbench'; + +export const WorkbenchLoader: React.FunctionComponent<{ +}> = () => { + const [traceURLs, setTraceURLs] = React.useState([]); + const [uploadedTraceNames, setUploadedTraceNames] = React.useState([]); + const [model, setModel] = React.useState(emptyModel); + const [progress, setProgress] = React.useState<{ done: number, total: number }>({ done: 0, total: 0 }); + const [dragOver, setDragOver] = React.useState(false); + const [processingErrorMessage, setProcessingErrorMessage] = React.useState(null); + const [fileForLocalModeError, setFileForLocalModeError] = React.useState(null); + + const processTraceFiles = (files: FileList) => { + const blobUrls = []; + const fileNames = []; + const url = new URL(window.location.href); + for (let i = 0; i < files.length; i++) { + const file = files.item(i); + if (!file) + continue; + const blobTraceURL = URL.createObjectURL(file); + blobUrls.push(blobTraceURL); + fileNames.push(file.name); + url.searchParams.append('trace', blobTraceURL); + url.searchParams.append('traceFileName', file.name); + } + const href = url.toString(); + // Snapshot loaders will inherit the trace url from the query parameters, + // so set it here. + window.history.pushState({}, '', href); + setTraceURLs(blobUrls); + setUploadedTraceNames(fileNames); + setDragOver(false); + setProcessingErrorMessage(null); + }; + + const handleDropEvent = (event: React.DragEvent) => { + event.preventDefault(); + processTraceFiles(event.dataTransfer.files); + }; + + const handleFileInputChange = (event: any) => { + event.preventDefault(); + if (!event.target.files) + return; + processTraceFiles(event.target.files); + }; + + React.useEffect(() => { + const newTraceURLs = new URL(window.location.href).searchParams.getAll('trace'); + // Don't accept file:// URLs - this means we re opened locally. + for (const url of newTraceURLs) { + if (url.startsWith('file:')) { + setFileForLocalModeError(url || null); + return; + } + } + + // Don't re-use blob file URLs on page load (results in Fetch error) + if (!newTraceURLs.some(url => url.startsWith('blob:'))) + setTraceURLs(newTraceURLs); + }, [setTraceURLs]); + + React.useEffect(() => { + (async () => { + if (traceURLs.length) { + const swListener = (event: any) => { + if (event.data.method === 'progress') + setProgress(event.data.params); + }; + navigator.serviceWorker.addEventListener('message', swListener); + setProgress({ done: 0, total: 1 }); + const contextEntries: ContextEntry[] = []; + for (let i = 0; i < traceURLs.length; i++) { + const url = traceURLs[i]; + const params = new URLSearchParams(); + params.set('trace', url); + if (uploadedTraceNames.length) + params.set('traceFileName', uploadedTraceNames[i]); + const response = await fetch(`contexts?${params.toString()}`); + if (!response.ok) { + setTraceURLs([]); + setProcessingErrorMessage((await response.json()).error); + return; + } + contextEntries.push(...(await response.json())); + } + navigator.serviceWorker.removeEventListener('message', swListener); + const model = new MultiTraceModel(contextEntries); + setProgress({ done: 0, total: 0 }); + setModel(model); + } else { + setModel(emptyModel); + } + })(); + }, [traceURLs, uploadedTraceNames]); + + return
{ event.preventDefault(); setDragOver(true); }}> +
+
🎭
+
Playwright
+ {model.title &&
{model.title}
} +
+ toggleTheme()}> +
+ + {!!progress.total &&
+
+
} + {fileForLocalModeError &&
+
Trace Viewer uses Service Workers to show traces. To view trace:
+
+
1. Click here to put your trace into the download shelf
+ +
3. Drop the trace from the download shelf into the page
+
+
} + {!dragOver && !fileForLocalModeError && (!traceURLs.length || processingErrorMessage) &&
+
{processingErrorMessage}
+
Drop Playwright Trace to load
+
or
+ +
Playwright Trace Viewer is a Progressive Web App, it does not send your trace anywhere, + it opens it locally.
+
} + {dragOver &&
{ setDragOver(false); }} + onDrop={event => handleDropEvent(event)}> +
Release to analyse the Playwright Trace
+
} +
; +}; + +export const emptyModel = new MultiTraceModel([]);