Convert Flipper plugin "Network" to TypeScript

Summary: _typescript_

Reviewed By: danielbuechele

Differential Revision: D17155509

fbshipit-source-id: 45ae3e2de8cd7b3cdf7271905ef7c318d4289391
This commit is contained in:
Chaiwat Ekkaewnumchai
2019-09-05 02:46:27 -07:00
committed by Facebook Github Bot
parent 0a53cccb40
commit 705ba8eaa8
4 changed files with 214 additions and 184 deletions

View File

@@ -5,13 +5,7 @@
* @format * @format
*/ */
import type { import {Request, Response, Header, Insights, RetryInsights} from './types';
Request,
Response,
Header,
Insights,
RetryInsights,
} from './types.tsx';
import { import {
Component, Component,
@@ -24,11 +18,11 @@ import {
styled, styled,
colors, colors,
} from 'flipper'; } from 'flipper';
import {decodeBody, getHeaderValue} from './utils.tsx'; import {decodeBody, getHeaderValue} from './utils';
import {formatBytes} from './index.js'; import {formatBytes} from './index';
import React from 'react';
import querystring from 'querystring'; import querystring from 'querystring';
// $FlowFixMe
import xmlBeautifier from 'xml-beautifier'; import xmlBeautifier from 'xml-beautifier';
const WrappingText = styled(Text)({ const WrappingText = styled(Text)({
@@ -55,17 +49,17 @@ const KeyValueColumns = {
}; };
type RequestDetailsProps = { type RequestDetailsProps = {
request: Request, request: Request;
response: ?Response, response: Response | null | undefined;
}; };
type RequestDetailsState = { type RequestDetailsState = {
bodyFormat: string, bodyFormat: string;
}; };
export default class RequestDetails extends Component< export default class RequestDetails extends Component<
RequestDetailsProps, RequestDetailsProps,
RequestDetailsState, RequestDetailsState
> { > {
static Container = styled(FlexColumn)({ static Container = styled(FlexColumn)({
height: '100%', height: '100%',
@@ -219,21 +213,21 @@ class QueryInspector extends Component<{queryParams: URLSearchParams}> {
render() { render() {
const {queryParams} = this.props; const {queryParams} = this.props;
const rows = []; const rows: any = [];
for (const kv of queryParams.entries()) { queryParams.forEach((value: string, key: string) => {
rows.push({ rows.push({
columns: { columns: {
key: { key: {
value: <WrappingText>{kv[0]}</WrappingText>, value: <WrappingText>{key}</WrappingText>,
}, },
value: { value: {
value: <WrappingText>{kv[1]}</WrappingText>, value: <WrappingText>{value}</WrappingText>,
}, },
}, },
copyText: kv[1], copyText: value,
key: kv[0], key: key,
});
}); });
}
return rows.length > 0 ? ( return rows.length > 0 ? (
<ManagedTable <ManagedTable
@@ -250,37 +244,40 @@ class QueryInspector extends Component<{queryParams: URLSearchParams}> {
} }
type HeaderInspectorProps = { type HeaderInspectorProps = {
headers: Array<Header>, headers: Array<Header>;
}; };
type HeaderInspectorState = { type HeaderInspectorState = {
computedHeaders: Object, computedHeaders: Object;
}; };
class HeaderInspector extends Component< class HeaderInspector extends Component<
HeaderInspectorProps, HeaderInspectorProps,
HeaderInspectorState, HeaderInspectorState
> { > {
render() { render() {
const computedHeaders = this.props.headers.reduce((sum, header) => { const computedHeaders: Map<string, string> = this.props.headers.reduce(
return {...sum, [header.key]: header.value}; (sum, header) => {
}, {}); return sum.set(header.key, header.value);
},
new Map(),
);
const rows = []; const rows: any = [];
for (const key in computedHeaders) { computedHeaders.forEach((value: string, key: string) => {
rows.push({ rows.push({
columns: { columns: {
key: { key: {
value: <WrappingText>{key}</WrappingText>, value: <WrappingText>{key}</WrappingText>,
}, },
value: { value: {
value: <WrappingText>{computedHeaders[key]}</WrappingText>, value: <WrappingText>{value}</WrappingText>,
}, },
}, },
copyText: computedHeaders[key], copyText: value,
key, key,
}); });
} });
return rows.length > 0 ? ( return rows.length > 0 ? (
<ManagedTable <ManagedTable
@@ -302,13 +299,13 @@ const BodyContainer = styled('div')({
}); });
type BodyFormatter = { type BodyFormatter = {
formatRequest?: (request: Request) => any, formatRequest?: (request: Request) => any;
formatResponse?: (request: Request, response: Response) => any, formatResponse?: (request: Request, response: Response) => any;
}; };
class RequestBodyInspector extends Component<{ class RequestBodyInspector extends Component<{
request: Request, request: Request;
formattedText: boolean, formattedText: boolean;
}> { }> {
render() { render() {
const {request, formattedText} = this.props; const {request, formattedText} = this.props;
@@ -337,9 +334,9 @@ class RequestBodyInspector extends Component<{
} }
class ResponseBodyInspector extends Component<{ class ResponseBodyInspector extends Component<{
response: Response, response: Response;
request: Request, request: Request;
formattedText: boolean, formattedText: boolean;
}> { }> {
render() { render() {
const {request, response, formattedText} = this.props; const {request, response, formattedText} = this.props;
@@ -374,12 +371,12 @@ const MediaContainer = styled(FlexColumn)({
}); });
type ImageWithSizeProps = { type ImageWithSizeProps = {
src: string, src: string;
}; };
type ImageWithSizeState = { type ImageWithSizeState = {
width: number, width: number;
height: number, height: number;
}; };
class ImageWithSize extends Component<ImageWithSizeProps, ImageWithSizeState> { class ImageWithSize extends Component<ImageWithSizeProps, ImageWithSizeState> {
@@ -394,7 +391,7 @@ class ImageWithSize extends Component<ImageWithSizeProps, ImageWithSizeState> {
fontSize: 14, fontSize: 14,
}); });
constructor(props, context) { constructor(props: ImageWithSizeProps, context: any) {
super(props, context); super(props, context);
this.state = { this.state = {
width: 0, width: 0,
@@ -595,7 +592,7 @@ class LogEventFormatter {
formatRequest = (request: Request) => { formatRequest = (request: Request) => {
if (request.url.indexOf('logging_client_event') > 0) { if (request.url.indexOf('logging_client_event') > 0) {
const data = querystring.parse(decodeBody(request)); const data = querystring.parse(decodeBody(request));
if (data.message) { if (typeof data.message === 'string') {
data.message = JSON.parse(data.message); data.message = JSON.parse(data.message);
} }
return <ManagedDataInspector expandRoot={true} data={data} />; return <ManagedDataInspector expandRoot={true} data={data} />;
@@ -607,7 +604,7 @@ class GraphQLBatchFormatter {
formatRequest = (request: Request) => { formatRequest = (request: Request) => {
if (request.url.indexOf('graphqlbatch') > 0) { if (request.url.indexOf('graphqlbatch') > 0) {
const data = querystring.parse(decodeBody(request)); const data = querystring.parse(decodeBody(request));
if (data.queries) { if (typeof data.queries === 'string') {
data.queries = JSON.parse(data.queries); data.queries = JSON.parse(data.queries);
} }
return <ManagedDataInspector expandRoot={true} data={data} />; return <ManagedDataInspector expandRoot={true} data={data} />;
@@ -643,10 +640,10 @@ class GraphQLFormatter {
formatRequest = (request: Request) => { formatRequest = (request: Request) => {
if (request.url.indexOf('graphql') > 0) { if (request.url.indexOf('graphql') > 0) {
const data = querystring.parse(decodeBody(request)); const data = querystring.parse(decodeBody(request));
if (data.variables) { if (typeof data.variables === 'string') {
data.variables = JSON.parse(data.variables); data.variables = JSON.parse(data.variables);
} }
if (data.query_params) { if (typeof data.query_params === 'string') {
data.query_params = JSON.parse(data.query_params); data.query_params = JSON.parse(data.query_params);
} }
return <ManagedDataInspector expandRoot={true} data={data} />; return <ManagedDataInspector expandRoot={true} data={data} />;
@@ -745,7 +742,11 @@ class InsightsInspector extends Component<{insights: Insights}> {
} ${timesWord} out of ${retry.limit})`; } ${timesWord} out of ${retry.limit})`;
} }
buildRow<T>(name: string, value: ?T, formatter: T => string): any { buildRow<T>(
name: string,
value: T | null | undefined,
formatter: (value: T) => string,
): any {
return value return value
? { ? {
columns: { columns: {

View File

@@ -5,13 +5,10 @@
* @format * @format
*/ */
import type { import {TableHighlightedRows, TableRows, TableBodyRow} from 'flipper';
TableHighlightedRows,
TableRows,
TableBodyRow,
MetricType,
} from 'flipper';
import {padStart} from 'lodash'; import {padStart} from 'lodash';
import React from 'react';
import {MenuItemConstructorOptions} from 'electron';
import { import {
ContextMenu, ContextMenu,
@@ -26,25 +23,21 @@ import {
SearchableTable, SearchableTable,
FlipperPlugin, FlipperPlugin,
} from 'flipper'; } from 'flipper';
import type {Request, RequestId, Response} from './types.tsx'; import {Request, RequestId, Response} from './types';
import { import {convertRequestToCurlCommand, getHeaderValue, decodeBody} from './utils';
convertRequestToCurlCommand, import RequestDetails from './RequestDetails';
getHeaderValue,
decodeBody,
} from './utils.tsx';
import RequestDetails from './RequestDetails.js';
import {clipboard} from 'electron'; import {clipboard} from 'electron';
import {URL} from 'url'; import {URL} from 'url';
import type {Notification} from '../../plugin.tsx'; import {DefaultKeyboardAction} from 'src/MenuBar';
type PersistedState = {| type PersistedState = {
requests: {[id: RequestId]: Request}, requests: Map<RequestId, Request>;
responses: {[id: RequestId]: Response}, responses: Map<RequestId, Response>;
|}; };
type State = {| type State = {
selectedIds: Array<RequestId>, selectedIds: Array<RequestId>;
|}; };
const COLUMN_SIZE = { const COLUMN_SIZE = {
requestTimestamp: 100, requestTimestamp: 100,
@@ -67,27 +60,13 @@ const COLUMN_ORDER = [
]; ];
const COLUMNS = { const COLUMNS = {
requestTimestamp: { requestTimestamp: {value: 'Request Time'},
value: 'Request Time', responseTimestamp: {value: 'Response Time'},
}, domain: {value: 'Domain'},
responseTimestamp: { method: {value: 'Method'},
value: 'Response Time', status: {value: 'Status'},
}, size: {value: 'Size'},
domain: { duration: {value: 'Duration'},
value: 'Domain',
},
method: {
value: 'Method',
},
status: {
value: 'Status',
},
size: {
value: 'Size',
},
duration: {
value: 'Duration',
},
}; };
export function formatBytes(count: number): string { export function formatBytes(count: number): string {
@@ -108,66 +87,75 @@ const TextEllipsis = styled(Text)({
paddingTop: 4, paddingTop: 4,
}); });
export default class extends FlipperPlugin<State, *, PersistedState> { export default class extends FlipperPlugin<State, any, PersistedState> {
static keyboardActions = ['clear']; static keyboardActions: Array<DefaultKeyboardAction> = ['clear'];
static subscribed = []; static subscribed = [];
static defaultPersistedState = { static defaultPersistedState = {
requests: {}, requests: new Map(),
responses: {}, responses: new Map(),
}; };
static metricsReducer = ( static metricsReducer(persistedState: PersistedState) {
persistedState: PersistedState, const failures = Object.values(persistedState.responses).reduce(function(
): Promise<MetricType> => {
const failures = Object.keys(persistedState.responses).reduce(function(
previous, previous,
key, values,
) { ) {
return previous + (persistedState.responses[key].status >= 400); return previous + (values.status >= 400 ? 1 : 0);
}, },
0); 0);
return Promise.resolve({NUMBER_NETWORK_FAILURES: failures}); return Promise.resolve({NUMBER_NETWORK_FAILURES: failures});
}; }
static persistedStateReducer = ( static persistedStateReducer(
persistedState: PersistedState, persistedState: PersistedState,
method: string, method: string,
data: Request | Response, data: Request | Response,
): PersistedState => { ) {
const dataType: 'requests' | 'responses' = data.url switch (method) {
? 'requests' case 'newRequest':
: 'responses'; return Object.assign({}, persistedState, {
return { requests: new Map(persistedState.requests).set(
...persistedState, data.id,
[dataType]: { data as Request,
...persistedState[dataType], ),
[data.id]: data, });
}, case 'newResponse':
}; return Object.assign({}, persistedState, {
}; responses: new Map(persistedState.responses).set(
data.id,
data as Response,
),
});
default:
return persistedState;
}
}
static getActiveNotifications = ( static getActiveNotifications(persistedState: PersistedState) {
persistedState: PersistedState, const responses = persistedState
): Array<Notification> => { ? persistedState.responses || new Map()
const responses = persistedState ? persistedState.responses || [] : []; : new Map();
const r: Array<Response> = Object.values(responses); const r: Array<Response> = Array.from(responses.values());
return ( return (
r r
// Show error messages for all status codes indicating a client or server error // Show error messages for all status codes indicating a client or server error
.filter((response: Response) => response.status >= 400) .filter((response: Response) => response.status >= 400)
.map((response: Response) => ({ .map((response: Response) => {
const request = persistedState.requests.get(response.id);
const url: string = (request && request.url) || '(URL missing)';
return {
id: response.id, id: response.id,
title: `HTTP ${response.status}: Network request failed`, title: `HTTP ${response.status}: Network request failed`,
message: `Request to "${persistedState.requests[response.id]?.url || message: `Request to ${url} failed. ${response.reason}`,
'(URL missing)'}" failed. ${response.reason}`, severity: 'error' as 'error',
severity: 'error',
timestamp: response.timestamp, timestamp: response.timestamp,
category: `HTTP${response.status}`, category: `HTTP${response.status}`,
action: response.id, action: response.id,
}))
);
}; };
})
);
}
onKeyboardAction = (action: string) => { onKeyboardAction = (action: string) => {
if (action === 'clear') { if (action === 'clear') {
@@ -190,14 +178,17 @@ export default class extends FlipperPlugin<State, *, PersistedState> {
return; return;
} }
const request = requests[selectedIds[0]]; const request = requests.get(selectedIds[0]);
if (!request) {
return;
}
const command = convertRequestToCurlCommand(request); const command = convertRequestToCurlCommand(request);
clipboard.writeText(command); clipboard.writeText(command);
}; };
clearLogs = () => { clearLogs = () => {
this.setState({selectedIds: []}); this.setState({selectedIds: []});
this.props.setPersistedState({responses: {}, requests: {}}); this.props.setPersistedState({responses: new Map(), requests: new Map()});
}; };
renderSidebar = () => { renderSidebar = () => {
@@ -205,13 +196,20 @@ export default class extends FlipperPlugin<State, *, PersistedState> {
const {selectedIds} = this.state; const {selectedIds} = this.state;
const selectedId = selectedIds.length === 1 ? selectedIds[0] : null; const selectedId = selectedIds.length === 1 ? selectedIds[0] : null;
return selectedId != null ? ( if (!selectedId) {
return null;
}
const requestWithId = requests.get(selectedId);
if (!requestWithId) {
return null;
}
return (
<RequestDetails <RequestDetails
key={selectedId} key={selectedId}
request={requests[selectedId]} request={requestWithId}
response={responses[selectedId]} response={responses.get(selectedId)}
/> />
) : null; );
}; };
render() { render() {
@@ -220,8 +218,8 @@ export default class extends FlipperPlugin<State, *, PersistedState> {
return ( return (
<FlexColumn grow={true}> <FlexColumn grow={true}>
<NetworkTable <NetworkTable
requests={requests || {}} requests={requests || new Map()}
responses={responses || {}} responses={responses || new Map()}
clear={this.clearLogs} clear={this.clearLogs}
copyRequestCurlCommand={this.copyRequestCurlCommand} copyRequestCurlCommand={this.copyRequestCurlCommand}
onRowHighlighted={this.onRowHighlighted} onRowHighlighted={this.onRowHighlighted}
@@ -236,17 +234,17 @@ export default class extends FlipperPlugin<State, *, PersistedState> {
} }
type NetworkTableProps = { type NetworkTableProps = {
requests: {[id: RequestId]: Request}, requests: Map<RequestId, Request>;
responses: {[id: RequestId]: Response}, responses: Map<RequestId, Response>;
clear: () => void, clear: () => void;
copyRequestCurlCommand: () => void, copyRequestCurlCommand: () => void;
onRowHighlighted: (keys: TableHighlightedRows) => void, onRowHighlighted: (keys: TableHighlightedRows) => void;
highlightedRows: ?Set<string>, highlightedRows: Set<string> | null | undefined;
}; };
type NetworkTableState = {| type NetworkTableState = {
sortedRows: TableRows, sortedRows: TableRows;
|}; };
function formatTimestamp(timestamp: number): string { function formatTimestamp(timestamp: number): string {
const date = new Date(timestamp); const date = new Date(timestamp);
@@ -261,9 +259,12 @@ function formatTimestamp(timestamp: number): string {
)}`; )}`;
} }
function buildRow(request: Request, response: ?Response): ?TableBodyRow { function buildRow(
request: Request,
response: Response | null | undefined,
): TableBodyRow | null | undefined {
if (request == null) { if (request == null) {
return; return null;
} }
const url = new URL(request.url); const url = new URL(request.url);
const domain = url.host + url.pathname; const domain = url.host + url.pathname;
@@ -273,7 +274,10 @@ function buildRow(request: Request, response: ?Response): ?TableBodyRow {
## Request ## Request
HTTP ${request.method} ${request.url} HTTP ${request.method} ${request.url}
${request.headers ${request.headers
.map(({key, value}) => `${key}: ${String(value)}`) .map(
({key, value}: {key: string; value: string}): string =>
`${key}: ${String(value)}`,
)
.join('\n')}`; .join('\n')}`;
if (request.data) { if (request.data) {
@@ -286,7 +290,10 @@ ${request.headers
## Response ## Response
HTTP ${response.status} ${response.reason} HTTP ${response.status} ${response.reason}
${response.headers ${response.headers
.map(({key, value}) => `${key}: ${String(value)}`) .map(
({key, value}: {key: string; value: string}): string =>
`${key}: ${String(value)}`,
)
.join('\n')}`; .join('\n')}`;
} }
@@ -341,50 +348,49 @@ ${response.headers
function calculateState( function calculateState(
props: { props: {
requests: {[id: RequestId]: Request}, requests: Map<RequestId, Request>;
responses: {[id: RequestId]: Response}, responses: Map<RequestId, Response>;
}, },
nextProps: NetworkTableProps, nextProps: NetworkTableProps,
rows: TableRows = [], rows: TableRows = [],
): NetworkTableState { ): NetworkTableState {
rows = [...rows]; rows = [...rows];
if (Object.keys(nextProps.requests).length === 0) { // if (nextProps.requests.size === undefined || nextProps.requests.size === 0) {
if (nextProps.requests.size === 0) {
// cleared // cleared
rows = []; rows = [];
} else if (props.requests !== nextProps.requests) { } else if (props.requests !== nextProps.requests) {
// new request // new request
for (const requestId in nextProps.requests) { nextProps.requests.forEach((request: Request, requestId: RequestId) => {
if (props.requests[requestId] == null) { if (props.requests.get(requestId) == null) {
const newRow = buildRow( const newRow = buildRow(request, nextProps.responses.get(requestId));
nextProps.requests[requestId],
nextProps.responses[requestId],
);
if (newRow) { if (newRow) {
rows.push(newRow); rows.push(newRow);
} }
} }
} });
} else if (props.responses !== nextProps.responses) { } else if (props.responses !== nextProps.responses) {
// new response // new response
for (const responseId in nextProps.responses) { const resId = Array.from(nextProps.responses.keys()).find(
if (props.responses[responseId] == null) { (responseId: RequestId) => !props.responses.get(responseId),
const newRow = buildRow(
nextProps.requests[responseId],
nextProps.responses[responseId],
);
const index = rows.findIndex(
r => r.key === nextProps.requests[responseId]?.id,
); );
if (resId) {
const request = nextProps.requests.get(resId);
// sanity check; to pass null check
if (request) {
const newRow = buildRow(request, nextProps.responses.get(resId));
const index = rows.findIndex((r: TableBodyRow) => r.key === request.id);
if (index > -1 && newRow) { if (index > -1 && newRow) {
rows[index] = newRow; rows[index] = newRow;
} }
break;
} }
} }
} }
rows.sort((a, b) => Number(a.sortKey) - Number(b.sortKey)); rows.sort(
(a: TableBodyRow, b: TableBodyRow) => Number(a.sortKey) - Number(b.sortKey),
);
return { return {
sortedRows: rows, sortedRows: rows,
@@ -400,8 +406,8 @@ class NetworkTable extends PureComponent<NetworkTableProps, NetworkTableState> {
super(props); super(props);
this.state = calculateState( this.state = calculateState(
{ {
requests: {}, requests: new Map(),
responses: {}, responses: new Map(),
}, },
props, props,
); );
@@ -411,13 +417,20 @@ class NetworkTable extends PureComponent<NetworkTableProps, NetworkTableState> {
this.setState(calculateState(this.props, nextProps, this.state.sortedRows)); this.setState(calculateState(this.props, nextProps, this.state.sortedRows));
} }
contextMenuItems() { contextMenuItems(): Array<MenuItemConstructorOptions> {
type ContextMenuType =
| 'normal'
| 'separator'
| 'submenu'
| 'checkbox'
| 'radio';
const separator: ContextMenuType = 'separator';
const {clear, copyRequestCurlCommand, highlightedRows} = this.props; const {clear, copyRequestCurlCommand, highlightedRows} = this.props;
const highlightedMenuItems = const highlightedMenuItems =
highlightedRows && highlightedRows.size === 1 highlightedRows && highlightedRows.size === 1
? [ ? [
{ {
type: 'separator', type: separator,
}, },
{ {
label: 'Copy as cURL', label: 'Copy as cURL',
@@ -428,7 +441,7 @@ class NetworkTable extends PureComponent<NetworkTableProps, NetworkTableState> {
return highlightedMenuItems.concat([ return highlightedMenuItems.concat([
{ {
type: 'separator', type: separator,
}, },
{ {
label: 'Clear all', label: 'Clear all',
@@ -439,7 +452,9 @@ class NetworkTable extends PureComponent<NetworkTableProps, NetworkTableState> {
render() { render() {
return ( return (
<NetworkTable.ContextMenu items={this.contextMenuItems()}> <NetworkTable.ContextMenu
items={this.contextMenuItems()}
component={FlexColumn}>
<SearchableTable <SearchableTable
virtual={true} virtual={true}
multiline={false} multiline={false}
@@ -468,7 +483,7 @@ const Icon = styled(Glyph)({
}); });
class StatusColumn extends PureComponent<{ class StatusColumn extends PureComponent<{
children?: number, children?: number;
}> { }> {
render() { render() {
const {children} = this.props; const {children} = this.props;
@@ -488,8 +503,8 @@ class StatusColumn extends PureComponent<{
} }
class DurationColumn extends PureComponent<{ class DurationColumn extends PureComponent<{
request: Request, request: Request;
response: ?Response, response: Response | null | undefined;
}> { }> {
static Text = styled(Text)({ static Text = styled(Text)({
flex: 1, flex: 1,
@@ -511,7 +526,7 @@ class DurationColumn extends PureComponent<{
} }
class SizeColumn extends PureComponent<{ class SizeColumn extends PureComponent<{
response: ?Response, response: Response | null | undefined;
}> { }> {
static Text = styled(Text)({ static Text = styled(Text)({
flex: 1, flex: 1,
@@ -529,7 +544,11 @@ class SizeColumn extends PureComponent<{
} }
} }
getResponseLength(response) { getResponseLength(response: Response | null | undefined) {
if (!response) {
return 0;
}
let length = 0; let length = 0;
const lengthString = response.headers const lengthString = response.headers
? getHeaderValue(response.headers, 'content-length') ? getHeaderValue(response.headers, 'content-length')

View File

@@ -1,7 +1,7 @@
{ {
"name": "Network", "name": "Network",
"version": "1.0.0", "version": "1.0.0",
"main": "index.js", "main": "index.tsx",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"pako": "^1.0.6", "pako": "^1.0.6",

10
types/XmlBeautifier.d.tsx Normal file
View File

@@ -0,0 +1,10 @@
/**
* Copyright 2018-present Facebook.
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* @format
*/
declare module 'xml-beautifier' {
export default function(xml: string, indent?: string): string;
}