desktop/plugins/public/network/request-mocking/ManageMockResponsePanel.tsx (196 lines of code) (raw):

/** * 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 */ import React, { useContext, useState, useMemo, useEffect, useCallback, } from 'react'; import {MockResponseDetails} from './MockResponseDetails'; import {NetworkRouteContext, Route} from './NetworkRouteManager'; import {RequestId} from '../types'; import {Checkbox, Modal, Tooltip, Button, Typography} from 'antd'; import { NUX, Layout, DataList, Toolbar, createState, useValue, } from 'flipper-plugin'; import {CloseCircleOutlined, WarningOutlined} from '@ant-design/icons'; const {Text} = Typography; type Props = { routes: {[id: string]: Route}; }; type RouteItem = { id: string; title: string; route: Route; isDuplicate: boolean; }; // return ids that have the same pair of requestUrl and method; this will return only the duplicate function _duplicateIds(routes: {[id: string]: Route}): Array<RequestId> { const idSet: {[id: string]: {[method: string]: boolean}} = {}; return Object.entries(routes).reduce((acc: Array<RequestId>, [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; } }, []); } export function ManageMockResponsePanel(props: Props) { const networkRouteManager = useContext(NetworkRouteContext); const [selectedIdAtom] = useState(() => createState<RequestId | undefined>()); const selectedId = useValue(selectedIdAtom); useEffect(() => { selectedIdAtom.update((selectedId) => { const keys = Object.keys(props.routes); let returnValue: string | undefined = undefined; // selectId is undefined when there are no rows or it is the first time rows are shown if (selectedId === undefined) { if (keys.length === 0) { // there are no rows returnValue = undefined; } 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, selectedIdAtom]); const duplicatedIds = useMemo( () => _duplicateIds(props.routes), [props.routes], ); const items: RouteItem[] = Object.entries(props.routes).map( ([id, route]) => ({ id, route, title: route.requestUrl, isDuplicate: duplicatedIds.includes(id), }), ); const handleDelete = useCallback( (id: string) => { Modal.confirm({ title: 'Are you sure you want to delete this item?', icon: '', onOk() { networkRouteManager.removeRoute(id); selectedIdAtom.set(undefined); }, onCancel() {}, }); }, [networkRouteManager, selectedIdAtom], ); const handleToggle = useCallback( (id: string) => { networkRouteManager.enableRoute(id); }, [networkRouteManager], ); const handleRender = useCallback( (item: RouteItem) => ( <RouteEntry item={item} onDelete={handleDelete} onToggle={handleToggle} /> ), [handleDelete, handleToggle], ); const handleSelect = useCallback( (id: string) => { if (id) { selectedIdAtom.set(id); } }, [selectedIdAtom], ); return ( <Layout.Left resizable style={{minHeight: 400}}> <Layout.Top> <Toolbar> <Button onClick={() => { const newId = networkRouteManager.addRoute(); selectedIdAtom.set(newId); }}> Add Route </Button> <NUX title="It is now possible to select calls from the network call list and convert them into mock routes." placement="bottom"> <Button onClick={() => { networkRouteManager.copySelectedCalls(); }}> Copy Selected Calls </Button> </NUX> <Button onClick={networkRouteManager.importRoutes}>Import</Button> <Button onClick={networkRouteManager.exportRoutes}>Export</Button> <Button onClick={networkRouteManager.clearRoutes}>Clear</Button> </Toolbar> <DataList items={items} selection={selectedId} onRenderItem={handleRender} onSelect={handleSelect} scrollable /> </Layout.Top> <Layout.Container gap pad> {selectedId && props.routes.hasOwnProperty(selectedId) && ( <MockResponseDetails id={selectedId} route={props.routes[selectedId]} isDuplicated={duplicatedIds.includes(selectedId)} /> )} </Layout.Container> </Layout.Left> ); } const RouteEntry = ({ item, onToggle, onDelete, }: { item: RouteItem; onToggle(id: string): void; onDelete(id: string): void; }) => { const tip = item.route.enabled ? 'Un-check to disable mock route' : 'Check to enable mock route'; return ( <Layout.Horizontal gap center> <Tooltip title={tip} mouseEnterDelay={1.1}> <Checkbox onClick={() => onToggle(item.id)} checked={item.route.enabled}></Checkbox> </Tooltip> {item.route.requestUrl.length === 0 ? ( <Text ellipsis>untitled</Text> ) : ( <Text ellipsis>{item.route.requestUrl}</Text> )} <Tooltip title="Click to delete mock route" mouseEnterDelay={1.1}> <Layout.Horizontal onClick={() => onDelete(item.id)}> <CloseCircleOutlined /> </Layout.Horizontal> </Tooltip> {item.isDuplicate && <WarningOutlined />} </Layout.Horizontal> ); };