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
261 lines
6.4 KiB
TypeScript
261 lines
6.4 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 {
|
|
ManagedTable,
|
|
Text,
|
|
FlexBox,
|
|
FlexRow,
|
|
FlexColumn,
|
|
Glyph,
|
|
styled,
|
|
colors,
|
|
Panel,
|
|
} from 'flipper';
|
|
import React, {useContext, useState, useMemo, useEffect} from 'react';
|
|
|
|
import {Route, Request, Response} from './types';
|
|
|
|
import {MockResponseDetails} from './MockResponseDetails';
|
|
import {NetworkRouteContext} from './index';
|
|
import {RequestId} from './types';
|
|
|
|
type Props = {
|
|
routes: {[id: string]: Route};
|
|
highlightedRows: Set<string> | null | undefined;
|
|
requests: {[id: string]: Request};
|
|
responses: {[id: string]: Response};
|
|
};
|
|
|
|
const ColumnSizes = {route: 'flex'};
|
|
|
|
const Columns = {route: {value: 'Route', resizable: false}};
|
|
|
|
const AddRouteButton = styled(FlexBox)({
|
|
color: colors.blackAlpha50,
|
|
alignItems: 'center',
|
|
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',
|
|
textOverflow: 'ellipsis',
|
|
});
|
|
|
|
const Container = styled(FlexRow)({
|
|
flex: 1,
|
|
justifyContent: 'space-around',
|
|
alignItems: 'stretch',
|
|
});
|
|
|
|
const LeftPanel = styled(FlexColumn)({
|
|
flex: 1,
|
|
});
|
|
|
|
const RightPanel = styled(FlexColumn)({
|
|
flex: 3,
|
|
height: '100%',
|
|
});
|
|
|
|
const TextEllipsis = styled(Text)({
|
|
overflowX: 'hidden',
|
|
textOverflow: 'ellipsis',
|
|
maxWidth: '100%',
|
|
lineHeight: '18px',
|
|
paddingTop: 4,
|
|
display: 'block',
|
|
whiteSpace: 'nowrap',
|
|
});
|
|
|
|
const Icon = styled(Glyph)({
|
|
marginTop: 5,
|
|
marginRight: 8,
|
|
});
|
|
|
|
// return ids that have the same pair of requestUrl and method; this will return only the duplicate
|
|
function _duplicateIds(routes: {[id: string]: Route}): Array<RequestId> {
|
|
const idSet: {[id: string]: {[method: string]: boolean}} = {};
|
|
return Object.entries(routes).reduce((acc: Array<RequestId>, [id, route]) => {
|
|
if (idSet.hasOwnProperty(route.requestUrl)) {
|
|
if (idSet[route.requestUrl].hasOwnProperty(route.requestMethod)) {
|
|
return acc.concat(id);
|
|
}
|
|
idSet[route.requestUrl] = {
|
|
...idSet[route.requestUrl],
|
|
[route.requestMethod]: true,
|
|
};
|
|
return acc;
|
|
} else {
|
|
idSet[route.requestUrl] = {[route.requestMethod]: true};
|
|
return acc;
|
|
}
|
|
}, []);
|
|
}
|
|
|
|
function _buildRows(
|
|
routes: {[id: string]: Route},
|
|
duplicatedIds: Array<string>,
|
|
handleRemoveId: (id: string) => void,
|
|
) {
|
|
return Object.entries(routes).map(([id, route]) => ({
|
|
columns: {
|
|
route: {
|
|
value: (
|
|
<RouteRow
|
|
key={id}
|
|
text={route.requestUrl}
|
|
showWarning={duplicatedIds.includes(id)}
|
|
handleRemoveId={() => handleRemoveId(id)}
|
|
/>
|
|
),
|
|
},
|
|
},
|
|
key: id,
|
|
}));
|
|
}
|
|
|
|
function RouteRow(props: {
|
|
text: string;
|
|
showWarning: boolean;
|
|
handleRemoveId: () => void;
|
|
}) {
|
|
return (
|
|
<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} />
|
|
)}
|
|
{props.text.length === 0 ? (
|
|
<TextEllipsis style={{color: colors.blackAlpha50}}>
|
|
untitled
|
|
</TextEllipsis>
|
|
) : (
|
|
<TextEllipsis>{props.text}</TextEllipsis>
|
|
)}
|
|
</FlexRow>
|
|
</FlexRow>
|
|
);
|
|
}
|
|
|
|
function ManagedMockResponseRightPanel(props: {
|
|
id: string;
|
|
route: Route;
|
|
isDuplicated: boolean;
|
|
}) {
|
|
const {id, route, isDuplicated} = props;
|
|
return (
|
|
<Panel
|
|
grow={true}
|
|
collapsable={false}
|
|
floating={false}
|
|
heading={'Route Info'}>
|
|
<MockResponseDetails
|
|
key={id}
|
|
id={id}
|
|
route={route}
|
|
isDuplicated={isDuplicated}
|
|
/>
|
|
</Panel>
|
|
);
|
|
}
|
|
|
|
export function ManageMockResponsePanel(props: Props) {
|
|
const networkRouteManager = useContext(NetworkRouteContext);
|
|
const [selectedId, setSelectedId] = useState<RequestId | null>(null);
|
|
|
|
useEffect(() => {
|
|
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>
|
|
<AddRouteButton
|
|
onClick={() => {
|
|
networkRouteManager.addRoute();
|
|
}}>
|
|
<Glyph
|
|
name="plus-circle"
|
|
size={16}
|
|
variant="outline"
|
|
color={colors.blackAlpha30}
|
|
/>
|
|
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(props.routes, duplicatedIds, (id) => {
|
|
networkRouteManager.removeRoute(id);
|
|
setSelectedId(null);
|
|
})}
|
|
stickyBottom={true}
|
|
autoHeight={false}
|
|
floating={false}
|
|
zebra={false}
|
|
onRowHighlighted={(selectedIds) => {
|
|
const newSelectedId =
|
|
selectedIds.length === 1 ? selectedIds[0] : null;
|
|
setSelectedId(newSelectedId);
|
|
}}
|
|
highlightedRows={new Set(selectedId)}
|
|
/>
|
|
</LeftPanel>
|
|
<RightPanel>
|
|
{selectedId && props.routes.hasOwnProperty(selectedId) && (
|
|
<ManagedMockResponseRightPanel
|
|
id={selectedId}
|
|
route={props.routes[selectedId]}
|
|
isDuplicated={duplicatedIds.includes(selectedId)}
|
|
/>
|
|
)}
|
|
</RightPanel>
|
|
</Container>
|
|
);
|
|
}
|