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
This commit is contained in:
committed by
Facebook GitHub Bot
parent
84d65b1a77
commit
fc4a08eb55
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* 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 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],
|
||||
);
|
||||
|
||||
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.copyHighlightedCalls();
|
||||
}}>
|
||||
Copy Highlighted 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={selectedIdAtom}
|
||||
onRenderItem={handleRender}
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
* 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 React, {useContext, useState} from 'react';
|
||||
import {
|
||||
NetworkRouteContext,
|
||||
NetworkRouteManager,
|
||||
Route,
|
||||
} from './NetworkRouteManager';
|
||||
import {RequestId} from '../types';
|
||||
import {Button, Input, Select} from 'antd';
|
||||
import {Layout, produce, Tabs, Tab, theme} from 'flipper-plugin';
|
||||
import {CloseCircleOutlined, WarningOutlined} from '@ant-design/icons';
|
||||
|
||||
type Props = {
|
||||
id: RequestId;
|
||||
route: Route;
|
||||
isDuplicated: boolean;
|
||||
};
|
||||
|
||||
function HeaderInput(props: {
|
||||
initialValue: string;
|
||||
onUpdate: (newValue: string) => void;
|
||||
style?: React.CSSProperties;
|
||||
}) {
|
||||
const [value, setValue] = useState(props.initialValue);
|
||||
return (
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
value={value}
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
onBlur={() => props.onUpdate(value)}
|
||||
style={props.style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ResponseHeaders({
|
||||
routeId,
|
||||
route,
|
||||
networkRouteManager,
|
||||
}: {
|
||||
routeId: string;
|
||||
route: Route;
|
||||
networkRouteManager: NetworkRouteManager;
|
||||
}) {
|
||||
return (
|
||||
<Layout.Container gap style={{paddingRight: theme.space.small}}>
|
||||
{Object.entries(route.responseHeaders).map(([id, header]) => (
|
||||
<Layout.Horizontal center gap key={id}>
|
||||
<HeaderInput
|
||||
initialValue={header.key}
|
||||
onUpdate={(newValue: string) => {
|
||||
const newHeaders = produce(
|
||||
route.responseHeaders,
|
||||
(draftHeaders) => {
|
||||
draftHeaders[id].key = newValue;
|
||||
},
|
||||
);
|
||||
networkRouteManager.modifyRoute(routeId, {
|
||||
responseHeaders: newHeaders,
|
||||
});
|
||||
}}
|
||||
style={{width: 300}}
|
||||
/>
|
||||
<HeaderInput
|
||||
initialValue={header.value}
|
||||
onUpdate={(newValue: string) => {
|
||||
const newHeaders = produce(
|
||||
route.responseHeaders,
|
||||
(draftHeaders) => {
|
||||
draftHeaders[id].value = newValue;
|
||||
},
|
||||
);
|
||||
networkRouteManager.modifyRoute(routeId, {
|
||||
responseHeaders: newHeaders,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Layout.Container
|
||||
onClick={() => {
|
||||
const newHeaders = produce(
|
||||
route.responseHeaders,
|
||||
(draftHeaders) => {
|
||||
delete draftHeaders[id];
|
||||
},
|
||||
);
|
||||
networkRouteManager.modifyRoute(routeId, {
|
||||
responseHeaders: newHeaders,
|
||||
});
|
||||
}}>
|
||||
<CloseCircleOutlined />
|
||||
</Layout.Container>
|
||||
</Layout.Horizontal>
|
||||
))}
|
||||
</Layout.Container>
|
||||
);
|
||||
}
|
||||
|
||||
const httpMethods = [
|
||||
'GET',
|
||||
'POST',
|
||||
'PATCH',
|
||||
'HEAD',
|
||||
'PUT',
|
||||
'DELETE',
|
||||
'TRACE',
|
||||
'OPTIONS',
|
||||
'CONNECT',
|
||||
].map((v) => ({value: v, label: v}));
|
||||
|
||||
export function MockResponseDetails({id, route, isDuplicated}: Props) {
|
||||
const networkRouteManager = useContext(NetworkRouteContext);
|
||||
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 (
|
||||
<Layout.Container gap>
|
||||
<Layout.Horizontal gap>
|
||||
<Select
|
||||
value={requestMethod}
|
||||
options={httpMethods}
|
||||
onChange={(text) =>
|
||||
networkRouteManager.modifyRoute(id, {requestMethod: text})
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="URL"
|
||||
value={requestUrl}
|
||||
onChange={(event) =>
|
||||
networkRouteManager.modifyRoute(id, {
|
||||
requestUrl: event.target.value,
|
||||
})
|
||||
}
|
||||
style={{flex: 1}}
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="STATUS"
|
||||
value={responseStatus}
|
||||
onChange={(event) =>
|
||||
networkRouteManager.modifyRoute(id, {
|
||||
responseStatus: event.target.value,
|
||||
})
|
||||
}
|
||||
style={{width: 100}}
|
||||
/>
|
||||
</Layout.Horizontal>
|
||||
{isDuplicated && (
|
||||
<Layout.Horizontal gap>
|
||||
<WarningOutlined />
|
||||
Route is duplicated (Same URL and Method)
|
||||
</Layout.Horizontal>
|
||||
)}
|
||||
<Layout.Container height={500}>
|
||||
<Tabs grow>
|
||||
<Tab tab={'Data'}>
|
||||
<Input.TextArea
|
||||
wrap="soft"
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
value={formattedResponse}
|
||||
onChange={(event) =>
|
||||
networkRouteManager.modifyRoute(id, {
|
||||
responseData: event.target.value,
|
||||
})
|
||||
}
|
||||
style={{flex: 1}}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab tab={'Headers'}>
|
||||
<Layout.Top gap>
|
||||
<Layout.Horizontal>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const newHeaders = {
|
||||
...route.responseHeaders,
|
||||
[nextHeaderId.toString()]: {key: '', value: ''},
|
||||
};
|
||||
setNextHeaderId(nextHeaderId + 1);
|
||||
networkRouteManager.modifyRoute(id, {
|
||||
responseHeaders: newHeaders,
|
||||
});
|
||||
}}>
|
||||
Add Header
|
||||
</Button>
|
||||
</Layout.Horizontal>
|
||||
<Layout.ScrollContainer>
|
||||
<ResponseHeaders
|
||||
routeId={id}
|
||||
route={route}
|
||||
networkRouteManager={networkRouteManager}
|
||||
/>
|
||||
</Layout.ScrollContainer>
|
||||
</Layout.Top>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Layout.Container>
|
||||
</Layout.Container>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* 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 fs from 'fs';
|
||||
// eslint-disable-next-line
|
||||
import electron, {OpenDialogOptions, remote} from 'electron';
|
||||
import {Atom, DataTableManager} from 'flipper-plugin';
|
||||
import {createContext} from 'react';
|
||||
import {Header, Request} from '../types';
|
||||
import {decodeBody} from '../utils';
|
||||
import {message} from 'antd';
|
||||
|
||||
export type Route = {
|
||||
requestUrl: string;
|
||||
requestMethod: string;
|
||||
responseData: string;
|
||||
responseHeaders: {[id: string]: Header};
|
||||
responseStatus: string;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
export type MockRoute = {
|
||||
requestUrl: string;
|
||||
method: string;
|
||||
data: string;
|
||||
headers: Header[];
|
||||
status: string;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
export interface NetworkRouteManager {
|
||||
addRoute(): string | undefined;
|
||||
modifyRoute(id: string, routeChange: Partial<Route>): void;
|
||||
removeRoute(id: string): void;
|
||||
enableRoute(id: string): void;
|
||||
copyHighlightedCalls(): void;
|
||||
importRoutes(): void;
|
||||
exportRoutes(): void;
|
||||
clearRoutes(): void;
|
||||
}
|
||||
|
||||
export const nullNetworkRouteManager: NetworkRouteManager = {
|
||||
addRoute(): string | undefined {
|
||||
return '';
|
||||
},
|
||||
modifyRoute(_id: string, _routeChange: Partial<Route>) {},
|
||||
removeRoute(_id: string) {},
|
||||
enableRoute(_id: string) {},
|
||||
copyHighlightedCalls() {},
|
||||
importRoutes() {},
|
||||
exportRoutes() {},
|
||||
clearRoutes() {},
|
||||
};
|
||||
|
||||
export const NetworkRouteContext = createContext<NetworkRouteManager>(
|
||||
nullNetworkRouteManager,
|
||||
);
|
||||
|
||||
export function createNetworkManager(
|
||||
nextRouteId: Atom<number>,
|
||||
routes: Atom<{[id: string]: any}>,
|
||||
informClientMockChange: (routes: {[id: string]: any}) => Promise<void>,
|
||||
tableManagerRef: React.RefObject<DataTableManager<Request> | undefined>,
|
||||
): NetworkRouteManager {
|
||||
return {
|
||||
addRoute(): string | undefined {
|
||||
const newNextRouteId = nextRouteId.get();
|
||||
routes.update((draft) => {
|
||||
draft[newNextRouteId.toString()] = {
|
||||
requestUrl: '',
|
||||
requestMethod: 'GET',
|
||||
responseData: '',
|
||||
responseHeaders: {},
|
||||
responseStatus: '200',
|
||||
enabled: true,
|
||||
};
|
||||
});
|
||||
nextRouteId.set(newNextRouteId + 1);
|
||||
return String(newNextRouteId);
|
||||
},
|
||||
modifyRoute(id: string, routeChange: Partial<Route>) {
|
||||
if (!routes.get().hasOwnProperty(id)) {
|
||||
return;
|
||||
}
|
||||
routes.update((draft) => {
|
||||
Object.assign(draft[id], routeChange);
|
||||
});
|
||||
informClientMockChange(routes.get());
|
||||
},
|
||||
removeRoute(id: string) {
|
||||
if (routes.get().hasOwnProperty(id)) {
|
||||
routes.update((draft) => {
|
||||
delete draft[id];
|
||||
});
|
||||
}
|
||||
informClientMockChange(routes.get());
|
||||
},
|
||||
enableRoute(id: string) {
|
||||
if (routes.get().hasOwnProperty(id)) {
|
||||
routes.update((draft) => {
|
||||
draft[id].enabled = !draft[id].enabled;
|
||||
});
|
||||
}
|
||||
informClientMockChange(routes.get());
|
||||
},
|
||||
copyHighlightedCalls() {
|
||||
tableManagerRef.current?.getSelectedItems().forEach((request) => {
|
||||
// convert headers
|
||||
const headers: {[id: string]: Header} = {};
|
||||
request.responseHeaders?.forEach((e) => {
|
||||
headers[e.key] = e;
|
||||
});
|
||||
|
||||
// convert data TODO: we only want this for non-binary data! See D23403095
|
||||
const responseData =
|
||||
request && request.responseData
|
||||
? decodeBody({
|
||||
headers: request.responseHeaders ?? [],
|
||||
data: request.responseData,
|
||||
})
|
||||
: '';
|
||||
|
||||
const newNextRouteId = nextRouteId.get();
|
||||
routes.update((draft) => {
|
||||
draft[newNextRouteId.toString()] = {
|
||||
requestUrl: request.url,
|
||||
requestMethod: request.method,
|
||||
responseData: responseData as string,
|
||||
responseHeaders: headers,
|
||||
responseStatus: request.status?.toString() ?? '',
|
||||
enabled: true,
|
||||
};
|
||||
});
|
||||
nextRouteId.set(newNextRouteId + 1);
|
||||
});
|
||||
|
||||
informClientMockChange(routes.get());
|
||||
},
|
||||
importRoutes() {
|
||||
const options: OpenDialogOptions = {
|
||||
properties: ['openFile'],
|
||||
filters: [{extensions: ['json'], name: 'Flipper Route Files'}],
|
||||
};
|
||||
remote.dialog.showOpenDialog(options).then((result) => {
|
||||
const filePaths = result.filePaths;
|
||||
if (filePaths.length > 0) {
|
||||
fs.readFile(filePaths[0], 'utf8', (err, data) => {
|
||||
if (err) {
|
||||
message.error('Unable to import file');
|
||||
return;
|
||||
}
|
||||
const importedRoutes = JSON.parse(data);
|
||||
importedRoutes?.forEach((importedRoute: Route) => {
|
||||
if (importedRoute != null) {
|
||||
const newNextRouteId = nextRouteId.get();
|
||||
routes.update((draft) => {
|
||||
draft[newNextRouteId.toString()] = {
|
||||
requestUrl: importedRoute.requestUrl,
|
||||
requestMethod: importedRoute.requestMethod,
|
||||
responseData: importedRoute.responseData as string,
|
||||
responseHeaders: importedRoute.responseHeaders,
|
||||
responseStatus: importedRoute.responseStatus,
|
||||
enabled: true,
|
||||
};
|
||||
});
|
||||
nextRouteId.set(newNextRouteId + 1);
|
||||
}
|
||||
});
|
||||
informClientMockChange(routes.get());
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
exportRoutes() {
|
||||
remote.dialog
|
||||
.showSaveDialog(
|
||||
// @ts-ignore This appears to work but isn't allowed by the types
|
||||
null,
|
||||
{
|
||||
title: 'Export Routes',
|
||||
defaultPath: 'NetworkPluginRoutesExport.json',
|
||||
},
|
||||
)
|
||||
.then((result: electron.SaveDialogReturnValue) => {
|
||||
const file = result.filePath;
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
fs.writeFile(
|
||||
file,
|
||||
JSON.stringify(Object.values(routes.get()), null, 2),
|
||||
'utf8',
|
||||
(err) => {
|
||||
if (err) {
|
||||
message.error('Failed to store mock routes: ' + err);
|
||||
} else {
|
||||
message.info('Successfully exported mock routes');
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
clearRoutes() {
|
||||
routes.set({});
|
||||
informClientMockChange(routes.get());
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function computeMockRoutes(routes: {[id: string]: Route}) {
|
||||
const existedIdSet: {[id: string]: {[method: string]: boolean}} = {};
|
||||
const filteredRoutes: {[id: string]: Route} = Object.entries(routes).reduce(
|
||||
(accRoutes, [id, route]) => {
|
||||
if (existedIdSet.hasOwnProperty(route.requestUrl)) {
|
||||
if (
|
||||
existedIdSet[route.requestUrl].hasOwnProperty(route.requestMethod)
|
||||
) {
|
||||
return accRoutes;
|
||||
}
|
||||
existedIdSet[route.requestUrl] = {
|
||||
...existedIdSet[route.requestUrl],
|
||||
[route.requestMethod]: true,
|
||||
};
|
||||
return Object.assign({[id]: route}, accRoutes);
|
||||
} else {
|
||||
existedIdSet[route.requestUrl] = {
|
||||
[route.requestMethod]: true,
|
||||
};
|
||||
return Object.assign({[id]: route}, accRoutes);
|
||||
}
|
||||
},
|
||||
{},
|
||||
);
|
||||
return filteredRoutes;
|
||||
}
|
||||
Reference in New Issue
Block a user