diff --git a/desktop/plugins/network/RequestDetails.tsx b/desktop/plugins/network/RequestDetails.tsx index 2284f7521..28107e3ef 100644 --- a/desktop/plugins/network/RequestDetails.tsx +++ b/desktop/plugins/network/RequestDetails.tsx @@ -184,7 +184,9 @@ export default class RequestDetails extends Component< {response.headers.length > 0 ? ( diff --git a/desktop/plugins/network/index.tsx b/desktop/plugins/network/index.tsx index 8526924b5..9ae67b509 100644 --- a/desktop/plugins/network/index.tsx +++ b/desktop/plugins/network/index.tsx @@ -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; 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 { 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 { ); } + 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) { + 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 => { const searchTermDelim = 'searchTerm='; if (deepLinkPayload === null) { return { @@ -201,8 +290,6 @@ export default class extends FlipperPlugin { }; }; - state = this.parseDeepLinkPayload(this.props.deepLinkPayload); - onRowHighlighted = (selectedIds: Array) => this.setState({selectedIds}); @@ -227,6 +314,52 @@ export default class extends FlipperPlugin { 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 { render() { const {requests, responses} = this.props.persistedState; + const { + selectedIds, + searchTerm, + routes, + isMockResponseSupported, + showMockResponseDialog, + } = this.state; return ( - + {this.renderSidebar()} @@ -275,15 +418,21 @@ export default class extends FlipperPlugin { type NetworkTableProps = { requests: {[id: string]: Request}; responses: {[id: string]: Response}; + routes: {[id: string]: Route}; clear: () => void; copyRequestCurlCommand: () => void; onRowHighlighted: (keys: TableHighlightedRows) => void; highlightedRows: Set | 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 { constructor(props: NetworkTableProps) { super(props); - this.state = calculateState( - { - requests: {}, - responses: {}, - }, - props, - ); + this.state = calculateState({requests: {}, responses: {}}, props); } UNSAFE_componentWillReceiveProps(nextProps: NetworkTableProps) { @@ -498,30 +643,52 @@ class NetworkTable extends PureComponent { render() { return ( - - Clear Table} - clearSearchTerm={this.props.searchTerm !== ''} - defaultSearchTerm={this.props.searchTerm} - /> - + <> + + + + {this.props.isMockResponseSupported && ( + + )} + + } + /> + + {this.props.showMockResponseDialog ? ( + + {onHide => ( + { + onHide(); + this.props.onCloseButtonPressed(); + }} + /> + )} + + ) : null} + ); } }