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.  2). Go to the Mock dialog by clicking on the "Mock" button  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.   Close the Dialog 4). Run the request again from the sample app and verify that a mock request is returned with the correct data.   Reviewed By: cekkaewnumchai Differential Revision: D23027793 Pulled By: mweststrate fbshipit-source-id: 197fd5c3d120a20b6bc5d9121ae781923d69b748
This commit is contained in:
committed by
Facebook GitHub Bot
parent
ddc9c3e243
commit
0a06d6c546
@@ -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) {
|
||||
/>
|
||||
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}
|
||||
/>
|
||||
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)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user