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/shenyu-dashboard.git


The following commit(s) were added to refs/heads/master by this push:
     new f2c574cb feature: tcp-proxy front design and develop (#289)
f2c574cb is described below

commit f2c574cbb4d85eba9df232f24e83dc6190cc592d
Author: lulu <[email protected]>
AuthorDate: Sat Jun 24 22:07:37 2023 +0800

    feature: tcp-proxy front design and develop (#289)
    
    * feature: tcp-proxy front design and develop
    
    * feature: tcp-proxy front design and develop
    
    * fix: change 'Upstreams' to 'upstreams'
    
    * fix: editableTable cannot be hidden
    
    * fix: change editableTable to table and add text to divider
---
 src/common/router.js                              |   5 +
 src/locales/en-US.json                            |   4 +-
 src/locales/zh-CN.json                            |   4 +-
 src/models/discovery.js                           | 177 +++++++
 src/routes/Plugin/Common/index.js                 |   4 +-
 src/routes/Plugin/Discovery/card.js               |  89 ++++
 src/routes/Plugin/Discovery/configModal.js        | 111 +++++
 src/routes/Plugin/Discovery/index.js              | 552 ++++++++++++++++++++++
 src/routes/Plugin/Discovery/proxySelectorModal.js | 234 +++++++++
 src/routes/Plugin/Discovery/tcp.less              |  88 ++++
 src/routes/Plugin/Discovery/upstreamTable.js      | 209 ++++++++
 src/routes/Plugin/index.less                      |   5 +-
 src/routes/System/Plugin/AddModal.js              |  15 +-
 src/services/api.js                               |  53 +++
 src/utils/utils.js                                |  14 +-
 15 files changed, 1550 insertions(+), 14 deletions(-)

diff --git a/src/common/router.js b/src/common/router.js
index 2a4f1814..299e0ab8 100644
--- a/src/common/router.js
+++ b/src/common/router.js
@@ -96,6 +96,11 @@ export const getRouterData = app => {
     "/home": {
       component: dynamicWrapper(app, [], () => import("../routes/Home"))
     },
+    "/plug/Proxy/tcp":{
+      component: dynamicWrapper(app, ["discovery"], () =>
+        import("../routes/Plugin/Discovery")
+      )
+    },
     "/plug/:index/:id": {
       component: dynamicWrapper(app, ["common"], () =>
         import("../routes/Plugin/Common")
diff --git a/src/locales/en-US.json b/src/locales/en-US.json
index a4f2f231..46e4112a 100644
--- a/src/locales/en-US.json
+++ b/src/locales/en-US.json
@@ -353,5 +353,7 @@
   "SHENYU.DOCUMENT.APIDOC.DEBUG.MOCK.RESET": "Reset",
   "SHENYU.DOCUMENT.APIDOC.DEBUG.MOCK.ADD.HEADER": "Add header",
   "SHENYU.DOCUMENT.APIDOC.DEBUG.MOCK.ADD.QUERY": "Add Query",
-  "SHENYU.DOCUMENT.APIDOC.DEBUG.MOCK.ADDRESS.VALIDATE": "Request Address 
format error."
+  "SHENYU.DOCUMENT.APIDOC.DEBUG.MOCK.ADDRESS.VALIDATE": "Request Address 
format error.",
+  "SHENYU.PLUGIN.SELECTOR.LIST.CONFIGURATION": "Discovery Configuration",
+  "SHENYU.COMMON.RESPONSE.CONFIGURATION.SUCCESS": "Set Discovery Configuration 
Success"
 }
diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json
index c8a69f1f..ffb5389c 100644
--- a/src/locales/zh-CN.json
+++ b/src/locales/zh-CN.json
@@ -340,5 +340,7 @@
   "SHENYU.DOCUMENT.APIDOC.DEBUG.MOCK.RESET": "重置",
   "SHENYU.DOCUMENT.APIDOC.DEBUG.MOCK.ADD.HEADER": "添加请求头参数",
   "SHENYU.DOCUMENT.APIDOC.DEBUG.MOCK.ADD.QUERY": "添加查询参数",
-  "SHENYU.DOCUMENT.APIDOC.DEBUG.MOCK.ADDRESS.VALIDATE": "请求地址格式错误."
+  "SHENYU.DOCUMENT.APIDOC.DEBUG.MOCK.ADDRESS.VALIDATE": "请求地址格式错误.",
+  "SHENYU.PLUGIN.SELECTOR.LIST.CONFIGURATION": "插件级别服务发现配置",
+  "SHENYU.COMMON.RESPONSE.CONFIGURATION.SUCCESS": "配置成功"
 }
diff --git a/src/models/discovery.js b/src/models/discovery.js
new file mode 100644
index 00000000..71d5dafb
--- /dev/null
+++ b/src/models/discovery.js
@@ -0,0 +1,177 @@
+/*
+ * 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 {message} from "antd";
+import {
+  addProxySelector,
+  deleteProxySelector,
+  fetchProxySelector,  getDiscoveryTypeEnums,
+  postDiscoveryInsertOrUpdate,
+  updateProxySelector,
+  getDiscovery
+
+} from "../services/api";
+import {getIntlContent} from "../utils/IntlUtils";
+
+export default {
+  namespace: "discovery",
+
+  state: {
+    typeEnums: [],
+    selectorList: [],
+    chosenType: '',
+    total: 0,
+    currentPage: 1,
+    pageSize: 4
+  },
+
+  effects: {
+    * fetchSelector(params, {call, put}) {
+      const {payload, callback} = params;
+      const json = yield call(fetchProxySelector, payload);
+      if (json.code === 200) {
+        const {page, dataList} = json.data;
+        if (callback) {
+          callback(dataList, page.totalCount);
+        }
+        yield put({
+          type: "saveProxySelectors",
+          payload: {total: page.totalCount, dataList}
+        });
+      }
+    },
+
+    * fetchEnumType(params, {call, put}) {
+      const {payload} = params;
+      const json = yield call(getDiscoveryTypeEnums, payload);
+      if (json.code === 200) {
+        const data = json.data;
+        yield put({
+          type: "saveEnumTypes",
+          payload: {data}
+        });
+      }
+    },
+
+    * add(params, {call, put}) {
+      const {payload, callback, fetchValue} = params;
+      const json = yield call(addProxySelector, payload);
+      if (json.code === 200) {
+        message.success(getIntlContent('SHENYU.COMMON.RESPONSE.ADD.SUCCESS'));
+        callback();
+        yield put({
+          type: "reload", fetchValue
+        })
+      } else {
+        message.warn(json.message);
+      }
+    },
+    * delete(params, {call, put}) {
+      const {payload, fetchValue} = params;
+      const { list } = payload;
+      const json = yield call(deleteProxySelector, { list });
+      if (json.code === 200) {
+        
message.success(getIntlContent('SHENYU.COMMON.RESPONSE.DELETE.SUCCESS'));
+        // callback();
+        yield put({type: "reload", fetchValue});
+      } else {
+        message.warn(json.message);
+      }
+    },
+    * update(params, {call, put}) {
+      const {payload, callback, fetchValue} = params;
+      const json = yield call(updateProxySelector, payload);
+      if (json.code === 200) {
+        
message.success(getIntlContent('SHENYU.COMMON.RESPONSE.UPDATE.SUCCESS'));
+        callback();
+        yield put({type: "reload", fetchValue});
+      } else {
+        message.warn(json.message);
+      }
+    },
+    * reload(params, {put}) {
+      const {fetchValue} = params;
+      const {name = '', currentPage, pageSize} = fetchValue;
+      const payload = {name, currentPage, pageSize};
+      yield put({type: "fetchSelector", payload});
+    },
+
+
+    * set(params, {call}) {
+      const {payload, callback} = params;
+      const json = yield call(postDiscoveryInsertOrUpdate, payload);
+      if (json.code === 200) {
+        
message.success(getIntlContent('SHENYU.COMMON.RESPONSE.CONFIGURATION.SUCCESS'));
+        const { data } = json;
+        if (callback) {
+          callback(data);
+        }
+      } else {
+        message.warn(json.message);
+      }
+    },
+
+
+    * fetchDiscovery(params, {call, put}) {
+      const {payload, callback} = params;
+      const json = yield call(getDiscovery, payload);
+      if (json.code === 200) {
+        const {data} = json;
+        if (callback) {
+          callback(data);
+        }
+        yield put({
+          type: "saveConfig",
+          payload: data
+        });
+      }
+    },
+
+  },
+
+  reducers: {
+    saveProxySelectors(state, {payload}) {
+      return {
+        ...state,
+        selectorList: payload.dataList,
+        total: payload.total
+      };
+    },
+
+    saveEnumTypes(state, {payload}) {
+      return {
+        ...state,
+        typeEnums: payload.data
+      }
+    },
+
+    saveGlobalType(state, {payload}) {
+      return {
+        ...state,
+        chosenType: payload.chosenType
+      }
+    },
+    setCurrentPage(state, {payload}) {
+      return {
+        ...state,
+        currentPage: payload.currentPage,
+        pageSize: payload.pageSize
+      }
+    },
+
+  }
+};
diff --git a/src/routes/Plugin/Common/index.js 
b/src/routes/Plugin/Common/index.js
index 37bd5ef7..756ff4be 100644
--- a/src/routes/Plugin/Common/index.js
+++ b/src/routes/Plugin/Common/index.js
@@ -313,7 +313,9 @@ export default class Common extends Component {
                   },
                   fetchValue: {
                     name: pluginName,
-                    enabled: enabledStr
+                    enabled: enabledStr,
+                    currentPage: 1,
+                    pageSize: 50
                   },
                   callback: () => {
                     this.setState({ isPluginEnabled: enabled })
diff --git a/src/routes/Plugin/Discovery/card.js 
b/src/routes/Plugin/Discovery/card.js
new file mode 100644
index 00000000..be7d2297
--- /dev/null
+++ b/src/routes/Plugin/Discovery/card.js
@@ -0,0 +1,89 @@
+/*
+ * 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 React, {Component} from "react";
+import {Button, Card} from "antd";
+import {getIntlContent} from "../../../utils/IntlUtils";
+import tcpStyles from "./tcp.less";
+
+import { formatTimestamp } from "../../../utils/utils";
+import AuthButton from "../../../utils/AuthButton";
+
+
+
+export class TcpCard extends Component {
+
+  renderCardItems = () => {
+    const {forwardPort, createTime, updateTime} = this.props.data
+
+    return (
+      <div style={{display: 'flex', flexDirection: 'column'}}>
+        <div className={tcpStyles.cardItem}>
+          <div style={{ fontSize: '15px', marginLeft: '30px' 
}}>ForwardPort</div>
+          <div className={tcpStyles.cardTag}>{forwardPort}</div>
+        </div>
+        <div className={tcpStyles.cardItem}>
+          <div style={{ fontSize: '15px', marginLeft: '30px' 
}}>DateCreated</div>
+          <div 
className={tcpStyles.cardTag}>{formatTimestamp(createTime)}</div>
+        </div>
+        <div className={tcpStyles.cardItem}>
+          <div style={{ fontSize: '15px', marginLeft: '30px'  
}}>DateUpdated</div>
+          <div 
className={tcpStyles.cardTag}>{formatTimestamp(updateTime)}</div>
+        </div>
+      </div>
+    )
+  }
+
+  render() {
+    const {updateSelector, data, handleDelete} = this.props
+    return (
+      <Card
+        title={<div style={{ marginLeft: '30px', fontSize: '20px' 
}}>{data.name}</div>}
+        style={{  borderRadius: '5px' , boxShadow: '1px 2px 2px rgba(191, 189, 
189, 0.5)' }}
+        extra={(
+          <div>
+            <AuthButton perms="plugin:tcp:modify">
+              <Button
+                type="primary"
+                onClick={() => {
+                  updateSelector(data.id)
+                }}
+                style={{ marginRight: '20px' }}
+              >
+                {getIntlContent("SHENYU.COMMON.CHANGE")}
+              </Button>
+            </AuthButton>
+            <AuthButton perms="plugin:tcpSelector:delete">
+              <Button
+                type="primary"
+                onClick={() => {
+                  handleDelete(data.id)
+                }}
+                style={{ marginRight: '30px' }}
+              >
+                {getIntlContent("SHENYU.COMMON.DELETE.NAME")}
+              </Button>
+            </AuthButton>
+          </div>
+        )}
+      >
+        {this.renderCardItems()}
+      </Card>
+
+    )
+  }
+}
diff --git a/src/routes/Plugin/Discovery/configModal.js 
b/src/routes/Plugin/Discovery/configModal.js
new file mode 100644
index 00000000..0ca81d5e
--- /dev/null
+++ b/src/routes/Plugin/Discovery/configModal.js
@@ -0,0 +1,111 @@
+/*
+ * 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 React, {Component} from "react";
+import {Form, Input, Modal, Select} from "antd";
+import {getIntlContent} from "../../../utils/IntlUtils";
+
+
+const FormItem = Form.Item;
+
+
+class ConfigModal extends Component {
+
+
+  handleSubmit = e => {
+    const { form, handleOk } = this.props;
+    // console.log("I'm submit");
+    e.preventDefault();
+    form.validateFieldsAndScroll((err, values) => {
+      if (!err) {
+        let { name, serverList, props, tcpType } = values;
+        // console.log("id", id);
+        handleOk({ name, serverList, props, tcpType});
+      }
+    });
+  };
+
+
+  handleOptions() {
+    const {Option} = Select;
+    return this.props.typeEnums
+      .filter(value => value !== "local")
+      .map(value => <Option key={value} 
value={value.toString()}>{value}</Option>)
+  }
+
+  render() {
+    const { handleCancel, form, data } = this.props
+    const { getFieldDecorator } = form;
+    const { name, serverList, props, type: tcpType} = data || {};
+    return (
+      <Modal
+        visible
+        centered
+        title={getIntlContent("SHENYU.PLUGIN.SELECTOR.LIST.CONFIGURATION")}
+        onCancel={handleCancel}
+        onOk={this.handleSubmit}
+        okText={getIntlContent("SHENYU.COMMON.SURE")}
+        cancelText={getIntlContent("SHENYU.COMMON.CALCEL")}
+        destroyOnClose
+      >
+        <Form onSubmit={this.handleSubmit}>
+          <Form.Item label="Type">
+            {getFieldDecorator('tcpType', {
+              rules: [{ required: true, message: 'Please select the discovery 
type!' }],
+              initialValue: tcpType !== "" ? tcpType : undefined
+            })(
+              <Select
+                placeholder="Please select the discovery type"
+              >
+                {this.handleOptions()}
+              </Select>,
+            )}
+          </Form.Item>
+          <FormItem label="Name">
+            {getFieldDecorator('name', {
+              rules: [{ required: true, message: 'Please input the discovery 
name!' }],
+              initialValue: name
+            })(<Input
+              placeholder="the discovery name"
+            />)}
+          </FormItem>
+
+          <FormItem label="ServerList">
+            {getFieldDecorator('serverList', {
+              rules: [{ required: true, message: 'Please input the register 
server url!' }],
+              initialValue: serverList
+            })(<Input
+              placeholder="register server url"
+            />)}
+          </FormItem>
+
+          <FormItem label="Props">
+            {getFieldDecorator('props', {
+              rules: [{ required: true, message: 'Please input the props!' }],
+              initialValue: props
+            })(<Input.TextArea
+              placeholder="the discovery props"
+            />)}
+          </FormItem>
+
+        </Form>
+      </Modal>
+    )
+  }
+}
+
+export default Form.create()(ConfigModal);
diff --git a/src/routes/Plugin/Discovery/index.js 
b/src/routes/Plugin/Discovery/index.js
new file mode 100644
index 00000000..e36ad049
--- /dev/null
+++ b/src/routes/Plugin/Discovery/index.js
@@ -0,0 +1,552 @@
+/*
+ * 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 React, {Component} from 'react';
+import {connect} from 'dva';
+import {Button, Pagination, Row, Switch, Tag, Input, Typography} from "antd";
+import {getIntlContent} from "../../../utils/IntlUtils";
+import tcpStyles from './tcp.less'
+import ConfigModal from "./configModal";
+import ProxySelectorModal from "./proxySelectorModal";
+import {TcpCard} from "./card";
+import AddModal from "../../System/Plugin/AddModal";
+import AuthButton from "../../../utils/AuthButton";
+
+const {Search} = Input;
+const {Title} = Typography;
+
+@connect(({global, discovery, loading}) => ({
+  ...global,
+  ...discovery,
+  loading: loading.effects["global/fetchPlatform"]
+}))
+export default class TCPProxy extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      searchKey: '',
+      cardData: {
+        tcpType: '',
+        name: '',
+        forwardPort: '',
+        type: 'tcp',
+        props: '',
+        listenerNode: '',
+        handler: '',
+        discovery: {
+          serverList: '',
+          props: ''
+        },
+        discoveryUpstreams: [
+          // {
+          //   protocol: '1',
+          //   url: '1',
+          //   status:'1',
+          //   weight: '1',
+          //   key: '1'
+          // }
+        ]
+      },
+      isPluginEnabled: false,
+      popup: "",
+    };
+  }
+
+  componentDidMount() {
+    const {dispatch} = this.props
+    dispatch({
+      type: "discovery/fetchSelector",
+      payload: {
+        name: '',
+        currentPage: this.props.currentPage,
+        pageSize: this.props.pageSize
+      },
+      callback: (value) => {
+        const {dataList} = value;
+        dispatch({
+          type: "discovery/saveProxySelectors",
+          payload: {dataList}
+        });
+      }
+    })
+
+    dispatch({
+      type: "discovery/fetchEnumType",
+      callback: (value) => {
+        const {data} = value;
+        dispatch({
+          type: "discovery/saveEnumTypes",
+          payload: {data}
+        });
+      }
+    })
+
+  }
+
+  // eslint-disable-next-line react/sort-comp
+  renderCards(selectorList = []) {
+    return selectorList.map(selector =>
+      <TcpCard key={selector.id} updateSelector={this.updateSelector} 
data={selector} handleDelete={this.handleDelete} />
+    );
+  }
+
+  onPageChange = (page, pageSize) => {
+    this.props.dispatch({
+      type: "discovery/setCurrentPage",
+      payload: {
+        currentPage: page,
+        pageSize
+      }
+    })
+    const {searchKey} = this.state;
+    this.props.dispatch({
+      type: "discovery/fetchSelector",
+      payload: {
+        currentPage: page,
+        pageSize,
+        name: searchKey
+      }
+    });
+  }
+
+  getPlugin = (plugins, name) => {
+    const plugin = plugins.filter(item => {
+      return item.name === name;
+    });
+    return plugin && plugin.length > 0 ? plugin[0] : null;
+  };
+
+
+  togglePluginStatus = () => {
+    const {dispatch, plugins} = this.props;
+    const {name, id, role, config, sort, file} = this.getPlugin(plugins, 
"tcp");
+    const enabled = !this.state.isPluginEnabled
+    const enabledStr = enabled ? '1' : '0';
+    dispatch({
+      type: "plugin/update",
+      payload: {
+        config,
+        role,
+        name,
+        enabled,
+        id,
+        sort,
+        file
+      },
+      fetchValue: {
+        name: "tcp",
+        enabled: enabledStr,
+        currentPage: 1,
+        pageSize: 50
+      },
+      callback: () => {
+        this.setState({isPluginEnabled: enabled})
+      }
+    });
+  }
+
+
+  closeModal = () => {
+    this.setState({popup: ""});
+  };
+
+  closeUpdateModal = () => {
+    this.setState({popup: ""});
+    this.setState({
+      cardData: {
+        tcpType: '',
+        name: '',
+        forwardPort: '',
+        type: 'tcp',
+        props: '',
+        listenerNode: '',
+        handler: '',
+        discovery: {
+          serverList: '',
+          props: ''
+        },
+        discoveryUpstreams: [
+          // {
+          //   protocol: '1',
+          //   url: '1',
+          //   status:'1',
+          //   weight: '1',
+          //   key: '1'
+          // }
+        ]
+      }
+    });
+  };
+
+  addConfiguration = () => {
+    const {dispatch} = this.props;
+    dispatch({
+      type: "discovery/fetchDiscovery",
+      payload: {
+        pluginName: "tcp",
+        level: "1"
+      },
+      callback: discoveryConfigList => {
+        let discoveryId = '';
+        if (discoveryConfigList !== null) {
+          discoveryId = discoveryConfigList.id;
+        }
+        this.setState({
+          popup: (
+            <ConfigModal
+              data={discoveryConfigList}
+              typeEnums={this.props.typeEnums}
+              handleOk={values => {
+                const {name, serverList, props, tcpType} = values;
+                dispatch({
+                  type: "discovery/set",
+                  payload: {
+                    type: tcpType,
+                    serverList,
+                    name,
+                    props,
+                    pluginName: "tcp",
+                    level: 1,
+                    id: discoveryId
+                  },
+                  callback: () => {
+                    this.closeModal();
+                  }
+                });
+              }}
+              handleCancel={() => {
+                this.closeModal();
+              }}
+            />
+          )
+        });
+      }
+    });
+  };
+
+  editClick = () => {
+    const {dispatch, plugins} = this.props;
+    const plugin = this.getPlugin(plugins, "tcp");
+    plugin.enabled = this.state.isPluginEnabled;
+    dispatch({
+      type: "plugin/fetchByPluginId",
+      payload: {
+        pluginId: plugin.id,
+        type: "3"
+      },
+
+      callback: pluginConfigList => {
+        this.setState({
+          popup: (
+            <AddModal
+              disabled={true}
+              {...plugin}
+              {...pluginConfigList}
+              handleOk={values => {
+                const {name, enabled, id, role, config, sort, file} = values;
+                const enabledStr = enabled ? '1' : '0';
+                dispatch({
+                  type: "plugin/update",
+                  payload: {
+                    config,
+                    role,
+                    name,
+                    enabled,
+                    id,
+                    sort,
+                    file
+                  },
+                  fetchValue: {
+                    name: "tcp",
+                    enabled: enabledStr,
+                    currentPage: 1,
+                    pageSize: 50
+                  },
+                  callback: () => {
+                    this.setState({isPluginEnabled: enabled})
+                    this.closeModal();
+                  }
+                });
+              }}
+              handleCancel={() => {
+                this.closeModal();
+              }}
+            />
+          )
+        });
+      }
+    });
+  };
+
+  searchSelectorOnchange = (e) => {
+    const searchKey = e.target.value;
+    this.setState({searchKey});
+  }
+
+  searchSelector = () => {
+    const {searchKey} = this.state;
+    const {currentPage, pageSize} = this.props
+    this.props.dispatch({
+      type: "discovery/fetchSelector",
+      payload: {
+        currentPage,
+        pageSize,
+        name: searchKey
+      }
+    });
+  }
+
+
+  addSelector = () => {
+    const {dispatch, currentPage, pageSize} = this.props;
+    const {cardData} = this.state;
+    dispatch({
+      type: "discovery/fetchDiscovery",
+      payload: {
+        pluginName: "tcp",
+        level: "1"
+      },
+      callback: discoveryConfigList => {
+        let tcpType = '';
+        let isSetConfig = false;
+        if (discoveryConfigList !== null) {
+          tcpType = discoveryConfigList.type;
+          isSetConfig = true;
+        }
+        this.setState({
+          popup: (
+            <ProxySelectorModal
+              recordCount={cardData.discoveryUpstreams.length}
+              typeEnums={this.props.typeEnums}
+              data={cardData}
+              discoveryUpstreams={cardData.discoveryUpstreams}
+              tcpType={tcpType}
+              isAdd={true}
+              isSetConfig={isSetConfig}
+              handleOk={values => {
+                const {name, forwardPort, props, listenerNode, handler, 
discoveryProps, serverList, discoveryType, upstreams} = values;
+                dispatch({
+                  type: 'discovery/add',
+                  payload: {
+                    name,
+                    forwardPort,
+                    type: "tcp",
+                    props,
+                    listenerNode,
+                    handler,
+                    discovery: {
+                      level: "0", // 0 selector
+                      pluginName: "tcp",
+                      discoveryType,
+                      serverList,
+                      props: discoveryProps
+                    },
+                    discoveryUpstreams: upstreams
+                  },
+                  callback: () => {
+                    this.closeModal();
+                  },
+                  fetchValue: {
+                    currentPage,
+                    pageSize
+                  }
+                })
+              }}
+              handleCancel={() => {
+                this.closeModal();
+              }}
+            />
+          )
+        });
+      }
+    });
+  }
+
+  updateSelector = (id) => {
+    const {dispatch, selectorList, tcpType: discoveryType, currentPage, 
pageSize} = this.props;
+    const data = selectorList.find(value => value.id === id)
+    let isSetConfig = false
+    this.setState({
+      cardData: data
+    })
+    if (data.discovery.serverList === null){
+      isSetConfig = true
+    }
+    const updateArray = data.discoveryUpstreams.map((item) => {
+      return { ...item, key: item.id };
+    });
+    this.setState({
+      popup: (
+        <ProxySelectorModal
+          recordCount={updateArray.length}
+          discoveryUpstreams={updateArray}
+          tcpType={data.discovery.type}
+          typeEnums={this.props.typeEnums}
+          isAdd={false}
+          isSetConfig={isSetConfig}
+          data={data}
+          handleOk={values => {
+            const {name, forwardPort, props, listenerNode, handler, 
discoveryProps, serverList, upstreams} = values;
+            dispatch({
+              type: 'discovery/update',
+              payload: {
+                id: data.id,
+                name,
+                forwardPort,
+                type: "tcp",
+                props,
+                listenerNode,
+                handler,
+                discovery: {
+                  discoveryType,
+                  serverList,
+                  props: discoveryProps
+                },
+                discoveryUpstreams: upstreams
+              },
+              callback: () => {
+                this.closeUpdateModal();
+              },
+              fetchValue: {
+                currentPage,
+                pageSize
+              }
+            })
+          }}
+          handleCancel={() => {
+            this.closeUpdateModal();
+          }}
+        />
+      )
+    });
+
+  }
+
+
+  handleDelete = (id) => {
+    const {currentPage, pageSize} = this.props
+    this.props.dispatch({
+      type: "discovery/delete",
+      payload: {
+        list: [id]
+      },
+      fetchValue: {
+        currentPage,
+        pageSize
+      }
+    })
+  }
+
+
+  render() {
+    const {popup} = this.state;
+    const {selectorList, total, currentPage, pageSize} = this.props;
+    const tag = {
+      text: this.state.isPluginEnabled ? getIntlContent("SHENYU.COMMON.OPEN") 
: getIntlContent("SHENYU.COMMON.CLOSE"),
+      color: this.state.isPluginEnabled ? 'green' : 'red'
+    }
+
+    return (
+      <>
+        <div className={tcpStyles.main}>
+          <Row style={{marginBottom: '0px', display: 'flex', justifyContent: 
'space-between', alignItems: 'center'}}>
+            <div style={{display: 'flex', alignItems: 'end', flex: 1, margin: 
0}}>
+              <Title level={2} style={{textTransform: 'capitalize', margin: '0 
20px 0 0'}}>
+                TCP
+              </Title>
+              <Title level={3} type="secondary" style={{margin: '0 20px 0 
0'}}>Proxy</Title>
+              <Tag color={tag.color}>{tag.text}</Tag>
+            </div>
+            <div style={{display: 'flex', alignItems: 'end', gap: 10}}>
+              <Switch
+                checked={this.state.isPluginEnabled ?? false}
+                onChange={this.togglePluginStatus}
+              />
+              <AuthButton perms="system:plugin:edit">
+                <div className="edit" onClick={this.editClick}>
+                  {getIntlContent("SHENYU.SYSTEM.EDITOR")}
+                </div>
+              </AuthButton>
+            </div>
+          </Row>
+
+          <Row className={tcpStyles.bar}>
+            <h3 style={{overflow: "visible", margin: 0}}>
+              {getIntlContent("SHENYU.PLUGIN.SELECTOR.LIST.TITLE")}
+            </h3>
+            <AuthButton perms="plugin:tcpSelector:add">
+              <Button
+                type="primary"
+                onClick={this.addConfiguration}
+              >
+                {getIntlContent("SHENYU.PLUGIN.SELECTOR.LIST.CONFIGURATION")}
+              </Button>
+            </AuthButton>
+            {/* <div className={styles.headerSearch}> */}
+            <div>
+              <AuthButton perms="plugin:tcpSelector:query">
+                <Search
+                  className={tcpStyles.search}
+                  placeholder={getIntlContent(
+                    "SHENYU.PLUGIN.SEARCH.SELECTOR.NAME"
+                  )}
+                  enterButton={getIntlContent("SHENYU.SYSTEM.SEARCH")}
+                  size="default"
+                  onChange={this.searchSelectorOnchange}
+                  onSearch={this.searchSelector}
+                />
+              </AuthButton>
+            </div>
+
+            <AuthButton
+              perms="plugin:tcpSelector:add"
+            >
+              <Button type="primary" onClick={this.addSelector}>
+                {getIntlContent("SHENYU.PLUGIN.SELECTOR.LIST.ADD")}
+              </Button>
+            </AuthButton>
+          </Row>
+
+          <Row>
+            <div style={{
+              margin: '0px 0',
+              display: 'grid',
+              gridTemplateColumns: '1fr 1fr',
+              gridAutoFlow: 'row',
+              gridGap: '20px',
+              justifyContent: 'stretch',
+              alignItems: 'stretch'
+            }}
+            >
+              {this.renderCards(selectorList)}
+            </div>
+          </Row>
+
+          <Row style={{marginTop: '20px'}}>
+            <Pagination
+              onChange={this.onPageChange}
+              current={currentPage}
+              pageSize={pageSize}
+              total={total}
+            />
+          </Row>
+          {popup}
+        </div>
+      </>
+    );
+  }
+}
diff --git a/src/routes/Plugin/Discovery/proxySelectorModal.js 
b/src/routes/Plugin/Discovery/proxySelectorModal.js
new file mode 100644
index 00000000..f902101c
--- /dev/null
+++ b/src/routes/Plugin/Discovery/proxySelectorModal.js
@@ -0,0 +1,234 @@
+/*
+ * 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 React, {Component} from "react";
+import {connect} from "dva";
+import {Divider, Form, Input, Modal, Select, Table} from "antd";
+import {getIntlContent} from "../../../utils/IntlUtils";
+import EditableTable from './upstreamTable';
+
+const FormItem = Form.Item;
+
+
+@connect(({discovery}) => ({
+  ...discovery
+}))
+
+class ProxySelectorModal extends Component {
+
+  constructor(props) {
+    super(props);
+    this.state = {
+      recordCount: this.props.recordCount,
+      upstreams: this.props.discoveryUpstreams,
+    };
+  }
+
+  handleSubmit = e => {
+    const {form, handleOk} = this.props;
+    e.preventDefault();
+    form.validateFieldsAndScroll((err, values) => {
+      if (!err) {
+        let {name, forwardPort, props, listenerNode, handler, discoveryProps, 
serverList, discoveryType} = values;
+        const {upstreams} = this.state
+        handleOk({name, forwardPort, props, listenerNode, handler, 
discoveryProps, serverList, discoveryType, upstreams});
+      }
+    });
+  };
+
+  handleTableChange = (newData) => {
+    this.setState({ upstreams: newData });
+  };
+
+  handleCountChange = (newCount) => {
+    this.setState({ recordCount: newCount });
+  };
+
+  handleChange = (index, value) => {
+    this.setState({
+      [index]: value
+    });
+  }
+
+  handleOptions() {
+    const {Option} = Select
+    return this.props.typeEnums.map(value =>
+      <Option key={value} value={value.toString()}>{value}</Option>
+    )
+  }
+
+  render() {
+    const { tcpType, form, handleCancel, isSetConfig, isAdd, chosenType} = 
this.props;
+    const {recordCount, upstreams } = this.state;
+    const {getFieldDecorator} = form;
+    const { name, forwardPort, props, listenerNode, handler, discovery} = 
this.props.data || {};
+    const columns = [
+      {
+        title: 'protocol',
+        dataIndex: 'protocol',
+        key: 'protocol',
+      },
+      {
+        title: 'url',
+        dataIndex: 'url',
+        key: 'url',
+      },
+      {
+        title: 'status',
+        dataIndex: 'status',
+        key: 'status',
+      },
+      {
+        title: 'weight',
+        dataIndex: 'weight',
+        key: 'weight',
+      },
+    ];
+    return (
+      <Modal
+        destroyOnClose
+        centered
+        visible
+        onCancel={handleCancel}
+        onOk={this.handleSubmit}
+        title={getIntlContent("SHENYU.SELECTOR.NAME")}
+        okText={getIntlContent("SHENYU.COMMON.SURE")}
+        cancelText={getIntlContent("SHENYU.COMMON.CALCEL")}
+      >
+        <Form onSubmit={this.handleSubmit}>
+          <FormItem label="Type">
+            {getFieldDecorator('discoveryType', {
+              rules: [{required: true, message: 'Please select the discovery 
type!'}],
+              initialValue: tcpType !== '' ? tcpType : undefined
+            })(
+              <Select
+                placeholder="Please select the discovery type"
+                disabled={isSetConfig}
+                onChange={value => this.props.dispatch({
+                  type: 'discovery/saveGlobalType',
+                  payload: {
+                    chosenType: value
+                  }
+                })}
+              >
+                {this.handleOptions()}
+              </Select>,
+            )}
+          </FormItem>
+
+          <FormItem label="Name">
+            {getFieldDecorator('name', {
+              rules: [{required: true, message: 'Please input the proxy 
selector name!'}],
+              initialValue: name
+            })(<Input
+              placeholder="the proxy selector name"
+            />)}
+          </FormItem>
+
+          <FormItem label="ForwardPort">
+            {getFieldDecorator('forwardPort', {
+              rules: [{required: true, message: 'Please input the 
forwardPort!'}],
+              initialValue: forwardPort
+            })(<Input
+              placeholder="the forwardPort"
+            />)}
+          </FormItem>
+
+          <FormItem label="Props">
+            {getFieldDecorator('props', {
+              rules: [{required: true, message: 'Please input the proxy 
selector props!'}],
+              initialValue: props
+            })(<Input.TextArea
+              placeholder="the proxy selector props"
+            />)}
+          </FormItem>
+
+          {
+            chosenType !== 'local' ? (
+              <>
+                <FormItem label="ListenerNode">
+                  {getFieldDecorator('listenerNode', {
+                    rules: [{required: true, message: 'Please input the 
listenerNode!'}],
+                    initialValue: listenerNode
+                  })(<Input
+                    placeholder="the listenerNode"
+                  />)}
+                </FormItem>
+
+                <FormItem label="Handler">
+                  {getFieldDecorator('handler', {
+                    rules: [{required: true, message: 'Please input the 
handler!'}],
+                    initialValue: handler
+                  })(<Input.TextArea
+                    placeholder="the handler"
+                  />)}
+                </FormItem>
+
+                {
+                  isSetConfig !== true ? (
+                    <>
+                      <Divider>Discovery Configuration</Divider>
+                      <FormItem label="ServerList">
+                        {getFieldDecorator('serverList', {
+                          rules: [{required: true, message: 'Please input the 
discovery server list!'}],
+                          initialValue: discovery.serverList
+                        })(<Input
+                          placeholder="the discovery server list"
+                        />)}
+                      </FormItem>
+
+                      <FormItem label="Props">
+                        {getFieldDecorator('discoveryProps', {
+                          rules: [{required: true, message: 'Please input the 
discovery props!'}],
+                          initialValue: discovery.props
+                        })(<Input.TextArea
+                          placeholder="the discovery props"
+                        />)}
+                      </FormItem>
+
+                    </>
+                  ) : null
+                }
+
+                {
+                  isAdd !== true ? (
+                    <>
+                      <Divider>Discovery Upstreams</Divider>
+                      <Table dataSource={upstreams} columns={columns} />;
+                    </>
+                  ):null
+                }
+              </>
+            ) : (
+              <>
+                <Divider>Discovery Upstreams</Divider>
+                <EditableTable
+                  dataSource={upstreams}
+                  recordCount={recordCount}
+                  onTableChange={this.handleTableChange}
+                  onCountChange={this.handleCountChange}
+                />
+              </>
+            )
+          }
+        </Form>
+      </Modal>
+    );
+  }
+}
+
+export default Form.create()(ProxySelectorModal);
diff --git a/src/routes/Plugin/Discovery/tcp.less 
b/src/routes/Plugin/Discovery/tcp.less
new file mode 100644
index 00000000..8d59f357
--- /dev/null
+++ b/src/routes/Plugin/Discovery/tcp.less
@@ -0,0 +1,88 @@
+.cardTag {
+  min-width: 200px;
+  height: 32px;
+  border: rgba(112, 109, 109, 0.4) 1px solid;
+  border-radius: 5px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  margin-right: 30px
+}
+
+.cardInput {
+  max-width: 50%;
+}
+
+.main {
+  width: 100%;
+  height: 100%;
+  padding: 24px;
+  display: flex;
+  flex-direction: column;
+}
+
+.header {
+  display: flex;
+  flex-direction: column;
+
+  .titleBar {
+    width: 100%;
+    display: flex;
+    justify-content: space-between;
+    align-items: end;
+    flex: 1 1 0;
+
+    .left {
+      display: flex;
+      align-items: end;
+    }
+  }
+}
+
+
+.search {
+  margin-right: 10px;
+  display: flex;
+  align-items: center;
+
+}
+
+.bar {
+  margin: 20px 0;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  line-height: 40px;
+  padding: 0 20px;
+  border-radius: 5px;
+  box-shadow: 1px 2px 2px rgba(191, 189, 189, 0.5);
+  height: 40px;
+  background: #fff;
+}
+
+.formItem {
+  display: flex;
+  justify-content: space-around;
+}
+
+.cardItem {
+  display: flex;
+  justify-content: space-between;
+  margin: 5px 0;
+}
+
+.editable-cell {
+  position: relative;
+}
+
+.editable-cell-value-wrap {
+  padding: 5px 12px;
+  cursor: pointer;
+}
+
+.editable-row:hover .editable-cell-value-wrap {
+  border: 1px solid #d9d9d9;
+  border-radius: 4px;
+  padding: 4px 11px;
+}
+
diff --git a/src/routes/Plugin/Discovery/upstreamTable.js 
b/src/routes/Plugin/Discovery/upstreamTable.js
new file mode 100644
index 00000000..f3da1f1d
--- /dev/null
+++ b/src/routes/Plugin/Discovery/upstreamTable.js
@@ -0,0 +1,209 @@
+import React, {Component, createContext} from "react";
+import { Table, Input, Button, Popconfirm, Form } from 'antd';
+
+// import {getIntlContent} from "../../../utils/IntlUtils";
+// import tcpStyles from "./tcp.less";
+
+
+
+
+const EditableContext = createContext();
+
+const EditableRow = ({ form, index, ...props }) => (
+  <EditableContext.Provider value={form}>
+    <tr {...props} />
+  </EditableContext.Provider>
+);
+
+const EditableFormRow = Form.create()(EditableRow);
+
+class EditableCell extends Component {
+  state = {
+    editing: false,
+  };
+
+  toggleEdit = () => {
+    this.setState(prevState => ({ editing: !prevState.editing }), () => {
+      if (this.state.editing) {
+        this.input.focus();
+      }
+    });
+  };
+
+  save = e => {
+    const { record, handleSave } = this.props;
+    this.form.validateFields((error, values) => {
+      if (error && error[e.currentTarget.id]) {
+        return;
+      }
+      this.toggleEdit();
+      handleSave({ ...record, ...values });
+    });
+  };
+
+
+  renderCell = form => {
+    this.form = form;
+    const { children, dataIndex, record, title } = this.props;
+    const { editing } = this.state;
+    return editing ? (
+      <Form.Item style={{ margin: 0 }}>
+        {form.getFieldDecorator(dataIndex, {
+          rules: [
+            {
+              required: true,
+              message: `${title} is required.`,
+            },
+          ],
+          initialValue: record[dataIndex],
+        })(<Input ref={node => { this.input = node }} onPressEnter={this.save} 
onBlur={this.save} />
+        )}
+      </Form.Item>
+    ) : (
+      <div
+        className="editable-cell-value-wrap"
+        style={{ paddingRight: 24 }}
+        onClick={this.toggleEdit}
+      >
+        {children}
+      </div>
+    );
+  };
+
+  render() {
+    const {
+      editable,
+      dataIndex,
+      title,
+      record,
+      index,
+      handleSave,
+      children,
+      ...restProps
+    } = this.props;
+    return (
+      <td {...restProps}>
+        {editable ? (
+          
<EditableContext.Consumer>{this.renderCell}</EditableContext.Consumer>
+        ) : (
+          children
+        )}
+      </td>
+    );
+  }
+}
+
+
+export default class EditableTable extends Component {
+  constructor(props) {
+    super(props);
+    this.columns = [
+      {
+        title: 'protocol',
+        dataIndex: 'protocol',
+        editable: true,
+      },
+      {
+        title: 'url',
+        dataIndex: 'url',
+        editable: true,
+      },
+      {
+        title: 'status',
+        dataIndex: 'status',
+        editable: true,
+      },
+      {
+        title: 'weight',
+        dataIndex: 'weight',
+        editable: true,
+      },
+      {
+        title: 'operation',
+        dataIndex: 'operation',
+        render: (text, record) =>
+          this.props.dataSource.length >= 1 ? (
+            <Popconfirm title="Sure to delete?" onConfirm={() => 
this.handleDelete(record.key)}>
+              <a>Delete</a>
+            </Popconfirm>
+          ) : null,
+      },
+    ];
+
+  }
+
+  handleDelete = key => {
+    // console.log("Deleting key:", key);
+    const { dataSource } = this.props;
+    // console.log("Current dataSource:", dataSource);
+    const newData = dataSource.filter(item => item.key !== key);
+    // console.log("Updated dataSource:", newData);
+    this.props.onTableChange(newData);
+  };
+
+  handleAdd = () => {
+    const { dataSource, recordCount} = this.props;
+    const newRecordCount = recordCount + 1;
+    const newData = {
+      key: newRecordCount,
+      protocol: 'protocol',
+      url: 'url',
+      status: '0',
+      weight: '0',
+    };
+    this.props.onTableChange([...dataSource, newData]);
+    this.props.onCountChange(newRecordCount);
+  };
+
+  handleSave = row => {
+    const newData = [...this.props.dataSource];
+    const index = newData.findIndex(item => row.key === item.key);
+    const item = newData[index];
+    newData.splice(index, 1, {
+      ...item,
+      ...row,
+    });
+    this.props.onTableChange(newData);
+  };
+
+  render() {
+    const { dataSource } = this.props;
+    const components = {
+      body: {
+        row: EditableFormRow,
+        cell: EditableCell,
+      },
+    };
+    const columns = this.columns.map(col => {
+      if (!col.editable) {
+        return col;
+      }
+      return {
+        ...col,
+        onCell: record => ({
+          record,
+          editable: col.editable,
+          dataIndex: col.dataIndex,
+          title: col.title,
+          handleSave: this.handleSave,
+        }),
+      };
+    });
+    return (
+      <div>
+        <Button onClick={this.handleAdd} type="primary" style={{ marginBottom: 
16 }}>
+          Add Discovery Upstream
+        </Button>
+        <Table
+          components={components}
+          rowClassName={() => 'editable-row'}
+          bordered
+          dataSource={dataSource}
+          columns={columns}
+        />
+      </div>
+    );
+  }
+}
+
+// export default EditableTable;
diff --git a/src/routes/Plugin/index.less b/src/routes/Plugin/index.less
index 26c67c8c..d38c6e02 100644
--- a/src/routes/Plugin/index.less
+++ b/src/routes/Plugin/index.less
@@ -24,9 +24,12 @@
   display: flex;
   justify-content: space-between;
   margin-left: 10px;
+  align-items: center;
 
   .search {
     margin-right: 10px;
+    display: flex;
+    align-items: center;
 
     :global(.ant-input) {
       margin-top: 4px;
@@ -192,4 +195,4 @@
   .labelwidth {
     width: 180px;
   }
-}
\ No newline at end of file
+}
diff --git a/src/routes/System/Plugin/AddModal.js 
b/src/routes/System/Plugin/AddModal.js
index aa3ec98f..960d7278 100644
--- a/src/routes/System/Plugin/AddModal.js
+++ b/src/routes/System/Plugin/AddModal.js
@@ -15,28 +15,25 @@
  * limitations under the License.
  */
 
-import React, { Component, Fragment } from "react";
+import React, { Component, forwardRef, Fragment } from "react";
 import { Modal, Form, Switch, Input, Select, Divider, InputNumber, Button} 
from "antd";
 import { connect } from "dva";
 import { getIntlContent } from "../../../utils/IntlUtils";
 
 const { Option } = Select;
 const FormItem = Form.Item;
-const ChooseFile = ({onChange, file})=>{
+const ChooseFile = forwardRef(({ onChange, file }, ref) => {
   const handleFileInput = (e) => {
     onChange(e.target.files[0]);
   };
 
   return (
     <>
-      <Button onClick={()=>{document.getElementById("file").click()}
-      }
-      >Upload
-      </Button> {file?.name}
-      <input type="file" onChange={handleFileInput} style={{display:'none'}} 
id="file" />
+      <Button onClick={() => { document.getElementById("file").click(); 
}}>Upload</Button> {file?.name}
+      <input ref={ref} type="file" onChange={handleFileInput} style={{ 
display: 'none' }} id="file" />
     </>
-)
-}
+  );
+});
 @connect(({ global }) => ({
   platform: global.platform
 }))
diff --git a/src/services/api.js b/src/services/api.js
index ff783fd2..3ead1ef0 100644
--- a/src/services/api.js
+++ b/src/services/api.js
@@ -926,4 +926,57 @@ export function deleteApi(params) {
   });
 }
 
+export function fetchProxySelector(params) {
+  return request(`${baseUrl}/proxy-selector?${stringify(params)}`,
+    {
+      method: `GET`
+    });
+}
+
+export function addProxySelector(params) {
+  return request(`${baseUrl}/proxy-selector/addProxySelector`,
+    {
+      method: `POST`,
+      body: params
+    });
+}
 
+export function deleteProxySelector(params) {
+  return request(`${baseUrl}/proxy-selector/batch`,
+    {
+      method: `DELETE`,
+      body: [...params.list]
+    });
+}
+
+export function updateProxySelector(params) {
+  return request(`${baseUrl}/proxy-selector/${params.id}`,
+    {
+      method: `PUT`,
+      body: {
+        ...params
+      }
+    });
+}
+
+export function getDiscoveryTypeEnums() {
+  return request(`${baseUrl}/discovery/typeEnums`,
+    {
+      method: `GET`
+    });
+}
+
+export function postDiscoveryInsertOrUpdate(params) {
+  return request(`${baseUrl}/discovery/insertOrUpdate`,
+    {
+      method: `POST`,
+      body: params
+    });
+}
+
+export function getDiscovery(params) {
+  return request(`${baseUrl}/discovery?${stringify(params)}`,
+    {
+      method: `GET`
+    });
+}
diff --git a/src/utils/utils.js b/src/utils/utils.js
index 56c98367..cc33097f 100644
--- a/src/utils/utils.js
+++ b/src/utils/utils.js
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-import { parse, stringify } from "qs";
+import {parse, stringify} from "qs";
 
 export function fixedZero(val) {
   return val * 1 < 10 ? `0${val}` : val;
@@ -184,3 +184,15 @@ export function guid() {
   }
   return `${S4() + S4()}-${S4()}-${S4()}-${S4()}-${S4()}${S4()}${S4()}`;
 }
+
+export function  formatTimestamp(timestamp) {
+  const date = new Date(timestamp);
+  const year = date.getFullYear();
+  const month = String(date.getMonth() + 1).padStart(2, '0');
+  const day = String(date.getDate()).padStart(2, '0');
+  const hours = String(date.getHours()).padStart(2, '0');
+  const minutes = String(date.getMinutes()).padStart(2, '0');
+  const seconds = String(date.getSeconds()).padStart(2, '0');
+
+  return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
+}

Reply via email to