(Server) Include Mock Component to Main Files

Summary:
- Add mock button if a client supports the function
- Open the dialog when clicking the button

Note:
- This is a part of this PR: https://github.com/facebook/flipper/pull/488

Reviewed By: mweststrate

Differential Revision: D20440145

fbshipit-source-id: 750099020e0b2d6ed10bb20e883f6b3be664ae79
This commit is contained in:
Chaiwat Ekkaewnumchai
2020-03-17 10:05:27 -07:00
committed by Facebook GitHub Bot
parent 84f36cd0ce
commit 1d23b5418a
2 changed files with 211 additions and 42 deletions

View File

@@ -184,7 +184,9 @@ export default class RequestDetails extends Component<
{response.headers.length > 0 ? (
<Panel
key={'responseheaders'}
heading={'Response Headers'}
heading={
response.isMock ? 'Response Body (Mock)' : 'Response Body'
}
floating={false}
padded={false}>
<HeaderInspector headers={response.headers} />

View File

@@ -7,7 +7,6 @@
* @format
*/
import {TableHighlightedRows, TableRows, TableBodyRow} from 'flipper';
import {padStart} from 'lodash';
import React, {createContext} from 'react';
import {MenuItemConstructorOptions} from 'electron';
@@ -15,6 +14,7 @@ import {MenuItemConstructorOptions} from 'electron';
import {
ContextMenu,
FlexColumn,
FlexRow,
Button,
Text,
Glyph,
@@ -24,6 +24,11 @@ import {
styled,
SearchableTable,
FlipperPlugin,
Sheet,
TableHighlightedRows,
TableRows,
TableBodyRow,
produce,
} from 'flipper';
import {Request, RequestId, Response, Route} from './types';
import {convertRequestToCurlCommand, getHeaderValue, decodeBody} from './utils';
@@ -31,6 +36,7 @@ import RequestDetails from './RequestDetails';
import {clipboard} from 'electron';
import {URL} from 'url';
import {DefaultKeyboardAction} from 'src/MenuBar';
import {MockResponseDialog} from './MockResponseDialog';
type PersistedState = {
requests: {[id: string]: Request};
@@ -40,6 +46,10 @@ type PersistedState = {
type State = {
selectedIds: Array<RequestId>;
searchTerm: string;
routes: {[id: string]: Route};
nextRouteId: number;
isMockResponseSupported: boolean;
showMockResponseDialog: boolean;
};
const COLUMN_SIZE = {
@@ -72,6 +82,12 @@ const COLUMNS = {
duration: {value: 'Duration'},
};
const mockingStyle = {
backgroundColor: colors.yellowTint,
color: colors.yellow,
fontWeight: 500,
};
export function formatBytes(count: number): string {
if (count > 1024 * 1024) {
return (count / (1024.0 * 1024)).toFixed(1) + ' MB';
@@ -112,6 +128,7 @@ export default class extends FlipperPlugin<State, any, PersistedState> {
requests: {},
responses: {},
};
networkRouteManager: NetworkRouteManager = nullNetworkRouteManager;
static metricsReducer(persistedState: PersistedState) {
const failures = Object.values(persistedState.responses).reduce(function(
@@ -176,13 +193,85 @@ export default class extends FlipperPlugin<State, any, PersistedState> {
);
}
constructor(props: any) {
super(props);
this.state = {
selectedIds: [],
searchTerm: '',
routes: {},
nextRouteId: 0,
isMockResponseSupported: false,
showMockResponseDialog: false,
};
}
init() {
this.client.supportsMethod('mockResponses').then(result =>
this.setState({
routes: {},
isMockResponseSupported: result,
showMockResponseDialog: false,
}),
);
this.informClientMockChange({});
this.setState(this.parseDeepLinkPayload(this.props.deepLinkPayload));
// declare new variable to be called inside the interface
const setState = this.setState.bind(this);
const informClientMockChange = this.informClientMockChange.bind(this);
this.networkRouteManager = {
addRoute() {
setState(
produce((draftState: State) => {
const nextRouteId = draftState.nextRouteId;
draftState.routes[nextRouteId.toString()] = {
requestUrl: '/',
requestMethod: 'GET',
responseData: '',
responseHeaders: {},
};
draftState.nextRouteId = nextRouteId + 1;
}),
);
},
modifyRoute(id: string, routeChange: Partial<Route>) {
setState(
produce((draftState: State) => {
if (!draftState.routes.hasOwnProperty(id)) {
return;
}
draftState.routes[id] = {...draftState.routes[id], ...routeChange};
informClientMockChange(draftState.routes);
}),
);
},
removeRoute(id: string) {
setState(
produce((draftState: State) => {
if (draftState.routes.hasOwnProperty(id)) {
delete draftState.routes[id];
}
informClientMockChange(draftState.routes);
}),
);
},
};
}
teardown() {
// Remove mock response inside client
this.informClientMockChange({});
}
onKeyboardAction = (action: string) => {
if (action === 'clear') {
this.clearLogs();
}
};
parseDeepLinkPayload = (deepLinkPayload: string | null) => {
parseDeepLinkPayload = (
deepLinkPayload: string | null,
): Pick<State, 'selectedIds' | 'searchTerm'> => {
const searchTermDelim = 'searchTerm=';
if (deepLinkPayload === null) {
return {
@@ -201,8 +290,6 @@ export default class extends FlipperPlugin<State, any, PersistedState> {
};
};
state = this.parseDeepLinkPayload(this.props.deepLinkPayload);
onRowHighlighted = (selectedIds: Array<RequestId>) =>
this.setState({selectedIds});
@@ -227,6 +314,52 @@ export default class extends FlipperPlugin<State, any, PersistedState> {
this.props.setPersistedState({responses: {}, requests: {}});
};
informClientMockChange = (routes: {[id: string]: Route}) => {
const existedIdSet: {[id: string]: {[method: string]: boolean}} = {};
const filteredRoutes: {[id: string]: Route} = Object.entries(routes).reduce(
(accRoutes, [id, route]) => {
if (existedIdSet.hasOwnProperty(route.requestUrl)) {
if (
existedIdSet[route.requestUrl].hasOwnProperty(route.requestMethod)
) {
return accRoutes;
}
existedIdSet[route.requestUrl] = {
...existedIdSet[route.requestUrl],
[route.requestMethod]: true,
};
return Object.assign({[id]: route}, accRoutes);
} else {
existedIdSet[route.requestUrl] = {
[route.requestMethod]: true,
};
return Object.assign({[id]: route}, accRoutes);
}
},
{},
);
if (this.state.isMockResponseSupported) {
const routesValuesArray = Object.values(filteredRoutes);
this.client.call('mockResponses', {
routes: routesValuesArray.map((route: Route) => ({
requestUrl: route.requestUrl,
method: route.requestMethod,
data: route.responseData,
headers: [...Object.values(route.responseHeaders)],
})),
});
}
};
onMockButtonPressed = () => {
this.setState({showMockResponseDialog: true});
};
onCloseButtonPressed = () => {
this.setState({showMockResponseDialog: false});
};
renderSidebar = () => {
const {requests, responses} = this.props.persistedState;
const {selectedIds} = this.state;
@@ -250,20 +383,30 @@ export default class extends FlipperPlugin<State, any, PersistedState> {
render() {
const {requests, responses} = this.props.persistedState;
const {
selectedIds,
searchTerm,
routes,
isMockResponseSupported,
showMockResponseDialog,
} = this.state;
return (
<FlexColumn grow={true}>
<NetworkRouteContext.Provider value={nullNetworkRouteManager}>
<NetworkRouteContext.Provider value={this.networkRouteManager}>
<NetworkTable
requests={requests || {}}
responses={responses || {}}
routes={routes}
onMockButtonPressed={this.onMockButtonPressed}
onCloseButtonPressed={this.onCloseButtonPressed}
showMockResponseDialog={showMockResponseDialog}
clear={this.clearLogs}
copyRequestCurlCommand={this.copyRequestCurlCommand}
onRowHighlighted={this.onRowHighlighted}
highlightedRows={
this.state.selectedIds ? new Set(this.state.selectedIds) : null
}
searchTerm={this.state.searchTerm}
highlightedRows={selectedIds ? new Set(selectedIds) : null}
searchTerm={searchTerm}
isMockResponseSupported={isMockResponseSupported}
/>
<DetailSidebar width={500}>{this.renderSidebar()}</DetailSidebar>
</NetworkRouteContext.Provider>
@@ -275,15 +418,21 @@ export default class extends FlipperPlugin<State, any, PersistedState> {
type NetworkTableProps = {
requests: {[id: string]: Request};
responses: {[id: string]: Response};
routes: {[id: string]: Route};
clear: () => void;
copyRequestCurlCommand: () => void;
onRowHighlighted: (keys: TableHighlightedRows) => void;
highlightedRows: Set<string> | null | undefined;
searchTerm: string;
onMockButtonPressed: () => void;
onCloseButtonPressed: () => void;
showMockResponseDialog: boolean;
isMockResponseSupported: boolean;
};
type NetworkTableState = {
sortedRows: TableRows;
routes: {[id: string]: Route};
};
function formatTimestamp(timestamp: number): string {
@@ -309,6 +458,7 @@ function buildRow(
const url = new URL(request.url);
const domain = url.host + url.pathname;
const friendlyName = getHeaderValue(request.headers, 'X-FB-Friendly-Name');
const style = response && response.isMock ? mockingStyle : undefined;
let copyText = `# HTTP request for ${domain} (ID: ${request.id})
## Request
@@ -386,6 +536,7 @@ ${response.headers
sortKey: request.timestamp,
copyText,
highlightOnHover: true,
style: style,
requestBody: requestData,
responseBody: responseData,
};
@@ -400,7 +551,6 @@ function calculateState(
rows: TableRows = [],
): NetworkTableState {
rows = [...rows];
if (Object.keys(nextProps.requests).length === 0) {
// cleared
rows = [];
@@ -440,6 +590,7 @@ function calculateState(
return {
sortedRows: rows,
routes: nextProps.routes,
};
}
@@ -450,13 +601,7 @@ class NetworkTable extends PureComponent<NetworkTableProps, NetworkTableState> {
constructor(props: NetworkTableProps) {
super(props);
this.state = calculateState(
{
requests: {},
responses: {},
},
props,
);
this.state = calculateState({requests: {}, responses: {}}, props);
}
UNSAFE_componentWillReceiveProps(nextProps: NetworkTableProps) {
@@ -498,6 +643,7 @@ class NetworkTable extends PureComponent<NetworkTableProps, NetworkTableState> {
render() {
return (
<>
<NetworkTable.ContextMenu
items={this.contextMenuItems()}
component={FlexColumn}>
@@ -517,11 +663,32 @@ class NetworkTable extends PureComponent<NetworkTableProps, NetworkTableState> {
allowRegexSearch={true}
allowBodySearch={true}
zebra={false}
actions={<Button onClick={this.props.clear}>Clear Table</Button>}
clearSearchTerm={this.props.searchTerm !== ''}
defaultSearchTerm={this.props.searchTerm}
actions={
<FlexRow>
<Button onClick={this.props.clear}>Clear Table</Button>
{this.props.isMockResponseSupported && (
<Button onClick={this.props.onMockButtonPressed}>Mock</Button>
)}
</FlexRow>
}
/>
</NetworkTable.ContextMenu>
{this.props.showMockResponseDialog ? (
<Sheet>
{onHide => (
<MockResponseDialog
routes={this.state.routes}
onHide={() => {
onHide();
this.props.onCloseButtonPressed();
}}
/>
)}
</Sheet>
) : null}
</>
);
}
}