Summary: 1. Remove "fs.writeFile" from plugins 2. Remove "showSaveDialog" from "FlipperLib" 3. Add "exportFile" to "FlipperLib" and "RenderHost" As we are going to use Flipper in a browser as well, instead of providing low-level abstraction like "showSaveDialog", we should provide more high-level ones like "exportFile". In browsers it does not make too much sense to expose "showSaveDialog" as there is not way to use the returned file path to write to it. In the end, "exportFile" is going to trigger a file download for browsers and show the save dialog and write to the returned file path for Electron. Reviewed By: mweststrate Differential Revision: D32492149 fbshipit-source-id: 0673119bdb7670a5872f5982c7d82dfc44eb7906
216 lines
6.4 KiB
TypeScript
216 lines
6.4 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';
|
|
import {Atom, DataTableManager, getFlipperLib} from 'flipper-plugin';
|
|
import {createContext} from 'react';
|
|
import {Header, Request} from '../types';
|
|
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;
|
|
copySelectedCalls(): 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) {},
|
|
copySelectedCalls() {},
|
|
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());
|
|
},
|
|
copySelectedCalls() {
|
|
tableManagerRef.current?.getSelectedItems().forEach((request) => {
|
|
// convert headers
|
|
const headers: {[id: string]: Header} = {};
|
|
request.responseHeaders?.forEach((e) => {
|
|
headers[e.key] = e;
|
|
});
|
|
|
|
// no need to convert data, already converted when real call was created
|
|
const responseData =
|
|
request && request.responseData ? 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() {
|
|
getFlipperLib()
|
|
.showOpenDialog?.({
|
|
filter: {extensions: ['json'], name: 'Flipper Route Files'},
|
|
})
|
|
.then((filePath) => {
|
|
if (filePath) {
|
|
fs.readFile(filePath, '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());
|
|
});
|
|
}
|
|
})
|
|
.catch((e) =>
|
|
console.error('[network] importRoutes dialogue failed:', e),
|
|
);
|
|
},
|
|
exportRoutes() {
|
|
getFlipperLib()
|
|
.exportFile(JSON.stringify(Object.values(routes.get()), null, 2), {
|
|
defaultPath: 'NetworkPluginRoutesExport.json',
|
|
})
|
|
.catch((e) =>
|
|
console.error('[network] exportRoutes saving failed:', e),
|
|
);
|
|
},
|
|
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;
|
|
}
|