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
	 Pavel Feldman
						Pavel Feldman