Summary: Currently the network plugin was always storing the transfer format of our request/ response bodies: a base64 string. However, those are not searchable, and every formatter (and multiple can be invoked in a single view) was responsible for its own decompressing. This diff changes parsing requests / responses into an accurate format: a decompressed, de-base64-ed utf8 string, or a Uint8array for binary data. We will use this in the next diffs to do some more efficient searching Reviewed By: passy Differential Revision: D28200190 fbshipit-source-id: 33a71aeb7b340b58305e97fff4fa5ce472169b25
242 lines
7.2 KiB
TypeScript
242 lines
7.2 KiB
TypeScript
/**
|
|
* 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 {bodyAsString, 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(
|
|
request.responseHeaders ?? [],
|
|
bodyAsString(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;
|
|
}
|