mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: unify recorder & tracer uis (#5791)
This commit is contained in:
parent
43de259522
commit
ad69b2af83
@ -138,7 +138,7 @@ function snapshotScript() {
|
|||||||
for (const iframe of root.querySelectorAll('iframe')) {
|
for (const iframe of root.querySelectorAll('iframe')) {
|
||||||
const src = iframe.getAttribute('src');
|
const src = iframe.getAttribute('src');
|
||||||
if (!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 {
|
} else {
|
||||||
// Append query parameters to inherit ?name= or ?time= values from parent.
|
// Append query parameters to inherit ?name= or ?time= values from parent.
|
||||||
iframe.setAttribute('src', window.location.origin + src + window.location.search);
|
iframe.setAttribute('src', window.location.origin + src + window.location.search);
|
||||||
|
@ -75,7 +75,7 @@ export class SnapshotServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function respondNotAvailable(): Response {
|
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) {
|
function removeHash(url: string) {
|
||||||
|
@ -30,11 +30,13 @@ export type UIState = {
|
|||||||
snapshotUrl?: string;
|
snapshotUrl?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CallLogStatus = 'in-progress' | 'done' | 'error' | 'paused';
|
||||||
|
|
||||||
export type CallLog = {
|
export type CallLog = {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
messages: string[];
|
messages: string[];
|
||||||
status: 'in-progress' | 'done' | 'error' | 'paused';
|
status: CallLogStatus;
|
||||||
error?: string;
|
error?: string;
|
||||||
reveal?: boolean;
|
reveal?: boolean;
|
||||||
duration?: number;
|
duration?: number;
|
||||||
@ -44,7 +46,7 @@ export type CallLog = {
|
|||||||
};
|
};
|
||||||
snapshots: {
|
snapshots: {
|
||||||
before: boolean,
|
before: boolean,
|
||||||
in: boolean,
|
action: boolean,
|
||||||
after: boolean,
|
after: boolean,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
60
src/server/supplements/recorder/recorderUtils.ts
Normal file
60
src/server/supplements/recorder/recorderUtils.ts
Normal 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);
|
||||||
|
}
|
@ -31,9 +31,10 @@ import * as consoleApiSource from '../../generated/consoleApiSource';
|
|||||||
import { RecorderApp } from './recorder/recorderApp';
|
import { RecorderApp } from './recorder/recorderApp';
|
||||||
import { CallMetadata, internalCallMetadata, SdkObject } from '../instrumentation';
|
import { CallMetadata, internalCallMetadata, SdkObject } from '../instrumentation';
|
||||||
import { Point } from '../../common/types';
|
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 { isUnderTest, monotonicTime } from '../../utils/utils';
|
||||||
import { InMemorySnapshotter } from '../snapshot/inMemorySnapshotter';
|
import { InMemorySnapshotter } from '../snapshot/inMemorySnapshotter';
|
||||||
|
import { metadataToCallLog } from './recorder/recorderUtils';
|
||||||
|
|
||||||
type BindingSource = { frame: Frame, page: Page };
|
type BindingSource = { frame: Frame, page: Page };
|
||||||
|
|
||||||
@ -56,7 +57,7 @@ export class RecorderSupplement {
|
|||||||
private _recorderSources: Source[];
|
private _recorderSources: Source[];
|
||||||
private _userSources = new Map<string, Source>();
|
private _userSources = new Map<string, Source>();
|
||||||
private _snapshotter: InMemorySnapshotter;
|
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 _snapshots = new Set<string>();
|
||||||
private _allMetadatas = new Map<number, CallMetadata>();
|
private _allMetadatas = new Map<number, CallMetadata>();
|
||||||
|
|
||||||
@ -209,7 +210,7 @@ export class RecorderSupplement {
|
|||||||
if (this._hoveredSnapshot) {
|
if (this._hoveredSnapshot) {
|
||||||
const metadata = this._allMetadatas.get(this._hoveredSnapshot.callLogId)!;
|
const metadata = this._allMetadatas.get(this._hoveredSnapshot.callLogId)!;
|
||||||
snapshotUrl = `${metadata.pageId}?name=${this._hoveredSnapshot.phase}@${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 {
|
} else {
|
||||||
for (const [metadata, sdkObject] of this._currentCallsMetadata) {
|
for (const [metadata, sdkObject] of this._currentCallsMetadata) {
|
||||||
if (source.page === sdkObject.attribution.page) {
|
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) });
|
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) {
|
if (sdkObject.attribution.page) {
|
||||||
const snapshotName = `${phase}@${metadata.id}`;
|
const snapshotName = `${phase}@${metadata.id}`;
|
||||||
this._snapshots.add(snapshotName);
|
this._snapshots.add(snapshotName);
|
||||||
@ -428,7 +429,7 @@ export class RecorderSupplement {
|
|||||||
async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||||
if (this._mode === 'recording')
|
if (this._mode === 'recording')
|
||||||
return;
|
return;
|
||||||
await this._captureSnapshot(sdkObject, metadata, 'after');
|
this._captureSnapshot(sdkObject, metadata, 'after');
|
||||||
if (!metadata.error)
|
if (!metadata.error)
|
||||||
this._currentCallsMetadata.delete(metadata);
|
this._currentCallsMetadata.delete(metadata);
|
||||||
this._pausedCallsMetadata.delete(metadata);
|
this._pausedCallsMetadata.delete(metadata);
|
||||||
@ -473,49 +474,24 @@ export class RecorderSupplement {
|
|||||||
async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||||
if (this._mode === 'recording')
|
if (this._mode === 'recording')
|
||||||
return;
|
return;
|
||||||
await this._captureSnapshot(sdkObject, metadata, 'in');
|
this._captureSnapshot(sdkObject, metadata, 'action');
|
||||||
if (this._pauseOnNextStatement)
|
if (this._pauseOnNextStatement)
|
||||||
await this.pause(metadata);
|
await this.pause(metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateCallLog(metadatas: CallMetadata[]): Promise<void> {
|
updateCallLog(metadatas: CallMetadata[]) {
|
||||||
if (this._mode === 'recording')
|
if (this._mode === 'recording')
|
||||||
return;
|
return;
|
||||||
const logs: CallLog[] = [];
|
const logs: CallLog[] = [];
|
||||||
for (const metadata of metadatas) {
|
for (const metadata of metadatas) {
|
||||||
if (!metadata.method)
|
if (!metadata.method)
|
||||||
continue;
|
continue;
|
||||||
const title = metadata.apiName || metadata.method;
|
let status: CallLogStatus = 'done';
|
||||||
let status: 'done' | 'in-progress' | 'paused' | 'error' = 'done';
|
|
||||||
if (this._currentCallsMetadata.has(metadata))
|
if (this._currentCallsMetadata.has(metadata))
|
||||||
status = 'in-progress';
|
status = 'in-progress';
|
||||||
if (this._pausedCallsMetadata.has(metadata))
|
if (this._pausedCallsMetadata.has(metadata))
|
||||||
status = 'paused';
|
status = 'paused';
|
||||||
if (metadata.error)
|
logs.push(metadataToCallLog(metadata, status, this._snapshots));
|
||||||
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}`),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
this._recorderApp?.updateCallLogs(logs);
|
this._recorderApp?.updateCallLogs(logs);
|
||||||
}
|
}
|
||||||
@ -548,15 +524,3 @@ function shouldPauseOnCall(sdkObject: SdkObject, metadata: CallMetadata): boolea
|
|||||||
function shouldPauseOnStep(sdkObject: SdkObject, metadata: CallMetadata): boolean {
|
function shouldPauseOnStep(sdkObject: SdkObject, metadata: CallMetadata): boolean {
|
||||||
return metadata.method === 'goto' || metadata.method === 'close';
|
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);
|
|
||||||
}
|
|
||||||
|
@ -108,7 +108,7 @@ class ContextTracer {
|
|||||||
await this._snapshotter.start();
|
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)
|
if (!sdkObject.attribution.page)
|
||||||
return;
|
return;
|
||||||
const snapshotName = `${name}@${metadata.id}`;
|
const snapshotName = `${name}@${metadata.id}`;
|
||||||
|
@ -115,7 +115,7 @@ class TraceViewer {
|
|||||||
const traceViewerPlaywright = createPlaywright(true);
|
const traceViewerPlaywright = createPlaywright(true);
|
||||||
const args = [
|
const args = [
|
||||||
'--app=data:text/html,',
|
'--app=data:text/html,',
|
||||||
'--window-position=1280,10',
|
'--window-size=1280,800'
|
||||||
];
|
];
|
||||||
if (isUnderTest())
|
if (isUnderTest())
|
||||||
args.push(`--remote-debugging-port=0`);
|
args.push(`--remote-debugging-port=0`);
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
:root {
|
:root {
|
||||||
--toolbar-bg-color: #fafafa;
|
--toolbar-bg-color: #fafafa;
|
||||||
--toolbar-color: #777;
|
--toolbar-color: #555;
|
||||||
|
|
||||||
--light-background: #f3f2f1;
|
--light-background: #f3f2f1;
|
||||||
--background: #edebe9;
|
--background: #edebe9;
|
||||||
@ -27,7 +27,7 @@
|
|||||||
--purple: #9C27B0;
|
--purple: #9C27B0;
|
||||||
--yellow: #FFC107;
|
--yellow: #FFC107;
|
||||||
--white: #FFFFFF;
|
--white: #FFFFFF;
|
||||||
--blue: #2196F3;
|
--blue: #0b7ad5;
|
||||||
--transparent-blue: #2196F355;
|
--transparent-blue: #2196F355;
|
||||||
--orange: #d24726;
|
--orange: #d24726;
|
||||||
--black: #1E1E1E;
|
--black: #1E1E1E;
|
||||||
@ -53,7 +53,6 @@ html, body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
background: var(--background);
|
|
||||||
overscroll-behavior-x: none;
|
overscroll-behavior-x: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,7 +63,6 @@ html, body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: var(--background);
|
|
||||||
color: var(--color);
|
color: var(--color);
|
||||||
font-size: 14px;
|
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;
|
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;
|
||||||
|
@ -24,6 +24,7 @@
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
background: white;
|
background: white;
|
||||||
|
user-select: text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.source-line {
|
.source-line {
|
||||||
@ -36,7 +37,7 @@
|
|||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
width: 30px;
|
width: 30px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
background: #edebe9;
|
background: #f6f5f4;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,6 +21,10 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.split-view.horizontal {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
.split-view-main {
|
.split-view-main {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: auto;
|
flex: auto;
|
||||||
@ -32,12 +36,29 @@
|
|||||||
border-top: 1px solid #ddd;
|
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 {
|
.split-view-resizer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-view.vertical > .split-view-resizer {
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
cursor: resize;
|
|
||||||
cursor: ns-resize;
|
cursor: ns-resize;
|
||||||
z-index: 100;
|
}
|
||||||
|
|
||||||
|
.split-view.horizontal > .split-view-resizer {
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 12px;
|
||||||
|
cursor: ew-resize;
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ import * as React from 'react';
|
|||||||
export interface SplitViewProps {
|
export interface SplitViewProps {
|
||||||
sidebarSize: number,
|
sidebarSize: number,
|
||||||
sidebarHidden?: boolean
|
sidebarHidden?: boolean
|
||||||
|
orientation?: 'vertical' | 'horizontal',
|
||||||
}
|
}
|
||||||
|
|
||||||
const kMinSidebarSize = 50;
|
const kMinSidebarSize = 50;
|
||||||
@ -27,26 +28,30 @@ const kMinSidebarSize = 50;
|
|||||||
export const SplitView: React.FC<SplitViewProps> = ({
|
export const SplitView: React.FC<SplitViewProps> = ({
|
||||||
sidebarSize,
|
sidebarSize,
|
||||||
sidebarHidden,
|
sidebarHidden,
|
||||||
|
orientation = 'vertical',
|
||||||
children
|
children
|
||||||
}) => {
|
}) => {
|
||||||
let [size, setSize] = React.useState<number>(Math.max(kMinSidebarSize, sidebarSize));
|
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);
|
const childrenArray = React.Children.toArray(children);
|
||||||
document.body.style.userSelect = resizing ? 'none' : 'inherit';
|
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>
|
<div className='split-view-main'>{childrenArray[0]}</div>
|
||||||
{ !sidebarHidden && <div style={{flexBasis: size}} className='split-view-sidebar'>{childrenArray[1]}</div> }
|
{ !sidebarHidden && <div style={{flexBasis: size}} className='split-view-sidebar'>{childrenArray[1]}</div> }
|
||||||
{ !sidebarHidden && <div
|
{ !sidebarHidden && <div
|
||||||
style={{bottom: resizing ? 0 : size - 4, top: resizing ? 0 : undefined, height: resizing ? 'initial' : 8 }}
|
style={resizerStyle}
|
||||||
className='split-view-resizer'
|
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)}
|
onMouseUp={() => setResizing(null)}
|
||||||
onMouseMove={event => {
|
onMouseMove={event => {
|
||||||
if (!event.buttons)
|
if (!event.buttons)
|
||||||
setResizing(null);
|
setResizing(null);
|
||||||
else if (resizing)
|
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> }
|
||||||
</div>;
|
</div>;
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.toolbar-button:not(.disabled):hover {
|
.toolbar-button:not(.disabled):hover {
|
||||||
color: #555;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar-button .codicon {
|
.toolbar-button .codicon {
|
||||||
|
@ -21,7 +21,7 @@ import { msToString } from '../uiUtils';
|
|||||||
|
|
||||||
export interface CallLogProps {
|
export interface CallLogProps {
|
||||||
log: CallLog[],
|
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> = ({
|
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}
|
{ typeof callLog.duration === 'number' ? <span className='call-log-time'>— {msToString(callLog.duration)}</span> : undefined}
|
||||||
{ <div style={{flex: 'auto'}}></div> }
|
{ <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-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>
|
<span className={'codicon codicon-vm-active preview' + (callLog.snapshots.after ? '' : ' invisible')} onMouseEnter={() => onHover(callLog, 'after')} onMouseLeave={() => onHover(undefined)}></span>
|
||||||
</div>
|
</div>
|
||||||
{ (isExpanded ? callLog.messages : []).map((message, i) => {
|
{ (isExpanded ? callLog.messages : []).map((message, i) => {
|
||||||
|
@ -21,28 +21,27 @@
|
|||||||
flex: none;
|
flex: none;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 0 var(--layout-gap);
|
padding: 0 var(--layout-gap);
|
||||||
|
user-select: none;
|
||||||
|
color: #555;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-entry {
|
.action-entry {
|
||||||
position: relative;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
flex: none;
|
flex: none;
|
||||||
overflow: hidden;
|
|
||||||
border: 3px solid var(--background);
|
|
||||||
margin-top: var(--layout-gap);
|
|
||||||
user-select: none;
|
|
||||||
padding: 0 5px 5px 5px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
outline: none;
|
align-items: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
line-height: 28px;
|
||||||
|
padding-left: 3px;
|
||||||
|
border-left: 3px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-entry:hover {
|
.action-entry:hover, .action-entry.selected {
|
||||||
border-color: var(--inactive-focus-ring);
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-entry.selected {
|
.action-entry.selected {
|
||||||
border-color: var(--inactive-focus-ring);
|
border-left: 3px solid #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-entry.selected:focus {
|
.action-entry.selected:focus {
|
||||||
@ -52,19 +51,9 @@
|
|||||||
.action-title {
|
.action-title {
|
||||||
display: inline;
|
display: inline;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-header {
|
.action-error {
|
||||||
display: block;
|
|
||||||
align-items: center;
|
|
||||||
margin: 5px 0;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-header .action-error {
|
|
||||||
color: red;
|
color: red;
|
||||||
top: 2px;
|
top: 2px;
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -74,9 +63,11 @@
|
|||||||
.action-selector {
|
.action-selector {
|
||||||
display: inline;
|
display: inline;
|
||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
|
color: var(--orange);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-url {
|
.action-url {
|
||||||
display: inline;
|
display: inline;
|
||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
|
color: var(--blue);
|
||||||
}
|
}
|
||||||
|
@ -33,22 +33,19 @@ export const ActionList: React.FC<ActionListProps> = ({
|
|||||||
onSelected = () => {},
|
onSelected = () => {},
|
||||||
onHighlighted = () => {},
|
onHighlighted = () => {},
|
||||||
}) => {
|
}) => {
|
||||||
const targetAction = highlightedAction || selectedAction;
|
|
||||||
return <div className='action-list'>{actions.map(actionEntry => {
|
return <div className='action-list'>{actions.map(actionEntry => {
|
||||||
const { metadata, actionId } = actionEntry;
|
const { metadata, actionId } = actionEntry;
|
||||||
return <div
|
return <div
|
||||||
className={'action-entry' + (actionEntry === targetAction ? ' selected' : '')}
|
className={'action-entry' + (actionEntry === selectedAction ? ' selected' : '')}
|
||||||
key={actionId}
|
key={actionId}
|
||||||
onClick={() => onSelected(actionEntry)}
|
onClick={() => onSelected(actionEntry)}
|
||||||
onMouseEnter={() => onHighlighted(actionEntry)}
|
onMouseEnter={() => onHighlighted(actionEntry)}
|
||||||
onMouseLeave={() => (highlightedAction === actionEntry) && onHighlighted(undefined)}
|
onMouseLeave={() => (highlightedAction === actionEntry) && onHighlighted(undefined)}
|
||||||
>
|
>
|
||||||
<div className='action-header'>
|
<div className={'action-error codicon codicon-issues'} hidden={!metadata.error} />
|
||||||
<div className={'action-error codicon codicon-issues'} hidden={!metadata.error} />
|
<div className='action-title'>{metadata.method}</div>
|
||||||
<div className='action-title'>{metadata.method}</div>
|
{metadata.params.selector && <div className='action-selector' title={metadata.params.selector}>{metadata.params.selector}</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>}
|
||||||
{metadata.method === 'goto' && metadata.params.url && <div className='action-url' title={metadata.params.url}>{metadata.params.url}</div>}
|
|
||||||
</div>
|
|
||||||
</div>;
|
</div>;
|
||||||
})}</div>;
|
})}</div>;
|
||||||
};
|
};
|
||||||
|
@ -16,14 +16,15 @@
|
|||||||
|
|
||||||
.logs-tab {
|
.logs-tab {
|
||||||
flex: auto;
|
flex: auto;
|
||||||
position: relative;
|
line-height: 20px;
|
||||||
|
white-space: pre;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
background: #fdfcfc;
|
padding-top: 3px;
|
||||||
font-family: var(--monospace-font);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-line {
|
.log-line {
|
||||||
margin: 0 10px;
|
flex: none;
|
||||||
white-space: pre;
|
padding: 3px 0 3px 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
@ -15,15 +15,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
.network-request {
|
.network-request {
|
||||||
box-shadow: var(--box-shadow);
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
margin-bottom: 10px;
|
|
||||||
background: #fdfcfc;
|
background: #fdfcfc;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 3px solid transparent;
|
|
||||||
flex: none;
|
flex: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
@ -38,23 +35,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.network-request-title {
|
.network-request-title {
|
||||||
height: 36px;
|
height: 28px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-request-title-status {
|
.network-request-title-status {
|
||||||
font-weight: bold;
|
padding-right: 5px;
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0px 5px;
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.network-request-title-status.status-success {
|
|
||||||
background-color: var(--green);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-request-title-status.status-failure {
|
.network-request-title-status.status-failure {
|
||||||
@ -66,20 +54,12 @@
|
|||||||
background-color: var(--white);
|
background-color: var(--white);
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-request-title-method {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.network-request-title-url {
|
.network-request-title-url {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-request-title-content-type {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.network-request-details {
|
.network-request-details {
|
||||||
font-family: var(--monospace-font);
|
font-family: var(--monospace-font);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
.snapshot-tab {
|
.snapshot-tab {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex: auto;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
@ -25,11 +26,18 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
padding: 10px 4px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.snapshot-toggle {
|
.snapshot-toggle {
|
||||||
padding: 5px 10px;
|
padding: 4px 8px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
border-radius: 20px;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.snapshot-toggle:hover {
|
||||||
|
background-color: #ededed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.snapshot-toggle.toggled {
|
.snapshot-toggle.toggled {
|
||||||
@ -39,12 +47,13 @@
|
|||||||
.snapshot-wrapper {
|
.snapshot-wrapper {
|
||||||
flex: auto;
|
flex: auto;
|
||||||
margin: 1px;
|
margin: 1px;
|
||||||
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.snapshot-container {
|
.snapshot-container {
|
||||||
display: block;
|
display: block;
|
||||||
background: white;
|
background: white;
|
||||||
outline: 1px solid #aaa;
|
box-shadow: rgb(0 0 0 / 15%) 0px 0.1em 4.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
iframe#snapshot {
|
iframe#snapshot {
|
||||||
|
@ -51,7 +51,7 @@ export const SnapshotTab: React.FunctionComponent<{
|
|||||||
point = actionEntry.metadata.point;
|
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 {
|
try {
|
||||||
(iframeRef.current.contentWindow as any).showSnapshot(snapshotUrl, { point });
|
(iframeRef.current.contentWindow as any).showSnapshot(snapshotUrl, { point });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -59,6 +59,10 @@ export const SnapshotTab: React.FunctionComponent<{
|
|||||||
}, [actionEntry, snapshotIndex, pageId, time]);
|
}, [actionEntry, snapshotIndex, pageId, time]);
|
||||||
|
|
||||||
const scale = Math.min(measure.width / snapshotSize.width, measure.height / snapshotSize.height);
|
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'>
|
return <div className='snapshot-tab'>
|
||||||
<div className='snapshot-controls'>{
|
<div className='snapshot-controls'>{
|
||||||
selection && <div key='selectedTime' className='snapshot-toggle'>
|
selection && <div key='selectedTime' className='snapshot-toggle'>
|
||||||
@ -77,7 +81,7 @@ export const SnapshotTab: React.FunctionComponent<{
|
|||||||
<div className='snapshot-container' style={{
|
<div className='snapshot-container' style={{
|
||||||
width: snapshotSize.width + 'px',
|
width: snapshotSize.width + 'px',
|
||||||
height: snapshotSize.height + '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>
|
<iframe ref={iframeRef} id='snapshot' name='snapshot' src='/snapshot/'></iframe>
|
||||||
</div>
|
</div>
|
||||||
|
@ -18,72 +18,6 @@
|
|||||||
flex: auto;
|
flex: auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #fdfcfc;
|
|
||||||
font-family: var(--monospace-font);
|
|
||||||
white-space: nowrap;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
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;
|
|
||||||
}
|
|
||||||
|
@ -19,8 +19,10 @@ import * as React from 'react';
|
|||||||
import { useAsyncMemo } from './helpers';
|
import { useAsyncMemo } from './helpers';
|
||||||
import './sourceTab.css';
|
import './sourceTab.css';
|
||||||
import '../../../third_party/highlightjs/highlightjs/tomorrow.css';
|
import '../../../third_party/highlightjs/highlightjs/tomorrow.css';
|
||||||
import * as highlightjs from '../../../third_party/highlightjs/highlightjs';
|
|
||||||
import { StackFrame } from '../../../common/types';
|
import { StackFrame } from '../../../common/types';
|
||||||
|
import { Source as SourceView } from '../../components/source';
|
||||||
|
import { StackTraceView } from './stackTrace';
|
||||||
|
import { SplitView } from '../../components/splitView';
|
||||||
|
|
||||||
type StackInfo = string | {
|
type StackInfo = string | {
|
||||||
frames: StackFrame[];
|
frames: StackFrame[];
|
||||||
@ -53,7 +55,7 @@ export const SourceTab: React.FunctionComponent<{
|
|||||||
};
|
};
|
||||||
}, [actionEntry]);
|
}, [actionEntry]);
|
||||||
|
|
||||||
const content = useAsyncMemo<string[]>(async () => {
|
const content = useAsyncMemo<string>(async () => {
|
||||||
let value: string;
|
let value: string;
|
||||||
if (typeof stackInfo === 'string') {
|
if (typeof stackInfo === 'string') {
|
||||||
value = stackInfo;
|
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}">`));
|
stackInfo.fileContent.set(filePath, await fetch(`/file?${filePath}`).then(response => response.text()).catch(e => `<Unable to read "${filePath}">`));
|
||||||
value = stackInfo.fileContent.get(filePath)!;
|
value = stackInfo.fileContent.get(filePath)!;
|
||||||
}
|
}
|
||||||
const result = [];
|
return value;
|
||||||
let continuation: any;
|
}, [stackInfo, selectedFrame], '');
|
||||||
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], []);
|
|
||||||
|
|
||||||
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>();
|
const targetLineRef = React.createRef<HTMLDivElement>();
|
||||||
React.useLayoutEffect(() => {
|
React.useLayoutEffect(() => {
|
||||||
@ -83,41 +78,8 @@ export const SourceTab: React.FunctionComponent<{
|
|||||||
}
|
}
|
||||||
}, [needReveal, targetLineRef]);
|
}, [needReveal, targetLineRef]);
|
||||||
|
|
||||||
return <div className='source-tab'>
|
return <SplitView sidebarSize={250} orientation='horizontal'>
|
||||||
<div className='source-content'>{
|
<SourceView text={content} language='javascript' highlight={[{ line: targetLine, type: 'running' }]} revealLine={targetLine}></SourceView>
|
||||||
content.map((markup, index) => {
|
<StackTraceView actionEntry={actionEntry} selectedFrame={selectedFrame} setSelectedFrame={setSelectedFrame}></StackTraceView>
|
||||||
const isTargetLine = (index + 1) === targetLine;
|
</SplitView>;
|
||||||
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>;
|
|
||||||
};
|
};
|
||||||
|
55
src/web/traceViewer/ui/stackTrace.css
Normal file
55
src/web/traceViewer/ui/stackTrace.css
Normal 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;
|
||||||
|
}
|
49
src/web/traceViewer/ui/stackTrace.tsx
Normal file
49
src/web/traceViewer/ui/stackTrace.tsx
Normal 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>;
|
||||||
|
};
|
@ -27,11 +27,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tab-strip {
|
.tab-strip {
|
||||||
flex: auto;
|
color: var(--toolbar-color);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
box-shadow: var(--box-shadow);
|
||||||
|
background-color: var(--toolbar-bg-color);
|
||||||
|
height: 40px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 34px;
|
padding-right: 10px;
|
||||||
|
flex: none;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-strip:focus {
|
.tab-strip:focus {
|
||||||
@ -50,6 +55,7 @@
|
|||||||
border-bottom: 3px solid transparent;
|
border-bottom: 3px solid transparent;
|
||||||
width: 80px;
|
width: 80px;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-label {
|
.tab-label {
|
||||||
@ -61,9 +67,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tab-element.selected {
|
.tab-element.selected {
|
||||||
border-bottom-color: var(--color);
|
border-bottom-color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-element:hover {
|
.tab-element:hover {
|
||||||
font-weight: 600;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: white;
|
border-bottom: 1px solid #ddd;
|
||||||
padding: 20px 0 5px;
|
padding: 20px 0 5px;
|
||||||
cursor: text;
|
cursor: text;
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
.workbench {
|
.workbench {
|
||||||
contain: size;
|
contain: size;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workbench .header {
|
.workbench .header {
|
||||||
|
@ -25,6 +25,8 @@ import { NetworkTab } from './networkTab';
|
|||||||
import { SourceTab } from './sourceTab';
|
import { SourceTab } from './sourceTab';
|
||||||
import { SnapshotTab } from './snapshotTab';
|
import { SnapshotTab } from './snapshotTab';
|
||||||
import { LogsTab } from './logsTab';
|
import { LogsTab } from './logsTab';
|
||||||
|
import { SplitView } from '../../components/splitView';
|
||||||
|
|
||||||
|
|
||||||
export const Workbench: React.FunctionComponent<{
|
export const Workbench: React.FunctionComponent<{
|
||||||
contexts: ContextEntry[],
|
contexts: ContextEntry[],
|
||||||
@ -71,7 +73,7 @@ export const Workbench: React.FunctionComponent<{
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='hbox'>
|
<div className='hbox'>
|
||||||
<div style={{ display: 'flex', flex: 'none', overflow: 'auto' }}>
|
<div style={{ display: 'flex', flex: 'none', overflow: 'auto', borderRight: '1px solid #ddd' }}>
|
||||||
<ActionList
|
<ActionList
|
||||||
actions={actions}
|
actions={actions}
|
||||||
selectedAction={selectedAction}
|
selectedAction={selectedAction}
|
||||||
@ -83,12 +85,15 @@ export const Workbench: React.FunctionComponent<{
|
|||||||
onHighlighted={action => setHighlightedAction(action)}
|
onHighlighted={action => setHighlightedAction(action)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<TabbedPane tabs={[
|
<SplitView sidebarSize={250}>
|
||||||
{ id: 'snapshot', title: 'Snapshot', render: () => <SnapshotTab actionEntry={selectedAction} snapshotSize={snapshotSize} selection={snapshotSelection} boundaries={boundaries} /> },
|
<SnapshotTab actionEntry={selectedAction} snapshotSize={snapshotSize} selection={snapshotSelection} boundaries={boundaries} />
|
||||||
{ id: 'source', title: 'Source', render: () => <SourceTab actionEntry={selectedAction} /> },
|
<TabbedPane tabs={[
|
||||||
{ id: 'network', title: 'Network', render: () => <NetworkTab actionEntry={selectedAction} /> },
|
{ id: 'logs', title: 'Log', render: () => <LogsTab actionEntry={selectedAction} /> },
|
||||||
{ id: 'logs', title: 'Logs', render: () => <LogsTab actionEntry={selectedAction} /> },
|
{ id: 'source', title: 'Source', render: () => <SourceTab actionEntry={selectedAction} /> },
|
||||||
]}/>
|
{ id: 'network', title: 'Network', render: () => <NetworkTab actionEntry={selectedAction} /> },
|
||||||
|
]}/>
|
||||||
|
|
||||||
|
</SplitView>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user