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}`;
+}