123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601 |
- /**
- * Copyright (c) Facebook, Inc. and its affiliates.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
- *
- * @format
- * @flow
- */
- 'use strict';
- const FlatList = require('../Lists/FlatList');
- const React = require('react');
- const ScrollView = require('../Components/ScrollView/ScrollView');
- const StyleSheet = require('../StyleSheet/StyleSheet');
- const Text = require('../Text/Text');
- const TouchableHighlight = require('../Components/Touchable/TouchableHighlight');
- const View = require('../Components/View/View');
- const WebSocketInterceptor = require('../WebSocket/WebSocketInterceptor');
- const XHRInterceptor = require('../Network/XHRInterceptor');
- const LISTVIEW_CELL_HEIGHT = 15;
- // Global id for the intercepted XMLHttpRequest objects.
- let nextXHRId = 0;
- type NetworkRequestInfo = {
- id: number,
- type?: string,
- url?: string,
- method?: string,
- status?: number,
- dataSent?: any,
- responseContentType?: string,
- responseSize?: number,
- requestHeaders?: Object,
- responseHeaders?: string,
- response?: Object | string,
- responseURL?: string,
- responseType?: string,
- timeout?: number,
- closeReason?: string,
- messages?: string,
- serverClose?: Object,
- serverError?: Object,
- ...
- };
- type Props = $ReadOnly<{||}>;
- type State = {|
- detailRowId: ?number,
- requests: Array<NetworkRequestInfo>,
- |};
- function getStringByValue(value: any): string {
- if (value === undefined) {
- return 'undefined';
- }
- if (typeof value === 'object') {
- return JSON.stringify(value);
- }
- if (typeof value === 'string' && value.length > 500) {
- return String(value)
- .substr(0, 500)
- .concat('\n***TRUNCATED TO 500 CHARACTERS***');
- }
- return value;
- }
- function getTypeShortName(type: any): string {
- if (type === 'XMLHttpRequest') {
- return 'XHR';
- } else if (type === 'WebSocket') {
- return 'WS';
- }
- return '';
- }
- function keyExtractor(request: NetworkRequestInfo): string {
- return String(request.id);
- }
- /**
- * Show all the intercepted network requests over the InspectorPanel.
- */
- class NetworkOverlay extends React.Component<Props, State> {
- _requestsListView: ?React.ElementRef<typeof FlatList>;
- _detailScrollView: ?React.ElementRef<typeof ScrollView>;
- // Metrics are used to decide when if the request list should be sticky, and
- // scroll to the bottom as new network requests come in, or if the user has
- // intentionally scrolled away from the bottom - to instead flash the scroll bar
- // and keep the current position
- _requestsListViewScrollMetrics = {
- offset: 0,
- visibleLength: 0,
- contentLength: 0,
- };
- // Map of `socketId` -> `index in `this.state.requests`.
- _socketIdMap = {};
- // Map of `xhr._index` -> `index in `this.state.requests`.
- _xhrIdMap: {[key: number]: number, ...} = {};
- state: State = {
- detailRowId: null,
- requests: [],
- };
- _enableXHRInterception(): void {
- if (XHRInterceptor.isInterceptorEnabled()) {
- return;
- }
- // Show the XHR request item in listView as soon as it was opened.
- XHRInterceptor.setOpenCallback((method, url, xhr) => {
- // Generate a global id for each intercepted xhr object, add this id
- // to the xhr object as a private `_index` property to identify it,
- // so that we can distinguish different xhr objects in callbacks.
- xhr._index = nextXHRId++;
- const xhrIndex = this.state.requests.length;
- this._xhrIdMap[xhr._index] = xhrIndex;
- const _xhr: NetworkRequestInfo = {
- id: xhrIndex,
- type: 'XMLHttpRequest',
- method: method,
- url: url,
- };
- this.setState(
- {
- requests: this.state.requests.concat(_xhr),
- },
- this._indicateAdditionalRequests,
- );
- });
- XHRInterceptor.setRequestHeaderCallback((header, value, xhr) => {
- const xhrIndex = this._getRequestIndexByXHRID(xhr._index);
- if (xhrIndex === -1) {
- return;
- }
- this.setState(({requests}) => {
- const networkRequestInfo = requests[xhrIndex];
- if (!networkRequestInfo.requestHeaders) {
- networkRequestInfo.requestHeaders = {};
- }
- networkRequestInfo.requestHeaders[header] = value;
- return {requests};
- });
- });
- XHRInterceptor.setSendCallback((data, xhr) => {
- const xhrIndex = this._getRequestIndexByXHRID(xhr._index);
- if (xhrIndex === -1) {
- return;
- }
- this.setState(({requests}) => {
- const networkRequestInfo = requests[xhrIndex];
- networkRequestInfo.dataSent = data;
- return {requests};
- });
- });
- XHRInterceptor.setHeaderReceivedCallback(
- (type, size, responseHeaders, xhr) => {
- const xhrIndex = this._getRequestIndexByXHRID(xhr._index);
- if (xhrIndex === -1) {
- return;
- }
- this.setState(({requests}) => {
- const networkRequestInfo = requests[xhrIndex];
- networkRequestInfo.responseContentType = type;
- networkRequestInfo.responseSize = size;
- networkRequestInfo.responseHeaders = responseHeaders;
- return {requests};
- });
- },
- );
- XHRInterceptor.setResponseCallback(
- (status, timeout, response, responseURL, responseType, xhr) => {
- const xhrIndex = this._getRequestIndexByXHRID(xhr._index);
- if (xhrIndex === -1) {
- return;
- }
- this.setState(({requests}) => {
- const networkRequestInfo = requests[xhrIndex];
- networkRequestInfo.status = status;
- networkRequestInfo.timeout = timeout;
- networkRequestInfo.response = response;
- networkRequestInfo.responseURL = responseURL;
- networkRequestInfo.responseType = responseType;
- return {requests};
- });
- },
- );
- // Fire above callbacks.
- XHRInterceptor.enableInterception();
- }
- _enableWebSocketInterception(): void {
- if (WebSocketInterceptor.isInterceptorEnabled()) {
- return;
- }
- // Show the WebSocket request item in listView when 'connect' is called.
- WebSocketInterceptor.setConnectCallback(
- (url, protocols, options, socketId) => {
- const socketIndex = this.state.requests.length;
- this._socketIdMap[socketId] = socketIndex;
- const _webSocket: NetworkRequestInfo = {
- id: socketIndex,
- type: 'WebSocket',
- url: url,
- protocols: protocols,
- };
- this.setState(
- {
- requests: this.state.requests.concat(_webSocket),
- },
- this._indicateAdditionalRequests,
- );
- },
- );
- WebSocketInterceptor.setCloseCallback(
- (statusCode, closeReason, socketId) => {
- const socketIndex = this._socketIdMap[socketId];
- if (socketIndex === undefined) {
- return;
- }
- if (statusCode !== null && closeReason !== null) {
- this.setState(({requests}) => {
- const networkRequestInfo = requests[socketIndex];
- networkRequestInfo.status = statusCode;
- networkRequestInfo.closeReason = closeReason;
- return {requests};
- });
- }
- },
- );
- WebSocketInterceptor.setSendCallback((data, socketId) => {
- const socketIndex = this._socketIdMap[socketId];
- if (socketIndex === undefined) {
- return;
- }
- this.setState(({requests}) => {
- const networkRequestInfo = requests[socketIndex];
- if (!networkRequestInfo.messages) {
- networkRequestInfo.messages = '';
- }
- networkRequestInfo.messages += 'Sent: ' + JSON.stringify(data) + '\n';
- return {requests};
- });
- });
- WebSocketInterceptor.setOnMessageCallback((socketId, message) => {
- const socketIndex = this._socketIdMap[socketId];
- if (socketIndex === undefined) {
- return;
- }
- this.setState(({requests}) => {
- const networkRequestInfo = requests[socketIndex];
- if (!networkRequestInfo.messages) {
- networkRequestInfo.messages = '';
- }
- networkRequestInfo.messages +=
- 'Received: ' + JSON.stringify(message) + '\n';
- return {requests};
- });
- });
- WebSocketInterceptor.setOnCloseCallback((socketId, message) => {
- const socketIndex = this._socketIdMap[socketId];
- if (socketIndex === undefined) {
- return;
- }
- this.setState(({requests}) => {
- const networkRequestInfo = requests[socketIndex];
- networkRequestInfo.serverClose = message;
- return {requests};
- });
- });
- WebSocketInterceptor.setOnErrorCallback((socketId, message) => {
- const socketIndex = this._socketIdMap[socketId];
- if (socketIndex === undefined) {
- return;
- }
- this.setState(({requests}) => {
- const networkRequestInfo = requests[socketIndex];
- networkRequestInfo.serverError = message;
- return {requests};
- });
- });
- // Fire above callbacks.
- WebSocketInterceptor.enableInterception();
- }
- componentDidMount() {
- this._enableXHRInterception();
- this._enableWebSocketInterception();
- }
- componentWillUnmount() {
- XHRInterceptor.disableInterception();
- WebSocketInterceptor.disableInterception();
- }
- _renderItem = ({item, index}): React.Element<any> => {
- const tableRowViewStyle = [
- styles.tableRow,
- index % 2 === 1 ? styles.tableRowOdd : styles.tableRowEven,
- index === this.state.detailRowId && styles.tableRowPressed,
- ];
- const urlCellViewStyle = styles.urlCellView;
- const methodCellViewStyle = styles.methodCellView;
- return (
- <TouchableHighlight
- onPress={() => {
- this._pressRow(index);
- }}>
- <View>
- <View style={tableRowViewStyle}>
- <View style={urlCellViewStyle}>
- <Text style={styles.cellText} numberOfLines={1}>
- {item.url}
- </Text>
- </View>
- <View style={methodCellViewStyle}>
- <Text style={styles.cellText} numberOfLines={1}>
- {getTypeShortName(item.type)}
- </Text>
- </View>
- </View>
- </View>
- </TouchableHighlight>
- );
- };
- _renderItemDetail(id) {
- const requestItem = this.state.requests[id];
- const details = Object.keys(requestItem).map(key => {
- if (key === 'id') {
- return;
- }
- return (
- <View style={styles.detailViewRow} key={key}>
- <Text style={[styles.detailViewText, styles.detailKeyCellView]}>
- {key}
- </Text>
- <Text style={[styles.detailViewText, styles.detailValueCellView]}>
- {getStringByValue(requestItem[key])}
- </Text>
- </View>
- );
- });
- return (
- <View>
- <TouchableHighlight
- style={styles.closeButton}
- onPress={this._closeButtonClicked}>
- <View>
- <Text style={styles.closeButtonText}>v</Text>
- </View>
- </TouchableHighlight>
- <ScrollView
- style={styles.detailScrollView}
- ref={scrollRef => (this._detailScrollView = scrollRef)}>
- {details}
- </ScrollView>
- </View>
- );
- }
- _indicateAdditionalRequests = (): void => {
- if (this._requestsListView) {
- const distanceFromEndThreshold = LISTVIEW_CELL_HEIGHT * 2;
- const {
- offset,
- visibleLength,
- contentLength,
- } = this._requestsListViewScrollMetrics;
- const distanceFromEnd = contentLength - visibleLength - offset;
- const isCloseToEnd = distanceFromEnd <= distanceFromEndThreshold;
- if (isCloseToEnd) {
- this._requestsListView.scrollToEnd();
- } else {
- this._requestsListView.flashScrollIndicators();
- }
- }
- };
- _captureRequestsListView = (listRef: ?FlatList<NetworkRequestInfo>): void => {
- this._requestsListView = listRef;
- };
- _requestsListViewOnScroll = (e: Object): void => {
- this._requestsListViewScrollMetrics.offset = e.nativeEvent.contentOffset.y;
- this._requestsListViewScrollMetrics.visibleLength =
- e.nativeEvent.layoutMeasurement.height;
- this._requestsListViewScrollMetrics.contentLength =
- e.nativeEvent.contentSize.height;
- };
- /**
- * Popup a scrollView to dynamically show detailed information of
- * the request, when pressing a row in the network flow listView.
- */
- _pressRow(rowId: number): void {
- this.setState({detailRowId: rowId}, this._scrollDetailToTop);
- }
- _scrollDetailToTop = (): void => {
- if (this._detailScrollView) {
- this._detailScrollView.scrollTo({
- y: 0,
- animated: false,
- });
- }
- };
- _closeButtonClicked = () => {
- this.setState({detailRowId: null});
- };
- _getRequestIndexByXHRID(index: number): number {
- if (index === undefined) {
- return -1;
- }
- const xhrIndex = this._xhrIdMap[index];
- if (xhrIndex === undefined) {
- return -1;
- } else {
- return xhrIndex;
- }
- }
- render(): React.Node {
- const {requests, detailRowId} = this.state;
- return (
- <View style={styles.container}>
- {detailRowId != null && this._renderItemDetail(detailRowId)}
- <View style={styles.listViewTitle}>
- {requests.length > 0 && (
- <View style={styles.tableRow}>
- <View style={styles.urlTitleCellView}>
- <Text style={styles.cellText} numberOfLines={1}>
- URL
- </Text>
- </View>
- <View style={styles.methodTitleCellView}>
- <Text style={styles.cellText} numberOfLines={1}>
- Type
- </Text>
- </View>
- </View>
- )}
- </View>
- <FlatList
- ref={this._captureRequestsListView}
- onScroll={this._requestsListViewOnScroll}
- style={styles.listView}
- data={requests}
- renderItem={this._renderItem}
- keyExtractor={keyExtractor}
- extraData={this.state}
- />
- </View>
- );
- }
- }
- const styles = StyleSheet.create({
- container: {
- paddingTop: 10,
- paddingBottom: 10,
- paddingLeft: 5,
- paddingRight: 5,
- },
- listViewTitle: {
- height: 20,
- },
- listView: {
- flex: 1,
- height: 60,
- },
- tableRow: {
- flexDirection: 'row',
- flex: 1,
- height: LISTVIEW_CELL_HEIGHT,
- },
- tableRowEven: {
- backgroundColor: '#555',
- },
- tableRowOdd: {
- backgroundColor: '#000',
- },
- tableRowPressed: {
- backgroundColor: '#3B5998',
- },
- cellText: {
- color: 'white',
- fontSize: 12,
- },
- methodTitleCellView: {
- height: 18,
- borderColor: '#DCD7CD',
- borderTopWidth: 1,
- borderBottomWidth: 1,
- borderRightWidth: 1,
- alignItems: 'center',
- justifyContent: 'center',
- backgroundColor: '#444',
- flex: 1,
- },
- urlTitleCellView: {
- height: 18,
- borderColor: '#DCD7CD',
- borderTopWidth: 1,
- borderBottomWidth: 1,
- borderLeftWidth: 1,
- borderRightWidth: 1,
- justifyContent: 'center',
- backgroundColor: '#444',
- flex: 5,
- paddingLeft: 3,
- },
- methodCellView: {
- height: 15,
- borderColor: '#DCD7CD',
- borderRightWidth: 1,
- alignItems: 'center',
- justifyContent: 'center',
- flex: 1,
- },
- urlCellView: {
- height: 15,
- borderColor: '#DCD7CD',
- borderLeftWidth: 1,
- borderRightWidth: 1,
- justifyContent: 'center',
- flex: 5,
- paddingLeft: 3,
- },
- detailScrollView: {
- flex: 1,
- height: 180,
- marginTop: 5,
- marginBottom: 5,
- },
- detailKeyCellView: {
- flex: 1.3,
- },
- detailValueCellView: {
- flex: 2,
- },
- detailViewRow: {
- flexDirection: 'row',
- paddingHorizontal: 3,
- },
- detailViewText: {
- color: 'white',
- fontSize: 11,
- },
- closeButtonText: {
- color: 'white',
- fontSize: 10,
- },
- closeButton: {
- marginTop: 5,
- backgroundColor: '#888',
- justifyContent: 'center',
- alignItems: 'center',
- },
- });
- module.exports = NetworkOverlay;
|