Summary: Bumps [prettier](https://github.com/prettier/prettier) from 2.2.1 to 2.3.0. <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/prettier/prettier/releases">prettier's releases</a>.</em></p> <blockquote> <h2>2.3.0</h2> <p><a href="https://github.com/prettier/prettier/compare/2.2.1...2.3.0">diff</a></p> <p>{emoji:1f517} <a href="https://prettier.io/blog/2021/05/09/2.3.0.html">Release Notes</a></p> </blockquote> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/prettier/prettier/blob/main/CHANGELOG.md">prettier's changelog</a>.</em></p> <blockquote> <h1>2.3.0</h1> <p><a href="https://github.com/prettier/prettier/compare/2.2.1...2.3.0">diff</a></p> <p>{emoji:1f517} <a href="https://prettier.io/blog/2021/05/09/2.3.0.html">Release Notes</a></p> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="2afc3b9ae6"><code>2afc3b9</code></a> Release 2.3.0</li> <li><a href="7cfa9aa89b"><code>7cfa9aa</code></a> Fix pre-commit hook setup command (<a href="https://github-redirect.dependabot.com/prettier/prettier/issues/10710">#10710</a>)</li> <li><a href="c8c02b4753"><code>c8c02b4</code></a> Build(deps-dev): Bump concurrently from 6.0.2 to 6.1.0 in /website (<a href="https://github-redirect.dependabot.com/prettier/prettier/issues/10834">#10834</a>)</li> <li><a href="6506e0f50e"><code>6506e0f</code></a> Build(deps-dev): Bump webpack-cli from 4.6.0 to 4.7.0 in /website (<a href="https://github-redirect.dependabot.com/prettier/prettier/issues/10836">#10836</a>)</li> <li><a href="69fae9c291"><code>69fae9c</code></a> Build(deps): Bump flow-parser from 0.150.0 to 0.150.1 (<a href="https://github-redirect.dependabot.com/prettier/prettier/issues/10839">#10839</a>)</li> <li><a href="164a6e2351"><code>164a6e2</code></a> Switch CLI to async (<a href="https://github-redirect.dependabot.com/prettier/prettier/issues/10804">#10804</a>)</li> <li><a href="d3e7e2f634"><code>d3e7e2f</code></a> Build(deps): Bump codecov/codecov-action from v1.4.1 to v1.5.0 (<a href="https://github-redirect.dependabot.com/prettier/prettier/issues/10833">#10833</a>)</li> <li><a href="9e09845da0"><code>9e09845</code></a> Build(deps): Bump <code>@angular/compiler</code> from 11.2.12 to 11.2.13 (<a href="https://github-redirect.dependabot.com/prettier/prettier/issues/10838">#10838</a>)</li> <li><a href="1bfab3d045"><code>1bfab3d</code></a> Build(deps-dev): Bump eslint from 7.25.0 to 7.26.0 (<a href="https://github-redirect.dependabot.com/prettier/prettier/issues/10840">#10840</a>)</li> <li><a href="387fce4ed8"><code>387fce4</code></a> Minor formatting tweaks (<a href="https://github-redirect.dependabot.com/prettier/prettier/issues/10807">#10807</a>)</li> <li>Additional commits viewable in <a href="https://github.com/prettier/prettier/compare/2.2.1...2.3.0">compare view</a></li> </ul> </details> <br /> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `dependabot rebase` will rebase this PR - `dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `dependabot merge` will merge this PR after your CI passes on it - `dependabot squash and merge` will squash and merge this PR after your CI passes on it - `dependabot cancel merge` will cancel a previously requested merge and block automerging - `dependabot reopen` will reopen this PR if it is closed - `dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details> Pull Request resolved: https://github.com/facebook/flipper/pull/2300 Reviewed By: passy Differential Revision: D28323849 Pulled By: cekkaewnumchai fbshipit-source-id: 1842877ccc9a9587af7f0d9ff9432c2075c8ee22
808 lines
21 KiB
TypeScript
808 lines
21 KiB
TypeScript
/**
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @format
|
|
*/
|
|
|
|
import React from 'react';
|
|
import {Component} from 'react';
|
|
import querystring from 'querystring';
|
|
import xmlBeautifier from 'xml-beautifier';
|
|
import {Base64} from 'js-base64';
|
|
|
|
import {
|
|
DataInspector,
|
|
Layout,
|
|
Panel,
|
|
styled,
|
|
theme,
|
|
CodeBlock,
|
|
} from 'flipper-plugin';
|
|
import {Select, Typography} from 'antd';
|
|
|
|
import {
|
|
bodyAsBinary,
|
|
bodyAsString,
|
|
formatBytes,
|
|
getHeaderValue,
|
|
isTextual,
|
|
} from './utils';
|
|
import {Request, Header, Insights, RetryInsights} from './types';
|
|
import {BodyOptions} from './index';
|
|
import {ProtobufDefinitionsRepository} from './ProtobufDefinitionsRepository';
|
|
import {KeyValueItem, KeyValueTable} from './KeyValueTable';
|
|
import {CopyOutlined} from '@ant-design/icons';
|
|
|
|
const {Text} = Typography;
|
|
|
|
type RequestDetailsProps = {
|
|
request: Request;
|
|
bodyFormat: string;
|
|
onSelectFormat: (bodyFormat: string) => void;
|
|
onCopyText(test: string): void;
|
|
};
|
|
export default class RequestDetails extends Component<RequestDetailsProps> {
|
|
urlColumns = (url: URL) => {
|
|
return [
|
|
{
|
|
key: 'Full URL',
|
|
value: url.href,
|
|
},
|
|
{
|
|
key: 'Host',
|
|
value: url.host,
|
|
},
|
|
{
|
|
key: 'Path',
|
|
value: url.pathname,
|
|
},
|
|
{
|
|
key: 'Query String',
|
|
value: url.search,
|
|
},
|
|
];
|
|
};
|
|
|
|
render() {
|
|
const {request, bodyFormat, onSelectFormat, onCopyText} = this.props;
|
|
const url = new URL(request.url);
|
|
|
|
const formattedText = bodyFormat == 'formatted';
|
|
|
|
return (
|
|
<>
|
|
<Panel key="request" title={'Request'}>
|
|
<KeyValueTable items={this.urlColumns(url)} />
|
|
</Panel>
|
|
|
|
{url.search ? (
|
|
<Panel title={'Request Query Parameters'}>
|
|
<QueryInspector queryParams={url.searchParams} />
|
|
</Panel>
|
|
) : null}
|
|
|
|
{request.requestHeaders.length > 0 ? (
|
|
<Panel key="headers" title={'Request Headers'}>
|
|
<HeaderInspector headers={request.requestHeaders} />
|
|
</Panel>
|
|
) : null}
|
|
|
|
{request.requestData != null ? (
|
|
<Panel
|
|
key="requestData"
|
|
title={'Request Body'}
|
|
extraActions={
|
|
isTextual(request.requestHeaders) ? (
|
|
<CopyOutlined
|
|
title="Copy request body"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onCopyText(request.requestData as string);
|
|
}}
|
|
/>
|
|
) : null
|
|
}
|
|
pad>
|
|
<RequestBodyInspector
|
|
formattedText={formattedText}
|
|
request={request}
|
|
/>
|
|
</Panel>
|
|
) : null}
|
|
{request.status ? (
|
|
<>
|
|
{request.responseHeaders?.length ? (
|
|
<Panel
|
|
key={'responseheaders'}
|
|
title={`Response Headers${
|
|
request.responseIsMock ? ' (Mocked)' : ''
|
|
}`}>
|
|
<HeaderInspector headers={request.responseHeaders} />
|
|
</Panel>
|
|
) : null}
|
|
<Panel
|
|
key={'responsebody'}
|
|
title={`Response Body${
|
|
request.responseIsMock ? ' (Mocked)' : ''
|
|
}`}
|
|
extraActions={
|
|
isTextual(request.responseHeaders) && request.responseData ? (
|
|
<CopyOutlined
|
|
title="Copy response body"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onCopyText(request.responseData as string);
|
|
}}
|
|
/>
|
|
) : null
|
|
}
|
|
pad>
|
|
<ResponseBodyInspector
|
|
formattedText={formattedText}
|
|
request={request}
|
|
/>
|
|
</Panel>
|
|
</>
|
|
) : null}
|
|
<Panel key="options" title={'Options'} collapsed pad>
|
|
<Text>Body formatting:</Text>
|
|
<Select
|
|
value={bodyFormat}
|
|
onChange={onSelectFormat}
|
|
options={BodyOptions}
|
|
/>
|
|
</Panel>
|
|
{request.insights ? (
|
|
<Panel key="insights" title={'Insights'} collapsed>
|
|
<InsightsInspector insights={request.insights} />
|
|
</Panel>
|
|
) : null}
|
|
</>
|
|
);
|
|
}
|
|
}
|
|
|
|
class QueryInspector extends Component<{queryParams: URLSearchParams}> {
|
|
render() {
|
|
const rows: KeyValueItem[] = [];
|
|
this.props.queryParams.forEach((value: string, key: string) => {
|
|
rows.push({
|
|
key,
|
|
value,
|
|
});
|
|
});
|
|
return rows.length > 0 ? <KeyValueTable items={rows} /> : null;
|
|
}
|
|
}
|
|
|
|
type HeaderInspectorProps = {
|
|
headers: Array<Header>;
|
|
};
|
|
|
|
type HeaderInspectorState = {
|
|
computedHeaders: Object;
|
|
};
|
|
|
|
class HeaderInspector extends Component<
|
|
HeaderInspectorProps,
|
|
HeaderInspectorState
|
|
> {
|
|
render() {
|
|
const computedHeaders: Map<string, string> = this.props.headers.reduce(
|
|
(sum, header) => {
|
|
return sum.set(header.key, header.value);
|
|
},
|
|
new Map(),
|
|
);
|
|
|
|
const rows = Array.from(computedHeaders.entries())
|
|
.sort((a, b) => (a[0] < b[0] ? -1 : a[0] == b[0] ? 0 : 1))
|
|
.map(([key, value]) => ({key, value}));
|
|
return rows.length > 0 ? (
|
|
<KeyValueTable items={this.props.headers} />
|
|
) : null;
|
|
}
|
|
}
|
|
|
|
type BodyFormatter = {
|
|
formatRequest?: (request: Request) => any;
|
|
formatResponse?: (request: Request) => any;
|
|
};
|
|
|
|
class RequestBodyInspector extends Component<{
|
|
request: Request;
|
|
formattedText: boolean;
|
|
}> {
|
|
render() {
|
|
const {request, formattedText} = this.props;
|
|
if (request.requestData == null || request.requestData === '') {
|
|
return <Empty />;
|
|
}
|
|
const bodyFormatters = formattedText ? TextBodyFormatters : BodyFormatters;
|
|
for (const formatter of bodyFormatters) {
|
|
if (formatter.formatRequest) {
|
|
try {
|
|
const component = formatter.formatRequest(request);
|
|
if (component) {
|
|
return (
|
|
<Layout.Container gap>
|
|
{component}
|
|
<FormattedBy>
|
|
Formatted by {formatter.constructor.name}
|
|
</FormattedBy>
|
|
</Layout.Container>
|
|
);
|
|
}
|
|
} catch (e) {
|
|
console.warn(
|
|
'BodyFormatter exception from ' + formatter.constructor.name,
|
|
e.message,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
return renderRawBody(request, 'request');
|
|
}
|
|
}
|
|
|
|
class ResponseBodyInspector extends Component<{
|
|
request: Request;
|
|
formattedText: boolean;
|
|
}> {
|
|
render() {
|
|
const {request, formattedText} = this.props;
|
|
if (request.responseData == null || request.responseData === '') {
|
|
return <Empty />;
|
|
}
|
|
const bodyFormatters = formattedText ? TextBodyFormatters : BodyFormatters;
|
|
for (const formatter of bodyFormatters) {
|
|
if (formatter.formatResponse) {
|
|
try {
|
|
const component = formatter.formatResponse(request);
|
|
if (component) {
|
|
return (
|
|
<Layout.Container gap>
|
|
{component}
|
|
<FormattedBy>
|
|
Formatted by {formatter.constructor.name}
|
|
</FormattedBy>
|
|
</Layout.Container>
|
|
);
|
|
}
|
|
} catch (e) {
|
|
console.warn(
|
|
'BodyFormatter exception from ' + formatter.constructor.name,
|
|
e.message,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
return renderRawBody(request, 'response');
|
|
}
|
|
}
|
|
|
|
const FormattedBy = styled(Text)({
|
|
marginTop: 8,
|
|
fontSize: '0.7em',
|
|
textAlign: 'center',
|
|
display: 'block',
|
|
color: theme.disabledColor,
|
|
});
|
|
|
|
const Empty = () => (
|
|
<Layout.Container pad>
|
|
<Text>(empty)</Text>
|
|
</Layout.Container>
|
|
);
|
|
|
|
function renderRawBody(request: Request, mode: 'request' | 'response') {
|
|
const data = mode === 'request' ? request.requestData : request.responseData;
|
|
return (
|
|
<Layout.Container gap>
|
|
<CodeBlock>{bodyAsString(data)}</CodeBlock>
|
|
</Layout.Container>
|
|
);
|
|
}
|
|
|
|
type ImageWithSizeProps = {
|
|
src: string;
|
|
};
|
|
|
|
type ImageWithSizeState = {
|
|
width: number;
|
|
height: number;
|
|
};
|
|
|
|
class ImageWithSize extends Component<ImageWithSizeProps, ImageWithSizeState> {
|
|
static Image = styled.img({
|
|
objectFit: 'scale-down',
|
|
maxWidth: '100%',
|
|
marginBottom: 10,
|
|
});
|
|
|
|
constructor(props: ImageWithSizeProps) {
|
|
super(props);
|
|
this.state = {
|
|
width: 0,
|
|
height: 0,
|
|
};
|
|
}
|
|
|
|
componentDidMount() {
|
|
const image = new Image();
|
|
image.src = this.props.src;
|
|
image.onload = () => {
|
|
image.width;
|
|
image.height;
|
|
this.setState({
|
|
width: image.width,
|
|
height: image.height,
|
|
});
|
|
};
|
|
}
|
|
|
|
render() {
|
|
return (
|
|
<Layout.Container center>
|
|
<ImageWithSize.Image src={this.props.src} />
|
|
<Text type="secondary">
|
|
{this.state.width} x {this.state.height}
|
|
</Text>
|
|
</Layout.Container>
|
|
);
|
|
}
|
|
}
|
|
|
|
class ImageFormatter {
|
|
formatResponse(request: Request) {
|
|
if (
|
|
getHeaderValue(request.responseHeaders, 'content-type').startsWith(
|
|
'image/',
|
|
)
|
|
) {
|
|
if (request.responseData) {
|
|
const src = `data:${getHeaderValue(
|
|
request.responseHeaders,
|
|
'content-type',
|
|
)};base64,${Base64.fromUint8Array(
|
|
bodyAsBinary(request.responseData)!,
|
|
)}`;
|
|
return <ImageWithSize src={src} />;
|
|
} else {
|
|
// fallback to using the request url
|
|
return <ImageWithSize src={request.url} />;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class VideoFormatter {
|
|
static Video = styled.video({
|
|
maxWidth: 500,
|
|
maxHeight: 500,
|
|
});
|
|
|
|
formatResponse = (request: Request) => {
|
|
const contentType = getHeaderValue(request.responseHeaders, 'content-type');
|
|
if (contentType.startsWith('video/')) {
|
|
return (
|
|
<Layout.Container center>
|
|
<VideoFormatter.Video controls={true}>
|
|
<source src={request.url} type={contentType} />
|
|
</VideoFormatter.Video>
|
|
</Layout.Container>
|
|
);
|
|
}
|
|
};
|
|
}
|
|
|
|
class JSONText extends Component<{children: any}> {
|
|
render() {
|
|
const jsonObject = this.props.children;
|
|
return (
|
|
<CodeBlock>
|
|
{JSON.stringify(jsonObject, null, 2)}
|
|
{'\n'}
|
|
</CodeBlock>
|
|
);
|
|
}
|
|
}
|
|
|
|
class XMLText extends Component<{body: any}> {
|
|
render() {
|
|
const xmlPretty = xmlBeautifier(this.props.body);
|
|
return (
|
|
<CodeBlock>
|
|
{xmlPretty}
|
|
{'\n'}
|
|
</CodeBlock>
|
|
);
|
|
}
|
|
}
|
|
|
|
class JSONTextFormatter {
|
|
formatRequest(request: Request) {
|
|
return this.format(
|
|
bodyAsString(request.requestData),
|
|
getHeaderValue(request.requestHeaders, 'content-type'),
|
|
);
|
|
}
|
|
|
|
formatResponse(request: Request) {
|
|
return this.format(
|
|
bodyAsString(request.responseData),
|
|
getHeaderValue(request.responseHeaders, 'content-type'),
|
|
);
|
|
}
|
|
|
|
format(body: string, contentType: string) {
|
|
if (
|
|
contentType.startsWith('application/json') ||
|
|
contentType.startsWith('application/hal+json') ||
|
|
contentType.startsWith('text/javascript') ||
|
|
contentType.startsWith('application/x-fb-flatbuffer')
|
|
) {
|
|
try {
|
|
const data = JSON.parse(body);
|
|
return <JSONText>{data}</JSONText>;
|
|
} catch (SyntaxError) {
|
|
// Multiple top level JSON roots, map them one by one
|
|
return body
|
|
.split('\n')
|
|
.map((json) => JSON.parse(json))
|
|
.map((data, idx) => <JSONText key={idx}>{data}</JSONText>);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class XMLTextFormatter {
|
|
formatRequest(request: Request) {
|
|
return this.format(
|
|
bodyAsString(request.requestData),
|
|
getHeaderValue(request.requestHeaders, 'content-type'),
|
|
);
|
|
}
|
|
|
|
formatResponse(request: Request) {
|
|
return this.format(
|
|
bodyAsString(request.responseData),
|
|
getHeaderValue(request.responseHeaders, 'content-type'),
|
|
);
|
|
}
|
|
|
|
format(body: string, contentType: string) {
|
|
if (contentType.startsWith('text/html')) {
|
|
return <XMLText body={body} />;
|
|
}
|
|
}
|
|
}
|
|
|
|
class JSONFormatter {
|
|
formatRequest(request: Request) {
|
|
return this.format(
|
|
bodyAsString(request.requestData),
|
|
getHeaderValue(request.requestHeaders, 'content-type'),
|
|
);
|
|
}
|
|
|
|
formatResponse(request: Request) {
|
|
return this.format(
|
|
bodyAsString(request.responseData),
|
|
getHeaderValue(request.responseHeaders, 'content-type'),
|
|
);
|
|
}
|
|
|
|
format(body: string, contentType: string) {
|
|
if (
|
|
contentType.startsWith('application/json') ||
|
|
contentType.startsWith('application/hal+json') ||
|
|
contentType.startsWith('text/javascript') ||
|
|
contentType.startsWith('application/x-fb-flatbuffer')
|
|
) {
|
|
try {
|
|
const data = JSON.parse(body);
|
|
return <DataInspector collapsed expandRoot data={data} />;
|
|
} catch (SyntaxError) {
|
|
// Multiple top level JSON roots, map them one by one
|
|
const roots = body.split('\n');
|
|
return (
|
|
<DataInspector
|
|
collapsed
|
|
expandRoot
|
|
data={roots.map((json) => JSON.parse(json))}
|
|
/>
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class LogEventFormatter {
|
|
formatRequest(request: Request) {
|
|
if (request.url.indexOf('logging_client_event') > 0) {
|
|
const data = querystring.parse(bodyAsString(request.requestData));
|
|
if (typeof data.message === 'string') {
|
|
data.message = JSON.parse(data.message);
|
|
}
|
|
return <DataInspector expandRoot data={data} />;
|
|
}
|
|
}
|
|
}
|
|
|
|
class GraphQLBatchFormatter {
|
|
formatRequest(request: Request) {
|
|
if (request.url.indexOf('graphqlbatch') > 0) {
|
|
const data = querystring.parse(bodyAsString(request.requestData));
|
|
if (typeof data.queries === 'string') {
|
|
data.queries = JSON.parse(data.queries);
|
|
}
|
|
return <DataInspector expandRoot data={data} />;
|
|
}
|
|
}
|
|
}
|
|
|
|
class GraphQLFormatter {
|
|
parsedServerTimeForFirstFlush(data: any) {
|
|
const firstResponse =
|
|
Array.isArray(data) && data.length > 0 ? data[0] : data;
|
|
if (!firstResponse) {
|
|
return null;
|
|
}
|
|
|
|
const extensions = firstResponse['extensions'];
|
|
if (!extensions) {
|
|
return null;
|
|
}
|
|
const serverMetadata = extensions['server_metadata'];
|
|
if (!serverMetadata) {
|
|
return null;
|
|
}
|
|
const requestStartMs = serverMetadata['request_start_time_ms'];
|
|
const timeAtFlushMs = serverMetadata['time_at_flush_ms'];
|
|
return (
|
|
<Text type="secondary">
|
|
{'Server wall time for initial response (ms): ' +
|
|
(timeAtFlushMs - requestStartMs)}
|
|
</Text>
|
|
);
|
|
}
|
|
formatRequest(request: Request) {
|
|
if (request.url.indexOf('graphql') > 0) {
|
|
const decoded = request.requestData;
|
|
if (!decoded) {
|
|
return undefined;
|
|
}
|
|
const data = querystring.parse(bodyAsString(decoded));
|
|
if (typeof data.variables === 'string') {
|
|
data.variables = JSON.parse(data.variables);
|
|
}
|
|
if (typeof data.query_params === 'string') {
|
|
data.query_params = JSON.parse(data.query_params);
|
|
}
|
|
return <DataInspector expandRoot data={data} />;
|
|
}
|
|
}
|
|
|
|
formatResponse(request: Request) {
|
|
return this.format(
|
|
bodyAsString(request.responseData!),
|
|
getHeaderValue(request.responseHeaders, 'content-type'),
|
|
);
|
|
}
|
|
|
|
format = (body: string, contentType: string) => {
|
|
if (
|
|
contentType.startsWith('application/json') ||
|
|
contentType.startsWith('application/hal+json') ||
|
|
contentType.startsWith('text/javascript') ||
|
|
contentType.startsWith('text/html') ||
|
|
contentType.startsWith('application/x-fb-flatbuffer')
|
|
) {
|
|
try {
|
|
const data = JSON.parse(body);
|
|
return (
|
|
<div>
|
|
{this.parsedServerTimeForFirstFlush(data)}
|
|
<DataInspector collapsed expandRoot data={data} />
|
|
</div>
|
|
);
|
|
} catch (SyntaxError) {
|
|
// Multiple top level JSON roots, map them one by one
|
|
const parsedResponses = body
|
|
.replace(/}{/g, '}\r\n{')
|
|
.split('\n')
|
|
.map((json) => JSON.parse(json));
|
|
return (
|
|
<div>
|
|
{this.parsedServerTimeForFirstFlush(parsedResponses)}
|
|
<DataInspector collapsed expandRoot data={parsedResponses} />
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
class FormUrlencodedFormatter {
|
|
formatRequest = (request: Request) => {
|
|
const contentType = getHeaderValue(request.requestHeaders, 'content-type');
|
|
if (contentType.startsWith('application/x-www-form-urlencoded')) {
|
|
const decoded = request.requestData;
|
|
if (!decoded) {
|
|
return undefined;
|
|
}
|
|
return (
|
|
<DataInspector
|
|
expandRoot
|
|
data={querystring.parse(bodyAsString(decoded))}
|
|
/>
|
|
);
|
|
}
|
|
};
|
|
}
|
|
|
|
class BinaryFormatter {
|
|
formatRequest(request: Request) {
|
|
if (
|
|
getHeaderValue(request.requestHeaders, 'content-type') ===
|
|
'application/octet-stream'
|
|
) {
|
|
return '(binary data)'; // we could offer a download button here?
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
formatResponse(request: Request) {
|
|
if (
|
|
getHeaderValue(request.responseHeaders, 'content-type') ===
|
|
'application/octet-stream'
|
|
) {
|
|
return '(binary data)'; // we could offer a download button here?
|
|
}
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
class ProtobufFormatter {
|
|
private protobufDefinitionRepository =
|
|
ProtobufDefinitionsRepository.getInstance();
|
|
|
|
formatRequest(request: Request) {
|
|
if (
|
|
getHeaderValue(request.requestHeaders, 'content-type') ===
|
|
'application/x-protobuf'
|
|
) {
|
|
const protobufDefinition =
|
|
this.protobufDefinitionRepository.getRequestType(
|
|
request.method,
|
|
request.url,
|
|
);
|
|
if (protobufDefinition == undefined) {
|
|
return (
|
|
<Text>
|
|
Could not locate protobuf definition for request body of{' '}
|
|
{request.url}
|
|
</Text>
|
|
);
|
|
}
|
|
|
|
if (request.requestData) {
|
|
const data = protobufDefinition.decode(
|
|
bodyAsBinary(request.requestData)!,
|
|
);
|
|
return <JSONText>{data.toJSON()}</JSONText>;
|
|
} else {
|
|
return (
|
|
<Text>Could not locate request body data for {request.url}</Text>
|
|
);
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
formatResponse(request: Request) {
|
|
if (
|
|
getHeaderValue(request.responseHeaders, 'content-type') ===
|
|
'application/x-protobuf' ||
|
|
request.url.endsWith('.proto')
|
|
) {
|
|
const protobufDefinition =
|
|
this.protobufDefinitionRepository.getResponseType(
|
|
request.method,
|
|
request.url,
|
|
);
|
|
if (protobufDefinition == undefined) {
|
|
return (
|
|
<Text>
|
|
Could not locate protobuf definition for response body of{' '}
|
|
{request.url}
|
|
</Text>
|
|
);
|
|
}
|
|
|
|
if (request.responseData) {
|
|
const data = protobufDefinition.decode(
|
|
bodyAsBinary(request.responseData)!,
|
|
);
|
|
return <JSONText>{data.toJSON()}</JSONText>;
|
|
} else {
|
|
return (
|
|
<Text>Could not locate response body data for {request.url}</Text>
|
|
);
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
const BodyFormatters: Array<BodyFormatter> = [
|
|
new ImageFormatter(),
|
|
new VideoFormatter(),
|
|
new LogEventFormatter(),
|
|
new GraphQLBatchFormatter(),
|
|
new GraphQLFormatter(),
|
|
new JSONFormatter(),
|
|
new FormUrlencodedFormatter(),
|
|
new XMLTextFormatter(),
|
|
new ProtobufFormatter(),
|
|
new BinaryFormatter(),
|
|
];
|
|
|
|
const TextBodyFormatters: Array<BodyFormatter> = [new JSONTextFormatter()];
|
|
|
|
class InsightsInspector extends Component<{insights: Insights}> {
|
|
formatTime(value: number): string {
|
|
return `${value} ms`;
|
|
}
|
|
|
|
formatSpeed(value: number): string {
|
|
return `${formatBytes(value)}/sec`;
|
|
}
|
|
|
|
formatRetries = (retry: RetryInsights): string => {
|
|
const timesWord = retry.limit === 1 ? 'time' : 'times';
|
|
|
|
return `${this.formatTime(retry.timeSpent)} (${
|
|
retry.count
|
|
} ${timesWord} out of ${retry.limit})`;
|
|
};
|
|
|
|
buildRow<T>(
|
|
name: string,
|
|
value: T | null | undefined,
|
|
formatter: (value: T) => string,
|
|
): any {
|
|
return value
|
|
? {
|
|
key: name,
|
|
value: formatter(value),
|
|
}
|
|
: null;
|
|
}
|
|
|
|
render() {
|
|
const insights = this.props.insights;
|
|
const {buildRow, formatTime, formatSpeed, formatRetries} = this;
|
|
|
|
const rows = [
|
|
buildRow('Retries', insights.retries, formatRetries),
|
|
buildRow('DNS lookup time', insights.dnsLookupTime, formatTime),
|
|
buildRow('Connect time', insights.connectTime, formatTime),
|
|
buildRow('SSL handshake time', insights.sslHandshakeTime, formatTime),
|
|
buildRow('Pretransfer time', insights.preTransferTime, formatTime),
|
|
buildRow('Redirect time', insights.redirectsTime, formatTime),
|
|
buildRow('First byte wait time', insights.timeToFirstByte, formatTime),
|
|
buildRow('Data transfer time', insights.transferTime, formatTime),
|
|
buildRow('Post processing time', insights.postProcessingTime, formatTime),
|
|
buildRow('Bytes transfered', insights.bytesTransfered, formatBytes),
|
|
buildRow('Transfer speed', insights.transferSpeed, formatSpeed),
|
|
].filter((r) => r != null);
|
|
|
|
return rows.length > 0 ? <KeyValueTable items={rows} /> : null;
|
|
}
|
|
}
|