This is an automated email from the ASF dual-hosted git repository.
juzhiyuan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-dashboard.git
The following commit(s) were added to refs/heads/master by this push:
new a476ad7 feat: Improve debug view (#1779)
a476ad7 is described below
commit a476ad7f264a1166290fe0bdb397d2d26cfd23c2
Author: qian0817 <[email protected]>
AuthorDate: Tue Apr 20 11:06:58 2021 +0800
feat: Improve debug view (#1779)
---
.../route_online_debug/route_online_debug.go | 32 +++++--
.../route_online_debug/route_online_debug_test.go | 97 +++++++++++++++++++
web/src/components/RawDataEditor/RawDataEditor.tsx | 6 +-
web/src/locales/en-US/component.ts | 4 +-
web/src/locales/zh-CN/component.ts | 4 +-
.../Route/components/DebugViews/DebugDrawView.tsx | 106 ++++++++++++++-------
web/src/pages/Route/constants.ts | 11 ++-
web/src/pages/Route/typing.d.ts | 8 ++
8 files changed, 220 insertions(+), 48 deletions(-)
diff --git a/api/internal/handler/route_online_debug/route_online_debug.go
b/api/internal/handler/route_online_debug/route_online_debug.go
index b7886a9..c65b69b 100644
--- a/api/internal/handler/route_online_debug/route_online_debug.go
+++ b/api/internal/handler/route_online_debug/route_online_debug.go
@@ -18,8 +18,10 @@ package route_online_debug
import (
"bytes"
+ "compress/gzip"
"encoding/json"
"fmt"
+ "io"
"io/ioutil"
"net/http"
"reflect"
@@ -97,23 +99,24 @@ func (h *HTTPProtocolSupport) RequestForwarding(c
droplet.Context) (interface{},
body := input.Body
contentType := input.ContentType
- if url == "" || method == "" {
- return &data.SpecCodeResponse{StatusCode:
http.StatusBadRequest}, fmt.Errorf("parameters error")
- }
+ transport := http.DefaultTransport.(*http.Transport).Clone()
+ transport.DisableCompression = true
- client := &http.Client{}
- client.Timeout = 5 * time.Second
+ client := &http.Client{
+ Transport: transport,
+ Timeout: 5 * time.Second,
+ }
var tempMap map[string][]string
err := json.Unmarshal([]byte(input.HeaderParams), &tempMap)
if err != nil {
- return &data.SpecCodeResponse{StatusCode:
http.StatusInternalServerError}, err
+ return &data.SpecCodeResponse{StatusCode:
http.StatusBadRequest}, fmt.Errorf("can not get header")
}
req, err := http.NewRequest(strings.ToUpper(method), url,
bytes.NewReader(body))
if err != nil {
- return &data.SpecCodeResponse{StatusCode:
http.StatusInternalServerError}, err
+ return &data.SpecCodeResponse{StatusCode:
http.StatusBadRequest}, err
}
req.Header.Add("Content-Type", contentType)
@@ -134,7 +137,20 @@ func (h *HTTPProtocolSupport) RequestForwarding(c
droplet.Context) (interface{},
defer resp.Body.Close()
- _body, err := ioutil.ReadAll(resp.Body)
+ // handle gzip content encoding
+ var reader io.ReadCloser
+ switch resp.Header.Get("Content-Encoding") {
+ case "gzip":
+ reader, err = gzip.NewReader(resp.Body)
+ if err != nil {
+ return &data.SpecCodeResponse{StatusCode:
http.StatusInternalServerError}, err
+ }
+ defer reader.Close()
+ default:
+ reader = resp.Body
+ }
+
+ _body, err := ioutil.ReadAll(reader)
if err != nil {
return &data.SpecCodeResponse{StatusCode:
http.StatusInternalServerError}, err
}
diff --git a/api/internal/handler/route_online_debug/route_online_debug_test.go
b/api/internal/handler/route_online_debug/route_online_debug_test.go
new file mode 100644
index 0000000..a049ef7
--- /dev/null
+++ b/api/internal/handler/route_online_debug/route_online_debug_test.go
@@ -0,0 +1,97 @@
+/*
+ * 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.
+ */
+
+package route_online_debug
+
+import (
+ "compress/gzip"
+ "github.com/shiningrush/droplet"
+ "github.com/shiningrush/droplet/data"
+ "github.com/stretchr/testify/assert"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+)
+
+var TestResponse = "test"
+
+func mockServer() *httptest.Server {
+ f := func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Add("Content-Type", "plain/text")
+ w.Header().Set("Content-Encoding", "gzip")
+ writer, _ := gzip.NewWriterLevel(w, gzip.BestCompression)
+ defer writer.Close()
+ _, _ = writer.Write([]byte(TestResponse))
+ }
+ return httptest.NewServer(http.HandlerFunc(f))
+}
+
+func TestHTTPProtocolSupport_RequestForwarding(t *testing.T) {
+ server := mockServer()
+ defer server.Close()
+ var cases = []struct {
+ Desc string
+ Input *DebugOnlineInput
+ Result interface{}
+ }{
+ {
+ Desc: "unsupported method",
+ Input: &DebugOnlineInput{
+ URL: server.URL,
+ Method: "Lock",
+ },
+ Result: &data.SpecCodeResponse{StatusCode:
http.StatusBadRequest},
+ },
+ {
+ Desc: "wrong url",
+ Input: &DebugOnlineInput{URL: "grpc://localhost"},
+ Result: &data.SpecCodeResponse{StatusCode:
http.StatusBadRequest},
+ },
+ {
+ Desc: "not specify the accept-encoding request header
explicitly",
+ Input: &DebugOnlineInput{
+ URL: server.URL,
+ Method: "Get",
+ HeaderParams: "{}",
+ },
+ Result: TestResponse,
+ },
+ {
+ Desc: "specify the accept-encoding request header
explicitly",
+ Input: &DebugOnlineInput{
+ URL: server.URL,
+ Method: "Get",
+ HeaderParams: `{"Accept-Encoding": ["gzip"]}`,
+ },
+ Result: TestResponse,
+ },
+ }
+ for _, c := range cases {
+ t.Run(c.Desc, func(t *testing.T) {
+ proto := &HTTPProtocolSupport{}
+ context := droplet.NewContext()
+ context.SetInput(c.Input)
+ result, _ := proto.RequestForwarding(context)
+ switch result.(type) {
+ case *Result:
+ assert.Equal(t, result.(*Result).Data,
c.Result.(string))
+ case *data.SpecCodeResponse:
+ assert.Equal(t, result, c.Result)
+ }
+ })
+ }
+}
diff --git a/web/src/components/RawDataEditor/RawDataEditor.tsx
b/web/src/components/RawDataEditor/RawDataEditor.tsx
index 880d00b..8f9ba0d 100644
--- a/web/src/components/RawDataEditor/RawDataEditor.tsx
+++ b/web/src/components/RawDataEditor/RawDataEditor.tsx
@@ -154,19 +154,19 @@ const RawDataEditor: React.FC<Props> = ({ visible,
readonly = true, type, data =
handleModeChange(value);
}}
data-cy='code-mirror-mode'
- ></Select>,
+ />,
<Button type="primary" onClick={formatCodes} key={2}>
{formatMessage({ id: 'component.global.format' })}
</Button>,
<CopyToClipboard text={JSON.stringify(data)} onCopy={(_: string,
result: boolean) => {
if (!result) {
notification.error({
- message: 'Copy Failed',
+ message: formatMessage({ id: 'component.global.copyFail' }),
});
return;
}
notification.success({
- message: 'Copy Successfully',
+ message: formatMessage({ id: 'component.global.copySuccess' }),
});
}}>
<Button type="primary" key={2}>
diff --git a/web/src/locales/en-US/component.ts
b/web/src/locales/en-US/component.ts
index 9c04262..4b9eba9 100644
--- a/web/src/locales/en-US/component.ts
+++ b/web/src/locales/en-US/component.ts
@@ -78,5 +78,7 @@ export default {
'component.label-manager': 'Label Manager',
'component.global.noConfigurationRequired': 'No configuration required',
- 'component.global.copy': 'Copy'
+ 'component.global.copy': 'Copy',
+ 'component.global.copySuccess': 'Copy Successfully ',
+ 'component.global.copyFail': 'Copy Failed',
};
diff --git a/web/src/locales/zh-CN/component.ts
b/web/src/locales/zh-CN/component.ts
index df60e7f..f3f813b 100644
--- a/web/src/locales/zh-CN/component.ts
+++ b/web/src/locales/zh-CN/component.ts
@@ -74,5 +74,7 @@ export default {
'component.label-manager': '标签管理器',
'component.global.noConfigurationRequired': '无需配置',
- 'component.global.copy': '复制'
+ 'component.global.copy': '复制',
+ 'component.global.copySuccess': '复制成功',
+ 'component.global.copyFail': '复制失败'
};
diff --git a/web/src/pages/Route/components/DebugViews/DebugDrawView.tsx
b/web/src/pages/Route/components/DebugViews/DebugDrawView.tsx
index b0f1761..a152222 100644
--- a/web/src/pages/Route/components/DebugViews/DebugDrawView.tsx
+++ b/web/src/pages/Route/components/DebugViews/DebugDrawView.tsx
@@ -15,12 +15,14 @@
* limitations under the License.
*/
import React, { useEffect, useState, useRef } from 'react';
-import { Input, Select, Card, Tabs, Form, Drawer, Spin, notification, Radio }
from 'antd';
+import { Button, Card, Drawer, Form, Input, notification, Radio, Select, Spin,
Tabs } from 'antd';
import { useIntl } from 'umi';
import CodeMirror from '@uiw/react-codemirror';
import queryString from 'query-string';
import Base64 from 'base-64';
import urlRegexSafe from 'url-regex-safe';
+import CopyToClipboard from "react-copy-to-clipboard";
+import { CopyOutlined } from "@ant-design/icons";
import PanelSection from '@/components/PanelSection';
@@ -31,6 +33,7 @@ import {
PROTOCOL_SUPPORTED,
DEBUG_BODY_TYPE_SUPPORTED,
DEBUG_BODY_CODEMIRROR_MODE_SUPPORTED,
+ DEBUG_RESPONSE_BODY_CODEMIRROR_MODE_SUPPORTED,
DebugBodyFormDataValueType,
} from '../../constants';
import { DebugParamsView, AuthenticationView, DebugFormDataView } from '.';
@@ -51,13 +54,15 @@ const DebugDrawView: React.FC<RouteModule.DebugDrawProps> =
(props) => {
const [formDataForm] = Form.useForm();
const [authForm] = Form.useForm();
const [headerForm] = Form.useForm();
- const [responseBody, setResponseBody] = useState<string>();
- const [responseHeader, setResponseHeader] = useState<string>();
+ const [response, setResponse] = useState<RouteModule.debugResponse | null>()
const [loading, setLoading] = useState(false);
const [codeMirrorHeight, setCodeMirrorHeight] = useState<number |
string>(50);
const bodyCodeMirrorRef = useRef<any>(null);
const [bodyType, setBodyType] = useState('none');
const methodWithoutBody = ['GET', 'HEAD'];
+ const [responseBodyCodeMirrorMode, setResponseBodyCodeMirrorMode] = useState(
+ DEBUG_RESPONSE_BODY_CODEMIRROR_MODE_SUPPORTED[0].mode,
+ );
const [bodyCodeMirrorMode, setBodyCodeMirrorMode] = useState(
DEBUG_BODY_CODEMIRROR_MODE_SUPPORTED[0].mode,
);
@@ -75,8 +80,7 @@ const DebugDrawView: React.FC<RouteModule.DebugDrawProps> =
(props) => {
formDataForm.setFieldsValue(DEFAULT_DEBUG_PARAM_FORM_DATA);
headerForm.setFieldsValue(DEFAULT_DEBUG_PARAM_FORM_DATA);
authForm.setFieldsValue(DEFAULT_DEBUG_AUTH_FORM_DATA);
- setResponseBody(formatMessage({ id:
'page.route.debug.showResultAfterSendRequest' }));
- setResponseHeader(formatMessage({ id:
'page.route.debug.showResultAfterSendRequest' }));
+ setResponse(null);
setBodyType(DEBUG_BODY_TYPE_SUPPORTED[DebugBodyType.None]);
};
@@ -230,8 +234,23 @@ const DebugDrawView: React.FC<RouteModule.DebugDrawProps>
= (props) => {
}, bodyFormData)
.then((req) => {
setLoading(false);
- setResponseBody(JSON.stringify(req.data.data, null, 2));
- setResponseHeader(JSON.stringify(req.data.header, null, 2));
+ 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) {
+ setResponseBodyCodeMirrorMode("TEXT");
+ } else if (contentType[0].toLowerCase().indexOf("json") !== -1) {
+ setResponseBodyCodeMirrorMode("JSON");
+ } else if (contentType[0].toLowerCase().indexOf("xml") !== -1) {
+ setResponseBodyCodeMirrorMode("XML");
+ } else if (contentType[0].toLowerCase().indexOf("html") !== -1) {
+ setResponseBodyCodeMirrorMode("HTML");
+ } else {
+ setResponseBodyCodeMirrorMode("TEXT");
+ }
setCodeMirrorHeight('auto');
})
.catch(() => {
@@ -390,15 +409,44 @@ const DebugDrawView: React.FC<RouteModule.DebugDrawProps>
= (props) => {
</Tabs>
</PanelSection>
<PanelSection title={formatMessage({ id:
'page.route.PanelSection.title.responseResult' })}>
- <Tabs>
- <TabPane tab={formatMessage({ id: 'page.route.TabPane.response'
})} key="response">
- <Spin tip="Loading..." spinning={loading}>
- <div id='codeMirror-response'>
+ <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={responseBodyCodeMirrorMode}
+ onSelect={(mode) => setResponseBodyCodeMirrorMode(mode as
string)}>
+ {
+ DEBUG_RESPONSE_BODY_CODEMIRROR_MODE_SUPPORTED.map(mode => {
+ return <Option value={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='codeMirror-response' style={{marginTop:16}}>
<CodeMirror
- value={responseBody}
+ value={response ? response.data : ""}
height={codeMirrorHeight}
options={{
- mode: 'json-ld',
+ mode: responseBodyCodeMirrorMode,
readOnly: 'nocursor',
lineWrapping: true,
lineNumbers: true,
@@ -408,26 +456,18 @@ const DebugDrawView: React.FC<RouteModule.DebugDrawProps>
= (props) => {
}}
/>
</div>
- </Spin>
- </TabPane>
- <TabPane tab={formatMessage({ id: 'page.route.TabPane.header' })}
key="header">
- <Spin tip="Loading..." spinning={loading}>
- <CodeMirror
- value={responseHeader}
- height={codeMirrorHeight}
- options={{
- mode: 'json-ld',
- readOnly: 'nocursor',
- lineWrapping: true,
- lineNumbers: true,
- showCursorWhenSelecting: true,
- autofocus: true,
- scrollbarStyle: null,
- }}
- />
- </Spin>
- </TabPane>
- </Tabs>
+ </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>
diff --git a/web/src/pages/Route/constants.ts b/web/src/pages/Route/constants.ts
index 59d928b..f362736 100644
--- a/web/src/pages/Route/constants.ts
+++ b/web/src/pages/Route/constants.ts
@@ -175,11 +175,18 @@ export const DEBUG_BODY_TYPE_SUPPORTED:
RouteModule.DebugBodyType[] = [
// Note: codemirror mode: apl for text; javascript for json(need to format);
xml for xml;
export const DEBUG_BODY_CODEMIRROR_MODE_SUPPORTED = [
- { name: 'Json', mode: 'javascript' },
- { name: 'Text', mode: 'apl' },
+ { name: 'JSON', mode: 'javascript' },
+ { name: 'TEXT', mode: 'apl' },
{ name: 'XML', mode: 'xml' },
];
+export const DEBUG_RESPONSE_BODY_CODEMIRROR_MODE_SUPPORTED = [
+ { name: 'JSON', mode: 'javascript' },
+ { name: 'XML', mode: 'xml' },
+ { name: 'HTML', mode: 'html' },
+ { name: 'TEXT', mode: 'apl' },
+];
+
export const EXPORT_FILE_MIME_TYPE_SUPPORTED = ['application/json',
'application/x-yaml'];
export enum DebugBodyFormDataValueType {
diff --git a/web/src/pages/Route/typing.d.ts b/web/src/pages/Route/typing.d.ts
index 0355c2c..7a8a3e6 100644
--- a/web/src/pages/Route/typing.d.ts
+++ b/web/src/pages/Route/typing.d.ts
@@ -217,6 +217,14 @@ declare namespace RouteModule {
body_params?: any;
header_params?: any;
};
+
+ type debugResponse ={
+ code: number,
+ message: string,
+ data: any,
+ header: Record<string, string[]>
+ }
+
type authData = {
authType: string;
username?: string;