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;

Reply via email to