Summary: Fix selection problem on ManageMockResponsePanel. Currently, when the user adds new routes, the detail section continues to show the detail for the first route. Select different routes in the list does not change the detail panel. The screen is currently not doing anything on selection. Also fixed a problem with updating on mock header names and values. Updates were not persisting if user did not exist input fields before leaving header input panel. ## Changelog Network Plugin - Fix selection problem on ManageMockResponsePanel Pull Request resolved: https://github.com/facebook/flipper/pull/2574 Test Plan: Add multiple mock routes Select various routes Verify that detail panel shows the selected route  Reviewed By: passy Differential Revision: D29582286 Pulled By: mweststrate fbshipit-source-id: 6690e2262a033cdfa5afa6e6fdfacc9694244590
220 lines
5.9 KiB
TypeScript
220 lines
5.9 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 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);
|
|
props.onUpdate(event.target.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>
|
|
);
|
|
}
|