mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: improve network panel rendering (#26708)
This commit is contained in:
parent
afb17b02a7
commit
0ecd561db2
@ -47,11 +47,10 @@ export class HarRecorder {
|
|||||||
includeTraceInfo: false,
|
includeTraceInfo: false,
|
||||||
recordRequestOverrides: true,
|
recordRequestOverrides: true,
|
||||||
waitForContentOnStop: true,
|
waitForContentOnStop: true,
|
||||||
skipScripts: false,
|
|
||||||
urlFilter: urlFilterRe ?? options.urlGlob,
|
urlFilter: urlFilterRe ?? options.urlGlob,
|
||||||
});
|
});
|
||||||
this._zipFile = content === 'attach' || expectsZip ? new yazl.ZipFile() : null;
|
this._zipFile = content === 'attach' || expectsZip ? new yazl.ZipFile() : null;
|
||||||
this._tracer.start();
|
this._tracer.start({ omitScripts: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
onEntryStarted(entry: har.Entry) {
|
onEntryStarted(entry: har.Entry) {
|
||||||
|
@ -43,7 +43,6 @@ export interface HarTracerDelegate {
|
|||||||
|
|
||||||
type HarTracerOptions = {
|
type HarTracerOptions = {
|
||||||
content: 'omit' | 'attach' | 'embed';
|
content: 'omit' | 'attach' | 'embed';
|
||||||
skipScripts: boolean;
|
|
||||||
includeTraceInfo: boolean;
|
includeTraceInfo: boolean;
|
||||||
recordRequestOverrides: boolean;
|
recordRequestOverrides: boolean;
|
||||||
waitForContentOnStop: boolean;
|
waitForContentOnStop: boolean;
|
||||||
@ -55,6 +54,7 @@ type HarTracerOptions = {
|
|||||||
omitServerIP?: boolean;
|
omitServerIP?: boolean;
|
||||||
omitPages?: boolean;
|
omitPages?: boolean;
|
||||||
omitSizes?: boolean;
|
omitSizes?: boolean;
|
||||||
|
omitScripts?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class HarTracer {
|
export class HarTracer {
|
||||||
@ -86,9 +86,10 @@ export class HarTracer {
|
|||||||
this._baseURL = context instanceof APIRequestContext ? context._defaultOptions().baseURL : context._options.baseURL;
|
this._baseURL = context instanceof APIRequestContext ? context._defaultOptions().baseURL : context._options.baseURL;
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start(options: { omitScripts: boolean }) {
|
||||||
if (this._started)
|
if (this._started)
|
||||||
return;
|
return;
|
||||||
|
this._options.omitScripts = options.omitScripts;
|
||||||
this._started = true;
|
this._started = true;
|
||||||
const apiRequest = this._context instanceof APIRequestContext ? this._context : this._context.fetchRequest;
|
const apiRequest = this._context instanceof APIRequestContext ? this._context : this._context.fetchRequest;
|
||||||
this._eventListeners = [
|
this._eventListeners = [
|
||||||
@ -338,7 +339,7 @@ export class HarTracer {
|
|||||||
this._addBarrier(page || request.serviceWorker(), compressionCalculationBarrier.barrier);
|
this._addBarrier(page || request.serviceWorker(), compressionCalculationBarrier.barrier);
|
||||||
|
|
||||||
const promise = response.body().then(buffer => {
|
const promise = response.body().then(buffer => {
|
||||||
if (this._options.skipScripts && request.resourceType() === 'script') {
|
if (this._options.omitScripts && request.resourceType() === 'script') {
|
||||||
compressionCalculationBarrier?.setDecodedBodySize(0);
|
compressionCalculationBarrier?.setDecodedBodySize(0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -92,7 +92,6 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
|||||||
includeTraceInfo: true,
|
includeTraceInfo: true,
|
||||||
recordRequestOverrides: false,
|
recordRequestOverrides: false,
|
||||||
waitForContentOnStop: false,
|
waitForContentOnStop: false,
|
||||||
skipScripts: true,
|
|
||||||
});
|
});
|
||||||
const testIdAttributeName = ('selectors' in context) ? context.selectors().testIdAttributeName() : undefined;
|
const testIdAttributeName = ('selectors' in context) ? context.selectors().testIdAttributeName() : undefined;
|
||||||
this._contextCreatedEvent = {
|
this._contextCreatedEvent = {
|
||||||
@ -151,8 +150,9 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
|||||||
};
|
};
|
||||||
this._fs.mkdir(this._state.resourcesDir);
|
this._fs.mkdir(this._state.resourcesDir);
|
||||||
this._fs.writeFile(this._state.networkFile, '');
|
this._fs.writeFile(this._state.networkFile, '');
|
||||||
|
// Tracing is 10x bigger if we include scripts in every trace.
|
||||||
if (options.snapshots)
|
if (options.snapshots)
|
||||||
this._harTracer.start();
|
this._harTracer.start({ omitScripts: !options.live });
|
||||||
}
|
}
|
||||||
|
|
||||||
async startChunk(options: { name?: string, title?: string } = {}): Promise<{ traceName: string }> {
|
async startChunk(options: { name?: string, title?: string } = {}): Promise<{ traceName: string }> {
|
||||||
|
@ -37,20 +37,20 @@ export class InMemorySnapshotter implements SnapshotterDelegate, HarTracerDelega
|
|||||||
|
|
||||||
constructor(context: BrowserContext) {
|
constructor(context: BrowserContext) {
|
||||||
this._snapshotter = new Snapshotter(context, this);
|
this._snapshotter = new Snapshotter(context, this);
|
||||||
this._harTracer = new HarTracer(context, null, this, { content: 'attach', includeTraceInfo: true, recordRequestOverrides: false, waitForContentOnStop: false, skipScripts: true });
|
this._harTracer = new HarTracer(context, null, this, { content: 'attach', includeTraceInfo: true, recordRequestOverrides: false, waitForContentOnStop: false });
|
||||||
this._storage = new SnapshotStorage();
|
this._storage = new SnapshotStorage();
|
||||||
}
|
}
|
||||||
|
|
||||||
async initialize(): Promise<void> {
|
async initialize(): Promise<void> {
|
||||||
await this._snapshotter.start();
|
await this._snapshotter.start();
|
||||||
this._harTracer.start();
|
this._harTracer.start({ omitScripts: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
async reset() {
|
async reset() {
|
||||||
await this._snapshotter.reset();
|
await this._snapshotter.reset();
|
||||||
await this._harTracer.flush();
|
await this._harTracer.flush();
|
||||||
this._harTracer.stop();
|
this._harTracer.stop();
|
||||||
this._harTracer.start();
|
this._harTracer.start({ omitScripts: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
async dispose() {
|
async dispose() {
|
||||||
|
@ -78,8 +78,10 @@ async function startTraceViewerServer(traceUrls: string[], options?: OpenTraceVi
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
response.statusCode = 404;
|
||||||
|
response.end();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
const absolutePath = path.join(__dirname, '..', '..', '..', 'vite', 'traceViewer', ...relativePath.split('/'));
|
const absolutePath = path.join(__dirname, '..', '..', '..', 'vite', 'traceViewer', ...relativePath.split('/'));
|
||||||
return server.serveFile(request, response, absolutePath);
|
return server.serveFile(request, response, absolutePath);
|
||||||
|
@ -14,7 +14,6 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MultiMap } from './multimap';
|
|
||||||
import { splitProgress } from './progress';
|
import { splitProgress } from './progress';
|
||||||
import { unwrapPopoutUrl } from './snapshotRenderer';
|
import { unwrapPopoutUrl } from './snapshotRenderer';
|
||||||
import { SnapshotServer } from './snapshotServer';
|
import { SnapshotServer } from './snapshotServer';
|
||||||
@ -36,10 +35,17 @@ const scopePath = new URL(self.registration.scope).pathname;
|
|||||||
|
|
||||||
const loadedTraces = new Map<string, { traceModel: TraceModel, snapshotServer: SnapshotServer }>();
|
const loadedTraces = new Map<string, { traceModel: TraceModel, snapshotServer: SnapshotServer }>();
|
||||||
|
|
||||||
const clientIdToTraceUrls = new MultiMap<string, string>();
|
const clientIdToTraceUrls = new Map<string, Set<string>>();
|
||||||
|
|
||||||
async function loadTrace(traceUrl: string, traceFileName: string | null, clientId: string, progress: (done: number, total: number) => void): Promise<TraceModel> {
|
async function loadTrace(traceUrl: string, traceFileName: string | null, clientId: string, progress: (done: number, total: number) => void): Promise<TraceModel> {
|
||||||
clientIdToTraceUrls.set(clientId, traceUrl);
|
await gc();
|
||||||
|
let set = clientIdToTraceUrls.get(clientId);
|
||||||
|
if (!set) {
|
||||||
|
set = new Set();
|
||||||
|
clientIdToTraceUrls.set(clientId, set);
|
||||||
|
}
|
||||||
|
set.add(traceUrl);
|
||||||
|
|
||||||
const traceModel = new TraceModel();
|
const traceModel = new TraceModel();
|
||||||
try {
|
try {
|
||||||
// Allow 10% to hop from sw to page.
|
// Allow 10% to hop from sw to page.
|
||||||
@ -162,7 +168,7 @@ async function gc() {
|
|||||||
for (const [clientId, traceUrls] of clientIdToTraceUrls) {
|
for (const [clientId, traceUrls] of clientIdToTraceUrls) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
if (!clients.find(c => c.id === clientId))
|
if (!clients.find(c => c.id === clientId))
|
||||||
clientIdToTraceUrls.deleteAll(clientId);
|
clientIdToTraceUrls.delete(clientId);
|
||||||
else
|
else
|
||||||
traceUrls.forEach(url => usedTraces.add(url));
|
traceUrls.forEach(url => usedTraces.add(url));
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,7 @@ export interface TraceModelBackend {
|
|||||||
isLive(): boolean;
|
isLive(): boolean;
|
||||||
traceURL(): string;
|
traceURL(): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TraceModel {
|
export class TraceModel {
|
||||||
contextEntries: ContextEntry[] = [];
|
contextEntries: ContextEntry[] = [];
|
||||||
pageEntries = new Map<string, PageEntry>();
|
pageEntries = new Map<string, PageEntry>();
|
||||||
|
@ -120,7 +120,7 @@ export class FetchTraceModelBackend implements TraceModelBackend {
|
|||||||
|
|
||||||
async readBlob(entryName: string): Promise<Blob | undefined> {
|
async readBlob(entryName: string): Promise<Blob | undefined> {
|
||||||
const response = await this._readEntry(entryName);
|
const response = await this._readEntry(entryName);
|
||||||
return response?.blob();
|
return response?.status === 200 ? await response?.blob() : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _readEntry(entryName: string): Promise<Response | undefined> {
|
private async _readEntry(entryName: string): Promise<Response | undefined> {
|
||||||
|
@ -61,40 +61,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.network-request-title-content-type {
|
.network-request-title-content-type {
|
||||||
margin-left: 6px;
|
margin: 0 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-request-details {
|
.network-request-details {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
user-select: text;
|
user-select: text;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-request-details-url {
|
.network-request-details-url {
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-request-headers {
|
.network-request-headers {
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
margin-left: 10px;
|
||||||
|
|
||||||
.network-request-body {
|
|
||||||
white-space: pre;
|
|
||||||
overflow: scroll;
|
|
||||||
background-color: var(--vscode-sideBar-background);
|
|
||||||
border: black 1px solid;
|
|
||||||
max-height: 500px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.network-request-response-body {
|
|
||||||
white-space: pre;
|
|
||||||
overflow: scroll;
|
|
||||||
background-color: var(--vscode-sideBar-background);
|
|
||||||
border: black 1px solid;
|
|
||||||
max-height: 500px;
|
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.network-request-details-header {
|
.network-request-details-header {
|
||||||
@ -105,3 +91,7 @@
|
|||||||
.network-request-details-time {
|
.network-request-details-time {
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.network-request .cm-wrapper {
|
||||||
|
max-height: 300px;
|
||||||
|
}
|
||||||
|
@ -20,58 +20,23 @@ import * as React from 'react';
|
|||||||
import './networkResourceDetails.css';
|
import './networkResourceDetails.css';
|
||||||
import type { Entry } from '@trace/har';
|
import type { Entry } from '@trace/har';
|
||||||
import { msToString } from '@web/uiUtils';
|
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 NetworkResourceDetails: React.FunctionComponent<{
|
export const NetworkResource: React.FunctionComponent<{
|
||||||
resource: ResourceSnapshot,
|
resource: Entry,
|
||||||
}> = ({ resource }) => {
|
}> = ({ resource }) => {
|
||||||
const [expanded, setExpanded] = React.useState(false);
|
const [expanded, setExpanded] = React.useState(false);
|
||||||
const [requestBody, setRequestBody] = React.useState<string | null>(null);
|
|
||||||
const [responseBody, setResponseBody] = React.useState<{ dataUrl?: string, text?: string } | null>(null);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
const { routeStatus, resourceName, contentType } = React.useMemo(() => {
|
||||||
setExpanded(false);
|
|
||||||
}, [resource]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const readResources = async () => {
|
|
||||||
if (resource.request.postData) {
|
|
||||||
if (resource.request.postData._sha1) {
|
|
||||||
const response = await fetch(`sha1/${resource.request.postData._sha1}`);
|
|
||||||
const requestResource = await response.text();
|
|
||||||
setRequestBody(requestResource);
|
|
||||||
} else {
|
|
||||||
setRequestBody(resource.request.postData.text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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<any>(f => reader.onload = f);
|
|
||||||
reader.readAsDataURL(blob);
|
|
||||||
setResponseBody({ dataUrl: (await eventPromise).target.result });
|
|
||||||
} else {
|
|
||||||
setResponseBody({ text: await response.text() });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
readResources();
|
|
||||||
}, [expanded, resource]);
|
|
||||||
|
|
||||||
const { routeStatus, requestContentType, resourceName, contentType } = React.useMemo(() => {
|
|
||||||
const routeStatus = formatRouteStatus(resource);
|
const routeStatus = formatRouteStatus(resource);
|
||||||
const requestContentTypeHeader = resource.request.headers.find(q => q.name === 'Content-Type');
|
|
||||||
const requestContentType = requestContentTypeHeader ? requestContentTypeHeader.value : '';
|
|
||||||
const resourceName = resource.request.url.substring(resource.request.url.lastIndexOf('/'));
|
const resourceName = resource.request.url.substring(resource.request.url.lastIndexOf('/'));
|
||||||
let contentType = resource.response.content.mimeType;
|
let contentType = resource.response.content.mimeType;
|
||||||
const charset = contentType.match(/^(.*);\s*charset=.*$/);
|
const charset = contentType.match(/^(.*);\s*charset=.*$/);
|
||||||
if (charset)
|
if (charset)
|
||||||
contentType = charset[1];
|
contentType = charset[1];
|
||||||
return { routeStatus, requestContentType, resourceName, contentType };
|
return { routeStatus, resourceName, contentType };
|
||||||
}, [resource]);
|
}, [resource]);
|
||||||
|
|
||||||
const renderTitle = React.useCallback(() => {
|
const renderTitle = React.useCallback(() => {
|
||||||
@ -85,26 +50,87 @@ export const NetworkResourceDetails: React.FunctionComponent<{
|
|||||||
</div>;
|
</div>;
|
||||||
}, [contentType, resource, resourceName, routeStatus]);
|
}, [contentType, resource, resourceName, routeStatus]);
|
||||||
|
|
||||||
return <div
|
return <div className='network-request'>
|
||||||
className='network-request'>
|
|
||||||
<Expandable expanded={expanded} setExpanded={setExpanded} title={renderTitle()}>
|
<Expandable expanded={expanded} setExpanded={setExpanded} title={renderTitle()}>
|
||||||
<div className='network-request-details'>
|
{expanded && <NetworkResourceDetails resource={resource} />}
|
||||||
<div className='network-request-details-time'>{msToString(resource.time)}</div>
|
</Expandable>
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
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<any>(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 <TabbedPane tabs={[
|
||||||
|
{
|
||||||
|
id: 'request',
|
||||||
|
title: 'Request',
|
||||||
|
render: () => <div className='network-request-details'>
|
||||||
<div className='network-request-details-header'>URL</div>
|
<div className='network-request-details-header'>URL</div>
|
||||||
<div className='network-request-details-url'>{resource.request.url}</div>
|
<div className='network-request-details-url'>{resource.request.url}</div>
|
||||||
<div className='network-request-details-header'>Request Headers</div>
|
<div className='network-request-details-header'>Request Headers</div>
|
||||||
<div className='network-request-headers'>{resource.request.headers.map(pair => `${pair.name}: ${pair.value}`).join('\n')}</div>
|
<div className='network-request-headers'>{resource.request.headers.map(pair => `${pair.name}: ${pair.value}`).join('\n')}</div>
|
||||||
|
{requestBody && <div className='network-request-details-header'>Request Body</div>}
|
||||||
|
{requestBody && <CodeMirrorWrapper text={requestBody.text} language={requestBody.language} />}
|
||||||
|
</div>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'response',
|
||||||
|
title: 'Response',
|
||||||
|
render: () => <div className='network-request-details'>
|
||||||
|
<div className='network-request-details-time'>{msToString(resource.time)}</div>
|
||||||
<div className='network-request-details-header'>Response Headers</div>
|
<div className='network-request-details-header'>Response Headers</div>
|
||||||
<div className='network-request-headers'>{resource.response.headers.map(pair => `${pair.name}: ${pair.value}`).join('\n')}</div>
|
<div className='network-request-headers'>{resource.response.headers.map(pair => `${pair.name}: ${pair.value}`).join('\n')}</div>
|
||||||
{resource.request.postData ? <div className='network-request-details-header'>Request Body</div> : ''}
|
</div>,
|
||||||
{resource.request.postData ? <div className='network-request-body'>{formatBody(requestBody, requestContentType)}</div> : ''}
|
},
|
||||||
<div className='network-request-details-header'>Response Body</div>
|
{
|
||||||
{!resource.response.content._sha1 ? <div className='network-request-response-body'>Response body is not available for this request.</div> : ''}
|
id: 'body',
|
||||||
{responseBody !== null && responseBody.dataUrl ? <img draggable='false' src={responseBody.dataUrl} /> : ''}
|
title: 'Body',
|
||||||
{responseBody !== null && responseBody.text ? <div className='network-request-response-body'>{formatBody(responseBody.text, resource.response.content.mimeType)}</div> : ''}
|
render: () => <div className='network-request-details'>
|
||||||
</div>
|
{!resource.response.content._sha1 && <div>Response body is not available for this request.</div>}
|
||||||
</Expandable>
|
{responseBody && responseBody.dataUrl && <img draggable='false' src={responseBody.dataUrl} />}
|
||||||
</div>;
|
{responseBody && responseBody.text && <CodeMirrorWrapper text={responseBody.text} language={responseBody.language} />}
|
||||||
|
</div>,
|
||||||
|
},
|
||||||
|
]} selectedTab={selectedTab} setSelectedTab={setSelectedTab}/>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatStatus(status: number): string {
|
function formatStatus(status: number): string {
|
||||||
@ -148,3 +174,12 @@ function formatRouteStatus(request: Entry): string {
|
|||||||
return 'api';
|
return 'api';
|
||||||
return '';
|
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';
|
||||||
|
}
|
||||||
|
@ -24,3 +24,7 @@
|
|||||||
.network-tab:focus {
|
.network-tab:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.network-request .expandable {
|
||||||
|
width: 100%;
|
||||||
|
}
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import type * as modelUtil from './modelUtil';
|
import type * as modelUtil from './modelUtil';
|
||||||
import { NetworkResourceDetails } from './networkResourceDetails';
|
import { NetworkResource } from './networkResourceDetails';
|
||||||
import './networkTab.css';
|
import './networkTab.css';
|
||||||
import type { Boundaries } from '../geometry';
|
import type { Boundaries } from '../geometry';
|
||||||
|
|
||||||
@ -33,11 +33,6 @@ export const NetworkTab: React.FunctionComponent<{
|
|||||||
});
|
});
|
||||||
}, [model, selectedTime]);
|
}, [model, selectedTime]);
|
||||||
return <div className='network-tab'> {
|
return <div className='network-tab'> {
|
||||||
resources.map((resource, index) => {
|
resources.map((resource, index) => <NetworkResource key={index} resource={resource}></NetworkResource>)
|
||||||
return <NetworkResourceDetails
|
|
||||||
resource={resource}
|
|
||||||
key={index}
|
|
||||||
/>;
|
|
||||||
})
|
|
||||||
}</div>;
|
}</div>;
|
||||||
};
|
};
|
||||||
|
@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
import codemirror from 'codemirror';
|
import codemirror from 'codemirror';
|
||||||
import 'codemirror/lib/codemirror.css';
|
import 'codemirror/lib/codemirror.css';
|
||||||
|
import 'codemirror/mode/css/css';
|
||||||
|
import 'codemirror/mode/htmlmixed/htmlmixed';
|
||||||
import 'codemirror/mode/javascript/javascript';
|
import 'codemirror/mode/javascript/javascript';
|
||||||
import 'codemirror/mode/python/python';
|
import 'codemirror/mode/python/python';
|
||||||
import 'codemirror/mode/clike/clike';
|
import 'codemirror/mode/clike/clike';
|
||||||
|
@ -26,11 +26,11 @@ export type SourceHighlight = {
|
|||||||
message?: string;
|
message?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Language = 'javascript' | 'python' | 'java' | 'csharp' | 'jsonl';
|
export type Language = 'javascript' | 'python' | 'java' | 'csharp' | 'jsonl' | 'html' | 'css';
|
||||||
|
|
||||||
export interface SourceProps {
|
export interface SourceProps {
|
||||||
text: string;
|
text: string;
|
||||||
language: Language;
|
language?: Language;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
// 1-based
|
// 1-based
|
||||||
highlight?: SourceHighlight[];
|
highlight?: SourceHighlight[];
|
||||||
@ -68,13 +68,19 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
|
|||||||
if (!element)
|
if (!element)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
let mode = 'javascript';
|
let mode = '';
|
||||||
|
if (language === 'javascript')
|
||||||
|
mode = 'javascript';
|
||||||
if (language === 'python')
|
if (language === 'python')
|
||||||
mode = 'python';
|
mode = 'python';
|
||||||
if (language === 'java')
|
if (language === 'java')
|
||||||
mode = 'text/x-java';
|
mode = 'text/x-java';
|
||||||
if (language === 'csharp')
|
if (language === 'csharp')
|
||||||
mode = 'text/x-csharp';
|
mode = 'text/x-csharp';
|
||||||
|
if (language === 'html')
|
||||||
|
mode = 'htmlmixed';
|
||||||
|
if (language === 'css')
|
||||||
|
mode = 'css';
|
||||||
|
|
||||||
if (codemirrorRef.current
|
if (codemirrorRef.current
|
||||||
&& mode === codemirrorRef.current.cm.getOption('mode')
|
&& mode === codemirrorRef.current.cm.getOption('mode')
|
||||||
|
Loading…
x
Reference in New Issue
Block a user