Migrate Network plugin to Sandy (#1583)
Summary: Pull Request resolved: https://github.com/facebook/flipper/pull/1583 Migrate Network plugin to Sandy Reviewed By: mweststrate Differential Revision: D24108772 fbshipit-source-id: e889b9f6b00398cd5f98cf15660b42b1d5496cea
This commit is contained in:
committed by
Facebook GitHub Bot
parent
5488dcf358
commit
fdde2761ef
@@ -8,8 +8,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {combineBase64Chunks} from '../chunks';
|
import {combineBase64Chunks} from '../chunks';
|
||||||
import network from '../index';
|
import {TestUtils} from 'flipper-plugin';
|
||||||
import {PersistedState} from '../types';
|
import * as NetworkPlugin from '..';
|
||||||
|
|
||||||
test('Test assembling base64 chunks', () => {
|
test('Test assembling base64 chunks', () => {
|
||||||
const message = 'wassup john?';
|
const message = 'wassup john?';
|
||||||
@@ -24,12 +24,10 @@ test('Test assembling base64 chunks', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Reducer correctly adds initial chunk', () => {
|
test('Reducer correctly adds initial chunk', () => {
|
||||||
const state: PersistedState = {
|
const {instance, sendEvent} = TestUtils.startPlugin(NetworkPlugin);
|
||||||
requests: {},
|
expect(instance.partialResponses.get()).toEqual({});
|
||||||
responses: {},
|
|
||||||
partialResponses: {},
|
sendEvent('partialResponse', {
|
||||||
};
|
|
||||||
const result = network.persistedStateReducer(state, 'partialResponse', {
|
|
||||||
id: '1',
|
id: '1',
|
||||||
timestamp: 123,
|
timestamp: 123,
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -41,7 +39,8 @@ test('Reducer correctly adds initial chunk', () => {
|
|||||||
index: 0,
|
index: 0,
|
||||||
totalChunks: 2,
|
totalChunks: 2,
|
||||||
});
|
});
|
||||||
expect(result.partialResponses['1']).toMatchInlineSnapshot(`
|
|
||||||
|
expect(instance.partialResponses.get()['1']).toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
"followupChunks": Object {},
|
"followupChunks": Object {},
|
||||||
"initialResponse": Object {
|
"initialResponse": Object {
|
||||||
@@ -61,18 +60,16 @@ test('Reducer correctly adds initial chunk', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Reducer correctly adds followup chunk', () => {
|
test('Reducer correctly adds followup chunk', () => {
|
||||||
const state: PersistedState = {
|
const {instance, sendEvent} = TestUtils.startPlugin(NetworkPlugin);
|
||||||
requests: {},
|
expect(instance.partialResponses.get()).toEqual({});
|
||||||
responses: {},
|
|
||||||
partialResponses: {},
|
sendEvent('partialResponse', {
|
||||||
};
|
|
||||||
const result = network.persistedStateReducer(state, 'partialResponse', {
|
|
||||||
id: '1',
|
id: '1',
|
||||||
totalChunks: 2,
|
totalChunks: 2,
|
||||||
index: 1,
|
index: 1,
|
||||||
data: 'hello',
|
data: 'hello',
|
||||||
});
|
});
|
||||||
expect(result.partialResponses['1']).toMatchInlineSnapshot(`
|
expect(instance.partialResponses.get()['1']).toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
"followupChunks": Object {
|
"followupChunks": Object {
|
||||||
"1": "hello",
|
"1": "hello",
|
||||||
@@ -82,10 +79,8 @@ test('Reducer correctly adds followup chunk', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Reducer correctly combines initial response and followup chunk', () => {
|
test('Reducer correctly combines initial response and followup chunk', () => {
|
||||||
const state: PersistedState = {
|
const {instance, sendEvent} = TestUtils.startPlugin(NetworkPlugin);
|
||||||
requests: {},
|
instance.partialResponses.set({
|
||||||
responses: {},
|
|
||||||
partialResponses: {
|
|
||||||
'1': {
|
'1': {
|
||||||
followupChunks: {},
|
followupChunks: {},
|
||||||
initialResponse: {
|
initialResponse: {
|
||||||
@@ -101,16 +96,17 @@ test('Reducer correctly combines initial response and followup chunk', () => {
|
|||||||
totalChunks: 2,
|
totalChunks: 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
};
|
expect(instance.responses.get()).toEqual({});
|
||||||
const result = network.persistedStateReducer(state, 'partialResponse', {
|
sendEvent('partialResponse', {
|
||||||
id: '1',
|
id: '1',
|
||||||
totalChunks: 2,
|
totalChunks: 2,
|
||||||
index: 1,
|
index: 1,
|
||||||
data: 'bG8=',
|
data: 'bG8=',
|
||||||
});
|
});
|
||||||
expect(result.partialResponses).toEqual({});
|
|
||||||
expect(result.responses['1']).toMatchInlineSnapshot(`
|
expect(instance.partialResponses.get()).toEqual({});
|
||||||
|
expect(instance.responses.get()['1']).toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
"data": "aGVsbG8=",
|
"data": "aGVsbG8=",
|
||||||
"headers": Array [],
|
"headers": Array [],
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import {
|
|||||||
DetailSidebar,
|
DetailSidebar,
|
||||||
styled,
|
styled,
|
||||||
SearchableTable,
|
SearchableTable,
|
||||||
FlipperPlugin,
|
|
||||||
Sheet,
|
Sheet,
|
||||||
TableHighlightedRows,
|
TableHighlightedRows,
|
||||||
TableRows,
|
TableRows,
|
||||||
@@ -36,8 +35,8 @@ import {
|
|||||||
Response,
|
Response,
|
||||||
Route,
|
Route,
|
||||||
ResponseFollowupChunk,
|
ResponseFollowupChunk,
|
||||||
PersistedState,
|
|
||||||
Header,
|
Header,
|
||||||
|
MockRoute,
|
||||||
} from './types';
|
} from './types';
|
||||||
import {convertRequestToCurlCommand, getHeaderValue, decodeBody} from './utils';
|
import {convertRequestToCurlCommand, getHeaderValue, decodeBody} from './utils';
|
||||||
import RequestDetails from './RequestDetails';
|
import RequestDetails from './RequestDetails';
|
||||||
@@ -45,7 +44,7 @@ import {clipboard} from 'electron';
|
|||||||
import {URL} from 'url';
|
import {URL} from 'url';
|
||||||
import {MockResponseDialog} from './MockResponseDialog';
|
import {MockResponseDialog} from './MockResponseDialog';
|
||||||
import {combineBase64Chunks} from './chunks';
|
import {combineBase64Chunks} from './chunks';
|
||||||
import {DefaultKeyboardAction} from 'flipper-plugin';
|
import {PluginClient, createState, usePlugin, useValue} from 'flipper-plugin';
|
||||||
|
|
||||||
const LOCALSTORAGE_MOCK_ROUTE_LIST_KEY = '__NETWORK_CACHED_MOCK_ROUTE_LIST';
|
const LOCALSTORAGE_MOCK_ROUTE_LIST_KEY = '__NETWORK_CACHED_MOCK_ROUTE_LIST';
|
||||||
|
|
||||||
@@ -54,17 +53,14 @@ export const BodyOptions = {
|
|||||||
parsed: 'parsed',
|
parsed: 'parsed',
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type Events = {
|
||||||
selectedIds: Array<RequestId>;
|
newRequest: Request;
|
||||||
searchTerm: string;
|
newResponse: Response;
|
||||||
routes: {[id: string]: Route};
|
partialResponse: Response | ResponseFollowupChunk;
|
||||||
nextRouteId: number;
|
};
|
||||||
isMockResponseSupported: boolean;
|
|
||||||
showMockResponseDialog: boolean;
|
type Methods = {
|
||||||
detailBodyFormat: string;
|
mockResponses(params: {routes: MockRoute[]}): Promise<void>;
|
||||||
highlightedRows: Set<string> | null | undefined;
|
|
||||||
requests: {[id: string]: Request};
|
|
||||||
responses: {[id: string]: Response};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const COLUMN_SIZE = {
|
const COLUMN_SIZE = {
|
||||||
@@ -146,46 +142,71 @@ export const NetworkRouteContext = createContext<NetworkRouteManager>(
|
|||||||
nullNetworkRouteManager,
|
nullNetworkRouteManager,
|
||||||
);
|
);
|
||||||
|
|
||||||
export default class extends FlipperPlugin<State, any, PersistedState> {
|
export function plugin(client: PluginClient<Events, Methods>) {
|
||||||
static keyboardActions: Array<DefaultKeyboardAction> = ['clear'];
|
const networkRouteManager = createState<NetworkRouteManager>(
|
||||||
static subscribed = [];
|
nullNetworkRouteManager,
|
||||||
static defaultPersistedState: PersistedState = {
|
);
|
||||||
requests: {},
|
|
||||||
responses: {},
|
const selectedIds = createState<Array<RequestId>>([]);
|
||||||
partialResponses: {},
|
const searchTerm = createState<string>('');
|
||||||
|
const routes = createState<{[id: string]: Route}>({});
|
||||||
|
const nextRouteId = createState<number>(0);
|
||||||
|
const isMockResponseSupported = createState<boolean>(false);
|
||||||
|
const showMockResponseDialog = createState<boolean>(false);
|
||||||
|
const detailBodyFormat = createState<string>(BodyOptions.parsed);
|
||||||
|
const highlightedRows = createState<Set<string> | null | undefined>(
|
||||||
|
new Set(),
|
||||||
|
);
|
||||||
|
const isDeeplinked = createState<boolean>(false);
|
||||||
|
const requests = createState<{[id: string]: Request}>(
|
||||||
|
{},
|
||||||
|
{persist: 'requests'},
|
||||||
|
);
|
||||||
|
const responses = createState<{[id: string]: Response}>(
|
||||||
|
{},
|
||||||
|
{persist: 'responses'},
|
||||||
|
);
|
||||||
|
|
||||||
|
const partialResponses = createState<{
|
||||||
|
[id: string]: {
|
||||||
|
initialResponse?: Response;
|
||||||
|
followupChunks: {[id: number]: string};
|
||||||
};
|
};
|
||||||
networkRouteManager: NetworkRouteManager = nullNetworkRouteManager;
|
}>({}, {persist: 'partialResponses'});
|
||||||
|
|
||||||
static metricsReducer(persistedState: PersistedState) {
|
client.onDeepLink((payload: unknown) => {
|
||||||
const failures = Object.values(persistedState.responses).reduce(function (
|
if (typeof payload === 'string') {
|
||||||
previous,
|
parseDeepLinkPayload(payload);
|
||||||
values,
|
isDeeplinked.set(true);
|
||||||
) {
|
|
||||||
return previous + (values.status >= 400 ? 1 : 0);
|
|
||||||
},
|
|
||||||
0);
|
|
||||||
return Promise.resolve({NUMBER_NETWORK_FAILURES: failures});
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
static persistedStateReducer(
|
client.addMenuEntry({
|
||||||
persistedState: PersistedState,
|
action: 'clear',
|
||||||
method: string,
|
handler: clearLogs,
|
||||||
data: Request | Response | ResponseFollowupChunk,
|
|
||||||
) {
|
|
||||||
switch (method) {
|
|
||||||
case 'newRequest':
|
|
||||||
return Object.assign({}, persistedState, {
|
|
||||||
requests: {...persistedState.requests, [data.id]: data as Request},
|
|
||||||
});
|
});
|
||||||
case 'newResponse':
|
|
||||||
const response: Response = data as Response;
|
client.onConnect(() => {
|
||||||
return Object.assign({}, persistedState, {
|
init();
|
||||||
responses: {
|
|
||||||
...persistedState.responses,
|
|
||||||
[response.id]: response,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
case 'partialResponse':
|
|
||||||
|
client.onDeactivate(() => {
|
||||||
|
isDeeplinked.set(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.onMessage('newRequest', (data) => {
|
||||||
|
requests.update((draft) => {
|
||||||
|
draft[data.id] = data;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
client.onMessage('newResponse', (data) => {
|
||||||
|
responses.update((draft) => {
|
||||||
|
draft[data.id] = data;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
client.onMessage('partialResponse', (data) => {
|
||||||
/* Some clients (such as low end Android devices) struggle to serialise large payloads in one go, so partial responses allow them
|
/* Some clients (such as low end Android devices) struggle to serialise large payloads in one go, so partial responses allow them
|
||||||
to split payloads into chunks and serialise each individually.
|
to split payloads into chunks and serialise each individually.
|
||||||
|
|
||||||
@@ -205,60 +226,47 @@ export default class extends FlipperPlugin<State, any, PersistedState> {
|
|||||||
if (message.index !== undefined && message.index > 0) {
|
if (message.index !== undefined && message.index > 0) {
|
||||||
// It's a follow up chunk
|
// It's a follow up chunk
|
||||||
const followupChunk: ResponseFollowupChunk = message as ResponseFollowupChunk;
|
const followupChunk: ResponseFollowupChunk = message as ResponseFollowupChunk;
|
||||||
const partialResponseEntry = persistedState.partialResponses[
|
const partialResponseEntry = partialResponses.get()[followupChunk.id] ?? {
|
||||||
followupChunk.id
|
followupChunks: {},
|
||||||
] ?? {followupChunks: []};
|
|
||||||
const newPartialResponseEntry = {
|
|
||||||
...partialResponseEntry,
|
|
||||||
followupChunks: {
|
|
||||||
...partialResponseEntry.followupChunks,
|
|
||||||
[followupChunk.index]: followupChunk.data,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
const newPersistedState = {
|
|
||||||
...persistedState,
|
const newPartialResponseEntry = produce(partialResponseEntry, (draft) => {
|
||||||
partialResponses: {
|
draft.followupChunks[followupChunk.index] = followupChunk.data;
|
||||||
...persistedState.partialResponses,
|
});
|
||||||
|
const newPartialResponse = {
|
||||||
|
...partialResponses.get(),
|
||||||
[followupChunk.id]: newPartialResponseEntry,
|
[followupChunk.id]: newPartialResponseEntry,
|
||||||
},
|
|
||||||
};
|
};
|
||||||
return this.assembleChunksIfResponseIsComplete(
|
|
||||||
newPersistedState,
|
assembleChunksIfResponseIsComplete(newPartialResponse, followupChunk.id);
|
||||||
followupChunk.id,
|
return;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
// It's an initial chunk
|
// It's an initial chunk
|
||||||
const partialResponse: Response = message as Response;
|
const partialResponse: Response = message as Response;
|
||||||
const partialResponseEntry = persistedState.partialResponses[
|
const partialResponseEntry = partialResponses.get()[partialResponse.id] ?? {
|
||||||
partialResponse.id
|
|
||||||
] ?? {
|
|
||||||
followupChunks: {},
|
followupChunks: {},
|
||||||
};
|
};
|
||||||
const newPartialResponseEntry = {
|
const newPartialResponseEntry = {
|
||||||
...partialResponseEntry,
|
...partialResponseEntry,
|
||||||
initialResponse: partialResponse,
|
initialResponse: partialResponse,
|
||||||
};
|
};
|
||||||
const newPersistedState = {
|
const newPartialResponse = {
|
||||||
...persistedState,
|
...partialResponses.get(),
|
||||||
partialResponses: {
|
|
||||||
...persistedState.partialResponses,
|
|
||||||
[partialResponse.id]: newPartialResponseEntry,
|
[partialResponse.id]: newPartialResponseEntry,
|
||||||
},
|
|
||||||
};
|
};
|
||||||
return this.assembleChunksIfResponseIsComplete(
|
assembleChunksIfResponseIsComplete(newPartialResponse, partialResponse.id);
|
||||||
newPersistedState,
|
});
|
||||||
partialResponse.id,
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return persistedState;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static assembleChunksIfResponseIsComplete(
|
function assembleChunksIfResponseIsComplete(
|
||||||
persistedState: PersistedState,
|
partialResp: {
|
||||||
|
[id: string]: {
|
||||||
|
initialResponse?: Response;
|
||||||
|
followupChunks: {[id: number]: string};
|
||||||
|
};
|
||||||
|
},
|
||||||
responseId: string,
|
responseId: string,
|
||||||
): PersistedState {
|
) {
|
||||||
const partialResponseEntry = persistedState.partialResponses[responseId];
|
const partialResponseEntry = partialResp[responseId];
|
||||||
const numChunks = partialResponseEntry.initialResponse?.totalChunks;
|
const numChunks = partialResponseEntry.initialResponse?.totalChunks;
|
||||||
if (
|
if (
|
||||||
!partialResponseEntry.initialResponse ||
|
!partialResponseEntry.initialResponse ||
|
||||||
@@ -266,7 +274,8 @@ export default class extends FlipperPlugin<State, any, PersistedState> {
|
|||||||
Object.keys(partialResponseEntry.followupChunks).length + 1 < numChunks
|
Object.keys(partialResponseEntry.followupChunks).length + 1 < numChunks
|
||||||
) {
|
) {
|
||||||
// Partial response not yet complete, do nothing.
|
// Partial response not yet complete, do nothing.
|
||||||
return persistedState;
|
partialResponses.set(partialResp);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
// Partial response has all required chunks, convert it to a full Response.
|
// Partial response has all required chunks, convert it to a full Response.
|
||||||
|
|
||||||
@@ -289,128 +298,65 @@ export default class extends FlipperPlugin<State, any, PersistedState> {
|
|||||||
data: btoa(data),
|
data: btoa(data),
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
responses.update((draft) => {
|
||||||
...persistedState,
|
draft[newResponse.id] = newResponse;
|
||||||
responses: {
|
});
|
||||||
...persistedState.responses,
|
|
||||||
[newResponse.id]: newResponse,
|
partialResponses.update((draft) => {
|
||||||
},
|
delete draft[newResponse.id];
|
||||||
partialResponses: Object.fromEntries(
|
});
|
||||||
Object.entries(persistedState.partialResponses).filter(
|
|
||||||
([k, _v]: [string, unknown]) => k !== newResponse.id,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static deserializePersistedState = (serializedString: string) => {
|
function init() {
|
||||||
return JSON.parse(serializedString);
|
client.supportsMethod('mockResponses').then((result) => {
|
||||||
};
|
const newRoutes = JSON.parse(
|
||||||
|
|
||||||
static getActiveNotifications(persistedState: PersistedState) {
|
|
||||||
const responses = persistedState
|
|
||||||
? persistedState.responses || new Map()
|
|
||||||
: new Map();
|
|
||||||
const r: Array<Response> = Object.values(responses);
|
|
||||||
return (
|
|
||||||
r
|
|
||||||
// Show error messages for all status codes indicating a client or server error
|
|
||||||
.filter((response: Response) => response.status >= 400)
|
|
||||||
.map((response: Response) => {
|
|
||||||
const request = persistedState.requests[response.id];
|
|
||||||
const url: string = (request && request.url) || '(URL missing)';
|
|
||||||
return {
|
|
||||||
id: response.id,
|
|
||||||
title: `HTTP ${response.status}: Network request failed`,
|
|
||||||
message: `Request to ${url} failed. ${response.reason}`,
|
|
||||||
severity: 'error' as 'error',
|
|
||||||
timestamp: response.timestamp,
|
|
||||||
category: `HTTP${response.status}`,
|
|
||||||
action: response.id,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props: any) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
selectedIds: [],
|
|
||||||
searchTerm: '',
|
|
||||||
routes: {},
|
|
||||||
nextRouteId: 0,
|
|
||||||
isMockResponseSupported: false,
|
|
||||||
showMockResponseDialog: false,
|
|
||||||
detailBodyFormat: BodyOptions.parsed,
|
|
||||||
highlightedRows: new Set(),
|
|
||||||
requests: {},
|
|
||||||
responses: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this.client.supportsMethod('mockResponses').then((result) => {
|
|
||||||
const routes = JSON.parse(
|
|
||||||
localStorage.getItem(LOCALSTORAGE_MOCK_ROUTE_LIST_KEY) || '{}',
|
localStorage.getItem(LOCALSTORAGE_MOCK_ROUTE_LIST_KEY) || '{}',
|
||||||
);
|
);
|
||||||
this.setState({
|
routes.set(newRoutes);
|
||||||
routes: routes,
|
isMockResponseSupported.set(result);
|
||||||
isMockResponseSupported: result,
|
showMockResponseDialog.set(false);
|
||||||
showMockResponseDialog: false,
|
nextRouteId.set(Object.keys(routes).length);
|
||||||
nextRouteId: Object.keys(routes).length,
|
|
||||||
});
|
|
||||||
informClientMockChange(routes);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setState(this.parseDeepLinkPayload(this.props.deepLinkPayload));
|
informClientMockChange(routes.get());
|
||||||
|
});
|
||||||
|
|
||||||
// declare new variable to be called inside the interface
|
// declare new variable to be called inside the interface
|
||||||
const setState = this.setState.bind(this);
|
networkRouteManager.set({
|
||||||
const informClientMockChange = this.informClientMockChange.bind(this);
|
|
||||||
this.networkRouteManager = {
|
|
||||||
addRoute() {
|
addRoute() {
|
||||||
setState(
|
const newNextRouteId = nextRouteId.get();
|
||||||
produce((draftState: State) => {
|
routes.update((draft) => {
|
||||||
const nextRouteId = draftState.nextRouteId;
|
draft[newNextRouteId.toString()] = {
|
||||||
draftState.routes[nextRouteId.toString()] = {
|
|
||||||
requestUrl: '',
|
requestUrl: '',
|
||||||
requestMethod: 'GET',
|
requestMethod: 'GET',
|
||||||
responseData: '',
|
responseData: '',
|
||||||
responseHeaders: {},
|
responseHeaders: {},
|
||||||
responseStatus: '200',
|
responseStatus: '200',
|
||||||
};
|
};
|
||||||
draftState.nextRouteId = nextRouteId + 1;
|
});
|
||||||
}),
|
nextRouteId.set(newNextRouteId + 1);
|
||||||
);
|
|
||||||
},
|
},
|
||||||
modifyRoute(id: string, routeChange: Partial<Route>) {
|
modifyRoute(id: string, routeChange: Partial<Route>) {
|
||||||
setState(
|
if (!routes.get().hasOwnProperty(id)) {
|
||||||
produce((draftState: State) => {
|
|
||||||
if (!draftState.routes.hasOwnProperty(id)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
draftState.routes[id] = {...draftState.routes[id], ...routeChange};
|
routes.update((draft) => {
|
||||||
informClientMockChange(draftState.routes);
|
Object.assign(draft[id], routeChange);
|
||||||
}),
|
});
|
||||||
);
|
informClientMockChange(routes.get());
|
||||||
},
|
},
|
||||||
removeRoute(id: string) {
|
removeRoute(id: string) {
|
||||||
setState(
|
if (routes.get().hasOwnProperty(id)) {
|
||||||
produce((draftState: State) => {
|
routes.update((draft) => {
|
||||||
if (draftState.routes.hasOwnProperty(id)) {
|
delete draft[id];
|
||||||
delete draftState.routes[id];
|
});
|
||||||
}
|
}
|
||||||
informClientMockChange(draftState.routes);
|
informClientMockChange(routes.get());
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
copyHighlightedCalls(
|
copyHighlightedCalls(
|
||||||
highlightedRows: Set<string> | null | undefined,
|
highlightedRows: Set<string> | null | undefined,
|
||||||
requests: {[id: string]: Request},
|
requests: {[id: string]: Request},
|
||||||
responses: {[id: string]: Response},
|
responses: {[id: string]: Response},
|
||||||
) {
|
) {
|
||||||
setState((state) => {
|
|
||||||
const nextState = produce(state, (state: State) => {
|
|
||||||
// iterate through highlighted rows
|
// iterate through highlighted rows
|
||||||
highlightedRows?.forEach((row) => {
|
highlightedRows?.forEach((row) => {
|
||||||
const response = responses[row];
|
const response = responses[row];
|
||||||
@@ -424,78 +370,59 @@ export default class extends FlipperPlugin<State, any, PersistedState> {
|
|||||||
const responseData =
|
const responseData =
|
||||||
response && response.data ? decodeBody(response) : null;
|
response && response.data ? decodeBody(response) : null;
|
||||||
|
|
||||||
const nextRouteId = state.nextRouteId;
|
const newNextRouteId = nextRouteId.get();
|
||||||
state.routes[nextRouteId.toString()] = {
|
routes.update((draft) => {
|
||||||
|
draft[newNextRouteId.toString()] = {
|
||||||
requestUrl: requests[row].url,
|
requestUrl: requests[row].url,
|
||||||
requestMethod: requests[row].method,
|
requestMethod: requests[row].method,
|
||||||
responseData: responseData as string,
|
responseData: responseData as string,
|
||||||
responseHeaders: headers,
|
responseHeaders: headers,
|
||||||
responseStatus: responses[row].status.toString(),
|
responseStatus: responses[row].status.toString(),
|
||||||
};
|
};
|
||||||
state.nextRouteId = nextRouteId + 1;
|
|
||||||
});
|
});
|
||||||
|
nextRouteId.set(newNextRouteId + 1);
|
||||||
});
|
});
|
||||||
informClientMockChange(nextState.routes);
|
|
||||||
return nextState;
|
informClientMockChange(routes.get());
|
||||||
});
|
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
teardown() {}
|
function parseDeepLinkPayload(deepLinkPayload: unknown) {
|
||||||
|
|
||||||
onKeyboardAction = (action: string) => {
|
|
||||||
if (action === 'clear') {
|
|
||||||
this.clearLogs();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
parseDeepLinkPayload = (
|
|
||||||
deepLinkPayload: unknown,
|
|
||||||
): Pick<State, 'selectedIds' | 'searchTerm'> => {
|
|
||||||
const searchTermDelim = 'searchTerm=';
|
const searchTermDelim = 'searchTerm=';
|
||||||
if (typeof deepLinkPayload !== 'string') {
|
if (typeof deepLinkPayload !== 'string') {
|
||||||
return {
|
selectedIds.set([]);
|
||||||
selectedIds: [],
|
searchTerm.set('');
|
||||||
searchTerm: '',
|
|
||||||
};
|
|
||||||
} else if (deepLinkPayload.startsWith(searchTermDelim)) {
|
} else if (deepLinkPayload.startsWith(searchTermDelim)) {
|
||||||
return {
|
selectedIds.set([]);
|
||||||
selectedIds: [],
|
searchTerm.set(deepLinkPayload.slice(searchTermDelim.length));
|
||||||
searchTerm: deepLinkPayload.slice(searchTermDelim.length),
|
} else {
|
||||||
};
|
selectedIds.set([deepLinkPayload]);
|
||||||
|
searchTerm.set('');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return {
|
|
||||||
selectedIds: [deepLinkPayload],
|
|
||||||
searchTerm: '',
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
onRowHighlighted = (selectedIds: Array<RequestId>) =>
|
function clearLogs() {
|
||||||
this.setState({selectedIds});
|
selectedIds.set([]);
|
||||||
|
responses.set({});
|
||||||
|
requests.set({});
|
||||||
|
}
|
||||||
|
|
||||||
copyRequestCurlCommand = () => {
|
function copyRequestCurlCommand() {
|
||||||
const {requests} = this.props.persistedState;
|
|
||||||
const {selectedIds} = this.state;
|
|
||||||
// Ensure there is only one row highlighted.
|
// Ensure there is only one row highlighted.
|
||||||
if (selectedIds.length !== 1) {
|
if (selectedIds.get().length !== 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const request = requests[selectedIds[0]];
|
const request = requests.get()[selectedIds.get()[0]];
|
||||||
if (!request) {
|
if (!request) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const command = convertRequestToCurlCommand(request);
|
const command = convertRequestToCurlCommand(request);
|
||||||
clipboard.writeText(command);
|
clipboard.writeText(command);
|
||||||
};
|
}
|
||||||
|
|
||||||
clearLogs = () => {
|
async function informClientMockChange(routes: {[id: string]: Route}) {
|
||||||
this.setState({selectedIds: []});
|
|
||||||
this.props.setPersistedState({responses: {}, requests: {}});
|
|
||||||
};
|
|
||||||
|
|
||||||
informClientMockChange = (routes: {[id: string]: Route}) => {
|
|
||||||
const existedIdSet: {[id: string]: {[method: string]: boolean}} = {};
|
const existedIdSet: {[id: string]: {[method: string]: boolean}} = {};
|
||||||
const filteredRoutes: {[id: string]: Route} = Object.entries(routes).reduce(
|
const filteredRoutes: {[id: string]: Route} = Object.entries(routes).reduce(
|
||||||
(accRoutes, [id, route]) => {
|
(accRoutes, [id, route]) => {
|
||||||
@@ -520,13 +447,15 @@ export default class extends FlipperPlugin<State, any, PersistedState> {
|
|||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.state.isMockResponseSupported) {
|
if (isMockResponseSupported.get()) {
|
||||||
const routesValuesArray = Object.values(filteredRoutes);
|
const routesValuesArray = Object.values(filteredRoutes);
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
LOCALSTORAGE_MOCK_ROUTE_LIST_KEY,
|
LOCALSTORAGE_MOCK_ROUTE_LIST_KEY,
|
||||||
JSON.stringify(routesValuesArray),
|
JSON.stringify(routesValuesArray),
|
||||||
);
|
);
|
||||||
this.client.call('mockResponses', {
|
|
||||||
|
try {
|
||||||
|
await client.send('mockResponses', {
|
||||||
routes: routesValuesArray.map((route: Route) => ({
|
routes: routesValuesArray.map((route: Route) => ({
|
||||||
requestUrl: route.requestUrl,
|
requestUrl: route.requestUrl,
|
||||||
method: route.requestMethod,
|
method: route.requestMethod,
|
||||||
@@ -535,76 +464,79 @@ export default class extends FlipperPlugin<State, any, PersistedState> {
|
|||||||
status: route.responseStatus,
|
status: route.responseStatus,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to mock responses.', e);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
onMockButtonPressed = () => {
|
|
||||||
this.setState({showMockResponseDialog: true});
|
|
||||||
};
|
|
||||||
|
|
||||||
onCloseButtonPressed = () => {
|
|
||||||
this.setState({showMockResponseDialog: false});
|
|
||||||
};
|
|
||||||
|
|
||||||
onSelectFormat = (bodyFormat: string) => {
|
|
||||||
this.setState({detailBodyFormat: bodyFormat});
|
|
||||||
};
|
|
||||||
|
|
||||||
renderSidebar = () => {
|
|
||||||
const {requests, responses} = this.props.persistedState;
|
|
||||||
const {selectedIds, detailBodyFormat} = this.state;
|
|
||||||
const selectedId = selectedIds.length === 1 ? selectedIds[0] : null;
|
|
||||||
|
|
||||||
if (!selectedId) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
const requestWithId = requests[selectedId];
|
|
||||||
if (!requestWithId) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
return (
|
|
||||||
<RequestDetails
|
|
||||||
key={selectedId}
|
|
||||||
request={requestWithId}
|
|
||||||
response={responses[selectedId]}
|
|
||||||
bodyFormat={detailBodyFormat}
|
|
||||||
onSelectFormat={this.onSelectFormat}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
return {
|
||||||
const {requests, responses} = this.props.persistedState;
|
|
||||||
const {
|
|
||||||
selectedIds,
|
selectedIds,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
routes,
|
routes,
|
||||||
|
nextRouteId,
|
||||||
isMockResponseSupported,
|
isMockResponseSupported,
|
||||||
showMockResponseDialog,
|
showMockResponseDialog,
|
||||||
} = this.state;
|
detailBodyFormat,
|
||||||
|
highlightedRows,
|
||||||
|
isDeeplinked,
|
||||||
|
requests,
|
||||||
|
responses,
|
||||||
|
partialResponses,
|
||||||
|
networkRouteManager,
|
||||||
|
clearLogs,
|
||||||
|
onRowHighlighted(selectedIdsArr: Array<RequestId>) {
|
||||||
|
selectedIds.set(selectedIdsArr);
|
||||||
|
},
|
||||||
|
onMockButtonPressed() {
|
||||||
|
showMockResponseDialog.set(true);
|
||||||
|
},
|
||||||
|
onCloseButtonPressed() {
|
||||||
|
showMockResponseDialog.set(false);
|
||||||
|
},
|
||||||
|
onSelectFormat(bodyFormat: string) {
|
||||||
|
detailBodyFormat.set(bodyFormat);
|
||||||
|
},
|
||||||
|
copyRequestCurlCommand,
|
||||||
|
init,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Component() {
|
||||||
|
const instance = usePlugin(plugin);
|
||||||
|
|
||||||
|
const requests = useValue(instance.requests);
|
||||||
|
const responses = useValue(instance.responses);
|
||||||
|
const selectedIds = useValue(instance.selectedIds);
|
||||||
|
const searchTerm = useValue(instance.searchTerm);
|
||||||
|
const routes = useValue(instance.routes);
|
||||||
|
const isMockResponseSupported = useValue(instance.isMockResponseSupported);
|
||||||
|
const showMockResponseDialog = useValue(instance.showMockResponseDialog);
|
||||||
|
const networkRouteManager = useValue(instance.networkRouteManager);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FlexColumn grow={true}>
|
<FlexColumn grow={true}>
|
||||||
<NetworkRouteContext.Provider value={this.networkRouteManager}>
|
<NetworkRouteContext.Provider value={networkRouteManager}>
|
||||||
<NetworkTable
|
<NetworkTable
|
||||||
requests={requests || {}}
|
requests={requests || {}}
|
||||||
responses={responses || {}}
|
responses={responses || {}}
|
||||||
routes={routes}
|
routes={routes}
|
||||||
onMockButtonPressed={this.onMockButtonPressed}
|
onMockButtonPressed={instance.onMockButtonPressed}
|
||||||
onCloseButtonPressed={this.onCloseButtonPressed}
|
onCloseButtonPressed={instance.onCloseButtonPressed}
|
||||||
showMockResponseDialog={showMockResponseDialog}
|
showMockResponseDialog={showMockResponseDialog}
|
||||||
clear={this.clearLogs}
|
clear={instance.clearLogs}
|
||||||
copyRequestCurlCommand={this.copyRequestCurlCommand}
|
copyRequestCurlCommand={instance.copyRequestCurlCommand}
|
||||||
onRowHighlighted={this.onRowHighlighted}
|
onRowHighlighted={instance.onRowHighlighted}
|
||||||
highlightedRows={selectedIds ? new Set(selectedIds) : null}
|
highlightedRows={selectedIds ? new Set(selectedIds) : null}
|
||||||
searchTerm={searchTerm}
|
searchTerm={searchTerm}
|
||||||
isMockResponseSupported={isMockResponseSupported}
|
isMockResponseSupported={isMockResponseSupported}
|
||||||
/>
|
/>
|
||||||
<DetailSidebar width={500}>{this.renderSidebar()}</DetailSidebar>
|
<DetailSidebar width={500}>
|
||||||
|
<Sidebar />
|
||||||
|
</DetailSidebar>
|
||||||
</NetworkRouteContext.Provider>
|
</NetworkRouteContext.Provider>
|
||||||
</FlexColumn>
|
</FlexColumn>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type NetworkTableProps = {
|
type NetworkTableProps = {
|
||||||
@@ -809,6 +741,33 @@ function calculateState(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Sidebar() {
|
||||||
|
const instance = usePlugin(plugin);
|
||||||
|
const requests = useValue(instance.requests);
|
||||||
|
const responses = useValue(instance.responses);
|
||||||
|
const selectedIds = useValue(instance.selectedIds);
|
||||||
|
const detailBodyFormat = useValue(instance.detailBodyFormat);
|
||||||
|
const selectedId = selectedIds.length === 1 ? selectedIds[0] : null;
|
||||||
|
|
||||||
|
if (!selectedId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const requestWithId = requests[selectedId];
|
||||||
|
if (!requestWithId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RequestDetails
|
||||||
|
key={selectedId}
|
||||||
|
request={requestWithId}
|
||||||
|
response={responses[selectedId]}
|
||||||
|
bodyFormat={detailBodyFormat}
|
||||||
|
onSelectFormat={instance.onSelectFormat}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
class NetworkTable extends PureComponent<NetworkTableProps, NetworkTableState> {
|
class NetworkTable extends PureComponent<NetworkTableProps, NetworkTableState> {
|
||||||
static ContextMenu = styled(ContextMenu)({
|
static ContextMenu = styled(ContextMenu)({
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|||||||
@@ -21,6 +21,10 @@
|
|||||||
"pako": "^1.0.11",
|
"pako": "^1.0.11",
|
||||||
"xml-beautifier": "^0.4.0"
|
"xml-beautifier": "^0.4.0"
|
||||||
},
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"flipper": "0.59.0",
|
||||||
|
"flipper-plugin": "0.59.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/pako": "^1.0.1"
|
"@types/pako": "^1.0.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,14 @@ export type Route = {
|
|||||||
responseStatus: string;
|
responseStatus: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type MockRoute = {
|
||||||
|
requestUrl: string;
|
||||||
|
method: string;
|
||||||
|
data: string;
|
||||||
|
headers: Header[];
|
||||||
|
status: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type PersistedState = {
|
export type PersistedState = {
|
||||||
requests: {[id: string]: Request};
|
requests: {[id: string]: Request};
|
||||||
responses: {[id: string]: Response};
|
responses: {[id: string]: Response};
|
||||||
|
|||||||
Reference in New Issue
Block a user