Add option to display network response as copyable json

Summary: This is meant to reduce the friction of getting network response payloads. Simple switch allows developers to go to "formatted" body ui which shows the json in a text blob.

Reviewed By: passy

Differential Revision: D10378877

fbshipit-source-id: 87aeff5318f0c2c6d3d91d7e3b491595794e69bf
This commit is contained in:
Daniel Mueller
2018-10-16 03:53:50 -07:00
committed by Facebook Github Bot
parent 92498ec6f8
commit 12a2c0ee70

View File

@@ -12,10 +12,12 @@ import type {Request, Response, Header} from './index.js';
import {
Component,
FlexColumn,
FlexRow,
ManagedTable,
ManagedDataInspector,
Text,
Panel,
Select,
styled,
colors,
} from 'flipper';
@@ -51,6 +53,10 @@ type RequestDetailsProps = {
response: ?Response,
};
type RequestDetailsState = {
bodyFormat: string,
};
function decodeBody(container: Request | Response): string {
if (!container.data) {
return '';
@@ -83,11 +89,20 @@ function decompress(body: string): string {
return String.fromCharCode.apply(null, new Uint8Array(data));
}
export default class RequestDetails extends Component<RequestDetailsProps> {
export default class RequestDetails extends Component<
RequestDetailsProps,
RequestDetailsState,
> {
static Container = styled(FlexColumn)({
height: '100%',
overflow: 'auto',
});
static BodyOptions = {
formatted: 'formatted',
parsed: 'parsed',
};
state: RequestDetailsState = {bodyFormat: RequestDetails.BodyOptions.parsed};
urlColumns = (url: URL) => {
return [
@@ -134,10 +149,17 @@ export default class RequestDetails extends Component<RequestDetailsProps> {
];
};
onSelectFormat = (bodyFormat: string) => {
this.setState(() => ({bodyFormat}));
};
render() {
const {request, response} = this.props;
const url = new URL(request.url);
const {bodyFormat} = this.state;
const formattedText = bodyFormat == RequestDetails.BodyOptions.formatted;
return (
<RequestDetails.Container>
<Panel heading={'Request'} floating={false} padded={false}>
@@ -168,11 +190,16 @@ export default class RequestDetails extends Component<RequestDetailsProps> {
) : null}
{request.data != null ? (
<Panel heading={'Request Body'} floating={false}>
<RequestBodyInspector request={request} />
<Panel
heading={'Request Body'}
floating={false}
padded={!formattedText}>
<RequestBodyInspector
formattedText={formattedText}
request={request}
/>
</Panel>
) : null}
{response
? [
response.headers.length > 0 ? (
@@ -183,11 +210,28 @@ export default class RequestDetails extends Component<RequestDetailsProps> {
<HeaderInspector headers={response.headers} />
</Panel>
) : null,
<Panel heading={'Response Body'} floating={false}>
<ResponseBodyInspector request={request} response={response} />
<Panel
heading={'Response Body'}
floating={false}
padded={!formattedText}>
<ResponseBodyInspector
formattedText={formattedText}
request={request}
response={response}
/>
</Panel>,
]
: null}
<Panel heading={'Options'} floating={false} collapsed={true}>
<FlexRow>
<Text>Body: </Text>
<Select
selected={bodyFormat}
onChange={this.onSelectFormat}
options={RequestDetails.BodyOptions}
/>
</FlexRow>
</Panel>
</RequestDetails.Container>
);
}
@@ -286,12 +330,14 @@ type BodyFormatter = {
class RequestBodyInspector extends Component<{
request: Request,
formattedText: boolean,
}> {
render() {
const {request} = this.props;
const {request, formattedText} = this.props;
const bodyFormatters = formattedText ? TextBodyFormatters : BodyFormatters;
let component;
try {
for (const formatter of BodyFormatters) {
for (const formatter of bodyFormatters) {
if (formatter.formatRequest) {
component = formatter.formatRequest(request);
if (component) {
@@ -316,13 +362,14 @@ class RequestBodyInspector extends Component<{
class ResponseBodyInspector extends Component<{
response: Response,
request: Request,
formattedText: boolean,
}> {
render() {
const {request, response} = this.props;
const {request, response, formattedText} = this.props;
const bodyFormatters = formattedText ? TextBodyFormatters : BodyFormatters;
let component;
try {
for (const formatter of BodyFormatters) {
for (const formatter of bodyFormatters) {
if (formatter.formatResponse) {
component = formatter.formatResponse(request, response);
if (component) {
@@ -427,6 +474,57 @@ class VideoFormatter {
};
}
class JSONText extends Component<{children: any}> {
static NoScrollbarText = styled(Text)({
overflowY: 'hidden',
});
render() {
const jsonObject = this.props.children;
return (
<JSONText.NoScrollbarText code whiteSpace="pre" selectable>
{JSON.stringify(jsonObject, null, 2)}
{'\n'}
</JSONText.NoScrollbarText>
);
}
}
class JSONTextFormatter {
formatRequest = (request: Request) => {
return this.format(
decodeBody(request),
getHeaderValue(request.headers, 'content-type'),
);
};
formatResponse = (request: Request, response: Response) => {
return this.format(
decodeBody(response),
getHeaderValue(response.headers, 'content-type'),
);
};
format = (body: string, contentType: string) => {
if (
contentType.startsWith('application/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 => <JSONText>{data}</JSONText>);
}
}
};
}
class JSONFormatter {
formatRequest = (request: Request) => {
return this.format(
@@ -534,3 +632,5 @@ const BodyFormatters: Array<BodyFormatter> = [
new JSONFormatter(),
new FormUrlencodedFormatter(),
];
const TextBodyFormatters: Array<BodyFormatter> = [new JSONTextFormatter()];