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

View File

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

View File

@@ -1,7 +1,7 @@
{
"name": "Network",
"version": "1.0.0",
"main": "index.js",
"main": "index.tsx",
"license": "MIT",
"dependencies": {
"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;
}