Add button to copy highlighted requests to routes (mock requests) (#1447)

Summary:
Add feature to Network mocks in the Network Plugin which would allow a user to highlight network requests and copy into new routes (mocks).

For many production apps, network requests can contain many headers (easily 20 or more) and a large amount of data  returned in the response (1000's of characters).  Creating mocks for these manually is time consuming and error prone.

It would be better to make mocks automatically by allowing the user to highlight desired requests and have them automatically copied into new routes, also copying the headers and the response data.

Changelog: [network] Allow user to create new mock routes by highlighting existing network requests in the Network plugin

Pull Request resolved: https://github.com/facebook/flipper/pull/1447

Test Plan:
Tested this change manually by running through the following scenario using the sample Android app:

1). Run a GET request from the Sample app.  Verify that the request/response is correct.  Highlight the request to be copied.

![request-screen](https://user-images.githubusercontent.com/337874/89750945-baf6b700-da93-11ea-86f6-3ec600e1727d.png)

2). Go to the Mock dialog by clicking on the "Mock" button

![mock-screen](https://user-images.githubusercontent.com/337874/89750979-e8436500-da93-11ea-9dde-8717436a03bb.png)

3).  Click on "Copy Highlighted Call" to create a mock Route from the selected request.  Verify that the "data" and "headers" tab panels are correct.

![mock-screen-2](https://user-images.githubusercontent.com/337874/89751029-132db900-da94-11ea-9419-6294a304f232.png)

![mock-screen-3](https://user-images.githubusercontent.com/337874/89751053-29d41000-da94-11ea-85db-a034a20e5c18.png)

Close the Dialog

4).  Run the request again from the sample app and verify that a mock request is returned with the correct data.

![response-mock-1](https://user-images.githubusercontent.com/337874/89751083-4cfebf80-da94-11ea-8523-192ebdc869f6.png)

![response-mock-2](https://user-images.githubusercontent.com/337874/89751092-58ea8180-da94-11ea-85fe-82b7a660789f.png)

Reviewed By: cekkaewnumchai

Differential Revision: D23027793

Pulled By: mweststrate

fbshipit-source-id: 197fd5c3d120a20b6bc5d9121ae781923d69b748
This commit is contained in:
James Harmon
2020-09-18 07:24:34 -07:00
committed by Facebook GitHub Bot
parent ddc9c3e243
commit 0a06d6c546
3 changed files with 124 additions and 31 deletions

View File

@@ -20,13 +20,18 @@ import {
} from 'flipper';
import React, {useContext, useState, useMemo, useEffect} from 'react';
import {Route} from './types';
import {Route, Request, Response} from './types';
import {MockResponseDetails} from './MockResponseDetails';
import {NetworkRouteContext} from './index';
import {RequestId} from './types';
type Props = {routes: {[id: string]: Route}};
type Props = {
routes: {[id: string]: Route};
highlightedRows: Set<string> | null | undefined;
requests: {[id: string]: Request};
responses: {[id: string]: Response};
};
const ColumnSizes = {route: 'flex'};
@@ -35,7 +40,17 @@ const Columns = {route: {value: 'Route', resizable: false}};
const AddRouteButton = styled(FlexBox)({
color: colors.blackAlpha50,
alignItems: 'center',
padding: 10,
padding: 5,
flexShrink: 0,
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
});
const CopyHighlightedCallsButton = styled(FlexBox)({
color: colors.blueDark,
alignItems: 'center',
padding: 5,
flexShrink: 0,
whiteSpace: 'nowrap',
overflow: 'hidden',
@@ -119,12 +134,11 @@ function RouteRow(props: {
showWarning: boolean;
handleRemoveId: () => void;
}) {
const [showCloseButton, setShowCloseButton] = useState(false);
return (
<FlexRow
grow={true}
onMouseEnter={() => setShowCloseButton(true)}
onMouseLeave={() => setShowCloseButton(false)}>
<FlexRow grow={true}>
<FlexRow onClick={props.handleRemoveId}>
<Icon name="cross-circle" color={colors.red} />
</FlexRow>
<FlexRow grow={true}>
{props.showWarning && (
<Icon name="caution-triangle" color={colors.yellow} />
@@ -137,11 +151,6 @@ function RouteRow(props: {
<TextEllipsis>{props.text}</TextEllipsis>
)}
</FlexRow>
{showCloseButton && (
<FlexRow onClick={props.handleRemoveId}>
<Icon name="cross-circle" color={colors.red} />
</FlexRow>
)}
</FlexRow>
);
}
@@ -171,21 +180,20 @@ function ManagedMockResponseRightPanel(props: {
export function ManageMockResponsePanel(props: Props) {
const networkRouteManager = useContext(NetworkRouteContext);
const [selectedId, setSelectedId] = useState<RequestId | null>(null);
const [currentRouteSize, setCurrentRouteSize] = useState(0);
const {routes} = props;
useEffect(() => {
const keys = Object.keys(routes);
const routeSize = keys.length;
if (currentRouteSize === routeSize) {
return;
}
if (routeSize > 0 && routeSize > currentRouteSize) {
setSelectedId(keys[routeSize - 1]);
}
setCurrentRouteSize(routeSize);
}, [routes]);
const duplicatedIds = useMemo(() => _duplicateIds(routes), [routes]);
setSelectedId((selectedId) => {
const keys = Object.keys(props.routes);
return keys.length === 0
? null
: selectedId === null || !keys.includes(selectedId)
? keys[keys.length - 1]
: selectedId;
});
}, [props.routes]);
const duplicatedIds = useMemo(() => _duplicateIds(props.routes), [
props.routes,
]);
return (
<Container>
<LeftPanel>
@@ -201,12 +209,28 @@ export function ManageMockResponsePanel(props: Props) {
/>
&nbsp;Add Route
</AddRouteButton>
<CopyHighlightedCallsButton
onClick={() => {
networkRouteManager.copyHighlightedCalls(
props.highlightedRows as Set<string>,
props.requests,
props.responses,
);
}}>
<Glyph
name="plus-circle"
size={16}
variant="outline"
color={colors.blackAlpha30}
/>
&nbsp;Copy Highlighted Calls
</CopyHighlightedCallsButton>
<ManagedTable
hideHeader={true}
multiline={true}
columnSizes={ColumnSizes}
columns={Columns}
rows={_buildRows(routes, duplicatedIds, (id) => {
rows={_buildRows(props.routes, duplicatedIds, (id) => {
networkRouteManager.removeRoute(id);
setSelectedId(null);
})}
@@ -223,10 +247,10 @@ export function ManageMockResponsePanel(props: Props) {
/>
</LeftPanel>
<RightPanel>
{selectedId && routes.hasOwnProperty(selectedId) && (
{selectedId && props.routes.hasOwnProperty(selectedId) && (
<ManagedMockResponseRightPanel
id={selectedId}
route={routes[selectedId]}
route={props.routes[selectedId]}
isDuplicated={duplicatedIds.includes(selectedId)}
/>
)}

View File

@@ -10,12 +10,15 @@
import {FlexColumn, Button, styled} from 'flipper';
import {ManageMockResponsePanel} from './ManageMockResponsePanel';
import {Route} from './types';
import {Route, Request, Response} from './types';
import React from 'react';
type Props = {
routes: {[id: string]: Route};
onHide: () => void;
highlightedRows: Set<string> | null | undefined;
requests: {[id: string]: Request};
responses: {[id: string]: Response};
};
const Title = styled('div')({
@@ -39,7 +42,12 @@ export function MockResponseDialog(props: Props) {
return (
<Container>
<Title>Mock Network Responses</Title>
<ManageMockResponsePanel routes={props.routes} />
<ManageMockResponsePanel
routes={props.routes}
highlightedRows={props.highlightedRows}
requests={props.requests}
responses={props.responses}
/>
<Row>
<Button compact padded onClick={props.onHide}>
Close

View File

@@ -37,6 +37,7 @@ import {
Route,
ResponseFollowupChunk,
PersistedState,
Header,
} from './types';
import {convertRequestToCurlCommand, getHeaderValue, decodeBody} from './utils';
import RequestDetails from './RequestDetails';
@@ -61,6 +62,9 @@ type State = {
isMockResponseSupported: boolean;
showMockResponseDialog: boolean;
detailBodyFormat: string;
highlightedRows: Set<string> | null | undefined;
requests: {[id: string]: Request};
responses: {[id: string]: Response};
};
const COLUMN_SIZE = {
@@ -122,11 +126,21 @@ export interface NetworkRouteManager {
addRoute(): void;
modifyRoute(id: string, routeChange: Partial<Route>): void;
removeRoute(id: string): void;
copyHighlightedCalls(
highlightedRows: Set<string>,
requests: {[id: string]: Request},
responses: {[id: string]: Response},
): void;
}
const nullNetworkRouteManager: NetworkRouteManager = {
addRoute() {},
modifyRoute(_id: string, _routeChange: Partial<Route>) {},
removeRoute(_id: string) {},
copyHighlightedCalls(
_highlightedRows: Set<string>,
_requests: {[id: string]: Request},
_responses: {[id: string]: Response},
) {},
};
export const NetworkRouteContext = createContext<NetworkRouteManager>(
nullNetworkRouteManager,
@@ -328,6 +342,9 @@ export default class extends FlipperPlugin<State, any, PersistedState> {
isMockResponseSupported: false,
showMockResponseDialog: false,
detailBodyFormat: BodyOptions.parsed,
highlightedRows: new Set(),
requests: {},
responses: {},
};
}
@@ -387,6 +404,41 @@ export default class extends FlipperPlugin<State, any, PersistedState> {
}),
);
},
copyHighlightedCalls(
highlightedRows: Set<string> | null | undefined,
requests: {[id: string]: Request},
responses: {[id: string]: Response},
) {
setState((state) => {
const nextState = produce(state, (state: State) => {
// iterate through highlighted rows
highlightedRows?.forEach((row) => {
const response = responses[row];
// convert headers
const headers: {[id: string]: Header} = {};
response.headers.forEach((e) => {
headers[e.key] = e;
});
// convert data
const responseData =
response && response.data ? decodeBody(response) : null;
const nextRouteId = state.nextRouteId;
state.routes[nextRouteId.toString()] = {
requestUrl: requests[row].url,
requestMethod: requests[row].method,
responseData: responseData as string,
responseHeaders: headers,
responseStatus: responses[row].status.toString(),
};
state.nextRouteId = nextRouteId + 1;
});
});
informClientMockChange(nextState.routes);
return nextState;
});
},
};
}
@@ -573,6 +625,9 @@ type NetworkTableProps = {
type NetworkTableState = {
sortedRows: TableRows;
routes: {[id: string]: Route};
highlightedRows: Set<string> | null | undefined;
requests: {[id: string]: Request};
responses: {[id: string]: Response};
};
function formatTimestamp(timestamp: number): string {
@@ -748,6 +803,9 @@ function calculateState(
return {
sortedRows: rows,
routes: nextProps.routes,
highlightedRows: nextProps.highlightedRows,
requests: props.requests,
responses: props.responses,
};
}
@@ -841,6 +899,9 @@ class NetworkTable extends PureComponent<NetworkTableProps, NetworkTableState> {
onHide();
this.props.onCloseButtonPressed();
}}
highlightedRows={this.state.highlightedRows}
requests={this.state.requests}
responses={this.state.responses}
/>
)}
</Sheet>