(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:
committed by
Facebook GitHub Bot
parent
84f36cd0ce
commit
1d23b5418a
@@ -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} />
|
||||||
|
|||||||
@@ -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,6 +643,7 @@ class NetworkTable extends PureComponent<NetworkTableProps, NetworkTableState> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<NetworkTable.ContextMenu
|
<NetworkTable.ContextMenu
|
||||||
items={this.contextMenuItems()}
|
items={this.contextMenuItems()}
|
||||||
component={FlexColumn}>
|
component={FlexColumn}>
|
||||||
@@ -517,11 +663,32 @@ class NetworkTable extends PureComponent<NetworkTableProps, NetworkTableState> {
|
|||||||
allowRegexSearch={true}
|
allowRegexSearch={true}
|
||||||
allowBodySearch={true}
|
allowBodySearch={true}
|
||||||
zebra={false}
|
zebra={false}
|
||||||
actions={<Button onClick={this.props.clear}>Clear Table</Button>}
|
|
||||||
clearSearchTerm={this.props.searchTerm !== ''}
|
clearSearchTerm={this.props.searchTerm !== ''}
|
||||||
defaultSearchTerm={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>
|
</NetworkTable.ContextMenu>
|
||||||
|
{this.props.showMockResponseDialog ? (
|
||||||
|
<Sheet>
|
||||||
|
{onHide => (
|
||||||
|
<MockResponseDialog
|
||||||
|
routes={this.state.routes}
|
||||||
|
onHide={() => {
|
||||||
|
onHide();
|
||||||
|
this.props.onCloseButtonPressed();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Sheet>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user