web/src/pages/Route/components/DebugViews/DebugDrawView.tsx (494 lines of code) (raw):

/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { CopyOutlined } from '@ant-design/icons'; import type { Monaco } from '@monaco-editor/react'; import Editor from '@monaco-editor/react'; import { Button, Card, Drawer, Form, Input, notification, Radio, Select, Spin, Tabs } from 'antd'; import Base64 from 'base-64'; import type * as monacoEditor from 'monaco-editor'; import queryString from 'query-string'; import React, { useEffect, useState } from 'react'; import CopyToClipboard from 'react-copy-to-clipboard'; import { useIntl } from 'umi'; import PanelSection from '@/components/PanelSection'; import { DEBUG_BODY_MODE_SUPPORTED, DEBUG_BODY_TYPE_SUPPORTED, DEBUG_RESPONSE_BODY_MODE_SUPPORTED, DebugBodyFormDataValueType, DEFAULT_DEBUG_AUTH_FORM_DATA, DEFAULT_DEBUG_PARAM_FORM_DATA, HTTP_METHOD_OPTION_LIST, PROTOCOL_SUPPORTED, } from '../../constants'; import { debugRoute } from '../../service'; import { AuthenticationView, DebugFormDataView, DebugParamsView } from '.'; import styles from './index.less'; const { Option } = Select; const { Search } = Input; const { TabPane } = Tabs; const DebugDrawView: React.FC<RouteModule.DebugDrawProps> = (props) => { const { formatMessage } = useIntl(); const [httpMethod, setHttpMethod] = useState(HTTP_METHOD_OPTION_LIST[0]); const [requestProtocol, setRequestProtocol] = useState(PROTOCOL_SUPPORTED[0]); const [showBodyTab, setShowBodyTab] = useState(false); const [queryForm] = Form.useForm(); const [urlencodedForm] = Form.useForm(); const [formDataForm] = Form.useForm(); const [authForm] = Form.useForm(); const [headerForm] = Form.useForm(); const [response, setResponse] = useState<RouteModule.debugResponse | null>(); const [loading, setLoading] = useState(false); const [body, setBody] = useState(''); const [height, setHeight] = useState(50); const [bodyType, setBodyType] = useState('none'); const methodWithoutBody = ['GET', 'HEAD']; const [responseBodyMode, setResponseBodyMode] = useState( DEBUG_RESPONSE_BODY_MODE_SUPPORTED[0].mode, ); const [bodyMode, setBodyCodeMode] = useState(DEBUG_BODY_MODE_SUPPORTED[0].mode); enum DebugBodyType { None = 0, FormUrlencoded, FormData, RawInput, } const resetForms = () => { queryForm.setFieldsValue(DEFAULT_DEBUG_PARAM_FORM_DATA); urlencodedForm.setFieldsValue(DEFAULT_DEBUG_PARAM_FORM_DATA); formDataForm.setFieldsValue(DEFAULT_DEBUG_PARAM_FORM_DATA); headerForm.setFieldsValue(DEFAULT_DEBUG_PARAM_FORM_DATA); authForm.setFieldsValue(DEFAULT_DEBUG_AUTH_FORM_DATA); setResponse(null); setBodyType(DEBUG_BODY_TYPE_SUPPORTED[DebugBodyType.None]); }; useEffect(() => { resetForms(); }, []); const transformBodyParamsFormData = () => { if (methodWithoutBody.includes(httpMethod)) { return { bodyFormData: undefined, }; } switch (bodyType) { case DEBUG_BODY_TYPE_SUPPORTED[DebugBodyType.FormUrlencoded]: { let transformFormUrlencoded: string[] = []; const FormUrlencodedData: RouteModule.debugRequestParamsFormData[] = urlencodedForm.getFieldsValue() .params; transformFormUrlencoded = (FormUrlencodedData || []) .filter((data) => data && data.check) .map((data) => { return `${data.key}=${data.value}`; }); return { bodyFormData: transformFormUrlencoded.join('&'), header: { 'Content-type': ['application/x-www-form-urlencoded;charset=UTF-8'], }, }; } case DEBUG_BODY_TYPE_SUPPORTED[DebugBodyType.RawInput]: { let contentType = ['']; switch (bodyMode) { case DEBUG_BODY_MODE_SUPPORTED[0].mode: contentType = ['application/json;charset=UTF-8']; break; case DEBUG_BODY_MODE_SUPPORTED[1].mode: contentType = ['text/plain;charset=UTF-8']; break; case DEBUG_BODY_MODE_SUPPORTED[2].mode: contentType = ['application/xml;charset=UTF-8']; break; default: break; } return { bodyFormData: body, header: { 'Content-type': contentType, }, }; } case DEBUG_BODY_TYPE_SUPPORTED[DebugBodyType.FormData]: { const transformFormData = new FormData(); const formDataData: RouteModule.debugRequestParamsFormData[] = formDataForm.getFieldsValue() .params; (formDataData || []) .filter((data) => data && data.check) .forEach((data) => { if (data.type === DebugBodyFormDataValueType.File) { transformFormData.append(data.key, data.value.originFileObj); } else { transformFormData.append(data.key, data.value); } }); return { bodyFormData: transformFormData, }; } case DEBUG_BODY_TYPE_SUPPORTED[DebugBodyType.None]: default: return { bodyFormData: undefined, }; } }; const transformHeaderAndQueryParamsFormData = ( formData: RouteModule.debugRequestParamsFormData[], ) => { let transformData = {}; (formData || []) .filter((data) => data && data.check) .forEach((data) => { transformData = { ...transformData, [data.key]: [...(transformData[data.key] || []), data.value], }; }); return transformData; }; const transformAuthFormData = ( formData: RouteModule.authData, userHeaderData: any, formHeaderData: any, ) => { const { authType } = formData; switch (authType) { case 'basic-auth': return { ...formHeaderData, ...userHeaderData, Authorization: [`Basic ${Base64.encode(`${formData.username}:${formData.password}`)}`], }; case 'jwt-auth': return { ...formHeaderData, ...userHeaderData, Authorization: [formData.Authorization], }; case 'key-auth': return { ...formHeaderData, ...userHeaderData, apikey: [formData.apikey], }; default: break; } return { ...formHeaderData, ...userHeaderData, }; }; const handleDebug = (url: string) => { const queryFormData = transformHeaderAndQueryParamsFormData(queryForm.getFieldsValue().params); const bodyFormRelateData = transformBodyParamsFormData(); const { bodyFormData, header: bodyFormHeader } = bodyFormRelateData; const pureHeaderFormData = transformHeaderAndQueryParamsFormData( headerForm.getFieldsValue().params, ); const headerFormData = transformAuthFormData( authForm.getFieldsValue(), pureHeaderFormData, bodyFormHeader, ); const urlQueryString = url.indexOf('?') === -1 ? `?${queryString.stringify(queryFormData)}` : `&${queryString.stringify(queryFormData)}`; setLoading(true); // TODO: grpc and websocket debugRoute( { online_debug_header_params: JSON.stringify(headerFormData), online_debug_url: `${requestProtocol}://${url}${urlQueryString}`, online_debug_request_protocol: requestProtocol, online_debug_method: httpMethod, }, bodyFormData, ) .then((req) => { setLoading(false); const resp: RouteModule.debugResponse = req.data; if (typeof resp.data !== 'string') { resp.data = JSON.stringify(resp.data, null, 2); } setResponse(resp); const contentType = resp.header['Content-Type']; if (contentType == null || contentType.length !== 1) { setResponseBodyMode('TEXT'); } else if (contentType[0].toLowerCase().indexOf('json') !== -1) { setResponseBodyMode('JSON'); } else if (contentType[0].toLowerCase().indexOf('xml') !== -1) { setResponseBodyMode('XML'); } else if (contentType[0].toLowerCase().indexOf('html') !== -1) { setResponseBodyMode('HTML'); } else { setResponseBodyMode('TEXT'); } }) .catch(() => { setLoading(false); }); }; const handleEditorMount = (editor: monacoEditor.editor.IStandaloneCodeEditor, monaco: Monaco) => { editor.onDidChangeModelDecorations(() => { if (!editor.getDomNode()) { return; } const padding = 40; const lineHeight = editor.getOption(monaco.editor.EditorOption.lineHeight); const lineCount = editor.getModel()?.getLineCount() || 1; setHeight(editor.getTopForLineNumber(lineCount + 1) + lineHeight + padding); }); }; return ( <Drawer title={formatMessage({ id: 'page.route.onlineDebug' })} mask={false} maskClosable={false} visible={props.visible} width={650} onClose={() => { props.onClose(); }} className={styles.routeDebugDraw} data-cy="debug-draw" > <Card bordered={false}> <Input.Group compact> <Select defaultValue={httpMethod} style={{ width: '20%' }} onChange={(value) => { setHttpMethod(value); setShowBodyTab(!(methodWithoutBody.indexOf(value) > -1)); }} size="large" data-cy="debug-method" > {HTTP_METHOD_OPTION_LIST.map((method) => { return ( <Option key={method} value={method}> {method} </Option> ); })} </Select> <Select defaultValue={requestProtocol} style={{ width: '18%' }} onChange={(value) => { setRequestProtocol(value); }} size="large" data-cy="debug-protocol" > {PROTOCOL_SUPPORTED.map((protocol) => { return ( <Option key={protocol} value={protocol}> {`${protocol}://`} </Option> ); })} </Select> <Search id="debugUri" placeholder={formatMessage({ id: 'page.route.input.placeholder.requestUrl' })} allowClear enterButton={formatMessage({ id: 'page.route.button.send' })} size="large" style={{ width: '62%' }} onSearch={handleDebug} onPressEnter={(e) => { handleDebug(e.currentTarget.value); }} onChange={(e) => { if (e.currentTarget.value === '') { resetForms(); } }} /> </Input.Group> <PanelSection title={formatMessage({ id: 'page.route.PanelSection.title.defineRequestParams' })} > <Tabs> <TabPane data-cy="query" tab={formatMessage({ id: 'page.route.TabPane.queryParams' })} key="query" > <DebugParamsView form={queryForm} name="queryForm" /> </TabPane> <TabPane data-cy="auth" tab={formatMessage({ id: 'page.route.TabPane.authentication' })} key="auth" > <AuthenticationView form={authForm} /> </TabPane> <TabPane data-cy="header" tab={formatMessage({ id: 'page.route.TabPane.headerParams' })} key="header" > <DebugParamsView form={headerForm} name="headerForm" inputType="header" /> </TabPane> {showBodyTab && ( <TabPane data-cy="body" tab={formatMessage({ id: 'page.route.TabPane.bodyParams' })} key="body" > <Radio.Group onChange={(e) => { setBodyType(e.target.value); }} value={bodyType} > {DEBUG_BODY_TYPE_SUPPORTED.map((type) => ( <Radio value={type} key={type}> {type} </Radio> ))} </Radio.Group> {bodyType === DEBUG_BODY_TYPE_SUPPORTED[DebugBodyType.RawInput] && ( <Select size="small" onChange={(value) => { setBodyCodeMode(value); }} style={{ width: 100 }} defaultValue={bodyMode} > {DEBUG_BODY_MODE_SUPPORTED.map((modeObj) => ( <Option key={modeObj.name} value={modeObj.mode}> {modeObj.name} </Option> ))} </Select> )} <div style={{ marginTop: 16 }}> {bodyType === DEBUG_BODY_TYPE_SUPPORTED[DebugBodyType.FormUrlencoded] && ( <DebugParamsView form={urlencodedForm} name="urlencodedForm" /> )} {bodyType === DEBUG_BODY_TYPE_SUPPORTED[DebugBodyType.FormData] && ( <DebugFormDataView form={formDataForm} /> )} {bodyType === DEBUG_BODY_TYPE_SUPPORTED[DebugBodyType.RawInput] && ( <Form> <Form.Item> <Editor value={body} language={bodyMode.toLowerCase()} onChange={(text) => { if (text) { setBody(text); } else { setBody(''); } }} height={250} beforeMount={(monaco) => { monaco?.languages.json.jsonDefaults.setDiagnosticsOptions({ validate: false, }); }} options={{ scrollbar: { vertical: 'hidden', horizontal: 'hidden', }, wordWrap: 'on', minimap: { enabled: false }, }} /> </Form.Item> </Form> )} </div> </TabPane> )} </Tabs> </PanelSection> <PanelSection title={formatMessage({ id: 'page.route.PanelSection.title.responseResult' })}> <Spin tip="Loading..." spinning={loading}> <Tabs tabBarExtraContent={ response ? response.message : formatMessage({ id: 'page.route.debug.showResultAfterSendRequest' }) } > <TabPane tab={formatMessage({ id: 'page.route.TabPane.response' })} key="response"> <Select disabled={response == null} value={responseBodyMode} onSelect={(mode) => setResponseBodyMode(mode as string)} > {DEBUG_RESPONSE_BODY_MODE_SUPPORTED.map((mode) => { return ( <Option value={mode.mode} key={mode.mode}> {mode.name} </Option> ); })} </Select> <CopyToClipboard text={response ? response.data : ''} onCopy={(_: string, result: boolean) => { if (!result) { notification.error({ message: formatMessage({ id: 'component.global.copyFail' }), }); return; } notification.success({ message: formatMessage({ id: 'component.global.copySuccess' }), }); }} > <Button type="text" disabled={!response}> <CopyOutlined /> </Button> </CopyToClipboard> <div id="monaco-response" style={{ marginTop: 16 }}> <Editor value={response ? response.data : ''} height={height} language={responseBodyMode.toLowerCase()} onMount={handleEditorMount} beforeMount={(monaco) => { monaco?.languages.json.jsonDefaults.setDiagnosticsOptions({ validate: false, }); }} options={{ automaticLayout: true, scrollbar: { vertical: 'hidden', horizontal: 'hidden', }, wordWrap: 'on', minimap: { enabled: false }, readOnly: true, }} /> </div> </TabPane> <TabPane tab={formatMessage({ id: 'page.route.TabPane.header' })} key="header"> {response && Object.keys(response.header).map((header) => { return response.header[header].map((value) => { return ( <div> <b>{header}</b>: {value} </div> ); }); })} </TabPane> </Tabs> </Spin> </PanelSection> </Card> </Drawer> ); }; export default DebugDrawView;