You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@shenyu.apache.org by mi...@apache.org on 2023/01/14 08:34:40 UTC
[shenyu-dashboard] branch master updated: feat: add tag operator (#267)
This is an automated email from the ASF dual-hosted git repository.
midnight2104 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu-dashboard.git
The following commit(s) were added to refs/heads/master by this push:
new 0b0d2768 feat: add tag operator (#267)
0b0d2768 is described below
commit 0b0d27682c1f2dae88c50c8d0a9e2015679ad093
Author: dayu <we...@qq.com>
AuthorDate: Sat Jan 14 16:34:35 2023 +0800
feat: add tag operator (#267)
Co-authored-by: mahaitao617 <ma...@mahaitao617deMacBook-Pro.local>
---
src/index.less | 11 +-
src/locales/en-US.json | 4 +
src/locales/zh-CN.json | 4 +
src/routes/Document/ApiDoc.js | 167 +++++++------
.../Document/components/AddAndUpdateApiDoc.js | 118 ++++++---
src/routes/Document/components/AddAndUpdateTag.js | 125 ++++++++++
src/routes/Document/components/ApiContext.js | 5 +-
src/routes/Document/components/ApiInfo.js | 28 ++-
src/routes/Document/components/SearchApi.js | 274 ++++++++++++++-------
src/routes/Document/components/TagInfo.js | 55 +++++
src/services/api.js | 36 +++
11 files changed, 602 insertions(+), 225 deletions(-)
diff --git a/src/index.less b/src/index.less
index 8a6eb80c..dcb224d8 100644
--- a/src/index.less
+++ b/src/index.less
@@ -23,7 +23,7 @@ body,
:global {
.plug-content-wrap {
- padding: 24px
+ padding: 24px;
}
.open {
@@ -33,11 +33,6 @@ body,
.close {
color: #ff586d;
}
-
- .ant-btn-danger {
- background: #f5222d !important;
- color: #fff !important;
- }
}
:global(.ant-layout) {
@@ -79,7 +74,7 @@ ol {
}
:global {
- .ant-table-small>.ant-table-content>.ant-table-body {
+ .ant-table-small > .ant-table-content > .ant-table-body {
margin: 0 !important;
}
}
@@ -146,4 +141,4 @@ body {
.ant-modal {
max-width: calc(100vw - 32px);
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/en-US.json b/src/locales/en-US.json
index 75b52a2b..a18b6eb1 100644
--- a/src/locales/en-US.json
+++ b/src/locales/en-US.json
@@ -324,6 +324,10 @@
"SHENYU.DOCUMENT.APIDOC.APIDESC": "apiDesc",
"SHENYU.DOCUMENT.APIDOC.APISOURCE": "apiSource",
"SHENYU.DOCUMENT.APIDOC.DOCUMENT": "document",
+ "SHENYU.DOCUMENT.TAG.NAME": "name",
+ "SHENYU.DOCUMENT.TAG.DESC": "tagDesc",
+ "SHENYU.DOCUMENT.TAG.PARENT.ID": "parentTagId",
+ "SHENYU.DOCUMENT.TAG.ext": "ext",
"SHENYU.COMMON.REQUIRED": "Required",
"SHENYU.COMMON.MAX.LENGTH": "Max Length",
"SHENYU.COMMON.MAX.EXAMPLE": "Example",
diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json
index a26509a4..84c2da21 100644
--- a/src/locales/zh-CN.json
+++ b/src/locales/zh-CN.json
@@ -312,6 +312,10 @@
"SHENYU.DOCUMENT.APIDOC.APIDESC": "api描述",
"SHENYU.DOCUMENT.APIDOC.APISOURCE": "api来源",
"SHENYU.DOCUMENT.APIDOC.DOCUMENT": "文档说明",
+ "SHENYU.DOCUMENT.TAG.NAME": "标签名称",
+ "SHENYU.DOCUMENT.TAG.DESC": "标签描述",
+ "SHENYU.DOCUMENT.TAG.EXT": "标签扩展信息",
+ "SHENYU.DOCUMENT.TAG.PARENT.ID": "父节点id",
"SHENYU.COMMON.REQUIRED": "必填",
"SHENYU.COMMON.MAX.LENGTH": "最大长度",
"SHENYU.COMMON.MAX.EXAMPLE": "示例值",
diff --git a/src/routes/Document/ApiDoc.js b/src/routes/Document/ApiDoc.js
index b06b6486..12b417cb 100644
--- a/src/routes/Document/ApiDoc.js
+++ b/src/routes/Document/ApiDoc.js
@@ -15,38 +15,30 @@
* limitations under the License.
*/
+/* eslint-disable no-unused-expressions */
+
import { Col, Row, Card, BackTop, Empty, message } from "antd";
import React, { useEffect, useState } from "react";
import SearchApi from "./components/SearchApi";
-import AddAndUpdateApiDoc from "./components/AddAndUpdateApiDoc";
import ApiInfo from "./components/ApiInfo";
-import { getDocMenus, getApiDetail, addApi, updateApi, deleteApi, getApiMockRequest} from "../../services/api";
+import TagInfo from "./components/TagInfo";
+import {
+ getDocMenus,
+ getApiDetail,
+ deleteApi,
+ getTagDetail,
+ deleteTag,
+ getApiMockRequest
+} from "../../services/api";
import ApiContext from "./components/ApiContext";
function ApiDoc() {
+ const [tagDetail, setTagDetail] = useState({});
const [apiDetail, setApiDetail] = useState({});
const [apiData, setApiData] = useState({});
const [apiMock, setApiMock] = useState({});
- const [open, setOpen] = useState(false);
- const [flag, setflag] = useState('add');
- const [initialValue, setInitialValue] = useState({
- id: '',
- contextPath: '',
- apiPath: '',
- httpMethod: '',
- consume: '',
- produce: '',
- version: '',
- rpcType: '',
- state: '',
- ext: '',
- apiOwner: '',
- apiDesc: '',
- apiSource: '',
- document: '',
- tagIds: []
- })
+ const searchApiRef = React.createRef();
const initData = async () => {
const { code, data = {} } = await getDocMenus();
@@ -69,69 +61,74 @@ function ApiDoc() {
setApiData(data);
}
};
+
const handleSelectNode = async (_, e) => {
const {
node: {
- props: {
- dataRef: { id, isLeaf }
- }
+ props: { id, isLeaf }
}
} = e;
- if (!isLeaf) {
- return;
+ if (isLeaf) {
+ const { code, message: msg, data } = await getApiDetail(id);
+ if (code !== 200) {
+ message.error(msg);
+ return;
+ }
+ setApiDetail(data);
+ setTagDetail({});
+
+ const {
+ code: mockCode,
+ message: mockMsg,
+ data: mockData
+ } = await getApiMockRequest(id);
+ if (mockCode !== 200) {
+ message.error(mockMsg);
+ return;
+ }
+ setApiMock(mockData);
+ } else {
+ const { code, message: msg, data } = await getTagDetail(id);
+ if (code !== 200) {
+ message.error(msg);
+ return;
+ }
+ setTagDetail(data);
+ setApiDetail({});
+ }
+ };
+
+ const handleDelete = async () => {
+ let res = {};
+ if (tagDetail.id) {
+ res = await deleteTag([tagDetail.id]);
}
- if (!id) {
- const targetId = _
- handleAddApi(targetId)
- return;
+ if (apiDetail.id) {
+ res = await deleteApi([apiDetail.id]);
}
- const { code, message: msg, data } = await getApiDetail(id);
+ const { code, message: msg } = res;
if (code !== 200) {
message.error(msg);
- return;
+ } else {
+ message.success(msg);
+ searchApiRef.current?.updateTree();
}
- setInitialValue({
- id
- });
- setApiDetail(data);
+ };
- const { code: mockCode, message: mockMsg, data: mockData} = await getApiMockRequest(id);
- if (mockCode !== 200) {
- message.error(mockMsg);
- return;
+ const handleUpdate = () => {
+ if (tagDetail.id) {
+ searchApiRef.current?.addOrUpdateTag(tagDetail);
}
- setApiMock(mockData);
- };
- const handleAddApi = (targetId) => {
- setflag('add')
- setInitialValue({
- tagIds: [targetId]
- });
- setOpen(true)
- };
- const callSaveOrUpdateApi = async (params) => {
- let rs = (flag === 'add' ? await addApi({ ...params, tagIds: initialValue.tagIds[0] }) : await updateApi({ ...params, id: initialValue.id, tagIds: initialValue.tagIds }));
- if (rs.code !== 200) {
- message.error(rs.msg);
- } else {
- setOpen(false)
- location.reload()
+ if (apiDetail.id) {
+ searchApiRef.current?.addOrUpdateApi(apiDetail);
}
};
- const handleDeleteApi = async () => {
- const { code, message: msg } = await deleteApi([initialValue.id]);
- if (code !== 200) {
- message.error(msg);
- } else {
- location.reload()
- }
+
+ // eslint-disable-next-line no-unused-vars
+ const handleAfterUpdate = data => {
+ setApiDetail({});
+ setTagDetail({});
};
- const handleUpdateApi = async () => {
- let queryData = await getApiDetail(initialValue.id)
- setInitialValue(queryData.data);
- setOpen(true)
- setflag('update')
- }
useEffect(() => {
initData();
@@ -142,24 +139,36 @@ function ApiDoc() {
value={{
apiDetail,
apiData,
- apiMock
+ apiMock,
+ tagDetail
}}
>
<Card style={{ margin: 24 }}>
- {open && <AddAndUpdateApiDoc onCancel={() => setOpen(false)} handleOk={callSaveOrUpdateApi} {...initialValue} />
- }
<Row gutter={24}>
<Col span={6}>
- <SearchApi onSelect={handleSelectNode} />
+ <SearchApi
+ onSelect={handleSelectNode}
+ ref={searchApiRef}
+ afterUpdate={handleAfterUpdate}
+ />
</Col>
<Col span={18}>
+ {tagDetail.id ? (
+ <TagInfo
+ handleUpdate={handleUpdate}
+ handleDelete={handleDelete}
+ />
+ ) : null}
{apiDetail.id ? (
- <>
- <ApiInfo handleUpdateApi={handleUpdateApi} handleDeleteApi={handleDeleteApi} />
- </>
- ) : (
- <Empty description={false} style={{ padding: "160px 0" }} />
- )}
+ <ApiInfo
+ handleUpdate={handleUpdate}
+ handleDelete={handleDelete}
+ />
+ ) : null}
+ {!tagDetail.id &&
+ !apiDetail.id && (
+ <Empty description={false} style={{ padding: "160px 0" }} />
+ )}
</Col>
</Row>
<BackTop />
diff --git a/src/routes/Document/components/AddAndUpdateApiDoc.js b/src/routes/Document/components/AddAndUpdateApiDoc.js
index b8b5485e..91382322 100644
--- a/src/routes/Document/components/AddAndUpdateApiDoc.js
+++ b/src/routes/Document/components/AddAndUpdateApiDoc.js
@@ -15,30 +15,78 @@
* limitations under the License.
*/
-import { Modal, Form, Input, Select } from "antd";
+/* eslint-disable no-unused-expressions */
+/* eslint-disable radix */
+import { Modal, Form, Input, Select, message } from "antd";
import React, { Component } from "react";
+import PropTypes from "prop-types";
import { Method } from "./globalData";
import { getIntlContent } from "../../../utils/IntlUtils";
+import { addApi, updateApi } from "../../../services/api";
+
+const RPCTYPE = [
+ "http",
+ "dubbo",
+ "sofa",
+ "tars",
+ "websocket",
+ "springCloud",
+ "motan",
+ "grpc"
+];
+
+const API_SOURCE_TYPE = [
+ "swagger",
+ "annotation generation",
+ "create manuallym",
+ "import swagger",
+ "import yapi"
+];
class AddAndUpdateApiDoc extends Component {
+ static defaultProps = {
+ form: PropTypes.object,
+ visible: PropTypes.bool,
+ formLoaded: PropTypes.func,
+ onOk: PropTypes.func,
+ onCancel: PropTypes.func
+ };
+
+ componentDidMount() {
+ const { form, formLoaded } = this.props;
+ formLoaded?.(form);
+ }
+
+ handleSubmit = () => {
+ const { form, onOk } = this.props;
+ form.validateFieldsAndScroll(async (err, values) => {
+ if (!err) {
+ const { id } = values;
+ let res = {};
+ values.state = parseInt(values.state);
+ values.apiSource = parseInt(values.apiSource);
+ values.httpMethod = parseInt(values.httpMethod);
+ if (!id) {
+ res = await addApi({
+ ...values
+ });
+ } else {
+ res = await updateApi({
+ ...values
+ });
+ }
+
+ if (res.code !== 200) {
+ message.error(res.message);
+ } else {
+ message.success(res.message);
+ onOk?.(values);
+ }
+ }
+ });
+ };
+
render() {
- const RPCTYPE = [
- "http",
- "dubbo",
- "sofa",
- "tars",
- "websocket",
- "springCloud",
- "motan",
- "grpc"
- ];
- const API_SOURCE_TYPE = [
- "swagger",
- "annotation generation",
- "create manuallym",
- "import swagger",
- "import yapi"
- ];
const {
onCancel,
form,
@@ -54,7 +102,8 @@ class AddAndUpdateApiDoc extends Component {
apiOwner = "",
apiDesc = "",
apiSource = "",
- document = ""
+ document = "",
+ visible = false
} = this.props;
const { getFieldDecorator } = form;
const formItemLayout = {
@@ -65,25 +114,16 @@ class AddAndUpdateApiDoc extends Component {
sm: { span: 19 }
}
};
- const handleSubmit = () => {
- const { handleOk } = this.props;
- let newValues = "";
- form.validateFieldsAndScroll((err, values) => {
- if (!err) {
- // eslint-disable-next-line radix
- values.state = parseInt(values.state);
- // eslint-disable-next-line radix
- values.apiSource = parseInt(values.apiSource);
- // eslint-disable-next-line radix
- values.httpMethod = parseInt(values.httpMethod);
- newValues = values;
- }
- });
- handleOk(newValues);
- };
+
return (
- <Modal visible onCancel={onCancel} onOk={handleSubmit}>
- <Form onSubmit={handleSubmit} className="login-form">
+ <Modal
+ visible={visible}
+ onCancel={onCancel}
+ onOk={this.handleSubmit}
+ closable={false}
+ forceRender
+ >
+ <Form className="login-form">
<Form.Item
label={`${getIntlContent("SHENYU.DOCUMENT.APIDOC.CONTEXTPATH")}`}
{...formItemLayout}
@@ -338,6 +378,10 @@ class AddAndUpdateApiDoc extends Component {
/>
)}
</Form.Item>
+
+ <Form.Item hidden>{getFieldDecorator("tagIds")(<Input />)}</Form.Item>
+
+ <Form.Item hidden>{getFieldDecorator("id")(<Input />)}</Form.Item>
</Form>
</Modal>
);
diff --git a/src/routes/Document/components/AddAndUpdateTag.js b/src/routes/Document/components/AddAndUpdateTag.js
new file mode 100644
index 00000000..687fed97
--- /dev/null
+++ b/src/routes/Document/components/AddAndUpdateTag.js
@@ -0,0 +1,125 @@
+/*
+ * 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.
+ */
+
+/* eslint-disable no-unused-expressions */
+import { Modal, Form, Input, message } from "antd";
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+import { getIntlContent } from "../../../utils/IntlUtils";
+import { addTag, updateTag } from "../../../services/api";
+
+class AddAndUpdateTag extends Component {
+ static defaultProps = {
+ form: PropTypes.object,
+ visible: PropTypes.bool,
+ formLoaded: PropTypes.func,
+ onOk: PropTypes.func,
+ onCancel: PropTypes.func
+ };
+
+ componentDidMount() {
+ const { form, formLoaded } = this.props;
+ formLoaded?.(form);
+ }
+
+ handleSubmit = () => {
+ const {
+ onOk,
+ form: { validateFieldsAndScroll }
+ } = this.props;
+ validateFieldsAndScroll(async (err, values) => {
+ if (!err) {
+ const { id } = values;
+ let res = {};
+ if (!id) {
+ // add
+ res = await addTag(values);
+ } else {
+ // update
+ res = await updateTag(values);
+ }
+ if (res.code !== 200) {
+ message.error(res.message);
+ } else {
+ message.success(res.message);
+ onOk?.(values);
+ }
+ }
+ });
+ };
+
+ render() {
+ const { onCancel, form, name = "", tagDesc = "", visible } = this.props;
+ const { getFieldDecorator } = form;
+ const formItemLayout = {
+ labelCol: {
+ sm: { span: 5 }
+ },
+ wrapperCol: {
+ sm: { span: 19 }
+ }
+ };
+
+ return (
+ <Modal
+ visible={visible}
+ onCancel={onCancel}
+ onOk={this.handleSubmit}
+ closable={false}
+ forceRender
+ >
+ <Form className="login-form" {...formItemLayout}>
+ <Form.Item label={`${getIntlContent("SHENYU.DOCUMENT.TAG.NAME")}`}>
+ {getFieldDecorator("name", {
+ rules: [
+ {
+ required: true,
+ message: getIntlContent("SHENYU.DOCUMENT.TAG.NAME")
+ }
+ ],
+ initialValue: name
+ })(
+ <Input placeholder={getIntlContent("SHENYU.DOCUMENT.TAG.NAME")} />
+ )}
+ </Form.Item>
+
+ <Form.Item label={`${getIntlContent("SHENYU.DOCUMENT.TAG.DESC")}`}>
+ {getFieldDecorator("tagDesc", {
+ rules: [
+ {
+ required: true,
+ message: getIntlContent("SHENYU.DOCUMENT.TAG.DESC")
+ }
+ ],
+ initialValue: tagDesc
+ })(
+ <Input placeholder={getIntlContent("SHENYU.DOCUMENT.TAG.DESC")} />
+ )}
+ </Form.Item>
+
+ <Form.Item hidden>
+ {getFieldDecorator("parentTagId")(<Input />)}
+ </Form.Item>
+
+ <Form.Item hidden>{getFieldDecorator("id")(<Input />)}</Form.Item>
+ </Form>
+ </Modal>
+ );
+ }
+}
+
+export default Form.create()(AddAndUpdateTag);
diff --git a/src/routes/Document/components/ApiContext.js b/src/routes/Document/components/ApiContext.js
index 58d7d19f..eb6e52ab 100644
--- a/src/routes/Document/components/ApiContext.js
+++ b/src/routes/Document/components/ApiContext.js
@@ -15,10 +15,13 @@
* limitations under the License.
*/
+/* eslint-disable no-unused-vars */
+
import { createContext } from "react";
export default createContext({
apiDetail: {},
apiData: {},
- apiMock: {}
+ apiMock: {},
+ tagDetail: {}
});
diff --git a/src/routes/Document/components/ApiInfo.js b/src/routes/Document/components/ApiInfo.js
index 8ca7e49e..44011d2d 100644
--- a/src/routes/Document/components/ApiInfo.js
+++ b/src/routes/Document/components/ApiInfo.js
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-import { Typography, Table, Tabs, Icon, Row, Col , Button } from "antd";
+import { Typography, Table, Tabs, Icon, Row, Col, Button } from "antd";
import React, { useContext } from "react";
import ApiDebug from "./ApiDebug";
import ApiContext from "./ApiContext";
@@ -30,8 +30,9 @@ function ApiInfo(props) {
apiDetail,
apiDetail: { document, responseParameters, requestHeaders }
} = useContext(ApiContext);
+ const { handleUpdate, handleDelete } = props;
+
let documentJSON = {};
- let {handleUpdateApi,handleDeleteApi} = props
try {
documentJSON = JSON.parse(document);
documentJSON.errorCode = [];
@@ -135,6 +136,7 @@ function ApiInfo(props) {
dataIndex: "envDesc"
}
];
+
return (
<>
<Tabs>
@@ -147,11 +149,23 @@ function ApiInfo(props) {
}
key="1"
>
- <Title level={2}>
- {apiDetail.tags[apiDetail.tags.length - 1].name}
- <Button style={{float:'right'}} onClick={handleDeleteApi}>delete</Button>
- <Button style={{float:'right'}} onClick={handleUpdateApi}>edit</Button>
- </Title>
+ <Row gutter={24}>
+ <Col span={12}>
+ <Title level={2}>
+ {apiDetail.tags[apiDetail.tags.length - 1].name}
+ </Title>
+ </Col>
+ <Col span={12} style={{ textAlign: "right" }}>
+ <Button onClick={handleUpdate}>
+ {getIntlContent("SHENYU.BUTTON.SYSTEM.EDIT")}
+ </Button>
+
+ <Button ghost type="danger" onClick={handleDelete}>
+ {getIntlContent("SHENYU.BUTTON.SYSTEM.DELETE")}
+ </Button>
+ </Col>
+ </Row>
+
<Paragraph>
<Title level={4}>
{getIntlContent("SHENYU.DOCUMENT.APIDOC.INFO.INTERFACE.ADDRESS")}
diff --git a/src/routes/Document/components/SearchApi.js b/src/routes/Document/components/SearchApi.js
index 132e0334..9fef08b4 100644
--- a/src/routes/Document/components/SearchApi.js
+++ b/src/routes/Document/components/SearchApi.js
@@ -15,143 +15,231 @@
* limitations under the License.
*/
-import { Tree, Empty, message, Typography } from "antd";
-import React, { useEffect, useState } from "react";
-// import ApiContext from "./ApiContext";
-// import { getIntlContent } from "../../../utils/IntlUtils";
+/* eslint-disable no-unused-expressions */
+
+import { Tree, Empty, message, Typography, Button, Row, Col, Spin } from "antd";
+import React, { useEffect, useImperativeHandle, useState } from "react";
import { getRootTag, getParentTagId, getApi } from "../../../services/api";
import { Method } from "./globalData";
+import AddAndUpdateTag from "./AddAndUpdateTag";
+import AddAndUpdateApiDoc from "./AddAndUpdateApiDoc";
const { Text } = Typography;
-const { TreeNode } = Tree;
-// const { Search } = Input;
-
-function SearchApi(props) {
- const { onSelect } = props;
- // const [searchValue, setSearchValue] = useState("");
-
- // 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);
- // }
- // });
- // setSearchValue(value);
- // };
-
- const [apiTree, setApiTree] = useState([]);
-
- const renderTreeNodes = data => {
- return data.map(item => {
- if (item.children) {
- return (
- <TreeNode
- title={item.title}
- key={item.key}
- dataRef={item}
- selectable={item.isLeaf}
- isLeaf={item.isLeaf}
- >
- {renderTreeNodes(item.children)}
- </TreeNode>
- );
- }
- return <TreeNode key={item.key} {...item} dataRef={item} />;
- });
- };
+
+const SearchApi = React.forwardRef((props, ref) => {
+ const { onSelect, afterUpdate } = props;
+ const [loading, setLoading] = useState(false);
+ const [treeData, setTreeData] = useState({});
+ const [expandedKeys, setExpandedKeys] = useState([]);
const queryRootTag = async () => {
+ setLoading(true);
const { code, data = [], message: msg } = await getRootTag();
+ setLoading(false);
if (code !== 200) {
message.error(msg);
return;
}
- setApiTree(
+ const arr =
data?.map((item, index) => ({
...item,
title: item.name,
key: index.toString(),
- isLeaf: !item.hasChildren
- })) || []
- );
+ isLeaf: false
+ })) || [];
+ setTreeData(arr);
};
- const onLoadData = async treeNode => {
- if (treeNode.props.children) {
- return Promise.resolve();
+ const onExpand = async (keys, { expanded, node }) => {
+ setExpandedKeys(keys);
+ if (expanded === false) {
+ return;
}
- const { id, hasChildren } = treeNode.props.dataRef;
+ setLoading(true);
+ const { id, hasChildren, eventKey } = node.props;
+ const newTreeData = [...treeData];
+ let showAddTag = true;
+ let resData = [];
+ const eventKeys = eventKey
+ .split("-")
+ .map((v, i, arr) => arr.slice(0, i + 1).join("-"));
+
if (hasChildren) {
const { code, message: msg, data } = await getParentTagId(id);
+ setLoading(false);
if (code !== 200) {
message.error(msg);
return Promise.reject();
}
- treeNode.props.dataRef.children = data?.map((item, index) => ({
- ...item,
- title: item.name,
- key: `${treeNode.props.eventKey}-${index}`
- }));
+ resData = data;
} else {
const { code, message: msg, data } = await getApi(id);
+ setLoading(false);
if (code !== 200) {
message.error(msg);
return Promise.reject();
}
const { dataList } = data;
- treeNode.props.dataRef.children = dataList?.map((item, index) => ({
- ...item,
- title: (
- <>
- <Text code>{Method[item.httpMethod]}</Text> {item.apiPath}
- </>
- ),
- key: `${treeNode.props.eventKey}-${index}`,
- isLeaf: true
- }))
- treeNode.props.dataRef.children.push({
- title: (
- <>
- <Text code> + </Text>
- </>
- ),
- key: treeNode.props.dataRef.id,
- isLeaf: true
- })
- ;
+ if (dataList.length) {
+ showAddTag = false;
+ }
+ resData = dataList;
}
- setApiTree([...apiTree]);
- return Promise.resolve();
+ const curNode = eventKeys.reduce((pre, cur, curIndex, curArray) => {
+ const el = pre.find(item => item.key === cur);
+ if (curIndex === curArray.length - 1) {
+ return el;
+ } else {
+ return el.children || [];
+ }
+ }, newTreeData);
+
+ curNode.children = resData?.map((item, index) => ({
+ ...item,
+ title: hasChildren ? (
+ item.name
+ ) : (
+ <>
+ <Text code>{Method[item.httpMethod]}</Text> {item.apiPath}
+ </>
+ ),
+ key: `${eventKey}-${index}`,
+ isLeaf: !hasChildren
+ }));
+ curNode.children.push({
+ selectable: false,
+ title: (
+ <Row gutter={8}>
+ {showAddTag && (
+ <Col span={12}>
+ <Button
+ type="primary"
+ ghost
+ size="small"
+ onClick={() =>
+ addOrUpdateTag({
+ parentTagId: id
+ })
+ }
+ >
+ + Tag
+ </Button>
+ </Col>
+ )}
+
+ <Col span={12}>
+ <Button
+ type="primary"
+ ghost
+ size="small"
+ onClick={() =>
+ addOrUpdateApi({
+ tagIds: [id]
+ })
+ }
+ >
+ + Api
+ </Button>
+ </Col>
+ </Row>
+ ),
+ key: `${eventKey}-operator`,
+ isLeaf: true
+ });
+ setTreeData(newTreeData);
+ };
+
+ const [openTag, setOpenTag] = useState(false);
+ const [tagForm, setTagForm] = useState({});
+
+ const handleTagCancel = () => {
+ setOpenTag(false);
+ tagForm.resetFields();
+ };
+
+ const handleTagOk = data => {
+ handleTagCancel();
+ updateTree(data);
+ };
+
+ const [openApi, setOpenApi] = useState(false);
+ const [apiForm, setApiForm] = useState({});
+
+ const handleApiCancel = () => {
+ setOpenApi(false);
+ tagForm.resetFields();
+ };
+
+ const handleApiOk = data => {
+ handleApiCancel();
+ updateTree(data);
+ };
+
+ const addOrUpdateApi = data => {
+ apiForm.setFieldsValue({
+ ...data
+ });
+ setOpenApi(true);
+ };
+
+ const addOrUpdateTag = data => {
+ tagForm.setFieldsValue({
+ ...data
+ });
+ setOpenTag(true);
};
+ const updateTree = data => {
+ setExpandedKeys([]);
+ queryRootTag();
+ afterUpdate(data);
+ };
+
+ useImperativeHandle(ref, () => ({
+ addOrUpdateApi,
+ addOrUpdateTag,
+ updateTree
+ }));
+
useEffect(() => {
queryRootTag();
}, []);
return (
<div style={{ overflow: "auto" }}>
- {/* <Search
- allowClear
- onChange={handleSearchChange}
- placeholder={getIntlContent(
- "SHENYU.DOCUMENT.APIDOC.SEARCH.PLACEHOLDER"
- )}
- /> */}
- {apiTree?.length ? (
- <Tree loadData={onLoadData} onSelect={onSelect}>
- {renderTreeNodes(apiTree)}
- </Tree>
+ {treeData?.length ? (
+ <Spin spinning={loading}>
+ <Tree
+ onSelect={onSelect}
+ treeData={treeData}
+ onExpand={onExpand}
+ expandedKeys={expandedKeys}
+ />
+ </Spin>
) : (
<Empty style={{ padding: "80px 0" }} description={false} />
)}
+ <Button
+ block
+ type="dashed"
+ onClick={() => addOrUpdateTag({ parentTagId: "0" })}
+ >
+ Add Root Tag
+ </Button>
+ <AddAndUpdateTag
+ visible={openTag}
+ formLoaded={setTagForm}
+ onOk={handleTagOk}
+ onCancel={handleTagCancel}
+ />
+ <AddAndUpdateApiDoc
+ visible={openApi}
+ formLoaded={setApiForm}
+ onOk={handleApiOk}
+ onCancel={handleApiCancel}
+ />
</div>
);
-}
+});
export default SearchApi;
diff --git a/src/routes/Document/components/TagInfo.js b/src/routes/Document/components/TagInfo.js
new file mode 100644
index 00000000..a79d9fa4
--- /dev/null
+++ b/src/routes/Document/components/TagInfo.js
@@ -0,0 +1,55 @@
+/*
+ * 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, Button, Row, Col } from "antd";
+import React, { useContext } from "react";
+import ApiContext from "./ApiContext";
+import { getIntlContent } from "../../../utils/IntlUtils";
+
+const { Title, Text, Paragraph } = Typography;
+
+function TagInfo(props) {
+ const { handleDelete, handleUpdate } = props;
+ const { tagDetail } = useContext(ApiContext);
+
+ return (
+ <Row gutter={24}>
+ <Col span={12}>
+ <Title level={2}>{tagDetail.name}</Title>
+ </Col>
+ <Col span={12} style={{ textAlign: "right" }}>
+ <Button onClick={handleUpdate}>
+ {getIntlContent("SHENYU.BUTTON.SYSTEM.EDIT")}
+ </Button>
+
+ <Button ghost type="danger" onClick={handleDelete}>
+ {getIntlContent("SHENYU.BUTTON.SYSTEM.DELETE")}
+ </Button>
+ </Col>
+ <Col span={24}>
+ <Paragraph>
+ <Title level={4}>{getIntlContent("SHENYU.DOCUMENT.TAG.DESC")}</Title>
+ <Text>{tagDetail.tagDesc}</Text>
+ <Title level={4}>{getIntlContent("SHENYU.DOCUMENT.TAG.EXT")}</Title>
+ <Text code>{tagDetail.ext}</Text>
+ </Paragraph>
+ </Col>
+ </Row>
+ );
+}
+
+export default TagInfo;
diff --git a/src/services/api.js b/src/services/api.js
index 22234ff4..726a4148 100644
--- a/src/services/api.js
+++ b/src/services/api.js
@@ -809,6 +809,42 @@ export function getParentTagId(id) {
});
}
+/* getTagDetail */
+export function getTagDetail(id) {
+ return request(
+ `${baseUrl}/tag/id/${id}`, {
+ method: `GET`
+ });
+}
+
+/** add tag */
+export function addTag(params) {
+ return request(`${baseUrl}/tag`, {
+ method: `POST`,
+ body: {
+ ...params
+ }
+ });
+}
+
+/** delete tag */
+export function deleteTag(params) {
+ return request(`${baseUrl}/tag/batchDelete`, {
+ method: `DELETE`,
+ body: params
+ });
+}
+
+/** updateTag */
+export function updateTag(params) {
+ return request(`${baseUrl}/tag/id/${params.id}`, {
+ method: `PUT`,
+ body: {
+ ...params
+ }
+ });
+}
+
/* queryApi */
export function getApi(tagId) {
return request(`${baseUrl}/api?tagId=${tagId}¤tPage=0&pageSize=100`, {