You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@shenyu.apache.org by li...@apache.org on 2022/06/23 14:48:47 UTC

[incubator-shenyu-dashboard] branch master updated: api doc (#209)

This is an automated email from the ASF dual-hosted git repository.

likeguo pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-shenyu-dashboard.git


The following commit(s) were added to refs/heads/master by this push:
     new 233a85fb api doc (#209)
233a85fb is described below

commit 233a85fb19d05cee77068c245e66568aea4a3159
Author: dayu <we...@qq.com>
AuthorDate: Thu Jun 23 22:48:41 2022 +0800

    api doc (#209)
---
 package.json                                 |   5 +-
 src/common/menu.js                           | 106 +++++----
 src/common/router.js                         |   5 +
 src/layouts/UserLayout.js                    |   6 +-
 src/locales/en-US.json                       |  28 ++-
 src/locales/zh-CN.json                       |  24 +-
 src/routes/Document/ApiDoc.js                |  93 ++++++++
 src/routes/Document/components/ApiContext.js |  23 ++
 src/routes/Document/components/ApiDebug.js   | 326 +++++++++++++++++++++++++++
 src/routes/Document/components/ApiInfo.js    | 198 ++++++++++++++++
 src/routes/Document/components/SearchApi.js  | 134 +++++++++++
 src/services/api.js                          |  62 +++--
 12 files changed, 940 insertions(+), 70 deletions(-)

diff --git a/package.json b/package.json
index 2c088618..b4e80a09 100755
--- a/package.json
+++ b/package.json
@@ -33,12 +33,13 @@
     "path-to-regexp": "^2.1.0",
     "prop-types": "^15.5.10",
     "qs": "^6.5.0",
-    "react": "^16.4.1",
+    "react": "^16.14.0",
     "react-container-query": "^0.11.0",
     "react-document-title": "^2.0.3",
-    "react-dom": "^16.4.1",
+    "react-dom": "^16.13.1",
     "react-fittext": "^1.0.0",
     "react-intl-universal": "^2.4.2",
+    "react-json-view": "^1.21.3",
     "react-resizable": "^1.11.0",
     "redux-logger": "^3.0.6",
     "setprototypeof": "^1.1.0",
diff --git a/src/common/menu.js b/src/common/menu.js
index 47adc592..b2a2526a 100644
--- a/src/common/menu.js
+++ b/src/common/menu.js
@@ -15,76 +15,88 @@
  * limitations under the License.
  */
 
-import { isUrl } from '../utils/utils';
-import { getIntlContent } from '../utils/IntlUtils'
+import { isUrl } from "../utils/utils";
+import { getIntlContent } from "../utils/IntlUtils";
 
 export const menuData = [
   {
-    name: getIntlContent('SHENYU.MENU.PLUGIN.LIST'),
-    icon: 'dashboard',
-    path: 'plug',
-    locale: 'SHENYU.MENU.PLUGIN.LIST',
-    children: [
-    ],
+    name: getIntlContent("SHENYU.MENU.PLUGIN.LIST"),
+    icon: "dashboard",
+    path: "plug",
+    locale: "SHENYU.MENU.PLUGIN.LIST",
+    children: []
   },
   {
-    name: getIntlContent('SHENYU.MENU.SYSTEM.MANAGMENT'),
-    icon: 'setting',
-    path: 'system',
-    locale: 'SHENYU.MENU.SYSTEM.MANAGMENT',
+    name: getIntlContent("SHENYU.MENU.SYSTEM.MANAGMENT"),
+    icon: "setting",
+    path: "system",
+    locale: "SHENYU.MENU.SYSTEM.MANAGMENT",
     children: [
       {
-        name: getIntlContent('SHENYU.MENU.SYSTEM.MANAGMENT.ROLE'),
-        path: 'role',
-        locale: 'SHENYU.MENU.SYSTEM.MANAGMENT.ROLE'
+        name: getIntlContent("SHENYU.MENU.SYSTEM.MANAGMENT.ROLE"),
+        path: "role",
+        locale: "SHENYU.MENU.SYSTEM.MANAGMENT.ROLE"
       },
       {
-        name: getIntlContent('SHENYU.MENU.SYSTEM.MANAGMENT.USER'),
-        path: 'manage',
-        locale: 'SHENYU.MENU.SYSTEM.MANAGMENT.USER'
+        name: getIntlContent("SHENYU.MENU.SYSTEM.MANAGMENT.USER"),
+        path: "manage",
+        locale: "SHENYU.MENU.SYSTEM.MANAGMENT.USER"
       },
       {
-        name: getIntlContent('SHENYU.MENU.SYSTEM.MANAGMENT.RESOURCE'),
-        path: 'resource',
-        locale: 'SHENYU.MENU.SYSTEM.MANAGMENT.RESOURCE'
+        name: getIntlContent("SHENYU.MENU.SYSTEM.MANAGMENT.RESOURCE"),
+        path: "resource",
+        locale: "SHENYU.MENU.SYSTEM.MANAGMENT.RESOURCE"
       }
-    ],
+    ]
   },
   {
-    name: getIntlContent('SHENYU.MENU.CONFIG.MANAGMENT'),
-    icon: 'setting',
-    path: 'config',
-    locale: 'SHENYU.MENU.CONFIG.MANAGMENT',
+    name: getIntlContent("SHENYU.MENU.CONFIG.MANAGMENT"),
+    icon: "setting",
+    path: "config",
+    locale: "SHENYU.MENU.CONFIG.MANAGMENT",
     children: [
       {
-        name: getIntlContent('SHENYU.MENU.SYSTEM.MANAGMENT.PLUGIN'),
-        path: 'plugin',
-        locale: 'SHENYU.MENU.SYSTEM.MANAGMENT.PLUGIN'
+        name: getIntlContent("SHENYU.MENU.SYSTEM.MANAGMENT.PLUGIN"),
+        path: "plugin",
+        locale: "SHENYU.MENU.SYSTEM.MANAGMENT.PLUGIN"
       },
       {
-        name: getIntlContent('SHENYU.PLUGIN.PLUGINHANDLE'),
-        path: 'pluginhandle',
-        locale: 'SHENYU.PLUGIN.PLUGINHANDLE'
+        name: getIntlContent("SHENYU.PLUGIN.PLUGINHANDLE"),
+        path: "pluginhandle",
+        locale: "SHENYU.PLUGIN.PLUGINHANDLE"
       },
       {
-        name: getIntlContent('SHENYU.MENU.SYSTEM.MANAGMENT.AUTHEN'),
-        path: 'auth',
-        locale: 'SHENYU.MENU.SYSTEM.MANAGMENT.AUTHEN'
+        name: getIntlContent("SHENYU.MENU.SYSTEM.MANAGMENT.AUTHEN"),
+        path: "auth",
+        locale: "SHENYU.MENU.SYSTEM.MANAGMENT.AUTHEN"
       },
       {
-        name: getIntlContent('SHENYU.MENU.SYSTEM.MANAGMENT.METADATA'),
-        path: 'metadata',
-        locale: 'SHENYU.MENU.SYSTEM.MANAGMENT.METADATA'
+        name: getIntlContent("SHENYU.MENU.SYSTEM.MANAGMENT.METADATA"),
+        path: "metadata",
+        locale: "SHENYU.MENU.SYSTEM.MANAGMENT.METADATA"
       },
       {
-        name: getIntlContent('SHENYU.MENU.SYSTEM.MANAGMENT.DICTIONARY'),
-        path: 'dict',
-        locale: 'SHENYU.MENU.SYSTEM.MANAGMENT.DICTIONARY'
+        name: getIntlContent("SHENYU.MENU.SYSTEM.MANAGMENT.DICTIONARY"),
+        path: "dict",
+        locale: "SHENYU.MENU.SYSTEM.MANAGMENT.DICTIONARY"
       }
-    ],
+    ]
   },
+  {
+    name: getIntlContent("SHENYU.MENU.DOCUMENT"),
+    icon: "file-text",
+    path: "document",
+    locale: "SHENYU.MENU.DOCUMENT",
+    children: [
+      {
+        name: getIntlContent("SHENYU.MENU.DOCUMENT.APIDOC"),
+        path: "apidoc",
+        locale: "SHENYU.MENU.DOCUMENT.APIDOC"
+      }
+    ]
+  }
 ];
-function formatter(data, parentPath = '/', parentAuthority) {
+function formatter(data, parentPath = "/", parentAuthority) {
   return data.map(item => {
     let { path } = item;
     if (!isUrl(path)) {
@@ -93,10 +105,14 @@ function formatter(data, parentPath = '/', parentAuthority) {
     const result = {
       ...item,
       path,
-      authority: item.authority || parentAuthority,
+      authority: item.authority || parentAuthority
     };
     if (item.children) {
-      result.children = formatter(item.children, `${parentPath}${item.path}/`, item.authority);
+      result.children = formatter(
+        item.children,
+        `${parentPath}${item.path}/`,
+        item.authority
+      );
     }
 
     return result;
diff --git a/src/common/router.js b/src/common/router.js
index c28947bf..2a4f1814 100644
--- a/src/common/router.js
+++ b/src/common/router.js
@@ -176,6 +176,11 @@ export const getRouterData = app => {
       component: dynamicWrapper(app, ["login"], () =>
         import("../routes/User/Login")
       )
+    },
+    "/document/apidoc": {
+      component: dynamicWrapper(app, [], () =>
+        import("../routes/Document/ApiDoc")
+      )
     }
   };
   // Get name from ./menu.js or just set it in the router data.
diff --git a/src/layouts/UserLayout.js b/src/layouts/UserLayout.js
index fe76cab6..8c692825 100644
--- a/src/layouts/UserLayout.js
+++ b/src/layouts/UserLayout.js
@@ -34,7 +34,7 @@ const links = [];
 
 const copyright = (
   <Fragment>
-    Copyright <Icon type="copyright" /> 2018 出品
+    Copyright <Icon type="copyright" /> {new Date().getFullYear()} 出品
   </Fragment>
 );
 
@@ -69,7 +69,9 @@ class UserLayout extends React.PureComponent {
                   <img alt="logo" className={styles.logo} src={TitleLogo} />
                 </Link>
               </div>
-              <div className={styles.desc}>Apache ShenYu Gateway Management System</div>
+              <div className={styles.desc}>
+                Apache ShenYu Gateway Management System
+              </div>
             </div>
             <Switch>
               {getRoutes(match.path, routerData).map(item => (
diff --git a/src/locales/en-US.json b/src/locales/en-US.json
index c3c02eb1..31fbce90 100644
--- a/src/locales/en-US.json
+++ b/src/locales/en-US.json
@@ -285,5 +285,31 @@
   "SHENYU.PLUGIN.REQUEST.COOKIE.NEW.KEY": "New Cookie Key",
   "SHENYU.PLUGIN.PARAM.KEY": "Parameter Key",
   "SHENYU.PLUGIN.PARAM.PATH": "Parameter Path",
-  "SHENYU.PLUGIN.PARAM.VALUE": "Parameter Value"
+  "SHENYU.PLUGIN.PARAM.VALUE": "Parameter Value",
+  "SHENYU.MENU.DOCUMENT": "Document",
+  "SHENYU.MENU.DOCUMENT.APIDOC": "API Doc",
+  "SHENYU.DOCUMENT.APIDOC.SEARCH.PLACEHOLDER":
+    "Support interface name and document title",
+  "SHENYU.DOCUMENT.APIDOC.INFO.NAME": "Interface Name",
+  "SHENYU.DOCUMENT.APIDOC.INFO.DESCRIPTION": "Interface Description",
+  "SHENYU.DOCUMENT.APIDOC.INFO.ADDRESS": "Request Address",
+  "SHENYU.DOCUMENT.APIDOC.INFO.REQUEST.PARAMETERS": "Request Parameters",
+  "SHENYU.DOCUMENT.APIDOC.INFO.SERVICE.REQUEST.PARAMETERS":
+    "Service Request Parameters",
+  "SHENYU.DOCUMENT.APIDOC.INFO.RESPONSE.PARAMETERS": "Response Parameters",
+  "SHENYU.DOCUMENT.APIDOC.INFO.COMMON.RESPONSE.PARAMETERS":
+    "Common Response Parameters",
+  "SHENYU.DOCUMENT.APIDOC.INFO.BUSINESS.RESPONSE.PARAMETERS":
+    "Business Response Parameters",
+  "SHENYU.DOCUMENT.APIDOC.INFO.INTERFACE.DEBUG": "Interface Debug",
+  "SHENYU.DOCUMENT.APIDOC.INFO.REQUEST.INFORMATION": "Request Information",
+  "SHENYU.DOCUMENT.APIDOC.INFO.SEND.REQUEST": "Send Request",
+  "SHENYU.DOCUMENT.APIDOC.INFO.REQUEST.RESULTS": "Request Results",
+  "SHENYU.DOCUMENT.APIDOC.CONTENTS.TO.BE.SIGNED": "Contents To Be Signed",
+  "SHENYU.DOCUMENT.APIDOC.SIGNATURE": "Signature",
+  "SHENYU.COMMON.REQUIRED": "Required",
+  "SHENYU.COMMON.MAX.LENGTH": "Max Length",
+  "SHENYU.COMMON.MAX.EXAMPLE": "Example",
+  "SHENYU.COMMON.MAX.ENVIRONMENT": "Environment",
+  "SHENYU.COMMON.HTTP.METHOD": "HttpMethod"
 }
diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json
index b60ff7c5..4ca3f1c3 100644
--- a/src/locales/zh-CN.json
+++ b/src/locales/zh-CN.json
@@ -277,5 +277,27 @@
   "SHENYU.PLUGIN.REQUEST.COOKIE.NEW.KEY": "New Cookie Key",
   "SHENYU.PLUGIN.PARAM.KEY": "Parameter Key",
   "SHENYU.PLUGIN.PARAM.PATH": "Parameter Path",
-  "SHENYU.PLUGIN.PARAM.VALUE": "Parameter Value"
+  "SHENYU.PLUGIN.PARAM.VALUE": "Parameter Value",
+  "SHENYU.MENU.DOCUMENT": "文档说明",
+  "SHENYU.MENU.DOCUMENT.APIDOC": "API文档",
+  "SHENYU.DOCUMENT.APIDOC.SEARCH.PLACEHOLDER": "支持接口名称、文档标题",
+  "SHENYU.DOCUMENT.APIDOC.INFO.NAME": "接口名称",
+  "SHENYU.DOCUMENT.APIDOC.INFO.DESCRIPTION": "接口描述",
+  "SHENYU.DOCUMENT.APIDOC.INFO.ADDRESS": "请求地址",
+  "SHENYU.DOCUMENT.APIDOC.INFO.REQUEST.PARAMETERS": "请求参数",
+  "SHENYU.DOCUMENT.APIDOC.INFO.SERVICE.REQUEST.PARAMETERS": "业务请求参数",
+  "SHENYU.DOCUMENT.APIDOC.INFO.RESPONSE.PARAMETERS": "响应参数",
+  "SHENYU.DOCUMENT.APIDOC.INFO.COMMON.RESPONSE.PARAMETERS": "公共响应参数",
+  "SHENYU.DOCUMENT.APIDOC.INFO.BUSINESS.RESPONSE.PARAMETERS": "业务响应参数",
+  "SHENYU.DOCUMENT.APIDOC.INFO.INTERFACE.DEBUG": "接口调试",
+  "SHENYU.DOCUMENT.APIDOC.INFO.REQUEST.INFORMATION": "请求信息",
+  "SHENYU.DOCUMENT.APIDOC.INFO.SEND.REQUEST": "发送请求",
+  "SHENYU.DOCUMENT.APIDOC.INFO.REQUEST.RESULTS": "请求结果",
+  "SHENYU.DOCUMENT.APIDOC.CONTENTS.TO.BE.SIGNED": "待签名内容",
+  "SHENYU.DOCUMENT.APIDOC.SIGNATURE": "签名(sign)",
+  "SHENYU.COMMON.REQUIRED": "必填",
+  "SHENYU.COMMON.MAX.LENGTH": "最大长度",
+  "SHENYU.COMMON.MAX.EXAMPLE": "示例值",
+  "SHENYU.COMMON.MAX.ENVIRONMENT": "环境",
+  "SHENYU.COMMON.HTTP.METHOD": "请求方法"
 }
diff --git a/src/routes/Document/ApiDoc.js b/src/routes/Document/ApiDoc.js
new file mode 100644
index 00000000..b7fd63d7
--- /dev/null
+++ b/src/routes/Document/ApiDoc.js
@@ -0,0 +1,93 @@
+/*
+ * 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 { Col, Row, Card, BackTop, Empty } from "antd";
+import React, { useEffect, useState } from "react";
+import SearchApi from "./components/SearchApi";
+import ApiInfo from "./components/ApiInfo";
+import { getDocItem, getDocMenus } from "../../services/api";
+import ApiContext from "./components/ApiContext";
+
+function ApiDoc() {
+  const [apiDetail, setApiDetail] = useState({});
+  const [apiData, setApiData] = useState({});
+
+  const initData = async () => {
+    const { code, data = {} } = await getDocMenus();
+    if (code === 200) {
+      const { menuProjects = [] } = data;
+      const createKey = (treeData, keys) => {
+        treeData.forEach((item, index) => {
+          const { children, id, name } = item;
+          const key = [...keys, index].join("-");
+          item.key = key;
+          item.id = id;
+          item.name = name;
+          if (children?.length) {
+            createKey(children, [...keys, index]);
+          }
+        });
+      };
+      createKey(menuProjects, []);
+      data.menuProjects = menuProjects;
+      setApiData(data);
+    }
+  };
+
+  const handleSelectNode = async (_, e) => {
+    const {
+      node: {
+        props: { id }
+      }
+    } = e;
+    const { code, data } = await getDocItem({ id });
+    if (code === 200) {
+      setApiDetail(data);
+    }
+  };
+
+  useEffect(() => {
+    initData();
+  }, []);
+
+  return (
+    <ApiContext.Provider
+      value={{
+        apiDetail,
+        apiData
+      }}
+    >
+      <Card style={{ margin: 24 }}>
+        <Row gutter={24}>
+          <Col span={6}>
+            <SearchApi onSelect={handleSelectNode} />
+          </Col>
+          <Col span={18}>
+            {apiDetail.id ? (
+              <ApiInfo />
+            ) : (
+              <Empty description={false} style={{ padding: "160px 0" }} />
+            )}
+          </Col>
+        </Row>
+        <BackTop />
+      </Card>
+    </ApiContext.Provider>
+  );
+}
+
+export default ApiDoc;
diff --git a/src/routes/Document/components/ApiContext.js b/src/routes/Document/components/ApiContext.js
new file mode 100644
index 00000000..cebaf595
--- /dev/null
+++ b/src/routes/Document/components/ApiContext.js
@@ -0,0 +1,23 @@
+/*
+ * 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 { createContext } from "react";
+
+export default createContext({
+  apiDetail: {},
+  apiData: {}
+});
diff --git a/src/routes/Document/components/ApiDebug.js b/src/routes/Document/components/ApiDebug.js
new file mode 100644
index 00000000..7fc13bf5
--- /dev/null
+++ b/src/routes/Document/components/ApiDebug.js
@@ -0,0 +1,326 @@
+/*
+ * 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 {
+  Typography,
+  Form,
+  Input,
+  Button,
+  Radio,
+  Row,
+  Col,
+  Tree,
+  Empty,
+  Tabs
+} from "antd";
+import React, {
+  useEffect,
+  useState,
+  forwardRef,
+  useImperativeHandle,
+  createRef,
+  useContext
+} from "react";
+import ReactJson from "react-json-view";
+import fetch from "dva/fetch";
+import { sandboxProxyGateway } from "../../../services/api";
+import ApiContext from "./ApiContext";
+import { getIntlContent } from "../../../utils/IntlUtils";
+import AuthButton from "../../../utils/AuthButton";
+
+const { Title, Text, Paragraph } = Typography;
+const { TreeNode } = Tree;
+const { TabPane } = Tabs;
+const FormItem = Form.Item;
+
+const FCForm = forwardRef(({ form, onSubmit }, ref) => {
+  useImperativeHandle(ref, () => ({
+    form
+  }));
+
+  const {
+    apiDetail: { name: apiUrl, httpMethodList, requestParameters },
+    apiData: { appKey, gatewayUrl, cookie }
+  } = useContext(ApiContext);
+  const [questJson, setRequestJson] = useState({});
+
+  const handleSubmit = e => {
+    e.preventDefault();
+    ref.current.form.validateFieldsAndScroll((errors, values) => {
+      if (!errors) {
+        onSubmit({
+          ...values,
+          bizParam: questJson
+        });
+      }
+    });
+  };
+
+  const createRequestJson = (params = []) => {
+    const exampleJSON = {};
+    const key = [];
+    const loopExample = (data, obj) => {
+      data.forEach(item => {
+        const { name, refs, example, type } = item;
+        key.push(name);
+        switch (type) {
+          case "array":
+            if (Array.isArray(refs)) {
+              obj[name] = [{}];
+              key.push(0);
+              loopExample(refs, obj[name][0]);
+              key.pop();
+            } else {
+              obj[name] = [];
+            }
+            break;
+          case "object":
+            obj[name] = {};
+            if (Array.isArray(refs)) {
+              loopExample(refs, obj[name]);
+            }
+            break;
+          default:
+            obj[name] = example;
+            break;
+        }
+        key.pop();
+      });
+    };
+
+    loopExample(params, exampleJSON);
+    setRequestJson(exampleJSON);
+  };
+
+  const renderTreeNode = (data, indexArr = []) => {
+    return data.map((item, index) => {
+      const { name, type, required, description } = item;
+      const TreeTitle = (
+        <>
+          <Text strong>{name}</Text>
+          &nbsp;<Text code>{type}</Text>
+          &nbsp;
+          {required ? (
+            <Text type="danger">required</Text>
+          ) : (
+            <Text type="warning">optional</Text>
+          )}
+          &nbsp;<Text type="secondary">{description}</Text>
+        </>
+      );
+      return (
+        <TreeNode key={[...indexArr, index].join("-")} title={TreeTitle}>
+          {item.refs && renderTreeNode(item.refs, [...indexArr, index])}
+        </TreeNode>
+      );
+    });
+  };
+
+  const updateJson = obj => {
+    setRequestJson(obj.updated_src);
+  };
+
+  useEffect(
+    () => {
+      createRequestJson(requestParameters);
+    },
+    [requestParameters]
+  );
+
+  return (
+    <Form onSubmit={handleSubmit}>
+      <Title level={4}>
+        {getIntlContent("SHENYU.DOCUMENT.APIDOC.INFO.REQUEST.INFORMATION")}
+      </Title>
+      <FormItem label={getIntlContent("SHENYU.DOCUMENT.APIDOC.INFO.ADDRESS")}>
+        {form.getFieldDecorator("requestUrl", {
+          initialValue: gatewayUrl + apiUrl,
+          rules: [{ type: "string", required: true }]
+        })(<Input allowClear />)}
+      </FormItem>
+      <FormItem label="AppKey">
+        {form.getFieldDecorator("appKey", {
+          initialValue: appKey,
+          rules: [{ type: "string" }]
+        })(
+          <Input
+            allowClear
+            placeholder=" If the current API requires signature authentication, this parameter is required"
+          />
+        )}
+      </FormItem>
+      <FormItem label="Cookie">
+        {form.getFieldDecorator("cookie", {
+          initialValue: cookie,
+          rules: [{ type: "string" }]
+        })(
+          <Input
+            allowClear
+            placeholder="Fill in the real cookie value.(signature authentication and login free API ignore this item)"
+          />
+        )}
+      </FormItem>
+      <FormItem label={getIntlContent("SHENYU.COMMON.HTTP.METHOD")}>
+        {form.getFieldDecorator("httpMethod", {
+          initialValue: httpMethodList?.[0]?.toLocaleUpperCase(),
+          rules: [{ type: "string", required: true }]
+        })(
+          <Radio.Group
+            options={httpMethodList?.map(v => v.toLocaleUpperCase())}
+          />
+        )}
+      </FormItem>
+      <FormItem
+        label={getIntlContent("SHENYU.DOCUMENT.APIDOC.INFO.REQUEST.PARAMETERS")}
+        required
+      />
+      <Row gutter={16}>
+        <Col span={14}>
+          <ReactJson
+            src={questJson}
+            theme="monokai"
+            displayDataTypes={false}
+            name={false}
+            onAdd={updateJson}
+            onEdit={updateJson}
+            onDelete={updateJson}
+            style={{ borderRadius: 4, padding: 16 }}
+          />
+        </Col>
+        <Col span={10}>
+          <div
+            style={{
+              borderRadius: 4,
+              border: "1px solid #e8e8e8",
+              overflow: "auto",
+              padding: 8
+            }}
+          >
+            {requestParameters && (
+              <Tree showLine defaultExpandAll>
+                {renderTreeNode(requestParameters)}
+              </Tree>
+            )}
+          </div>
+        </Col>
+      </Row>
+      <AuthButton perms="document:apirun:send">
+        <FormItem label=" " colon={false}>
+          <Button htmlType="submit" type="primary">
+            {getIntlContent("SHENYU.DOCUMENT.APIDOC.INFO.SEND.REQUEST")}
+          </Button>
+        </FormItem>
+      </AuthButton>
+    </Form>
+  );
+});
+
+const EnhancedFCForm = Form.create()(FCForm);
+
+function ApiDebug() {
+  const {
+    apiDetail: { id }
+  } = useContext(ApiContext);
+  const [responseInfo, setResponseInfo] = useState({});
+  const [activeKey, setActiveKey] = useState("2");
+
+  const formRef = createRef();
+
+  const handleSubmit = async values => {
+    fetch(sandboxProxyGateway(), {
+      method: "POST",
+      headers: {
+        "X-Access-Token": sessionStorage.token,
+        "Content-Type": "application/json"
+      },
+      body: JSON.stringify(values)
+    }).then(async response => {
+      const data = await response.json();
+      setResponseInfo({
+        "sandbox-params": response.headers.get("sandbox-params"),
+        "sandbox-beforesign": response.headers.get("sandbox-beforesign"),
+        "sandbox-sign": response.headers.get("sandbox-sign"),
+        body: data
+      });
+    });
+  };
+
+  useEffect(
+    () => {
+      setResponseInfo({});
+      // eslint-disable-next-line no-unused-expressions
+      formRef.current?.form.resetFields(["method"]);
+      setActiveKey("2");
+    },
+    [id]
+  );
+
+  return (
+    <>
+      <EnhancedFCForm wrappedComponentRef={formRef} onSubmit={handleSubmit} />
+      <Tabs
+        type="card"
+        activeKey={activeKey}
+        onChange={key => setActiveKey(key)}
+      >
+        <TabPane
+          tab={getIntlContent(
+            "SHENYU.DOCUMENT.APIDOC.INFO.REQUEST.INFORMATION"
+          )}
+          key="1"
+        >
+          {Object.keys(responseInfo).length ? (
+            <>
+              <Paragraph>
+                <Text strong>
+                  {getIntlContent(
+                    "SHENYU.DOCUMENT.APIDOC.CONTENTS.TO.BE.SIGNED"
+                  )}
+                </Text>
+                <br />
+                <Text code>
+                  {responseInfo["sandbox-beforesign"] || "undefined"}
+                </Text>
+              </Paragraph>
+              <Paragraph>
+                <Text strong>
+                  {getIntlContent("SHENYU.DOCUMENT.APIDOC.SIGNATURE")}
+                </Text>
+                <br />
+                <Text code>{responseInfo["sandbox-sign"] || "undefined"}</Text>
+              </Paragraph>
+            </>
+          ) : (
+            <Empty description={false} />
+          )}
+        </TabPane>
+        <TabPane
+          tab={getIntlContent("SHENYU.DOCUMENT.APIDOC.INFO.REQUEST.RESULTS")}
+          key="2"
+        >
+          {Object.keys(responseInfo).length ? (
+            <ReactJson src={responseInfo.body} name={false} />
+          ) : (
+            <Empty description={false} />
+          )}
+        </TabPane>
+      </Tabs>
+    </>
+  );
+}
+
+export default ApiDebug;
diff --git a/src/routes/Document/components/ApiInfo.js b/src/routes/Document/components/ApiInfo.js
new file mode 100644
index 00000000..fa8a8f7f
--- /dev/null
+++ b/src/routes/Document/components/ApiInfo.js
@@ -0,0 +1,198 @@
+/*
+ * 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 { Typography, Table } from "antd";
+import React, { useContext } from "react";
+import ApiDebug from "./ApiDebug";
+import ApiContext from "./ApiContext";
+import { getIntlContent } from "../../../utils/IntlUtils";
+
+const { Title, Text, Paragraph } = Typography;
+
+function ApiInfo() {
+  const {
+    apiData: { envProps = [] },
+    apiDetail: {
+      summary,
+      name: apiName,
+      description,
+      requestParameters,
+      responseParameters
+    }
+  } = useContext(ApiContext);
+
+  const columns = [
+    {
+      title: getIntlContent("SHENYU.PLUGIN.SELECTOR.LIST.COLUMN.NAME"),
+      dataIndex: "name"
+    },
+    {
+      title: getIntlContent("SHENYU.COMMON.TYPE"),
+      dataIndex: "type"
+    },
+    {
+      title: getIntlContent("SHENYU.COMMON.REQUIRED"),
+      dataIndex: "required",
+      render: v =>
+        v ? (
+          <Text type="danger">{getIntlContent("SHENYU.COMMON.YES")}</Text>
+        ) : (
+          getIntlContent("SHENYU.COMMON.NO")
+        )
+    },
+    {
+      title: getIntlContent("SHENYU.COMMON.MAX.LENGTH"),
+      dataIndex: "maxLength"
+    },
+    {
+      title: getIntlContent("SHENYU.PLUGIN.DESCRIBE"),
+      dataIndex: "description"
+    },
+    {
+      title: getIntlContent("SHENYU.COMMON.MAX.EXAMPLE"),
+      dataIndex: "example"
+    }
+  ];
+
+  const defaultCommonData = [
+    {
+      id: 1,
+      name: "code",
+      type: "integer",
+      description: "返回码",
+      example: "200"
+    },
+    {
+      id: 2,
+      name: "message",
+      type: "string",
+      description: "错误描述信息",
+      example: "非法的参数"
+    },
+    {
+      id: 3,
+      name: "data",
+      type: "object",
+      description: "响应的业务结果",
+      example: '{"id":"1988771289091030"}'
+    }
+  ];
+
+  const envPropsColumns = [
+    {
+      title: getIntlContent("SHENYU.COMMON.MAX.ENVIRONMENT"),
+      dataIndex: "envLabel"
+    },
+    {
+      title: getIntlContent("SHENYU.COMMON.TYPE"),
+      dataIndex: "addressLabel"
+    },
+    {
+      title: getIntlContent("SHENYU.DOCUMENT.APIDOC.INFO.ADDRESS"),
+      dataIndex: "addressUrl"
+    }
+  ];
+
+  return (
+    <>
+      <Title level={2}>{summary}</Title>
+      <Title level={4}>
+        {getIntlContent("SHENYU.DOCUMENT.APIDOC.INFO.NAME")}
+      </Title>
+      <Text code>{apiName}</Text>
+      <Title level={4}>
+        {getIntlContent("SHENYU.DOCUMENT.APIDOC.INFO.DESCRIPTION")}
+      </Title>
+      <Text type="secondary">{description}</Text>
+      <Title level={4}>
+        {getIntlContent("SHENYU.DOCUMENT.APIDOC.INFO.ADDRESS")}
+      </Title>
+      <Paragraph>
+        <Table
+          size="small"
+          rowKey="envLabel"
+          bordered
+          dataSource={envProps}
+          pagination={false}
+          columns={envPropsColumns}
+        />
+      </Paragraph>
+
+      <Title level={2}>
+        {getIntlContent("SHENYU.DOCUMENT.APIDOC.INFO.REQUEST.PARAMETERS")}
+      </Title>
+      <Title level={4}>
+        {getIntlContent(
+          "SHENYU.DOCUMENT.APIDOC.INFO.SERVICE.REQUEST.PARAMETERS"
+        )}
+      </Title>
+      <Paragraph>
+        <Table
+          size="small"
+          rowKey="id"
+          bordered
+          dataSource={requestParameters}
+          pagination={false}
+          childrenColumnName="refs"
+          columns={columns}
+        />
+      </Paragraph>
+
+      <Title level={2}>
+        {getIntlContent("SHENYU.DOCUMENT.APIDOC.INFO.RESPONSE.PARAMETERS")}
+      </Title>
+      <Title level={4}>
+        {getIntlContent(
+          "SHENYU.DOCUMENT.APIDOC.INFO.COMMON.RESPONSE.PARAMETERS"
+        )}
+      </Title>
+      <Paragraph>
+        <Table
+          size="small"
+          rowKey="id"
+          bordered
+          dataSource={defaultCommonData}
+          pagination={false}
+          columns={columns.filter((_, i) => ![2, 3].includes(i))}
+        />
+      </Paragraph>
+      <Title level={4}>
+        {getIntlContent(
+          "SHENYU.DOCUMENT.APIDOC.INFO.BUSINESS.RESPONSE.PARAMETERS"
+        )}
+      </Title>
+      <Paragraph>
+        <Table
+          size="small"
+          rowKey="id"
+          bordered
+          dataSource={responseParameters}
+          pagination={false}
+          childrenColumnName="refs"
+          columns={columns}
+        />
+      </Paragraph>
+
+      <Title level={2}>
+        {getIntlContent("SHENYU.DOCUMENT.APIDOC.INFO.INTERFACE.DEBUG")}
+      </Title>
+      <ApiDebug />
+    </>
+  );
+}
+
+export default ApiInfo;
diff --git a/src/routes/Document/components/SearchApi.js b/src/routes/Document/components/SearchApi.js
new file mode 100644
index 00000000..ca9b6a47
--- /dev/null
+++ b/src/routes/Document/components/SearchApi.js
@@ -0,0 +1,134 @@
+/*
+ * 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 { Tree, Input, Empty } from "antd";
+import React, { useContext, useEffect, useState } from "react";
+import ApiContext from "./ApiContext";
+import { getIntlContent } from "../../../utils/IntlUtils";
+
+const { TreeNode } = Tree;
+const { Search } = Input;
+
+function SearchApi(props) {
+  const { onSelect } = props;
+  const [expandedKeys, setExpandedKeys] = useState([]);
+  const [searchValue, setSearchValue] = useState("");
+  const [autoExpandParent, setAutoExpandParent] = useState(true);
+  const {
+    apiData: { menuProjects }
+  } = useContext(ApiContext);
+
+  const renderTreeNode = data => {
+    return data.map(item => {
+      const { children, id, label, key, name } = item;
+      const index = label.indexOf(searchValue);
+      const sameName = name?.indexOf(searchValue);
+      const beforeStr = label.substr(0, index);
+      const afterStr = label.substr(index + searchValue.length);
+      let titleObj = <span>{label}</span>;
+      if (index > -1) {
+        titleObj = (
+          <span>
+            {beforeStr}
+            <span style={{ color: "#f50" }}>{searchValue}</span>
+            {afterStr}
+          </span>
+        );
+      }
+      if (searchValue && sameName > -1) {
+        titleObj = <span style={{ color: "#f50" }}>{label}</span>;
+      }
+      return (
+        <TreeNode
+          key={key}
+          title={titleObj}
+          selectable={id !== undefined}
+          id={id}
+        >
+          {children?.length && renderTreeNode(children)}
+        </TreeNode>
+      );
+    });
+  };
+
+  const handleSearchChange = e => {
+    const { value } = e.target;
+    const keys = [];
+    const findSearchKeys = data =>
+      data.forEach(item => {
+        if (item.label.indexOf(value) > -1 || item.name?.indexOf(value) > -1) {
+          keys.push(item.key);
+        }
+        if (Array.isArray(item.children)) {
+          findSearchKeys(item.children);
+        }
+      });
+    findSearchKeys(menuProjects);
+    setExpandedKeys(keys);
+    setSearchValue(value);
+    setAutoExpandParent(true);
+  };
+
+  const handleExpandChange = keys => {
+    setExpandedKeys(keys);
+    setAutoExpandParent(false);
+  };
+
+  useEffect(
+    () => {
+      if (Array.isArray(menuProjects)) {
+        const allKeys = [];
+        const getAllParentsKey = data =>
+          data.forEach(item => {
+            if (item.children) {
+              allKeys.push(item.key);
+              getAllParentsKey(item.children);
+            }
+          });
+        getAllParentsKey(menuProjects);
+        setExpandedKeys(allKeys);
+      }
+    },
+    [menuProjects]
+  );
+
+  return (
+    <div style={{ overflow: "auto" }}>
+      <Search
+        allowClear
+        onChange={handleSearchChange}
+        placeholder={getIntlContent(
+          "SHENYU.DOCUMENT.APIDOC.SEARCH.PLACEHOLDER"
+        )}
+      />
+      {menuProjects?.length ? (
+        <Tree
+          autoExpandParent={autoExpandParent}
+          expandedKeys={expandedKeys}
+          onExpand={handleExpandChange}
+          onSelect={onSelect}
+        >
+          {renderTreeNode(menuProjects)}
+        </Tree>
+      ) : (
+        <Empty style={{ padding: "80px 0" }} description={false} />
+      )}
+    </div>
+  );
+}
+
+export default SearchApi;
diff --git a/src/services/api.js b/src/services/api.js
index 46f81425..6c02c8f7 100644
--- a/src/services/api.js
+++ b/src/services/api.js
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-import {stringify} from "qs";
+import { stringify } from "qs";
 import request from "../utils/request";
 
 const baseUrl = document.getElementById("httpPath").innerHTML;
@@ -66,10 +66,15 @@ export async function updatePassword(params) {
 
 /* get all metadata */
 export async function getAllMetadata(params) {
-  const {appName, currentPage, pageSize} = params;
-  return request(`${baseUrl}/meta-data/queryList?${stringify(appName ? params : {currentPage, pageSize})}`, {
-    method: `GET`
-  });
+  const { appName, currentPage, pageSize } = params;
+  return request(
+    `${baseUrl}/meta-data/queryList?${stringify(
+      appName ? params : { currentPage, pageSize }
+    )}`,
+    {
+      method: `GET`
+    }
+  );
 }
 
 export async function findMetadata(params) {
@@ -143,8 +148,8 @@ export async function updateEnabled(params) {
 
 /* getAllUsers */
 export async function getAllUsers(params) {
-  const {userName, currentPage, pageSize} = params;
-  const myParams = userName ? params : {currentPage, pageSize};
+  const { userName, currentPage, pageSize } = params;
+  const myParams = userName ? params : { currentPage, pageSize };
   return request(`${baseUrl}/dashboardUser?${stringify(myParams)}`, {
     method: `GET`
   });
@@ -198,7 +203,7 @@ export async function getAllPlugins(params) {
 }
 
 /* get Plugins snapshot */
-export  function activePluginSnapshot() {
+export function activePluginSnapshot() {
   return request(`${baseUrl}/plugin/snapshot/active`, {
     method: `GET`
   });
@@ -254,8 +259,8 @@ export async function updateAuth(params) {
 
 /* getAllAuth */
 export async function getAllAuth(params) {
-  const {appKey, currentPage, pageSize} = params;
-  let myParams = appKey ? params : {currentPage, pageSize};
+  const { appKey, currentPage, pageSize } = params;
+  let myParams = appKey ? params : { currentPage, pageSize };
   return request(`${baseUrl}/appAuth?${stringify(myParams)}`, {
     method: `GET`
   });
@@ -271,8 +276,8 @@ export async function syncAuthsData() {
 
 /* getAllAuths */
 export async function getAllAuths(params) {
-  const {appKey, phone, currentPage, pageSize} = params;
-  const myParams = appKey || phone ? params : {currentPage, pageSize};
+  const { appKey, phone, currentPage, pageSize } = params;
+  const myParams = appKey || phone ? params : { currentPage, pageSize };
   return request(`${baseUrl}/appAuth/findPageByQuery?${stringify(myParams)}`, {
     method: `GET`
   });
@@ -585,10 +590,10 @@ export async function getAllRoles() {
 
 /* get roles by page */
 export async function getRoleList(params) {
-  const {roleName, currentPage, pageSize} = params;
-  let myParams = {...params};
+  const { roleName, currentPage, pageSize } = params;
+  let myParams = { ...params };
   if (!roleName) {
-    myParams = {currentPage, pageSize};
+    myParams = { currentPage, pageSize };
   }
   return request(`${baseUrl}/role?${stringify(myParams)}`, {
     method: `GET`
@@ -634,10 +639,10 @@ export async function updateRole(params) {
 
 /* get resources by page */
 export async function getAllResources(params) {
-  const {title, currentPage, pageSize} = params;
-  let myParams = {...params};
+  const { title, currentPage, pageSize } = params;
+  let myParams = { ...params };
   if (!title) {
-    myParams = {currentPage, pageSize};
+    myParams = { currentPage, pageSize };
   }
   return request(`${baseUrl}/resource?${stringify(myParams)}`, {
     method: `GET`
@@ -758,8 +763,27 @@ export async function deleteDataPermisionRule(params) {
 }
 
 /* get new event recode logs */
-export  function getNewEventRecodLogList() {
+export function getNewEventRecodLogList() {
   return request(`${baseUrl}/operation-record/log/list`, {
     method: `GET`
   });
 }
+
+/* get all api */
+export function getDocMenus() {
+  return request(`${baseUrl}/apidoc/getDocMenus`, {
+    method: `GET`
+  });
+}
+
+/* get api item */
+export function getDocItem(params) {
+  return request(`${baseUrl}/apidoc/getDocItem?${stringify(params)}`, {
+    method: `GET`
+  });
+}
+
+/* sandbox proxyGateway */
+export function sandboxProxyGateway() {
+  return `${baseUrl}/sandbox/proxyGateway`;
+}