2021-01-26 20:06:05 +01:00
/ * *
* 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 .
* /
2022-09-21 03:41:51 +02:00
import type { ResourceSnapshot } from '@trace/snapshot' ;
2021-01-26 20:06:05 +01:00
import * as React from 'react' ;
2022-03-25 22:12:00 +01:00
import './networkResourceDetails.css' ;
2023-08-25 21:10:28 +02:00
import { TabbedPane } from '@web/components/tabbedPane' ;
import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper' ;
2023-08-30 07:20:28 +02:00
import { ToolbarButton } from '@web/components/toolbarButton' ;
2024-09-18 14:35:11 +02:00
import { generateCurlCommand , generateFetchCall } from '../third_party/devtools' ;
2024-09-30 12:01:56 +02:00
import { CopyToClipboardTextButton } from './copyToClipboard' ;
2024-11-20 10:16:43 +01:00
import { getAPIRequestCodeGen } from './codegen' ;
import type { Language } from '@isomorphic/locatorGenerators' ;
2021-01-26 20:06:05 +01:00
2023-08-30 07:20:28 +02:00
export const NetworkResourceDetails : React.FunctionComponent < {
resource : ResourceSnapshot ;
onClose : ( ) = > void ;
2024-11-20 10:16:43 +01:00
sdkLanguage : Language ;
} > = ( { resource , onClose , sdkLanguage } ) = > {
2023-08-30 07:20:28 +02:00
const [ selectedTab , setSelectedTab ] = React . useState ( 'request' ) ;
2021-01-26 20:06:05 +01:00
2023-08-30 07:20:28 +02:00
return < TabbedPane
dataTestId = 'network-request-details'
2024-08-20 14:16:28 +02:00
leftToolbar = { [ < ToolbarButton key = 'close' icon = 'close' title = 'Close' onClick = { onClose } > < / ToolbarButton > ] }
2023-08-30 07:20:28 +02:00
tabs = { [
{
id : 'request' ,
title : 'Request' ,
2024-11-20 10:16:43 +01:00
render : ( ) = > < RequestTab resource = { resource } sdkLanguage = { sdkLanguage } / > ,
2023-08-30 07:20:28 +02:00
} ,
{
id : 'response' ,
title : 'Response' ,
render : ( ) = > < ResponseTab resource = { resource } / > ,
} ,
{
id : 'body' ,
title : 'Body' ,
render : ( ) = > < BodyTab resource = { resource } / > ,
} ,
] }
selectedTab = { selectedTab }
setSelectedTab = { setSelectedTab } / > ;
2023-08-25 21:10:28 +02:00
} ;
2023-08-30 07:20:28 +02:00
const RequestTab : React.FunctionComponent < {
resource : ResourceSnapshot ;
2024-11-20 10:16:43 +01:00
sdkLanguage : Language ;
} > = ( { resource , sdkLanguage } ) = > {
2024-08-01 18:27:45 +02:00
const [ requestBody , setRequestBody ] = React . useState < { text : string , mimeType? : string } | null > ( null ) ;
2023-08-25 21:10:28 +02:00
2021-01-26 20:06:05 +01:00
React . useEffect ( ( ) = > {
const readResources = async ( ) = > {
2021-08-24 22:17:58 +02:00
if ( resource . request . postData ) {
2024-08-30 16:21:51 +02:00
const requestContentTypeHeader = resource . request . headers . find ( q = > q . name . toLowerCase ( ) === 'content-type' ) ;
2023-08-25 21:10:28 +02:00
const requestContentType = requestContentTypeHeader ? requestContentTypeHeader . value : '' ;
2021-08-24 22:17:58 +02:00
if ( resource . request . postData . _sha1 ) {
2021-10-13 20:07:29 +02:00
const response = await fetch ( ` sha1/ ${ resource . request . postData . _sha1 } ` ) ;
2024-08-01 18:27:45 +02:00
setRequestBody ( { text : formatBody ( await response . text ( ) , requestContentType ) , mimeType : requestContentType } ) ;
2021-08-24 22:17:58 +02:00
} else {
2024-08-01 18:27:45 +02:00
setRequestBody ( { text : formatBody ( resource . request . postData . text , requestContentType ) , mimeType : requestContentType } ) ;
2021-08-24 22:17:58 +02:00
}
2024-05-24 18:27:49 +02:00
} else {
setRequestBody ( null ) ;
2021-01-26 20:06:05 +01:00
}
2023-08-30 07:20:28 +02:00
} ;
readResources ( ) ;
} , [ resource ] ) ;
2023-09-15 18:16:29 +02:00
return < div className = 'network-request-details-tab' >
2024-06-19 18:05:20 +02:00
< div className = 'network-request-details-header' > General < / div >
< div className = 'network-request-details-url' > { ` URL: ${ resource . request . url } ` } < / div >
< div className = 'network-request-details-general' > { ` Method: ${ resource . request . method } ` } < / div >
2024-08-02 02:26:52 +02:00
{ resource . response . status !== - 1 && < div className = 'network-request-details-general' style = { { display : 'flex' } } >
2024-08-01 02:29:05 +02:00
Status Code : < span className = { statusClass ( resource . response . status ) } style = { { display : 'inline-flex' } } >
{ ` ${ resource . response . status } ${ resource . response . statusText } ` }
2024-08-02 02:26:52 +02:00
< / span > < / div > }
2024-09-04 16:54:44 +02:00
{ resource . request . queryString . length ? < >
< div className = 'network-request-details-header' > Query String Parameters < / div >
< div className = 'network-request-details-headers' >
{ resource . request . queryString . map ( param = > ` ${ param . name } : ${ param . value } ` ) . join ( '\n' ) }
< / div >
< / > : null }
2023-08-30 07:20:28 +02:00
< div className = 'network-request-details-header' > Request Headers < / div >
< div className = 'network-request-details-headers' > { resource . request . headers . map ( pair = > ` ${ pair . name } : ${ pair . value } ` ) . join ( '\n' ) } < / div >
2024-09-30 12:01:56 +02:00
2024-09-18 14:35:11 +02:00
< div className = 'network-request-details-copy' >
2024-09-30 12:01:56 +02:00
< CopyToClipboardTextButton description = 'Copy as cURL' value = { ( ) = > generateCurlCommand ( resource ) } / >
< CopyToClipboardTextButton description = 'Copy as Fetch' value = { ( ) = > generateFetchCall ( resource ) } / >
2024-11-20 10:16:43 +01:00
< CopyToClipboardTextButton description = 'Copy as Playwright' value = { async ( ) = > getAPIRequestCodeGen ( sdkLanguage ) . generatePlaywrightRequestCall ( resource . request , requestBody ? . text ) } / >
2024-09-18 14:35:11 +02:00
< / div >
2024-09-30 12:01:56 +02:00
2023-08-30 07:20:28 +02:00
{ requestBody && < div className = 'network-request-details-header' > Request Body < / div > }
2024-08-01 18:27:45 +02:00
{ requestBody && < CodeMirrorWrapper text = { requestBody . text } mimeType = { requestBody . mimeType } readOnly lineNumbers = { true } / > }
2023-08-30 07:20:28 +02:00
< / div > ;
} ;
const ResponseTab : React.FunctionComponent < {
resource : ResourceSnapshot ;
} > = ( { resource } ) = > {
2023-09-15 18:16:29 +02:00
return < div className = 'network-request-details-tab' >
2023-08-30 07:20:28 +02:00
< div className = 'network-request-details-header' > Response Headers < / div >
< div className = 'network-request-details-headers' > { resource . response . headers . map ( pair = > ` ${ pair . name } : ${ pair . value } ` ) . join ( '\n' ) } < / div >
< / div > ;
} ;
2021-01-26 20:06:05 +01:00
2023-08-30 07:20:28 +02:00
const BodyTab : React.FunctionComponent < {
resource : ResourceSnapshot ;
} > = ( { resource } ) = > {
2024-08-20 08:28:02 +02:00
const [ responseBody , setResponseBody ] = React . useState < { dataUrl? : string , text? : string , mimeType? : string , font? : BinaryData } | null > ( null ) ;
2023-08-30 07:20:28 +02:00
React . useEffect ( ( ) = > {
const readResources = async ( ) = > {
2021-08-24 22:17:58 +02:00
if ( resource . response . content . _sha1 ) {
const useBase64 = resource . response . content . mimeType . includes ( 'image' ) ;
2024-08-20 08:28:02 +02:00
const isFont = resource . response . content . mimeType . includes ( 'font' ) ;
2021-10-13 20:07:29 +02:00
const response = await fetch ( ` sha1/ ${ resource . response . content . _sha1 } ` ) ;
2021-03-09 04:49:57 +01:00
if ( useBase64 ) {
const blob = await response . blob ( ) ;
const reader = new FileReader ( ) ;
const eventPromise = new Promise < any > ( f = > reader . onload = f ) ;
reader . readAsDataURL ( blob ) ;
setResponseBody ( { dataUrl : ( await eventPromise ) . target . result } ) ;
2024-08-20 08:28:02 +02:00
} else if ( isFont ) {
const font = await response . arrayBuffer ( ) ;
setResponseBody ( { font } ) ;
2021-03-09 04:49:57 +01:00
} else {
2023-08-25 21:10:28 +02:00
const formattedBody = formatBody ( await response . text ( ) , resource . response . content . mimeType ) ;
2024-08-01 18:27:45 +02:00
setResponseBody ( { text : formattedBody , mimeType : resource.response.content.mimeType } ) ;
2021-03-09 04:49:57 +01:00
}
2024-08-02 02:26:52 +02:00
} else {
setResponseBody ( null ) ;
2021-01-26 20:06:05 +01:00
}
} ;
readResources ( ) ;
2023-06-02 22:00:27 +02:00
} , [ resource ] ) ;
2023-09-15 18:16:29 +02:00
return < div className = 'network-request-details-tab' >
2023-08-30 07:20:28 +02:00
{ ! resource . response . content . _sha1 && < div > Response body is not available for this request . < / div > }
2024-08-20 08:28:02 +02:00
{ responseBody && responseBody . font && < FontPreview font = { responseBody . font } / > }
2023-08-30 07:20:28 +02:00
{ responseBody && responseBody . dataUrl && < img draggable = 'false' src = { responseBody . dataUrl } / > }
2024-08-01 18:27:45 +02:00
{ responseBody && responseBody . text && < CodeMirrorWrapper text = { responseBody . text } mimeType = { responseBody . mimeType } readOnly lineNumbers = { true } / > }
2023-08-30 07:20:28 +02:00
< / div > ;
2021-01-26 20:06:05 +01:00
} ;
2023-06-02 22:00:27 +02:00
2024-08-20 08:28:02 +02:00
const FontPreview : React.FunctionComponent < {
font : BinaryData ;
} > = ( { font } ) = > {
const [ isError , setIsError ] = React . useState ( false ) ;
React . useEffect ( ( ) = > {
let fontFace : FontFace ;
try {
// note: constant font family name will lead to bugs
// when displaying two font previews.
fontFace = new FontFace ( 'font-preview' , font ) ;
if ( fontFace . status === 'loaded' )
document . fonts . add ( fontFace ) ;
if ( fontFace . status === 'error' )
setIsError ( true ) ;
} catch {
setIsError ( true ) ;
}
return ( ) = > {
document . fonts . delete ( fontFace ) ;
} ;
} , [ font ] ) ;
if ( isError )
return < div className = 'network-font-preview-error' > Could not load font preview < / div > ;
return < div className = 'network-font-preview' >
ABCDEFGHIJKLM < br / >
NOPQRSTUVWXYZ < br / >
abcdefghijklm < br / >
nopqrstuvwxyz < br / >
1234567890
< / div > ;
} ;
2024-08-01 02:29:05 +02:00
function statusClass ( statusCode : number ) : string {
if ( statusCode < 300 || statusCode === 304 )
return 'green-circle' ;
if ( statusCode < 400 )
return 'yellow-circle' ;
return 'red-circle' ;
}
2023-06-02 22:00:27 +02:00
function formatBody ( body : string | null , contentType : string ) : string {
if ( body === null )
return 'Loading...' ;
const bodyStr = body ;
if ( bodyStr === '' )
return '<Empty>' ;
if ( contentType . includes ( 'application/json' ) ) {
try {
return JSON . stringify ( JSON . parse ( bodyStr ) , null , 2 ) ;
} catch ( err ) {
return bodyStr ;
}
}
if ( contentType . includes ( 'application/x-www-form-urlencoded' ) )
return decodeURIComponent ( bodyStr ) ;
return bodyStr ;
}