Network Plugin - New functions to import, export and clear Routes (#1855)
Summary: In the network plugin, add features to import and export routes as described in issue https://github.com/facebook/flipper/issues/1651 Primary use case is that external testers (such as QA teams) would be able to create test data, convert it to mocks and save the mocks to make bug fixes easier for devs. Here is a screenshot showing location of buttons to perform import/export (and clearing) of mock routes:  Here is another screenshot showing export dialog:  Changelog: [Network] Mock routes can now be imported and exported. Thanks bizzguy! Pull Request resolved: https://github.com/facebook/flipper/pull/1855 Test Plan: Performed manual testing - create new mocks - export mocks - clear mocks - import mocks - verify that mocks still work by making GET/POST requests in sample app Performed various permutations of above manual tests, including restarting Flipper at various points to ensure that test plan still worked. Also performed visual inspection of exported files to verify correctness. Would be very interested in learning how to create automated tests for this functionality. Reviewed By: passy Differential Revision: D26072928 Pulled By: mweststrate fbshipit-source-id: 51bd5e19e78d830b94add850d5dc9b9e45fa6fad
This commit is contained in:
committed by
Facebook GitHub Bot
parent
14997a5b98
commit
6df117ba04
@@ -38,7 +38,7 @@ const ColumnSizes = {route: 'flex'};
|
|||||||
|
|
||||||
const Columns = {route: {value: 'Route', resizable: false}};
|
const Columns = {route: {value: 'Route', resizable: false}};
|
||||||
|
|
||||||
const AddRouteButton = styled(FlexBox)({
|
const Button = styled(FlexBox)({
|
||||||
color: colors.blackAlpha50,
|
color: colors.blackAlpha50,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
padding: 5,
|
padding: 5,
|
||||||
@@ -48,16 +48,6 @@ const AddRouteButton = styled(FlexBox)({
|
|||||||
textOverflow: 'ellipsis',
|
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)({
|
const Container = styled(FlexRow)({
|
||||||
flex: 1,
|
flex: 1,
|
||||||
justifyContent: 'space-around',
|
justifyContent: 'space-around',
|
||||||
@@ -199,9 +189,9 @@ export function ManageMockResponsePanel(props: Props) {
|
|||||||
props.routes,
|
props.routes,
|
||||||
]);
|
]);
|
||||||
return (
|
return (
|
||||||
<Container style={{height: 580}}>
|
<Container style={{height: 560}}>
|
||||||
<LeftPanel>
|
<LeftPanel>
|
||||||
<AddRouteButton
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
networkRouteManager.addRoute();
|
networkRouteManager.addRoute();
|
||||||
}}>
|
}}>
|
||||||
@@ -212,8 +202,8 @@ export function ManageMockResponsePanel(props: Props) {
|
|||||||
color={colors.blackAlpha30}
|
color={colors.blackAlpha30}
|
||||||
/>
|
/>
|
||||||
Add Route
|
Add Route
|
||||||
</AddRouteButton>
|
</Button>
|
||||||
<CopyHighlightedCallsButton
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
networkRouteManager.copyHighlightedCalls(
|
networkRouteManager.copyHighlightedCalls(
|
||||||
props.highlightedRows as Set<string>,
|
props.highlightedRows as Set<string>,
|
||||||
@@ -228,7 +218,7 @@ export function ManageMockResponsePanel(props: Props) {
|
|||||||
color={colors.blackAlpha30}
|
color={colors.blackAlpha30}
|
||||||
/>
|
/>
|
||||||
Copy Highlighted Calls
|
Copy Highlighted Calls
|
||||||
</CopyHighlightedCallsButton>
|
</Button>
|
||||||
<hr
|
<hr
|
||||||
style={{
|
style={{
|
||||||
height: 1,
|
height: 1,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
FlexRow,
|
FlexRow,
|
||||||
FlexColumn,
|
FlexColumn,
|
||||||
FlexBox,
|
FlexBox,
|
||||||
|
Layout,
|
||||||
Input,
|
Input,
|
||||||
Text,
|
Text,
|
||||||
Tabs,
|
Tabs,
|
||||||
@@ -42,7 +43,7 @@ const StyledSelectContainer = styled(FlexRow)({
|
|||||||
|
|
||||||
const StyledSelect = styled(Select)({
|
const StyledSelect = styled(Select)({
|
||||||
height: '100%',
|
height: '100%',
|
||||||
maxWidth: 200,
|
maxWidth: 400,
|
||||||
});
|
});
|
||||||
|
|
||||||
const StyledText = styled(Text)({
|
const StyledText = styled(Text)({
|
||||||
@@ -53,7 +54,7 @@ const StyledText = styled(Text)({
|
|||||||
const textAreaStyle: React.CSSProperties = {
|
const textAreaStyle: React.CSSProperties = {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
marginTop: 8,
|
marginTop: 8,
|
||||||
height: 430,
|
height: 400,
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
color: '#333',
|
color: '#333',
|
||||||
padding: 10,
|
padding: 10,
|
||||||
@@ -114,21 +115,13 @@ const AddHeaderButton = styled(FlexBox)({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const HeadersColumnSizes = {
|
const HeadersColumnSizes = {
|
||||||
name: '40%',
|
close: '4%',
|
||||||
value: '40%',
|
warning: '4%',
|
||||||
close: '10%',
|
name: '35%',
|
||||||
warning: 'flex',
|
value: 'flex',
|
||||||
};
|
};
|
||||||
|
|
||||||
const HeadersColumns = {
|
const HeadersColumns = {
|
||||||
name: {
|
|
||||||
value: 'Name',
|
|
||||||
resizable: false,
|
|
||||||
},
|
|
||||||
value: {
|
|
||||||
value: 'Value',
|
|
||||||
resizable: false,
|
|
||||||
},
|
|
||||||
close: {
|
close: {
|
||||||
value: '',
|
value: '',
|
||||||
resizable: false,
|
resizable: false,
|
||||||
@@ -137,6 +130,14 @@ const HeadersColumns = {
|
|||||||
value: '',
|
value: '',
|
||||||
resizable: false,
|
resizable: false,
|
||||||
},
|
},
|
||||||
|
name: {
|
||||||
|
value: 'Name',
|
||||||
|
resizable: false,
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
value: 'Value',
|
||||||
|
resizable: false,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectedHighlight = {backgroundColor: colors.highlight};
|
const selectedHighlight = {backgroundColor: colors.highlight};
|
||||||
@@ -315,7 +316,7 @@ export function MockResponseDetails({id, route, isDuplicated}: Props) {
|
|||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab key={'headers'} label={'Headers'}>
|
<Tab key={'headers'} label={'Headers'}>
|
||||||
<FlexColumn>
|
<Layout.Container style={{width: '100%'}}>
|
||||||
<AddHeaderButton
|
<AddHeaderButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newHeaders = {
|
const newHeaders = {
|
||||||
@@ -335,25 +336,27 @@ export function MockResponseDetails({id, route, isDuplicated}: Props) {
|
|||||||
/>
|
/>
|
||||||
Add Header
|
Add Header
|
||||||
</AddHeaderButton>
|
</AddHeaderButton>
|
||||||
<ManagedTable
|
<Layout.ScrollContainer style={{width: '100%'}}>
|
||||||
hideHeader={true}
|
<ManagedTable
|
||||||
multiline={true}
|
hideHeader={true}
|
||||||
columnSizes={HeadersColumnSizes}
|
multiline={true}
|
||||||
columns={HeadersColumns}
|
columnSizes={HeadersColumnSizes}
|
||||||
rows={_buildMockResponseHeaderRows(
|
columns={HeadersColumns}
|
||||||
id,
|
rows={_buildMockResponseHeaderRows(
|
||||||
route,
|
id,
|
||||||
selectedHeaderIds.length === 1 ? selectedHeaderIds[0] : null,
|
route,
|
||||||
networkRouteManager,
|
selectedHeaderIds.length === 1 ? selectedHeaderIds[0] : null,
|
||||||
)}
|
networkRouteManager,
|
||||||
stickyBottom={true}
|
)}
|
||||||
autoHeight={true}
|
stickyBottom={true}
|
||||||
floating={false}
|
autoHeight={true}
|
||||||
zebra={false}
|
floating={false}
|
||||||
onRowHighlighted={setSelectedHeaderIds}
|
zebra={false}
|
||||||
highlightedRows={new Set(selectedHeaderIds)}
|
onRowHighlighted={setSelectedHeaderIds}
|
||||||
/>
|
highlightedRows={new Set(selectedHeaderIds)}
|
||||||
</FlexColumn>
|
/>
|
||||||
|
</Layout.ScrollContainer>
|
||||||
|
</Layout.Container>
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
@@ -7,12 +7,15 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {FlexColumn, Button, styled, Layout} from 'flipper';
|
import {FlexColumn, Button, styled, Layout, Spacer} from 'flipper';
|
||||||
|
|
||||||
import {ManageMockResponsePanel} from './ManageMockResponsePanel';
|
import {ManageMockResponsePanel} from './ManageMockResponsePanel';
|
||||||
import {Route, Request, Response} from './types';
|
import {Route, Request, Response} from './types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import {NetworkRouteContext} from './index';
|
||||||
|
import {useContext} from 'react';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
routes: {[id: string]: Route};
|
routes: {[id: string]: Route};
|
||||||
onHide: () => void;
|
onHide: () => void;
|
||||||
@@ -39,6 +42,7 @@ const Row = styled(FlexColumn)({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export function MockResponseDialog(props: Props) {
|
export function MockResponseDialog(props: Props) {
|
||||||
|
const networkRouteManager = useContext(NetworkRouteContext);
|
||||||
return (
|
return (
|
||||||
<Layout.Container pad width={1200}>
|
<Layout.Container pad width={1200}>
|
||||||
<Title>Mock Network Responses</Title>
|
<Title>Mock Network Responses</Title>
|
||||||
@@ -50,7 +54,32 @@ export function MockResponseDialog(props: Props) {
|
|||||||
responses={props.responses}
|
responses={props.responses}
|
||||||
/>
|
/>
|
||||||
</Layout.Horizontal>
|
</Layout.Horizontal>
|
||||||
<Layout.Horizontal>
|
<Layout.Horizontal gap>
|
||||||
|
<Button
|
||||||
|
compact
|
||||||
|
padded
|
||||||
|
onClick={() => {
|
||||||
|
networkRouteManager.importRoutes();
|
||||||
|
}}>
|
||||||
|
Import
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
compact
|
||||||
|
padded
|
||||||
|
onClick={() => {
|
||||||
|
networkRouteManager.exportRoutes();
|
||||||
|
}}>
|
||||||
|
Export
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
compact
|
||||||
|
padded
|
||||||
|
onClick={() => {
|
||||||
|
networkRouteManager.clearRoutes();
|
||||||
|
}}>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
<Spacer />
|
||||||
<Button compact padded onClick={props.onHide}>
|
<Button compact padded onClick={props.onHide}>
|
||||||
Close
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
import {padStart} from 'lodash';
|
import {padStart} from 'lodash';
|
||||||
import React, {createContext} from 'react';
|
import React, {createContext} from 'react';
|
||||||
import {MenuItemConstructorOptions} from 'electron';
|
import {MenuItemConstructorOptions} from 'electron';
|
||||||
|
import {message} from 'antd';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
@@ -45,6 +46,9 @@ import {URL} from 'url';
|
|||||||
import {MockResponseDialog} from './MockResponseDialog';
|
import {MockResponseDialog} from './MockResponseDialog';
|
||||||
import {combineBase64Chunks} from './chunks';
|
import {combineBase64Chunks} from './chunks';
|
||||||
import {PluginClient, createState, usePlugin, useValue} from 'flipper-plugin';
|
import {PluginClient, createState, usePlugin, useValue} from 'flipper-plugin';
|
||||||
|
import {remote, OpenDialogOptions} from 'electron';
|
||||||
|
import fs from 'fs';
|
||||||
|
import electron from 'electron';
|
||||||
|
|
||||||
const LOCALSTORAGE_MOCK_ROUTE_LIST_KEY = '__NETWORK_CACHED_MOCK_ROUTE_LIST';
|
const LOCALSTORAGE_MOCK_ROUTE_LIST_KEY = '__NETWORK_CACHED_MOCK_ROUTE_LIST';
|
||||||
|
|
||||||
@@ -127,6 +131,9 @@ export interface NetworkRouteManager {
|
|||||||
requests: {[id: string]: Request},
|
requests: {[id: string]: Request},
|
||||||
responses: {[id: string]: Response},
|
responses: {[id: string]: Response},
|
||||||
): void;
|
): void;
|
||||||
|
importRoutes(): void;
|
||||||
|
exportRoutes(): void;
|
||||||
|
clearRoutes(): void;
|
||||||
}
|
}
|
||||||
const nullNetworkRouteManager: NetworkRouteManager = {
|
const nullNetworkRouteManager: NetworkRouteManager = {
|
||||||
addRoute() {},
|
addRoute() {},
|
||||||
@@ -137,6 +144,9 @@ const nullNetworkRouteManager: NetworkRouteManager = {
|
|||||||
_requests: {[id: string]: Request},
|
_requests: {[id: string]: Request},
|
||||||
_responses: {[id: string]: Response},
|
_responses: {[id: string]: Response},
|
||||||
) {},
|
) {},
|
||||||
|
importRoutes() {},
|
||||||
|
exportRoutes() {},
|
||||||
|
clearRoutes() {},
|
||||||
};
|
};
|
||||||
export const NetworkRouteContext = createContext<NetworkRouteManager>(
|
export const NetworkRouteContext = createContext<NetworkRouteManager>(
|
||||||
nullNetworkRouteManager,
|
nullNetworkRouteManager,
|
||||||
@@ -385,6 +395,73 @@ export function plugin(client: PluginClient<Events, Methods>) {
|
|||||||
|
|
||||||
informClientMockChange(routes.get());
|
informClientMockChange(routes.get());
|
||||||
},
|
},
|
||||||
|
importRoutes() {
|
||||||
|
const options: OpenDialogOptions = {
|
||||||
|
properties: ['openFile'],
|
||||||
|
filters: [{extensions: ['json'], name: 'Flipper Route Files'}],
|
||||||
|
};
|
||||||
|
remote.dialog.showOpenDialog(options).then((result) => {
|
||||||
|
const filePaths = result.filePaths;
|
||||||
|
if (filePaths.length > 0) {
|
||||||
|
fs.readFile(filePaths[0], 'utf8', (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
message.error('Unable to import file');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const importedRoutes = JSON.parse(data);
|
||||||
|
importedRoutes?.forEach((importedRoute: Route) => {
|
||||||
|
if (importedRoute != null) {
|
||||||
|
const newNextRouteId = nextRouteId.get();
|
||||||
|
routes.update((draft) => {
|
||||||
|
draft[newNextRouteId.toString()] = {
|
||||||
|
requestUrl: importedRoute.requestUrl,
|
||||||
|
requestMethod: importedRoute.requestMethod,
|
||||||
|
responseData: importedRoute.responseData as string,
|
||||||
|
responseHeaders: importedRoute.responseHeaders,
|
||||||
|
responseStatus: importedRoute.responseStatus,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
nextRouteId.set(newNextRouteId + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
informClientMockChange(routes.get());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
exportRoutes() {
|
||||||
|
remote.dialog
|
||||||
|
.showSaveDialog(
|
||||||
|
// @ts-ignore This appears to work but isn't allowed by the types
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
title: 'Export Routes',
|
||||||
|
defaultPath: 'NetworkPluginRoutesExport.json',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.then((result: electron.SaveDialogReturnValue) => {
|
||||||
|
const file = result.filePath;
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fs.writeFile(
|
||||||
|
file,
|
||||||
|
JSON.stringify(routes.get(), null, 2),
|
||||||
|
'utf8',
|
||||||
|
(err) => {
|
||||||
|
if (err) {
|
||||||
|
message.error('Failed to store mock routes: ' + err);
|
||||||
|
} else {
|
||||||
|
message.info('Successfully exported mock routes');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
clearRoutes() {
|
||||||
|
routes.set({});
|
||||||
|
informClientMockChange(routes.get());
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user