/** * 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 type { ResourceSnapshot } from '@trace/snapshot'; import { Expandable } from '@web/components/expandable'; import * as React from 'react'; import './networkResourceDetails.css'; import type { Entry } from '@trace/har'; import { msToString } from '@web/uiUtils'; import { TabbedPane } from '@web/components/tabbedPane'; import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper'; import type { Language } from '@web/components/codeMirrorWrapper'; export const NetworkResource: React.FunctionComponent<{ resource: Entry, }> = ({ resource }) => { const [expanded, setExpanded] = React.useState(false); const { routeStatus, resourceName, contentType } = React.useMemo(() => { const routeStatus = formatRouteStatus(resource); const resourceName = resource.request.url.substring(resource.request.url.lastIndexOf('/')); let contentType = resource.response.content.mimeType; const charset = contentType.match(/^(.*);\s*charset=.*$/); if (charset) contentType = charset[1]; return { routeStatus, resourceName, contentType }; }, [resource]); const renderTitle = React.useCallback(() => { return
{routeStatus &&
{routeStatus}
} {resource.response._failureText &&
{resource.response._failureText}
} {!resource.response._failureText &&
{resource.response.status}
}
{resource.request.method}
{resourceName}
{contentType}
; }, [contentType, resource, resourceName, routeStatus]); return
{expanded && }
; }; const NetworkResourceDetails: React.FunctionComponent<{ resource: ResourceSnapshot, }> = ({ resource }) => { const [requestBody, setRequestBody] = React.useState<{ text: string, language?: Language } | null>(null); const [responseBody, setResponseBody] = React.useState<{ dataUrl?: string, text?: string, language?: Language } | null>(null); const [selectedTab, setSelectedTab] = React.useState('request'); React.useEffect(() => { const readResources = async () => { if (resource.request.postData) { const requestContentTypeHeader = resource.request.headers.find(q => q.name === 'Content-Type'); const requestContentType = requestContentTypeHeader ? requestContentTypeHeader.value : ''; const language = mimeTypeToHighlighter(requestContentType); if (resource.request.postData._sha1) { const response = await fetch(`sha1/${resource.request.postData._sha1}`); setRequestBody({ text: formatBody(await response.text(), requestContentType), language }); } else { setRequestBody({ text: formatBody(resource.request.postData.text, requestContentType), language }); } } if (resource.response.content._sha1) { const useBase64 = resource.response.content.mimeType.includes('image'); const response = await fetch(`sha1/${resource.response.content._sha1}`); if (useBase64) { const blob = await response.blob(); const reader = new FileReader(); const eventPromise = new Promise(f => reader.onload = f); reader.readAsDataURL(blob); setResponseBody({ dataUrl: (await eventPromise).target.result }); } else { const formattedBody = formatBody(await response.text(), resource.response.content.mimeType); const language = mimeTypeToHighlighter(resource.response.content.mimeType); setResponseBody({ text: formattedBody, language }); } } }; readResources(); }, [resource]); return
URL
{resource.request.url}
Request Headers
{resource.request.headers.map(pair => `${pair.name}: ${pair.value}`).join('\n')}
{requestBody &&
Request Body
} {requestBody && }
, }, { id: 'response', title: 'Response', render: () =>
{msToString(resource.time)}
Response Headers
{resource.response.headers.map(pair => `${pair.name}: ${pair.value}`).join('\n')}
, }, { id: 'body', title: 'Body', render: () =>
{!resource.response.content._sha1 &&
Response body is not available for this request.
} {responseBody && responseBody.dataUrl && } {responseBody && responseBody.text && }
, }, ]} selectedTab={selectedTab} setSelectedTab={setSelectedTab}/>; }; function formatStatus(status: number): string { if (status >= 200 && status < 400) return 'status-success'; if (status >= 400) return 'status-failure'; return ''; } function formatBody(body: string | null, contentType: string): string { if (body === null) return 'Loading...'; const bodyStr = body; if (bodyStr === '') return ''; 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; } function formatRouteStatus(request: Entry): string { if (request._wasAborted) return 'aborted'; if (request._wasContinued) return 'continued'; if (request._wasFulfilled) return 'fulfilled'; if (request._apiRequest) return 'api'; return ''; } function mimeTypeToHighlighter(mimeType: string): Language | undefined { if (mimeType.includes('javascript') || mimeType.includes('json')) return 'javascript'; if (mimeType.includes('html')) return 'html'; if (mimeType.includes('css')) return 'css'; }