From 705ba8eaa81a3c131c3ea1b8f460decf77b7c9c1 Mon Sep 17 00:00:00 2001 From: Chaiwat Ekkaewnumchai Date: Thu, 5 Sep 2019 02:46:27 -0700 Subject: [PATCH] Convert Flipper plugin "Network" to TypeScript Summary: _typescript_ Reviewed By: danielbuechele Differential Revision: D17155509 fbshipit-source-id: 45ae3e2de8cd7b3cdf7271905ef7c318d4289391 --- .../{RequestDetails.js => RequestDetails.tsx} | 97 +++--- src/plugins/network/{index.js => index.tsx} | 289 ++++++++++-------- src/plugins/network/package.json | 2 +- types/XmlBeautifier.d.tsx | 10 + 4 files changed, 214 insertions(+), 184 deletions(-) rename src/plugins/network/{RequestDetails.js => RequestDetails.tsx} (92%) rename src/plugins/network/{index.js => index.tsx} (63%) create mode 100644 types/XmlBeautifier.d.tsx diff --git a/src/plugins/network/RequestDetails.js b/src/plugins/network/RequestDetails.tsx similarity index 92% rename from src/plugins/network/RequestDetails.js rename to src/plugins/network/RequestDetails.tsx index 34de689b4..ea373a0da 100644 --- a/src/plugins/network/RequestDetails.js +++ b/src/plugins/network/RequestDetails.tsx @@ -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: {kv[0]}, + value: {key}, }, value: { - value: {kv[1]}, + value: {value}, }, }, - copyText: kv[1], - key: kv[0], + copyText: value, + key: key, }); - } + }); return rows.length > 0 ? ( { } type HeaderInspectorProps = { - headers: Array
, + headers: Array
; }; 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 = 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: {key}, }, value: { - value: {computedHeaders[key]}, + value: {value}, }, }, - copyText: computedHeaders[key], + copyText: value, key, }); - } + }); return rows.length > 0 ? ( 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 { @@ -394,7 +391,7 @@ class ImageWithSize extends Component { 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 ; @@ -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 ; @@ -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 ; @@ -745,7 +742,11 @@ class InsightsInspector extends Component<{insights: Insights}> { } ${timesWord} out of ${retry.limit})`; } - buildRow(name: string, value: ?T, formatter: T => string): any { + buildRow( + name: string, + value: T | null | undefined, + formatter: (value: T) => string, + ): any { return value ? { columns: { diff --git a/src/plugins/network/index.js b/src/plugins/network/index.tsx similarity index 63% rename from src/plugins/network/index.js rename to src/plugins/network/index.tsx index 71541821e..9a5acd24d 100644 --- a/src/plugins/network/index.js +++ b/src/plugins/network/index.tsx @@ -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; + responses: Map; +}; -type State = {| - selectedIds: Array, -|}; +type State = { + selectedIds: Array; +}; 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 { - static keyboardActions = ['clear']; +export default class extends FlipperPlugin { + static keyboardActions: Array = ['clear']; static subscribed = []; static defaultPersistedState = { - requests: {}, - responses: {}, + requests: new Map(), + responses: new Map(), }; - static metricsReducer = ( - persistedState: PersistedState, - ): Promise => { - 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 => { - const responses = persistedState ? persistedState.responses || [] : []; - const r: Array = Object.values(responses); + static getActiveNotifications(persistedState: PersistedState) { + const responses = persistedState + ? persistedState.responses || new Map() + : new Map(); + const r: Array = 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 { 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 { 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 ( - ) : null; + ); }; render() { @@ -220,8 +218,8 @@ export default class extends FlipperPlugin { return ( { } type NetworkTableProps = { - requests: {[id: RequestId]: Request}, - responses: {[id: RequestId]: Response}, - clear: () => void, - copyRequestCurlCommand: () => void, - onRowHighlighted: (keys: TableHighlightedRows) => void, - highlightedRows: ?Set, + requests: Map; + responses: Map; + clear: () => void; + copyRequestCurlCommand: () => void; + onRowHighlighted: (keys: TableHighlightedRows) => void; + highlightedRows: Set | 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; + responses: Map; }, 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 { super(props); this.state = calculateState( { - requests: {}, - responses: {}, + requests: new Map(), + responses: new Map(), }, props, ); @@ -411,13 +417,20 @@ class NetworkTable extends PureComponent { this.setState(calculateState(this.props, nextProps, this.state.sortedRows)); } - contextMenuItems() { + contextMenuItems(): Array { + 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 { return highlightedMenuItems.concat([ { - type: 'separator', + type: separator, }, { label: 'Clear all', @@ -439,7 +452,9 @@ class NetworkTable extends PureComponent { render() { return ( - + { 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') diff --git a/src/plugins/network/package.json b/src/plugins/network/package.json index 7b4492d49..c6070ce91 100644 --- a/src/plugins/network/package.json +++ b/src/plugins/network/package.json @@ -1,7 +1,7 @@ { "name": "Network", "version": "1.0.0", - "main": "index.js", + "main": "index.tsx", "license": "MIT", "dependencies": { "pako": "^1.0.6", diff --git a/types/XmlBeautifier.d.tsx b/types/XmlBeautifier.d.tsx new file mode 100644 index 000000000..38ba1b3a1 --- /dev/null +++ b/types/XmlBeautifier.d.tsx @@ -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; +}