mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
e7a4dbcefc
Summary: Add annotations to function parameters required for Flow's Local Type Inference project. This codemod prepares the codebase to match Flow's new typechecking algorithm. The new algorithm will make Flow more reliable and predicatable. Reviewed By: evanyeung Differential Revision: D37353648 fbshipit-source-id: e5a0c685ced85a8ff353d578b373f836b376bb28
604 lines
16 KiB
JavaScript
604 lines
16 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and 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';
|
|
|
|
import type {RenderItemProps} from '../Lists/VirtualizedList';
|
|
|
|
const ScrollView = require('../Components/ScrollView/ScrollView');
|
|
const TouchableHighlight = require('../Components/Touchable/TouchableHighlight');
|
|
const View = require('../Components/View/View');
|
|
const FlatList = require('../Lists/FlatList');
|
|
const XHRInterceptor = require('../Network/XHRInterceptor');
|
|
const StyleSheet = require('../StyleSheet/StyleSheet');
|
|
const Text = require('../Text/Text');
|
|
const WebSocketInterceptor = require('../WebSocket/WebSocketInterceptor');
|
|
const React = require('react');
|
|
|
|
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: {[string]: number} = {};
|
|
// 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,
|
|
}: RenderItemProps<NetworkRequestInfo>): 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: number) {
|
|
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;
|