chore: unify recorder & tracer uis (#5791)

This commit is contained in:
Pavel Feldman 2021-03-11 11:22:59 -08:00 committed by GitHub
parent 43de259522
commit ad69b2af83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 302 additions and 257 deletions

View File

@ -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,<body>Snapshot is not available</body>');
iframe.setAttribute('src', 'data:text/html,<body style="background: #ddd"></body>');
} else {
// Append query parameters to inherit ?name= or ?time= values from parent.
iframe.setAttribute('src', window.location.origin + src + window.location.search);

View File

@ -75,7 +75,7 @@ export class SnapshotServer {
}
function respondNotAvailable(): Response {
return new Response('<body>Snapshot is not available</body>', { status: 200, headers: { 'Content-Type': 'text/html' } });
return new Response('<body style="background: #ddd"></body>', { status: 200, headers: { 'Content-Type': 'text/html' } });
}
function removeHash(url: string) {

View File

@ -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,
}
};

View File

@ -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<string>): 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);
}

View File

@ -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<string, Source>();
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<string>();
private _allMetadatas = new Map<number, CallMetadata>();
@ -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<void> {
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<void> {
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<void> {
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);
}

View File

@ -108,7 +108,7 @@ class ContextTracer {
await this._snapshotter.start();
}
async _captureSnapshot(name: string, sdkObject: SdkObject, metadata: CallMetadata, element?: ElementHandle): Promise<void> {
async _captureSnapshot(name: 'before' | 'after' | 'action', sdkObject: SdkObject, metadata: CallMetadata, element?: ElementHandle): Promise<void> {
if (!sdkObject.attribution.page)
return;
const snapshotName = `${name}@${metadata.id}`;

View File

@ -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`);

View File

@ -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;

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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<SplitViewProps> = ({
sidebarSize,
sidebarHidden,
orientation = 'vertical',
children
}) => {
let [size, setSize] = React.useState<number>(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 <div className='split-view'>
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 <div className={'split-view ' + orientation}>
<div className='split-view-main'>{childrenArray[0]}</div>
{ !sidebarHidden && <div style={{flexBasis: size}} className='split-view-sidebar'>{childrenArray[1]}</div> }
{ !sidebarHidden && <div
style={{bottom: resizing ? 0 : size - 4, top: resizing ? 0 : undefined, height: resizing ? 'initial' : 8 }}
style={resizerStyle}
className='split-view-resizer'
onMouseDown={event => 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));
}}
></div> }
</div>;

View File

@ -32,7 +32,7 @@
}
.toolbar-button:not(.disabled):hover {
color: #555;
color: #333;
}
.toolbar-button .codicon {

View File

@ -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<CallLogProps> = ({
@ -53,7 +53,7 @@ export const CallLogView: React.FC<CallLogProps> = ({
{ typeof callLog.duration === 'number' ? <span className='call-log-time'> {msToString(callLog.duration)}</span> : undefined}
{ <div style={{flex: 'auto'}}></div> }
<span className={'codicon codicon-vm-outline preview' + (callLog.snapshots.before ? '' : ' invisible')} onMouseEnter={() => onHover(callLog, 'before')} onMouseLeave={() => onHover(undefined)}></span>
<span className={'codicon codicon-vm-running preview' + (callLog.snapshots.in ? '' : ' invisible')} onMouseEnter={() => onHover(callLog, 'in')} onMouseLeave={() => onHover(undefined)}></span>
<span className={'codicon codicon-vm-running preview' + (callLog.snapshots.action ? '' : ' invisible')} onMouseEnter={() => onHover(callLog, 'action')} onMouseLeave={() => onHover(undefined)}></span>
<span className={'codicon codicon-vm-active preview' + (callLog.snapshots.after ? '' : ' invisible')} onMouseEnter={() => onHover(callLog, 'after')} onMouseLeave={() => onHover(undefined)}></span>
</div>
{ (isExpanded ? callLog.messages : []).map((message, i) => {

View File

@ -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);
}

View File

@ -33,22 +33,19 @@ export const ActionList: React.FC<ActionListProps> = ({
onSelected = () => {},
onHighlighted = () => {},
}) => {
const targetAction = highlightedAction || selectedAction;
return <div className='action-list'>{actions.map(actionEntry => {
const { metadata, actionId } = actionEntry;
return <div
className={'action-entry' + (actionEntry === targetAction ? ' selected' : '')}
className={'action-entry' + (actionEntry === selectedAction ? ' selected' : '')}
key={actionId}
onClick={() => onSelected(actionEntry)}
onMouseEnter={() => onHighlighted(actionEntry)}
onMouseLeave={() => (highlightedAction === actionEntry) && onHighlighted(undefined)}
>
<div className='action-header'>
<div className={'action-error codicon codicon-issues'} hidden={!metadata.error} />
<div className='action-title'>{metadata.method}</div>
{metadata.params.selector && <div className='action-selector' title={metadata.params.selector}>{metadata.params.selector}</div>}
{metadata.method === 'goto' && metadata.params.url && <div className='action-url' title={metadata.params.url}>{metadata.params.url}</div>}
</div>
<div className={'action-error codicon codicon-issues'} hidden={!metadata.error} />
<div className='action-title'>{metadata.method}</div>
{metadata.params.selector && <div className='action-selector' title={metadata.params.selector}>{metadata.params.selector}</div>}
{metadata.method === 'goto' && metadata.params.url && <div className='action-url' title={metadata.params.url}>{metadata.params.url}</div>}
</div>;
})}</div>;
};

View File

@ -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;
}

View File

@ -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%;

View File

@ -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 {

View File

@ -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,<body style="background: #ddd"></body>';
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 <div className='snapshot-tab'>
<div className='snapshot-controls'>{
selection && <div key='selectedTime' className='snapshot-toggle'>
@ -77,7 +81,7 @@ export const SnapshotTab: React.FunctionComponent<{
<div className='snapshot-container' style={{
width: snapshotSize.width + 'px',
height: snapshotSize.height + 'px',
transform: `translate(${-snapshotSize.width * (1 - scale) / 2}px, ${-snapshotSize.height * (1 - scale) / 2}px) scale(${scale})`,
transform: `translate(${-snapshotSize.width * (1 - scale) / 2 + (measure.width - scaledSize.width) / 2}px, ${-snapshotSize.height * (1 - scale) / 2 + (measure.height - scaledSize.height) / 2}px) scale(${scale})`,
}}>
<iframe ref={iframeRef} id='snapshot' name='snapshot' src='/snapshot/'></iframe>
</div>

View File

@ -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;
}

View File

@ -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<string[]>(async () => {
const content = useAsyncMemo<string>(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 => `<Unable to read "${filePath}">`));
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<HTMLDivElement>();
React.useLayoutEffect(() => {
@ -83,41 +78,8 @@ export const SourceTab: React.FunctionComponent<{
}
}, [needReveal, targetLineRef]);
return <div className='source-tab'>
<div className='source-content'>{
content.map((markup, index) => {
const isTargetLine = (index + 1) === targetLine;
return <div
key={index}
className={isTargetLine ? 'source-line-highlight' : ''}
ref={isTargetLine ? targetLineRef : null}
>
<div className='source-line-number'>{index + 1}</div>
<div className='source-code' dangerouslySetInnerHTML={{ __html: markup }}></div>
</div>;
})
}</div>
{typeof stackInfo !== 'string' && <div className='source-stack'>{
stackInfo.frames.map((frame, index) => {
return <div
key={index}
className={'source-stack-frame' + (selectedFrame === index ? ' selected' : '')}
onClick={() => {
setSelectedFrame(index);
setNeedReveal(true);
}}
>
<span className='source-stack-frame-function'>
{frame.function || '(anonymous)'}
</span>
<span className='source-stack-frame-location'>
{frame.file}
</span>
<span className='source-stack-frame-line'>
{':' + frame.line}
</span>
</div>;
})
}</div>}
</div>;
return <SplitView sidebarSize={250} orientation='horizontal'>
<SourceView text={content} language='javascript' highlight={[{ line: targetLine, type: 'running' }]} revealLine={targetLine}></SourceView>
<StackTraceView actionEntry={actionEntry} selectedFrame={selectedFrame} setSelectedFrame={setSelectedFrame}></StackTraceView>
</SplitView>;
};

View File

@ -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;
}

View File

@ -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 <div className='stack-trace'>{
frames.map((frame, index) => {
return <div
key={index}
className={'stack-trace-frame' + (selectedFrame === index ? ' selected' : '')}
onClick={() => {
setSelectedFrame(index);
}}
>
<span className='stack-trace-frame-function'>
{frame.function || '(anonymous)'}
</span>
<span className='stack-trace-frame-location'>
{frame.file.split('/').pop()}
</span>
<span className='stack-trace-frame-line'>
{':' + frame.line}
</span>
</div>;
})
}
</div>;
};

View File

@ -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;
}

View File

@ -20,7 +20,7 @@
position: relative;
display: flex;
flex-direction: column;
background: white;
border-bottom: 1px solid #ddd;
padding: 20px 0 5px;
cursor: text;
}

View File

@ -16,6 +16,7 @@
.workbench {
contain: size;
user-select: none;
}
.workbench .header {

View File

@ -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<{
/>
</div>
<div className='hbox'>
<div style={{ display: 'flex', flex: 'none', overflow: 'auto' }}>
<div style={{ display: 'flex', flex: 'none', overflow: 'auto', borderRight: '1px solid #ddd' }}>
<ActionList
actions={actions}
selectedAction={selectedAction}
@ -83,12 +85,15 @@ export const Workbench: React.FunctionComponent<{
onHighlighted={action => setHighlightedAction(action)}
/>
</div>
<TabbedPane tabs={[
{ id: 'snapshot', title: 'Snapshot', render: () => <SnapshotTab actionEntry={selectedAction} snapshotSize={snapshotSize} selection={snapshotSelection} boundaries={boundaries} /> },
{ id: 'source', title: 'Source', render: () => <SourceTab actionEntry={selectedAction} /> },
{ id: 'network', title: 'Network', render: () => <NetworkTab actionEntry={selectedAction} /> },
{ id: 'logs', title: 'Logs', render: () => <LogsTab actionEntry={selectedAction} /> },
]}/>
<SplitView sidebarSize={250}>
<SnapshotTab actionEntry={selectedAction} snapshotSize={snapshotSize} selection={snapshotSelection} boundaries={boundaries} />
<TabbedPane tabs={[
{ id: 'logs', title: 'Log', render: () => <LogsTab actionEntry={selectedAction} /> },
{ id: 'source', title: 'Source', render: () => <SourceTab actionEntry={selectedAction} /> },
{ id: 'network', title: 'Network', render: () => <NetworkTab actionEntry={selectedAction} /> },
]}/>
</SplitView>
</div>
</div>;
};