From ad69b2af83ea9d60b4b2e3361c72c824dc9e49fb Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 11 Mar 2021 11:22:59 -0800 Subject: [PATCH] chore: unify recorder & tracer uis (#5791) --- src/server/snapshot/snapshotRenderer.ts | 2 +- src/server/snapshot/snapshotServer.ts | 2 +- .../supplements/recorder/recorderTypes.ts | 6 +- .../supplements/recorder/recorderUtils.ts | 60 +++++++++++++++++ src/server/supplements/recorderSupplement.ts | 56 +++------------- src/server/trace/recorder/tracer.ts | 2 +- src/server/trace/viewer/traceViewer.ts | 2 +- src/web/common.css | 6 +- src/web/components/source.css | 3 +- src/web/components/splitView.css | 25 ++++++- src/web/components/splitView.tsx | 15 +++-- src/web/components/toolbarButton.css | 2 +- src/web/recorder/callLog.tsx | 4 +- src/web/traceViewer/ui/actionList.css | 35 ++++------ src/web/traceViewer/ui/actionList.tsx | 13 ++-- src/web/traceViewer/ui/logsTab.css | 13 ++-- .../traceViewer/ui/networkResourceDetails.css | 24 +------ src/web/traceViewer/ui/snapshotTab.css | 13 +++- src/web/traceViewer/ui/snapshotTab.tsx | 8 ++- src/web/traceViewer/ui/sourceTab.css | 66 ------------------- src/web/traceViewer/ui/sourceTab.tsx | 60 ++++------------- src/web/traceViewer/ui/stackTrace.css | 55 ++++++++++++++++ src/web/traceViewer/ui/stackTrace.tsx | 49 ++++++++++++++ src/web/traceViewer/ui/tabbedPane.css | 16 +++-- src/web/traceViewer/ui/timeline.css | 2 +- src/web/traceViewer/ui/workbench.css | 1 + src/web/traceViewer/ui/workbench.tsx | 19 ++++-- 27 files changed, 302 insertions(+), 257 deletions(-) create mode 100644 src/server/supplements/recorder/recorderUtils.ts create mode 100644 src/web/traceViewer/ui/stackTrace.css create mode 100644 src/web/traceViewer/ui/stackTrace.tsx diff --git a/src/server/snapshot/snapshotRenderer.ts b/src/server/snapshot/snapshotRenderer.ts index aa13f6dc1642f..30d18d82ec1a6 100644 --- a/src/server/snapshot/snapshotRenderer.ts +++ b/src/server/snapshot/snapshotRenderer.ts @@ -138,7 +138,7 @@ function snapshotScript() { for (const iframe of root.querySelectorAll('iframe')) { const src = iframe.getAttribute('src'); if (!src) { - iframe.setAttribute('src', 'data:text/html,Snapshot is not available'); + iframe.setAttribute('src', 'data:text/html,'); } else { // Append query parameters to inherit ?name= or ?time= values from parent. iframe.setAttribute('src', window.location.origin + src + window.location.search); diff --git a/src/server/snapshot/snapshotServer.ts b/src/server/snapshot/snapshotServer.ts index 40a1edd9acf56..57e8d02747f3d 100644 --- a/src/server/snapshot/snapshotServer.ts +++ b/src/server/snapshot/snapshotServer.ts @@ -75,7 +75,7 @@ export class SnapshotServer { } function respondNotAvailable(): Response { - return new Response('Snapshot is not available', { status: 200, headers: { 'Content-Type': 'text/html' } }); + return new Response('', { status: 200, headers: { 'Content-Type': 'text/html' } }); } function removeHash(url: string) { diff --git a/src/server/supplements/recorder/recorderTypes.ts b/src/server/supplements/recorder/recorderTypes.ts index 3c84726a82975..372e16916a70f 100644 --- a/src/server/supplements/recorder/recorderTypes.ts +++ b/src/server/supplements/recorder/recorderTypes.ts @@ -30,11 +30,13 @@ export type UIState = { snapshotUrl?: string; }; +export type CallLogStatus = 'in-progress' | 'done' | 'error' | 'paused'; + export type CallLog = { id: number; title: string; messages: string[]; - status: 'in-progress' | 'done' | 'error' | 'paused'; + status: CallLogStatus; error?: string; reveal?: boolean; duration?: number; @@ -44,7 +46,7 @@ export type CallLog = { }; snapshots: { before: boolean, - in: boolean, + action: boolean, after: boolean, } }; diff --git a/src/server/supplements/recorder/recorderUtils.ts b/src/server/supplements/recorder/recorderUtils.ts new file mode 100644 index 0000000000000..7fe390778b296 --- /dev/null +++ b/src/server/supplements/recorder/recorderUtils.ts @@ -0,0 +1,60 @@ +/** + * 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 { CallMetadata } from '../../instrumentation'; +import { CallLog, CallLogStatus } from './recorderTypes'; + +export function metadataToCallLog(metadata: CallMetadata, status: CallLogStatus, snapshots: Set): CallLog { + const title = metadata.apiName || metadata.method; + if (metadata.error) + status = 'error'; + const params = { + url: metadata.params?.url, + selector: metadata.params?.selector, + }; + let duration = metadata.endTime ? metadata.endTime - metadata.startTime : undefined; + if (typeof duration === 'number' && metadata.pauseStartTime && metadata.pauseEndTime) { + duration -= (metadata.pauseEndTime - metadata.pauseStartTime); + duration = Math.max(duration, 0); + } + const callLog: CallLog = { + id: metadata.id, + messages: metadata.log, + title, + status, + error: metadata.error, + params, + duration, + snapshots: { + before: showBeforeSnapshot(metadata) && snapshots.has(`before@${metadata.id}`), + action: showActionSnapshot(metadata) && snapshots.has(`action@${metadata.id}`), + after: showAfterSnapshot(metadata) && snapshots.has(`after@${metadata.id}`), + } + }; + return callLog; +} + +function showBeforeSnapshot(metadata: CallMetadata): boolean { + return metadata.method === 'close'; +} + +function showActionSnapshot(metadata: CallMetadata): boolean { + return ['click', 'dblclick', 'check', 'uncheck', 'fill', 'press'].includes(metadata.method); +} + +function showAfterSnapshot(metadata: CallMetadata): boolean { + return ['goto', 'click', 'dblclick', 'dblclick', 'check', 'uncheck', 'fill', 'press'].includes(metadata.method); +} diff --git a/src/server/supplements/recorderSupplement.ts b/src/server/supplements/recorderSupplement.ts index 95fccaa0a3700..5f742a3f6e4e4 100644 --- a/src/server/supplements/recorderSupplement.ts +++ b/src/server/supplements/recorderSupplement.ts @@ -31,9 +31,10 @@ import * as consoleApiSource from '../../generated/consoleApiSource'; import { RecorderApp } from './recorder/recorderApp'; import { CallMetadata, internalCallMetadata, SdkObject } from '../instrumentation'; import { Point } from '../../common/types'; -import { CallLog, EventData, Mode, Source, UIState } from './recorder/recorderTypes'; +import { CallLog, CallLogStatus, EventData, Mode, Source, UIState } from './recorder/recorderTypes'; import { isUnderTest, monotonicTime } from '../../utils/utils'; import { InMemorySnapshotter } from '../snapshot/inMemorySnapshotter'; +import { metadataToCallLog } from './recorder/recorderUtils'; type BindingSource = { frame: Frame, page: Page }; @@ -56,7 +57,7 @@ export class RecorderSupplement { private _recorderSources: Source[]; private _userSources = new Map(); private _snapshotter: InMemorySnapshotter; - private _hoveredSnapshot: { callLogId: number, phase: 'before' | 'after' | 'in' } | undefined; + private _hoveredSnapshot: { callLogId: number, phase: 'before' | 'after' | 'action' } | undefined; private _snapshots = new Set(); private _allMetadatas = new Map(); @@ -209,7 +210,7 @@ export class RecorderSupplement { if (this._hoveredSnapshot) { const metadata = this._allMetadatas.get(this._hoveredSnapshot.callLogId)!; snapshotUrl = `${metadata.pageId}?name=${this._hoveredSnapshot.phase}@${this._hoveredSnapshot.callLogId}`; - actionPoint = this._hoveredSnapshot.phase === 'in' ? metadata?.point : undefined; + actionPoint = this._hoveredSnapshot.phase === 'action' ? metadata?.point : undefined; } else { for (const [metadata, sdkObject] of this._currentCallsMetadata) { if (source.page === sdkObject.attribution.page) { @@ -401,7 +402,7 @@ export class RecorderSupplement { this._generator.signal(pageAlias, page.mainFrame(), { name: 'dialog', dialogAlias: String(++this._lastDialogOrdinal) }); } - _captureSnapshot(sdkObject: SdkObject, metadata: CallMetadata, phase: 'before' | 'after' | 'in') { + _captureSnapshot(sdkObject: SdkObject, metadata: CallMetadata, phase: 'before' | 'after' | 'action') { if (sdkObject.attribution.page) { const snapshotName = `${phase}@${metadata.id}`; this._snapshots.add(snapshotName); @@ -428,7 +429,7 @@ export class RecorderSupplement { async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise { if (this._mode === 'recording') return; - await this._captureSnapshot(sdkObject, metadata, 'after'); + this._captureSnapshot(sdkObject, metadata, 'after'); if (!metadata.error) this._currentCallsMetadata.delete(metadata); this._pausedCallsMetadata.delete(metadata); @@ -473,49 +474,24 @@ export class RecorderSupplement { async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise { if (this._mode === 'recording') return; - await this._captureSnapshot(sdkObject, metadata, 'in'); + this._captureSnapshot(sdkObject, metadata, 'action'); if (this._pauseOnNextStatement) await this.pause(metadata); } - async updateCallLog(metadatas: CallMetadata[]): Promise { + updateCallLog(metadatas: CallMetadata[]) { if (this._mode === 'recording') return; const logs: CallLog[] = []; for (const metadata of metadatas) { if (!metadata.method) continue; - const title = metadata.apiName || metadata.method; - let status: 'done' | 'in-progress' | 'paused' | 'error' = 'done'; + let status: CallLogStatus = 'done'; if (this._currentCallsMetadata.has(metadata)) status = 'in-progress'; if (this._pausedCallsMetadata.has(metadata)) status = 'paused'; - if (metadata.error) - status = 'error'; - const params = { - url: metadata.params?.url, - selector: metadata.params?.selector, - }; - let duration = metadata.endTime ? metadata.endTime - metadata.startTime : undefined; - if (typeof duration === 'number' && metadata.pauseStartTime && metadata.pauseEndTime) { - duration -= (metadata.pauseEndTime - metadata.pauseStartTime); - duration = Math.max(duration, 0); - } - logs.push({ - id: metadata.id, - messages: metadata.log, - title, - status, - error: metadata.error, - params, - duration, - snapshots: { - before: showBeforeSnapshot(metadata) && this._snapshots.has(`before@${metadata.id}`), - in: showInSnapshot(metadata) && this._snapshots.has(`in@${metadata.id}`), - after: showAfterSnapshot(metadata) && this._snapshots.has(`after@${metadata.id}`), - } - }); + logs.push(metadataToCallLog(metadata, status, this._snapshots)); } this._recorderApp?.updateCallLogs(logs); } @@ -548,15 +524,3 @@ function shouldPauseOnCall(sdkObject: SdkObject, metadata: CallMetadata): boolea function shouldPauseOnStep(sdkObject: SdkObject, metadata: CallMetadata): boolean { return metadata.method === 'goto' || metadata.method === 'close'; } - -function showBeforeSnapshot(metadata: CallMetadata): boolean { - return metadata.method === 'close'; -} - -function showInSnapshot(metadata: CallMetadata): boolean { - return ['click', 'dblclick', 'check', 'uncheck', 'fill', 'press'].includes(metadata.method); -} - -function showAfterSnapshot(metadata: CallMetadata): boolean { - return ['goto', 'click', 'dblclick', 'dblclick', 'check', 'uncheck', 'fill', 'press'].includes(metadata.method); -} diff --git a/src/server/trace/recorder/tracer.ts b/src/server/trace/recorder/tracer.ts index 3f23fad025b54..05c21524d37e8 100644 --- a/src/server/trace/recorder/tracer.ts +++ b/src/server/trace/recorder/tracer.ts @@ -108,7 +108,7 @@ class ContextTracer { await this._snapshotter.start(); } - async _captureSnapshot(name: string, sdkObject: SdkObject, metadata: CallMetadata, element?: ElementHandle): Promise { + async _captureSnapshot(name: 'before' | 'after' | 'action', sdkObject: SdkObject, metadata: CallMetadata, element?: ElementHandle): Promise { if (!sdkObject.attribution.page) return; const snapshotName = `${name}@${metadata.id}`; diff --git a/src/server/trace/viewer/traceViewer.ts b/src/server/trace/viewer/traceViewer.ts index 4d80f2f2f6900..5dbdf88987d6d 100644 --- a/src/server/trace/viewer/traceViewer.ts +++ b/src/server/trace/viewer/traceViewer.ts @@ -115,7 +115,7 @@ class TraceViewer { const traceViewerPlaywright = createPlaywright(true); const args = [ '--app=data:text/html,', - '--window-position=1280,10', + '--window-size=1280,800' ]; if (isUnderTest()) args.push(`--remote-debugging-port=0`); diff --git a/src/web/common.css b/src/web/common.css index c18c515339bf6..805d7a156cf2a 100644 --- a/src/web/common.css +++ b/src/web/common.css @@ -16,7 +16,7 @@ :root { --toolbar-bg-color: #fafafa; - --toolbar-color: #777; + --toolbar-color: #555; --light-background: #f3f2f1; --background: #edebe9; @@ -27,7 +27,7 @@ --purple: #9C27B0; --yellow: #FFC107; --white: #FFFFFF; - --blue: #2196F3; + --blue: #0b7ad5; --transparent-blue: #2196F355; --orange: #d24726; --black: #1E1E1E; @@ -53,7 +53,6 @@ html, body { margin: 0; overflow: hidden; display: flex; - background: var(--background); overscroll-behavior-x: none; } @@ -64,7 +63,6 @@ html, body { } body { - background-color: var(--background); color: var(--color); font-size: 14px; font-family: SegoeUI-SemiBold-final,Segoe UI Semibold,SegoeUI-Regular-final,Segoe UI,"Segoe UI Web (West European)",Segoe,-apple-system,BlinkMacSystemFont,Roboto,Helvetica Neue,Tahoma,Helvetica,Arial,sans-serif; diff --git a/src/web/components/source.css b/src/web/components/source.css index 2e0eed549aca5..ccd74c80c0ec2 100644 --- a/src/web/components/source.css +++ b/src/web/components/source.css @@ -24,6 +24,7 @@ font-size: 11px; line-height: 16px; background: white; + user-select: text; } .source-line { @@ -36,7 +37,7 @@ padding: 0 8px; width: 30px; text-align: right; - background: #edebe9; + background: #f6f5f4; user-select: none; } diff --git a/src/web/components/splitView.css b/src/web/components/splitView.css index adc6dc5b6f2f5..7f3d2ef1c8e97 100644 --- a/src/web/components/splitView.css +++ b/src/web/components/splitView.css @@ -21,6 +21,10 @@ position: relative; } +.split-view.horizontal { + flex-direction: row; +} + .split-view-main { display: flex; flex: auto; @@ -32,12 +36,29 @@ border-top: 1px solid #ddd; } +.split-view.vertical > .split-view-sidebar { + border-top: 1px solid #ddd; +} + +.split-view.horizontal > .split-view-sidebar { + border-left: 1px solid #ddd; +} + .split-view-resizer { position: absolute; + z-index: 100; +} + +.split-view.vertical > .split-view-resizer { left: 0; right: 0; height: 12px; - cursor: resize; cursor: ns-resize; - z-index: 100; +} + +.split-view.horizontal > .split-view-resizer { + top: 0; + bottom: 0; + width: 12px; + cursor: ew-resize; } diff --git a/src/web/components/splitView.tsx b/src/web/components/splitView.tsx index 23fcd62c1f309..0ea8c665c73ea 100644 --- a/src/web/components/splitView.tsx +++ b/src/web/components/splitView.tsx @@ -20,6 +20,7 @@ import * as React from 'react'; export interface SplitViewProps { sidebarSize: number, sidebarHidden?: boolean + orientation?: 'vertical' | 'horizontal', } const kMinSidebarSize = 50; @@ -27,26 +28,30 @@ const kMinSidebarSize = 50; export const SplitView: React.FC = ({ sidebarSize, sidebarHidden, + orientation = 'vertical', children }) => { let [size, setSize] = React.useState(Math.max(kMinSidebarSize, sidebarSize)); - const [resizing, setResizing] = React.useState<{ offsetY: number, size: number } | null>(null); + const [resizing, setResizing] = React.useState<{ offset: number, size: number } | null>(null); const childrenArray = React.Children.toArray(children); document.body.style.userSelect = resizing ? 'none' : 'inherit'; - return
+ const resizerStyle = orientation === 'vertical' ? + {bottom: resizing ? 0 : size - 4, top: resizing ? 0 : undefined, height: resizing ? 'initial' : 8 } : + {right: resizing ? 0 : size - 4, left: resizing ? 0 : undefined, width: resizing ? 'initial' : 8 }; + return
{childrenArray[0]}
{ !sidebarHidden &&
{childrenArray[1]}
} { !sidebarHidden &&
setResizing({ offsetY: event.clientY, size })} + onMouseDown={event => setResizing({ offset: orientation === 'vertical' ? event.clientY : event.clientX, size })} onMouseUp={() => setResizing(null)} onMouseMove={event => { if (!event.buttons) setResizing(null); else if (resizing) - setSize(Math.max(kMinSidebarSize, resizing.size - event.clientY + resizing.offsetY)); + setSize(Math.max(kMinSidebarSize, resizing.size - (orientation === 'vertical' ? event.clientY : event.clientX) + resizing.offset)); }} >
}
; diff --git a/src/web/components/toolbarButton.css b/src/web/components/toolbarButton.css index e6839cedf8030..a1e136c511006 100644 --- a/src/web/components/toolbarButton.css +++ b/src/web/components/toolbarButton.css @@ -32,7 +32,7 @@ } .toolbar-button:not(.disabled):hover { - color: #555; + color: #333; } .toolbar-button .codicon { diff --git a/src/web/recorder/callLog.tsx b/src/web/recorder/callLog.tsx index 732383124f85a..e3955f52f96a2 100644 --- a/src/web/recorder/callLog.tsx +++ b/src/web/recorder/callLog.tsx @@ -21,7 +21,7 @@ import { msToString } from '../uiUtils'; export interface CallLogProps { log: CallLog[], - onHover: (callLog: CallLog | undefined, phase?: 'before' | 'after' | 'in') => void + onHover: (callLog: CallLog | undefined, phase?: 'before' | 'after' | 'action') => void } export const CallLogView: React.FC = ({ @@ -53,7 +53,7 @@ export const CallLogView: React.FC = ({ { typeof callLog.duration === 'number' ? — {msToString(callLog.duration)} : undefined} {
} onHover(callLog, 'before')} onMouseLeave={() => onHover(undefined)}> - onHover(callLog, 'in')} onMouseLeave={() => onHover(undefined)}> + onHover(callLog, 'action')} onMouseLeave={() => onHover(undefined)}> onHover(callLog, 'after')} onMouseLeave={() => onHover(undefined)}>
{ (isExpanded ? callLog.messages : []).map((message, i) => { diff --git a/src/web/traceViewer/ui/actionList.css b/src/web/traceViewer/ui/actionList.css index 97fc1a4cbc904..9c6dbdc3c3c6c 100644 --- a/src/web/traceViewer/ui/actionList.css +++ b/src/web/traceViewer/ui/actionList.css @@ -21,28 +21,27 @@ flex: none; position: relative; padding: 0 var(--layout-gap); + user-select: none; + color: #555; } .action-entry { - position: relative; display: flex; - flex-direction: column; flex: none; - overflow: hidden; - border: 3px solid var(--background); - margin-top: var(--layout-gap); - user-select: none; - padding: 0 5px 5px 5px; cursor: pointer; - outline: none; + align-items: center; + white-space: nowrap; + line-height: 28px; + padding-left: 3px; + border-left: 3px solid transparent; } -.action-entry:hover { - border-color: var(--inactive-focus-ring); +.action-entry:hover, .action-entry.selected { + color: #333; } .action-entry.selected { - border-color: var(--inactive-focus-ring); + border-left: 3px solid #666; } .action-entry.selected:focus { @@ -52,19 +51,9 @@ .action-title { display: inline; white-space: nowrap; - font-weight: 600; -} - -.action-header { - display: block; - align-items: center; - margin: 5px 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; } -.action-header .action-error { +.action-error { color: red; top: 2px; position: relative; @@ -74,9 +63,11 @@ .action-selector { display: inline; padding-left: 5px; + color: var(--orange); } .action-url { display: inline; padding-left: 5px; + color: var(--blue); } diff --git a/src/web/traceViewer/ui/actionList.tsx b/src/web/traceViewer/ui/actionList.tsx index ce5f95f7f3820..e6c28bf39e0d7 100644 --- a/src/web/traceViewer/ui/actionList.tsx +++ b/src/web/traceViewer/ui/actionList.tsx @@ -33,22 +33,19 @@ export const ActionList: React.FC = ({ onSelected = () => {}, onHighlighted = () => {}, }) => { - const targetAction = highlightedAction || selectedAction; return
{actions.map(actionEntry => { const { metadata, actionId } = actionEntry; return
onSelected(actionEntry)} onMouseEnter={() => onHighlighted(actionEntry)} onMouseLeave={() => (highlightedAction === actionEntry) && onHighlighted(undefined)} > -
- + ; })}
; }; diff --git a/src/web/traceViewer/ui/logsTab.css b/src/web/traceViewer/ui/logsTab.css index b177874fd5b2a..f1d98f4c6c099 100644 --- a/src/web/traceViewer/ui/logsTab.css +++ b/src/web/traceViewer/ui/logsTab.css @@ -16,14 +16,15 @@ .logs-tab { flex: auto; - position: relative; + line-height: 20px; + white-space: pre; overflow: auto; - background: #fdfcfc; - font-family: var(--monospace-font); - white-space: nowrap; + padding-top: 3px; } .log-line { - margin: 0 10px; - white-space: pre; + flex: none; + padding: 3px 0 3px 12px; + display: flex; + align-items: center; } diff --git a/src/web/traceViewer/ui/networkResourceDetails.css b/src/web/traceViewer/ui/networkResourceDetails.css index 613351645fc09..5cbf6e44a6251 100644 --- a/src/web/traceViewer/ui/networkResourceDetails.css +++ b/src/web/traceViewer/ui/networkResourceDetails.css @@ -15,15 +15,12 @@ */ .network-request { - box-shadow: var(--box-shadow); white-space: nowrap; display: flex; align-items: center; padding: 0 10px; - margin-bottom: 10px; background: #fdfcfc; width: 100%; - border: 3px solid transparent; flex: none; outline: none; } @@ -38,23 +35,14 @@ } .network-request-title { - height: 36px; + height: 28px; display: flex; align-items: center; flex: 1; } .network-request-title-status { - font-weight: bold; - height: 100%; - display: flex; - align-items: center; - padding: 0px 5px; - margin-right: 5px; -} - -.network-request-title-status.status-success { - background-color: var(--green); + padding-right: 5px; } .network-request-title-status.status-failure { @@ -66,20 +54,12 @@ background-color: var(--white); } -.network-request-title-method { - font-weight: bold; -} - .network-request-title-url { overflow: hidden; text-overflow: ellipsis; flex: 1; } -.network-request-title-content-type { - font-weight: bold; -} - .network-request-details { font-family: var(--monospace-font); width: 100%; diff --git a/src/web/traceViewer/ui/snapshotTab.css b/src/web/traceViewer/ui/snapshotTab.css index de0f67ecc5ef3..6d2760e761e73 100644 --- a/src/web/traceViewer/ui/snapshotTab.css +++ b/src/web/traceViewer/ui/snapshotTab.css @@ -16,6 +16,7 @@ .snapshot-tab { display: flex; + flex: auto; flex-direction: column; align-items: stretch; } @@ -25,11 +26,18 @@ display: flex; flex-direction: row; align-items: center; + padding: 10px 4px 0 0; } .snapshot-toggle { - padding: 5px 10px; + padding: 4px 8px; cursor: pointer; + border-radius: 20px; + margin-left: 4px; +} + +.snapshot-toggle:hover { + background-color: #ededed; } .snapshot-toggle.toggled { @@ -39,12 +47,13 @@ .snapshot-wrapper { flex: auto; margin: 1px; + padding: 10px; } .snapshot-container { display: block; background: white; - outline: 1px solid #aaa; + box-shadow: rgb(0 0 0 / 15%) 0px 0.1em 4.5em; } iframe#snapshot { diff --git a/src/web/traceViewer/ui/snapshotTab.tsx b/src/web/traceViewer/ui/snapshotTab.tsx index ce29b68e616c3..490f00a78b154 100644 --- a/src/web/traceViewer/ui/snapshotTab.tsx +++ b/src/web/traceViewer/ui/snapshotTab.tsx @@ -51,7 +51,7 @@ export const SnapshotTab: React.FunctionComponent<{ point = actionEntry.metadata.point; } } - const snapshotUrl = snapshotUri ? `${window.location.origin}/snapshot/${snapshotUri}` : 'data:text/html,Snapshot is not available'; + const snapshotUrl = snapshotUri ? `${window.location.origin}/snapshot/${snapshotUri}` : 'data:text/html,'; try { (iframeRef.current.contentWindow as any).showSnapshot(snapshotUrl, { point }); } catch (e) { @@ -59,6 +59,10 @@ export const SnapshotTab: React.FunctionComponent<{ }, [actionEntry, snapshotIndex, pageId, time]); const scale = Math.min(measure.width / snapshotSize.width, measure.height / snapshotSize.height); + const scaledSize = { + width: snapshotSize.width * scale, + height: snapshotSize.height * scale, + }; return
{ selection &&
@@ -77,7 +81,7 @@ export const SnapshotTab: React.FunctionComponent<{
diff --git a/src/web/traceViewer/ui/sourceTab.css b/src/web/traceViewer/ui/sourceTab.css index cd1487e6d001c..f3c0f6171bed0 100644 --- a/src/web/traceViewer/ui/sourceTab.css +++ b/src/web/traceViewer/ui/sourceTab.css @@ -18,72 +18,6 @@ flex: auto; position: relative; overflow: hidden; - background: #fdfcfc; - font-family: var(--monospace-font); - white-space: nowrap; display: flex; flex-direction: row; } - -.source-content { - flex: 1 1 600px; - overflow: auto; -} - -.source-stack { - flex: 1 1 120px; - display: flex; - flex-direction: column; - align-items: stretch; - overflow-y: auto; -} - -.source-stack-frame { - flex: 0 0 20px; - font-size: smaller; - display: flex; - flex-direction: row; - align-items: center; - cursor: pointer; -} - -.source-stack-frame.selected, -.source-stack-frame:hover { - background: var(--inactive-focus-ring); -} - -.source-stack-frame-function { - flex: 1 1 100px; - overflow: hidden; - text-overflow: ellipsis; -} - -.source-stack-frame-location { - flex: 1 1 100px; - overflow: hidden; - text-overflow: ellipsis; - text-align: end; -} - -.source-stack-frame-line { - flex: none; -} - -.source-line-number { - width: 80px; - border-right: 1px solid var(--separator); - display: inline-block; - margin-right: 3px; - text-align: end; - padding-right: 4px; - color: var(--gray); -} - -.source-code { - white-space: pre; - display: inline-block; -} - -.source-line-highlight { - background-color: #ff69b460; -} diff --git a/src/web/traceViewer/ui/sourceTab.tsx b/src/web/traceViewer/ui/sourceTab.tsx index f917b8819fb9c..8d75454664aa0 100644 --- a/src/web/traceViewer/ui/sourceTab.tsx +++ b/src/web/traceViewer/ui/sourceTab.tsx @@ -19,8 +19,10 @@ import * as React from 'react'; import { useAsyncMemo } from './helpers'; import './sourceTab.css'; import '../../../third_party/highlightjs/highlightjs/tomorrow.css'; -import * as highlightjs from '../../../third_party/highlightjs/highlightjs'; import { StackFrame } from '../../../common/types'; +import { Source as SourceView } from '../../components/source'; +import { StackTraceView } from './stackTrace'; +import { SplitView } from '../../components/splitView'; type StackInfo = string | { frames: StackFrame[]; @@ -53,7 +55,7 @@ export const SourceTab: React.FunctionComponent<{ }; }, [actionEntry]); - const content = useAsyncMemo(async () => { + const content = useAsyncMemo(async () => { let value: string; if (typeof stackInfo === 'string') { value = stackInfo; @@ -63,17 +65,10 @@ export const SourceTab: React.FunctionComponent<{ stackInfo.fileContent.set(filePath, await fetch(`/file?${filePath}`).then(response => response.text()).catch(e => ``)); value = stackInfo.fileContent.get(filePath)!; } - const result = []; - let continuation: any; - for (const line of (value || '').split('\n')) { - const highlighted = highlightjs.highlight('javascript', line, true, continuation); - continuation = highlighted.top; - result.push(highlighted.value); - } - return result; - }, [stackInfo, selectedFrame], []); + return value; + }, [stackInfo, selectedFrame], ''); - const targetLine = typeof stackInfo === 'string' ? -1 : stackInfo.frames[selectedFrame].line; + const targetLine = typeof stackInfo === 'string' ? 0 : stackInfo.frames[selectedFrame].line || 0; const targetLineRef = React.createRef(); React.useLayoutEffect(() => { @@ -83,41 +78,8 @@ export const SourceTab: React.FunctionComponent<{ } }, [needReveal, targetLineRef]); - return
-
{ - content.map((markup, index) => { - const isTargetLine = (index + 1) === targetLine; - return
-
{index + 1}
-
-
; - }) - }
- {typeof stackInfo !== 'string' &&
{ - stackInfo.frames.map((frame, index) => { - return
{ - setSelectedFrame(index); - setNeedReveal(true); - }} - > - - {frame.function || '(anonymous)'} - - - {frame.file} - - - {':' + frame.line} - -
; - }) - }
} -
; + return + + + ; }; diff --git a/src/web/traceViewer/ui/stackTrace.css b/src/web/traceViewer/ui/stackTrace.css new file mode 100644 index 0000000000000..7e1bd990b7bea --- /dev/null +++ b/src/web/traceViewer/ui/stackTrace.css @@ -0,0 +1,55 @@ +/* + 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. +*/ + +.stack-trace { + flex: 1 1 120px; + display: flex; + flex-direction: column; + align-items: stretch; + overflow-y: auto; +} + +.stack-trace-frame { + flex: 0 0 20px; + font-size: smaller; + display: flex; + flex-direction: row; + align-items: center; + cursor: pointer; + padding: 0 5px; +} + +.stack-trace-frame.selected, +.stack-trace-frame:hover { + background-color: #eaeaea; +} + +.stack-trace-frame-function { + flex: 1 1 100px; + overflow: hidden; + text-overflow: ellipsis; +} + +.stack-trace-frame-location { + flex: 1 1 100px; + overflow: hidden; + text-overflow: ellipsis; + text-align: end; +} + +.stack-trace-frame-line { + flex: none; +} diff --git a/src/web/traceViewer/ui/stackTrace.tsx b/src/web/traceViewer/ui/stackTrace.tsx new file mode 100644 index 0000000000000..e2273f1b61498 --- /dev/null +++ b/src/web/traceViewer/ui/stackTrace.tsx @@ -0,0 +1,49 @@ +/** + * 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 '../../../server/trace/viewer/traceModel'; +import * as React from 'react'; +import './stackTrace.css'; + +export const StackTraceView: React.FunctionComponent<{ + actionEntry: ActionEntry | undefined, + selectedFrame: number, + setSelectedFrame: (index: number) => void +}> = ({ actionEntry, setSelectedFrame, selectedFrame }) => { + const frames = actionEntry?.metadata.stack || []; + return
{ + frames.map((frame, index) => { + return
{ + setSelectedFrame(index); + }} + > + + {frame.function || '(anonymous)'} + + + {frame.file.split('/').pop()} + + + {':' + frame.line} + +
; + }) + } +
; +}; diff --git a/src/web/traceViewer/ui/tabbedPane.css b/src/web/traceViewer/ui/tabbedPane.css index 8060c716e5f31..82a4964d71a1e 100644 --- a/src/web/traceViewer/ui/tabbedPane.css +++ b/src/web/traceViewer/ui/tabbedPane.css @@ -27,11 +27,16 @@ } .tab-strip { - flex: auto; + color: var(--toolbar-color); display: flex; - flex-direction: row; + box-shadow: var(--box-shadow); + background-color: var(--toolbar-bg-color); + height: 40px; align-items: center; - height: 34px; + padding-right: 10px; + flex: none; + width: 100%; + z-index: 2; } .tab-strip:focus { @@ -50,6 +55,7 @@ border-bottom: 3px solid transparent; width: 80px; outline: none; + height: 100%; } .tab-label { @@ -61,9 +67,9 @@ } .tab-element.selected { - border-bottom-color: var(--color); + border-bottom-color: #666; } .tab-element:hover { - font-weight: 600; + color: #333; } diff --git a/src/web/traceViewer/ui/timeline.css b/src/web/traceViewer/ui/timeline.css index 9ce247409e165..33d74b685069a 100644 --- a/src/web/traceViewer/ui/timeline.css +++ b/src/web/traceViewer/ui/timeline.css @@ -20,7 +20,7 @@ position: relative; display: flex; flex-direction: column; - background: white; + border-bottom: 1px solid #ddd; padding: 20px 0 5px; cursor: text; } diff --git a/src/web/traceViewer/ui/workbench.css b/src/web/traceViewer/ui/workbench.css index c27aa7cac8fae..a31c9574a0ebb 100644 --- a/src/web/traceViewer/ui/workbench.css +++ b/src/web/traceViewer/ui/workbench.css @@ -16,6 +16,7 @@ .workbench { contain: size; + user-select: none; } .workbench .header { diff --git a/src/web/traceViewer/ui/workbench.tsx b/src/web/traceViewer/ui/workbench.tsx index 02aeb83d22b92..a8476998a2c5d 100644 --- a/src/web/traceViewer/ui/workbench.tsx +++ b/src/web/traceViewer/ui/workbench.tsx @@ -25,6 +25,8 @@ import { NetworkTab } from './networkTab'; import { SourceTab } from './sourceTab'; import { SnapshotTab } from './snapshotTab'; import { LogsTab } from './logsTab'; +import { SplitView } from '../../components/splitView'; + export const Workbench: React.FunctionComponent<{ contexts: ContextEntry[], @@ -71,7 +73,7 @@ export const Workbench: React.FunctionComponent<{ />
-
+
setHighlightedAction(action)} />
- }, - { id: 'source', title: 'Source', render: () => }, - { id: 'network', title: 'Network', render: () => }, - { id: 'logs', title: 'Logs', render: () => }, - ]}/> + + + }, + { id: 'source', title: 'Source', render: () => }, + { id: 'network', title: 'Network', render: () => }, + ]}/> + +
; };