From fc4a08eb55efaf71f5c123cfc3c97974ab7d925d Mon Sep 17 00:00:00 2001 From: Michel Weststrate Date: Thu, 6 May 2021 04:26:41 -0700 Subject: [PATCH] Convert plugin UI to Sandy Summary: Changelog: Updated Network plugin to Sandy UI, including several UI improvements Converted UI to Sandy, and some minor code cleanups Moved all mock related logic to its own dir Fixes https://github.com/facebook/flipper/issues/2267 Reviewed By: passy Differential Revision: D27966606 fbshipit-source-id: a64e20276d7f0966ce7a95b22557762a32c184cd --- .../plugins/public/network/KeyValueTable.tsx | 46 ++ .../network/ManageMockResponsePanel.tsx | 301 ----------- .../public/network/MockResponseDetails.tsx | 362 ------------- .../public/network/MockResponseDialog.tsx | 81 --- .../plugins/public/network/RequestDetails.tsx | 372 ++++--------- desktop/plugins/public/network/index.tsx | 488 ++++-------------- .../ManageMockResponsePanel.tsx | 211 ++++++++ .../request-mocking/MockResponseDetails.tsx | 217 ++++++++ .../request-mocking/NetworkRouteManager.tsx | 241 +++++++++ desktop/plugins/public/network/types.tsx | 18 - desktop/plugins/public/network/utils.tsx | 76 +++ 11 files changed, 977 insertions(+), 1436 deletions(-) create mode 100644 desktop/plugins/public/network/KeyValueTable.tsx delete mode 100644 desktop/plugins/public/network/ManageMockResponsePanel.tsx delete mode 100644 desktop/plugins/public/network/MockResponseDetails.tsx delete mode 100644 desktop/plugins/public/network/MockResponseDialog.tsx create mode 100644 desktop/plugins/public/network/request-mocking/ManageMockResponsePanel.tsx create mode 100644 desktop/plugins/public/network/request-mocking/MockResponseDetails.tsx create mode 100644 desktop/plugins/public/network/request-mocking/NetworkRouteManager.tsx diff --git a/desktop/plugins/public/network/KeyValueTable.tsx b/desktop/plugins/public/network/KeyValueTable.tsx new file mode 100644 index 000000000..cd1c2c7b9 --- /dev/null +++ b/desktop/plugins/public/network/KeyValueTable.tsx @@ -0,0 +1,46 @@ +/** + * 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 + */ + +import * as React from 'react'; +import {DataTable, DataTableColumn} from 'flipper-plugin'; +import {useCallback} from 'react'; + +export type KeyValueItem = { + key: string; + value: string; +}; + +const columns: DataTableColumn[] = [ + { + key: 'key', + width: 160, + title: 'Key', + }, + { + key: 'value', + title: 'Value', + wrap: true, + }, +]; + +export function KeyValueTable({items}: {items: KeyValueItem[]}) { + const handleCopyRows = useCallback((rows: KeyValueItem[]) => { + return rows.map(({key, value}) => `${key}: ${value}`).join('\n'); + }, []); + + return ( + + columns={columns} + records={items} + enableSearchbar={false} + scrollable={false} + onCopyRows={handleCopyRows} + /> + ); +} diff --git a/desktop/plugins/public/network/ManageMockResponsePanel.tsx b/desktop/plugins/public/network/ManageMockResponsePanel.tsx deleted file mode 100644 index c385aebc3..000000000 --- a/desktop/plugins/public/network/ManageMockResponsePanel.tsx +++ /dev/null @@ -1,301 +0,0 @@ -/** - * 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 - */ - -import { - Button, - ManagedTable, - Text, - Glyph, - styled, - colors, - Panel, -} from 'flipper'; -import React, {useContext, useState, useMemo, useEffect} from 'react'; -import {Route, Requests} from './types'; -import {MockResponseDetails} from './MockResponseDetails'; -import {NetworkRouteContext} from './index'; -import {RequestId} from './types'; -import {message, Checkbox, Modal, Tooltip} from 'antd'; -import {NUX, Layout} from 'flipper-plugin'; - -type Props = { - routes: {[id: string]: Route}; - highlightedRows: Set | null | undefined; - requests: Requests; -}; - -const ColumnSizes = {route: 'flex'}; - -const Columns = {route: {value: 'Route', resizable: false}}; - -const TextEllipsis = styled(Text)({ - overflowX: 'hidden', - textOverflow: 'ellipsis', - maxWidth: '100%', - lineHeight: '18px', - paddingTop: 4, - display: 'block', - whiteSpace: 'nowrap', -}); - -const Icon = styled(Glyph)({ - marginTop: 5, - marginRight: 8, -}); - -// return ids that have the same pair of requestUrl and method; this will return only the duplicate -function _duplicateIds(routes: {[id: string]: Route}): Array { - const idSet: {[id: string]: {[method: string]: boolean}} = {}; - return Object.entries(routes).reduce((acc: Array, [id, route]) => { - if (idSet.hasOwnProperty(route.requestUrl)) { - if (idSet[route.requestUrl].hasOwnProperty(route.requestMethod)) { - return acc.concat(id); - } - idSet[route.requestUrl] = { - ...idSet[route.requestUrl], - [route.requestMethod]: true, - }; - return acc; - } else { - idSet[route.requestUrl] = {[route.requestMethod]: true}; - return acc; - } - }, []); -} - -function _buildRows( - routes: {[id: string]: Route}, - duplicatedIds: Array, - handleRemoveId: (id: string) => void, - handleEnableId: (id: string) => void, -) { - return Object.entries(routes).map(([id, route]) => ({ - columns: { - route: { - value: ( - handleRemoveId(id)} - handleEnableId={() => handleEnableId(id)} - enabled={route.enabled} - /> - ), - }, - }, - key: id, - })); -} - -function RouteRow(props: { - text: string; - showWarning: boolean; - handleRemoveId: () => void; - handleEnableId: () => void; - enabled: boolean; -}) { - const tip = props.enabled - ? 'Un-check to disable mock route' - : 'Check to enable mock route'; - return ( - - - - - - - - - - {props.showWarning && ( - - )} - {props.text.length === 0 ? ( - - untitled - - ) : ( - {props.text} - )} - - ); -} - -function ManagedMockResponseRightPanel(props: { - id: string; - route: Route; - isDuplicated: boolean; -}) { - const {id, route, isDuplicated} = props; - return ( - - - - ); -} - -export function ManageMockResponsePanel(props: Props) { - const networkRouteManager = useContext(NetworkRouteContext); - const [selectedId, setSelectedId] = useState(null); - - useEffect(() => { - setSelectedId((selectedId) => { - const keys = Object.keys(props.routes); - let returnValue: string | null = null; - // selectId is null when there are no rows or it is the first time rows are shown - if (selectedId === null) { - if (keys.length === 0) { - // there are no rows - returnValue = null; - } else { - // first time rows are shown - returnValue = keys[0]; - } - } else { - if (keys.includes(selectedId)) { - returnValue = selectedId; - } else { - // selectedId row value not in routes so default to first line - returnValue = keys[0]; - } - } - return returnValue; - }); - }, [props.routes]); - const duplicatedIds = useMemo(() => _duplicateIds(props.routes), [ - props.routes, - ]); - - function getSelectedIds(): Set { - const newSet = new Set(); - newSet.add(selectedId ?? ''); - return newSet; - } - - function getPreviousId(id: string): string | null { - const keys = Object.keys(props.routes); - const currentIndex = keys.indexOf(id); - if (currentIndex == 0) { - return null; - } else { - return keys[currentIndex - 1]; - } - } - - function getNextId(id: string): string | null { - const keys = Object.keys(props.routes); - const currentIndex = keys.indexOf(id); - if (currentIndex >= keys.length - 1) { - return getPreviousId(id); - } else { - return keys[currentIndex + 1]; - } - } - - return ( - - - - - - - - - - - { - Modal.confirm({ - title: 'Are you sure you want to delete this item?', - icon: '', - onOk() { - const nextId = getNextId(id); - networkRouteManager.removeRoute(id); - setSelectedId(nextId); - }, - onCancel() {}, - }); - }, - (id) => { - networkRouteManager.enableRoute(id); - }, - )} - stickyBottom={true} - autoHeight={false} - floating={false} - zebra={false} - onRowHighlighted={(selectedIds) => { - const newSelectedId = - selectedIds.length === 1 ? selectedIds[0] : null; - setSelectedId(newSelectedId); - }} - highlightedRows={getSelectedIds()} - /> - - - - {selectedId && props.routes.hasOwnProperty(selectedId) && ( - - )} - - - - ); -} diff --git a/desktop/plugins/public/network/MockResponseDetails.tsx b/desktop/plugins/public/network/MockResponseDetails.tsx deleted file mode 100644 index e47029b3a..000000000 --- a/desktop/plugins/public/network/MockResponseDetails.tsx +++ /dev/null @@ -1,362 +0,0 @@ -/** - * 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 - */ - -import { - FlexRow, - FlexColumn, - Layout, - Button, - Input, - Text, - Tabs, - Tab, - Glyph, - ManagedTable, - Select, - styled, - colors, - produce, -} from 'flipper'; -import React, {useContext, useState} from 'react'; -import {NetworkRouteContext, NetworkRouteManager} from './index'; -import {RequestId, Route} from './types'; - -type Props = { - id: RequestId; - route: Route; - isDuplicated: boolean; -}; - -const StyledSelectContainer = styled(FlexRow)({ - paddingLeft: 6, - paddingTop: 2, - paddingBottom: 24, - height: '100%', - flexGrow: 1, -}); - -const StyledSelect = styled(Select)({ - height: '100%', - maxWidth: 400, -}); - -const StyledText = styled(Text)({ - marginLeft: 6, - marginTop: 8, -}); - -const textAreaStyle: React.CSSProperties = { - width: '100%', - marginTop: 8, - height: 400, - fontSize: 15, - color: '#333', - padding: 10, - resize: 'none', - fontFamily: - 'source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace', - display: 'inline-block', - lineHeight: 1.5, - border: '1px solid #dcdee2', - borderRadius: 4, - backgroundColor: '#fff', - cursor: 'text', - WebkitTapHighlightColor: 'transparent', - whiteSpace: 'pre-wrap', - overflowWrap: 'break-word', -}; - -const StyledInput = styled(Input)({ - width: '100%', - height: 20, - marginLeft: 8, - flexGrow: 5, -}); - -const HeaderStyledInput = styled(Input)({ - width: '100%', - height: 20, - marginTop: 6, - marginBottom: 6, -}); - -const HeaderGlyph = styled(Glyph)({ - marginTop: 6, - marginBottom: 6, -}); - -const Container = styled(FlexColumn)({ - flexWrap: 'nowrap', - alignItems: 'flex-start', - alignContent: 'flex-start', - flexGrow: 1, - overflow: 'hidden', -}); - -const Warning = styled(FlexRow)({ - marginTop: 8, -}); - -const HeadersColumnSizes = { - close: '4%', - warning: '4%', - name: '35%', - value: 'flex', -}; - -const HeadersColumns = { - close: { - value: '', - resizable: false, - }, - warning: { - value: '', - resizable: false, - }, - name: { - value: 'Name', - resizable: false, - }, - value: { - value: 'Value', - resizable: false, - }, -}; - -const selectedHighlight = {backgroundColor: colors.highlight}; - -function HeaderInput(props: { - initialValue: string; - isSelected: boolean; - onUpdate: (newValue: string) => void; -}) { - const [value, setValue] = useState(props.initialValue); - return ( - setValue(event.target.value)} - onBlur={() => props.onUpdate(value)} - /> - ); -} - -function _buildMockResponseHeaderRows( - routeId: string, - route: Route, - selectedHeaderId: string | null, - networkRouteManager: NetworkRouteManager, -) { - return Object.entries(route.responseHeaders).map(([id, header]) => { - const selected = selectedHeaderId === id; - return { - columns: { - name: { - value: ( - { - const newHeaders = produce( - route.responseHeaders, - (draftHeaders) => { - draftHeaders[id].key = newValue; - }, - ); - networkRouteManager.modifyRoute(routeId, { - responseHeaders: newHeaders, - }); - }} - /> - ), - }, - value: { - value: ( - { - const newHeaders = produce( - route.responseHeaders, - (draftHeaders) => { - draftHeaders[id].value = newValue; - }, - ); - networkRouteManager.modifyRoute(routeId, { - responseHeaders: newHeaders, - }); - }} - /> - ), - }, - close: { - value: ( - { - const newHeaders = produce( - route.responseHeaders, - (draftHeaders) => { - delete draftHeaders[id]; - }, - ); - networkRouteManager.modifyRoute(routeId, { - responseHeaders: newHeaders, - }); - }}> - - - ), - }, - }, - key: id, - }; - }); -} - -export function MockResponseDetails({id, route, isDuplicated}: Props) { - const networkRouteManager = useContext(NetworkRouteContext); - const [activeTab, setActiveTab] = useState('data'); - const [selectedHeaderIds, setSelectedHeaderIds] = useState>( - [], - ); - const [nextHeaderId, setNextHeaderId] = useState(0); - - const {requestUrl, requestMethod, responseData, responseStatus} = route; - - let formattedResponse = ''; - try { - formattedResponse = JSON.stringify(JSON.parse(responseData), null, 2); - } catch (e) { - formattedResponse = responseData; - } - - return ( - - - - - networkRouteManager.modifyRoute(id, {requestMethod: text}) - } - /> - - - networkRouteManager.modifyRoute(id, { - requestUrl: event.target.value, - }) - } - /> - - - - networkRouteManager.modifyRoute(id, { - responseStatus: event.target.value, - }) - } - /> - - {isDuplicated && ( - - - - Route is duplicated (Same URL and Method) - - - )} - - { - if (newActiveTab != null) { - setActiveTab(newActiveTab); - } - }}> - -