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')) {
 | 
			
		||||
        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);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 { 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);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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}`;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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`);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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>;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -32,7 +32,7 @@
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
.toolbar-button:not(.disabled):hover {
 | 
			
		||||
  color: #555;
 | 
			
		||||
  color: #333;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.toolbar-button .codicon {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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) => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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>;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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%;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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>;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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 {
 | 
			
		||||
  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;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,7 +20,7 @@
 | 
			
		|||
  position: relative;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  background: white;
 | 
			
		||||
  border-bottom: 1px solid #ddd;
 | 
			
		||||
  padding: 20px 0 5px;
 | 
			
		||||
  cursor: text;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,6 +16,7 @@
 | 
			
		|||
 | 
			
		||||
.workbench {
 | 
			
		||||
  contain: size;
 | 
			
		||||
  user-select: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.workbench .header {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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>;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue