(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 ? ( {response.headers.length > 0 ? (
<Panel <Panel
key={'responseheaders'} key={'responseheaders'}
heading={'Response Headers'} heading={
response.isMock ? 'Response Body (Mock)' : 'Response Body'
}
floating={false} floating={false}
padded={false}> padded={false}>
<HeaderInspector headers={response.headers} /> <HeaderInspector headers={response.headers} />

View File

@@ -7,7 +7,6 @@
* @format * @format
*/ */
import {TableHighlightedRows, TableRows, TableBodyRow} from 'flipper';
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';
@@ -15,6 +14,7 @@ import {MenuItemConstructorOptions} from 'electron';
import { import {
ContextMenu, ContextMenu,
FlexColumn, FlexColumn,
FlexRow,
Button, Button,
Text, Text,
Glyph, Glyph,
@@ -24,6 +24,11 @@ import {
styled, styled,
SearchableTable, SearchableTable,
FlipperPlugin, FlipperPlugin,
Sheet,
TableHighlightedRows,
TableRows,
TableBodyRow,
produce,
} from 'flipper'; } from 'flipper';
import {Request, RequestId, Response, Route} from './types'; import {Request, RequestId, Response, Route} from './types';
import {convertRequestToCurlCommand, getHeaderValue, decodeBody} from './utils'; import {convertRequestToCurlCommand, getHeaderValue, decodeBody} from './utils';
@@ -31,6 +36,7 @@ import RequestDetails from './RequestDetails';
import {clipboard} from 'electron'; import {clipboard} from 'electron';
import {URL} from 'url'; import {URL} from 'url';
import {DefaultKeyboardAction} from 'src/MenuBar'; import {DefaultKeyboardAction} from 'src/MenuBar';
import {MockResponseDialog} from './MockResponseDialog';
type PersistedState = { type PersistedState = {
requests: {[id: string]: Request}; requests: {[id: string]: Request};
@@ -40,6 +46,10 @@ type PersistedState = {
type State = { type State = {
selectedIds: Array<RequestId>; selectedIds: Array<RequestId>;
searchTerm: string; searchTerm: string;
routes: {[id: string]: Route};
nextRouteId: number;
isMockResponseSupported: boolean;
showMockResponseDialog: boolean;
}; };
const COLUMN_SIZE = { const COLUMN_SIZE = {
@@ -72,6 +82,12 @@ const COLUMNS = {
duration: {value: 'Duration'}, duration: {value: 'Duration'},
}; };
const mockingStyle = {
backgroundColor: colors.yellowTint,
color: colors.yellow,
fontWeight: 500,
};
export function formatBytes(count: number): string { export function formatBytes(count: number): string {
if (count > 1024 * 1024) { if (count > 1024 * 1024) {
return (count / (1024.0 * 1024)).toFixed(1) + ' MB'; return (count / (1024.0 * 1024)).toFixed(1) + ' MB';
@@ -112,6 +128,7 @@ export default class extends FlipperPlugin<State, any, PersistedState> {
requests: {}, requests: {},
responses: {}, responses: {},
}; };
networkRouteManager: NetworkRouteManager = nullNetworkRouteManager;
static metricsReducer(persistedState: PersistedState) { static metricsReducer(persistedState: PersistedState) {
const failures = Object.values(persistedState.responses).reduce(function( 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) => { onKeyboardAction = (action: string) => {
if (action === 'clear') { if (action === 'clear') {
this.clearLogs(); this.clearLogs();
} }
}; };
parseDeepLinkPayload = (deepLinkPayload: string | null) => { parseDeepLinkPayload = (
deepLinkPayload: string | null,
): Pick<State, 'selectedIds' | 'searchTerm'> => {
const searchTermDelim = 'searchTerm='; const searchTermDelim = 'searchTerm=';
if (deepLinkPayload === null) { if (deepLinkPayload === null) {
return { return {
@@ -201,8 +290,6 @@ export default class extends FlipperPlugin<State, any, PersistedState> {
}; };
}; };
state = this.parseDeepLinkPayload(this.props.deepLinkPayload);
onRowHighlighted = (selectedIds: Array<RequestId>) => onRowHighlighted = (selectedIds: Array<RequestId>) =>
this.setState({selectedIds}); this.setState({selectedIds});
@@ -227,6 +314,52 @@ export default class extends FlipperPlugin<State, any, PersistedState> {
this.props.setPersistedState({responses: {}, requests: {}}); 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 = () => { renderSidebar = () => {
const {requests, responses} = this.props.persistedState; const {requests, responses} = this.props.persistedState;
const {selectedIds} = this.state; const {selectedIds} = this.state;
@@ -250,20 +383,30 @@ export default class extends FlipperPlugin<State, any, PersistedState> {
render() { render() {
const {requests, responses} = this.props.persistedState; const {requests, responses} = this.props.persistedState;
const {
selectedIds,
searchTerm,
routes,
isMockResponseSupported,
showMockResponseDialog,
} = this.state;
return ( return (
<FlexColumn grow={true}> <FlexColumn grow={true}>
<NetworkRouteContext.Provider value={nullNetworkRouteManager}> <NetworkRouteContext.Provider value={this.networkRouteManager}>
<NetworkTable <NetworkTable
requests={requests || {}} requests={requests || {}}
responses={responses || {}} responses={responses || {}}
routes={routes}
onMockButtonPressed={this.onMockButtonPressed}
onCloseButtonPressed={this.onCloseButtonPressed}
showMockResponseDialog={showMockResponseDialog}
clear={this.clearLogs} clear={this.clearLogs}
copyRequestCurlCommand={this.copyRequestCurlCommand} copyRequestCurlCommand={this.copyRequestCurlCommand}
onRowHighlighted={this.onRowHighlighted} onRowHighlighted={this.onRowHighlighted}
highlightedRows={ highlightedRows={selectedIds ? new Set(selectedIds) : null}
this.state.selectedIds ? new Set(this.state.selectedIds) : null searchTerm={searchTerm}
} isMockResponseSupported={isMockResponseSupported}
searchTerm={this.state.searchTerm}
/> />
<DetailSidebar width={500}>{this.renderSidebar()}</DetailSidebar> <DetailSidebar width={500}>{this.renderSidebar()}</DetailSidebar>
</NetworkRouteContext.Provider> </NetworkRouteContext.Provider>
@@ -275,15 +418,21 @@ export default class extends FlipperPlugin<State, any, PersistedState> {
type NetworkTableProps = { type NetworkTableProps = {
requests: {[id: string]: Request}; requests: {[id: string]: Request};
responses: {[id: string]: Response}; responses: {[id: string]: Response};
routes: {[id: string]: Route};
clear: () => void; clear: () => void;
copyRequestCurlCommand: () => void; copyRequestCurlCommand: () => void;
onRowHighlighted: (keys: TableHighlightedRows) => void; onRowHighlighted: (keys: TableHighlightedRows) => void;
highlightedRows: Set<string> | null | undefined; highlightedRows: Set<string> | null | undefined;
searchTerm: string; searchTerm: string;
onMockButtonPressed: () => void;
onCloseButtonPressed: () => void;
showMockResponseDialog: boolean;
isMockResponseSupported: boolean;
}; };
type NetworkTableState = { type NetworkTableState = {
sortedRows: TableRows; sortedRows: TableRows;
routes: {[id: string]: Route};
}; };
function formatTimestamp(timestamp: number): string { function formatTimestamp(timestamp: number): string {
@@ -309,6 +458,7 @@ function buildRow(
const url = new URL(request.url); const url = new URL(request.url);
const domain = url.host + url.pathname; const domain = url.host + url.pathname;
const friendlyName = getHeaderValue(request.headers, 'X-FB-Friendly-Name'); 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}) let copyText = `# HTTP request for ${domain} (ID: ${request.id})
## Request ## Request
@@ -386,6 +536,7 @@ ${response.headers
sortKey: request.timestamp, sortKey: request.timestamp,
copyText, copyText,
highlightOnHover: true, highlightOnHover: true,
style: style,
requestBody: requestData, requestBody: requestData,
responseBody: responseData, responseBody: responseData,
}; };
@@ -400,7 +551,6 @@ function calculateState(
rows: TableRows = [], rows: TableRows = [],
): NetworkTableState { ): NetworkTableState {
rows = [...rows]; rows = [...rows];
if (Object.keys(nextProps.requests).length === 0) { if (Object.keys(nextProps.requests).length === 0) {
// cleared // cleared
rows = []; rows = [];
@@ -440,6 +590,7 @@ function calculateState(
return { return {
sortedRows: rows, sortedRows: rows,
routes: nextProps.routes,
}; };
} }
@@ -450,13 +601,7 @@ class NetworkTable extends PureComponent<NetworkTableProps, NetworkTableState> {
constructor(props: NetworkTableProps) { constructor(props: NetworkTableProps) {
super(props); super(props);
this.state = calculateState( this.state = calculateState({requests: {}, responses: {}}, props);
{
requests: {},
responses: {},
},
props,
);
} }
UNSAFE_componentWillReceiveProps(nextProps: NetworkTableProps) { UNSAFE_componentWillReceiveProps(nextProps: NetworkTableProps) {
@@ -498,30 +643,52 @@ class NetworkTable extends PureComponent<NetworkTableProps, NetworkTableState> {
render() { render() {
return ( return (
<NetworkTable.ContextMenu <>
items={this.contextMenuItems()} <NetworkTable.ContextMenu
component={FlexColumn}> items={this.contextMenuItems()}
<SearchableTable component={FlexColumn}>
virtual={true} <SearchableTable
multiline={false} virtual={true}
multiHighlight={true} multiline={false}
stickyBottom={true} multiHighlight={true}
floating={false} stickyBottom={true}
columnSizes={COLUMN_SIZE} floating={false}
columns={COLUMNS} columnSizes={COLUMN_SIZE}
columnOrder={COLUMN_ORDER} columns={COLUMNS}
rows={this.state.sortedRows} columnOrder={COLUMN_ORDER}
onRowHighlighted={this.props.onRowHighlighted} rows={this.state.sortedRows}
highlightedRows={this.props.highlightedRows} onRowHighlighted={this.props.onRowHighlighted}
rowLineHeight={26} highlightedRows={this.props.highlightedRows}
allowRegexSearch={true} rowLineHeight={26}
allowBodySearch={true} allowRegexSearch={true}
zebra={false} allowBodySearch={true}
actions={<Button onClick={this.props.clear}>Clear Table</Button>} zebra={false}
clearSearchTerm={this.props.searchTerm !== ''} clearSearchTerm={this.props.searchTerm !== ''}
defaultSearchTerm={this.props.searchTerm} defaultSearchTerm={this.props.searchTerm}
/> actions={
</NetworkTable.ContextMenu> <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}
</>
); );
} }
} }