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 c7a1d978 add instance info (#524) c7a1d978 is described below commit c7a1d9786150cce303962fc9f20d8d930d35d75a Author: aias00 <rok...@163.com> AuthorDate: Fri Jul 4 11:03:06 2025 +0800 add instance info (#524) * instance info * instance info --- src/common/menu.js | 5 + src/common/router.js | 7 + src/locales/en-US.json | 7 + src/locales/zh-CN.json | 7 + src/models/instance.js | 79 +++++++++ src/routes/System/Instance/index.js | 316 ++++++++++++++++++++++++++++++++++++ src/services/api.js | 38 +++++ 7 files changed, 459 insertions(+) diff --git a/src/common/menu.js b/src/common/menu.js index 9c34d834..0f1ce8bd 100644 --- a/src/common/menu.js +++ b/src/common/menu.js @@ -100,6 +100,11 @@ export const menuData = [ path: "dict", locale: "SHENYU.MENU.SYSTEM.MANAGMENT.DICTIONARY", }, + { + name: getIntlContent("SHENYU.MENU.SYSTEM.MANAGMENT.INSTANCE"), + path: "instance", + locale: "SHENYU.MENU.SYSTEM.MANAGMENT.INSTANCE", + }, ], }, { diff --git a/src/common/router.js b/src/common/router.js index 8c178f55..3847aa11 100644 --- a/src/common/router.js +++ b/src/common/router.js @@ -168,6 +168,13 @@ export const getRouterData = (app) => { () => import("../routes/System/Plugin"), ), }, + "/config/instance": { + component: dynamicWrapper( + app, + ["instance"], + () => import("../routes/System/Instance"), + ), + }, "/config/namespacePlugin": { component: dynamicWrapper( app, diff --git a/src/locales/en-US.json b/src/locales/en-US.json index 6cb6342b..4a436f44 100644 --- a/src/locales/en-US.json +++ b/src/locales/en-US.json @@ -77,6 +77,7 @@ "SHENYU.MENU.SYSTEM.MANAGMENT.AUTHEN": "Authentication", "SHENYU.MENU.SYSTEM.MANAGMENT.METADATA": "Metadata", "SHENYU.MENU.SYSTEM.MANAGMENT.DICTIONARY": "Dictionary", + "SHENYU.MENU.SYSTEM.MANAGMENT.INSTANCE": "Instance", "SHENYU.MENU.SYSTEM.MANAGMENT.NAMESPACE": "Namespace", "SHENYU.MENU.CONFIG.MANAGMENT": "BasicConfig", "SHENYU.PLUGIN.SELECTOR.LIST.TITLE": "SelectorList", @@ -336,6 +337,12 @@ "SHENYU.BUTTON.DATA.PERMISSION.CONFIG": "ConfigureDataPermission", "SHENYU.MESSAGE.SESSION.INVALID": "Session is invalid", "SHENYU.MESSAGE.SESSION.RELOGIN": "Please login in again", + "SHENYU.INSTANCE.IP": "Instance IP", + "SHENYU.INSTANCE.PORT": "Instance Port", + "SHENYU.INSTANCE.INFO": "Instance Info", + "SHENYU.INSTANCE.SELECT.TYPE": "Instance Type", + "SHENYU.INSTANCE.SELECT.TYPE.BOOTSTRAP": "Bootstrap", + "SHENYU.INSTANCE.SELECT.TYPE.CLIENT": "Client", "SHENYU.PLUGIN.SELECT.STATUS": "Select Status", "SHENYU.PLUGIN.REQUEST.HEADER.KEY": "Header Key", "SHENYU.PLUGIN.REQUEST.HEADER.VALUE": "Header Value", diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json index ed08cd83..dc3e33f5 100644 --- a/src/locales/zh-CN.json +++ b/src/locales/zh-CN.json @@ -78,6 +78,7 @@ "SHENYU.MENU.SYSTEM.MANAGMENT.AUTHEN": "认证管理", "SHENYU.MENU.SYSTEM.MANAGMENT.METADATA": "元数据管理", "SHENYU.MENU.SYSTEM.MANAGMENT.DICTIONARY": "字典管理", + "SHENYU.MENU.SYSTEM.MANAGMENT.INSTANCE": "实例管理", "SHENYU.MENU.CONFIG.MANAGMENT": "基础配置", "SHENYU.MENU.SYSTEM.MANAGMENT.NAMESPACE": "命名空间管理", "SHENYU.PLUGIN.SELECTOR.LIST.TITLE": "选择器列表", @@ -340,6 +341,12 @@ "SHENYU.BUTTON.DATA.PERMISSION.CONFIG": "配置数据权限", "SHENYU.MESSAGE.SESSION.INVALID": "会话超时", "SHENYU.MESSAGE.SESSION.RELOGIN": "会话超时,请重新登录", + "SHENYU.INSTANCE.IP": "实例IP", + "SHENYU.INSTANCE.PORT": "实例端口", + "SHENYU.INSTANCE.INFO": "实例信息", + "SHENYU.INSTANCE.SELECT.TYPE": "实例类型", + "SHENYU.INSTANCE.SELECT.TYPE.BOOTSTRAP": "网关实例", + "SHENYU.INSTANCE.SELECT.TYPE.CLIENT": "客户端实例", "SHENYU.PLUGIN.SELECT.STATUS": "选择状态", "SHENYU.PLUGIN.REQUEST.HEADER.KEY": "Header Key", "SHENYU.PLUGIN.REQUEST.HEADER.VALUE": "Header Value", diff --git a/src/models/instance.js b/src/models/instance.js new file mode 100644 index 00000000..89a04937 --- /dev/null +++ b/src/models/instance.js @@ -0,0 +1,79 @@ +/* + * 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 { getInstancesByNamespace, findInstance } from "../services/api"; + +export default { + namespace: "instance", + + state: { + instanceList: [], + total: 0, + }, + + effects: { + *fetch(params, { call, put }) { + const { payload } = params; + const json = yield call(getInstancesByNamespace, payload); + if (json.code === 200) { + let { page, dataList } = json.data; + dataList = dataList.map((item) => { + item.key = item.id; + return item; + }); + yield put({ + type: "saveInstances", + payload: { + total: page.totalCount, + dataList, + }, + }); + } + }, + *fetchItem(params, { call }) { + const { payload, callback } = params; + const json = yield call(findInstance, payload); + if (json.code === 200) { + const instance = json.data; + callback(instance); + } + }, + *reload(params, { put }) { + const { fetchValue } = params; + const { name, currentPage, instanceType, instanceIp, pageSize } = + fetchValue; + const payload = { + name, + instanceType, + instanceIp, + currentPage, + pageSize, + }; + yield put({ type: "fetch", payload }); + }, + }, + + reducers: { + saveInstances(state, { payload }) { + return { + ...state, + instanceList: payload.dataList, + total: payload.total, + }; + }, + }, +}; diff --git a/src/routes/System/Instance/index.js b/src/routes/System/Instance/index.js new file mode 100644 index 00000000..e368a7e6 --- /dev/null +++ b/src/routes/System/Instance/index.js @@ -0,0 +1,316 @@ +/* + * 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, Input, Popover, Select, Table, Tag, Typography } from "antd"; +import { connect } from "dva"; +import { resizableComponents } from "../../../utils/resizable"; +import { getCurrentLocale, getIntlContent } from "../../../utils/IntlUtils"; +import AuthButton from "../../../utils/AuthButton"; + +const { Text } = Typography; + +const { Option } = Select; + +@connect(({ instance, loading, global }) => ({ + instance, + language: global.language, + currentNamespaceId: global.currentNamespaceId, + loading: loading.effects["instance/fetch"], +})) +export default class Instance extends Component { + components = resizableComponents; + + constructor(props) { + super(props); + this.state = { + currentPage: 1, + pageSize: 12, + selectedRowKeys: [], + instanceIp: "", + instanceType: null, + localeName: window.sessionStorage.getItem("locale") + ? window.sessionStorage.getItem("locale") + : "en-US", + columns: [], + }; + } + + componentDidMount() { + this.query(); + this.initInstanceColumns(); + } + + componentDidUpdate(prevProps) { + const { language, currentNamespaceId } = this.props; + const { localeName } = this.state; + if (language !== localeName) { + this.initInstanceColumns(); + this.changeLocale(language); + } + if (prevProps.currentNamespaceId !== currentNamespaceId) { + this.query(); + } + } + + handleResize = + (index) => + (e, { size }) => { + this.setState(({ columns }) => { + const nextColumns = [...columns]; + nextColumns[index] = { + ...nextColumns[index], + width: size.width, + }; + return { columns: nextColumns }; + }); + }; + + onSelectChange = (selectedRowKeys) => { + this.setState({ selectedRowKeys }); + }; + + currentQueryPayload = (override) => { + const { instanceIp, instanceType, currentPage, pageSize } = this.state; + const { currentNamespaceId } = this.props; + return { + instanceIp, + instanceType, + namespaceId: currentNamespaceId, + currentPage, + pageSize, + ...override, + }; + }; + + query = () => { + const { dispatch } = this.props; + dispatch({ + type: "instance/fetch", + payload: this.currentQueryPayload(), + }); + }; + + pageOnchange = (page) => { + this.setState({ currentPage: page }, this.query); + }; + + onShowSizeChange = (currentPage, pageSize) => { + this.setState({ currentPage: 1, pageSize }, this.query); + }; + + instanceIpOnchange = (e) => { + this.setState({ instanceIp: e.target.value }, this.query); + }; + + instanceTypeOnchange = (e) => { + this.setState({ instanceType: e }, this.query); + }; + + searchClick = () => { + this.setState({ currentPage: 1 }, this.query); + }; + + changeLocale(locale) { + this.setState({ + localeName: locale, + }); + getCurrentLocale(this.state.localeName); + } + + initInstanceColumns() { + this.setState({ + columns: [ + { + align: "center", + title: getIntlContent("SHENYU.INSTANCE.IP"), + dataIndex: "instanceIp", + key: "instanceIp", + ellipsis: true, + width: 120, + render: (text, record) => { + return record.instanceIp ? ( + <div + style={{ + color: "#1890ff", + fontWeight: "bold", + }} + > + {text || "----"} + </div> + ) : ( + <div style={{ color: "#260033", fontWeight: "bold" }}> + {text || "----"} + </div> + ); + }, + }, + { + align: "center", + title: getIntlContent("SHENYU.INSTANCE.PORT"), + dataIndex: "instancePort", + key: "instancePort", + ellipsis: true, + width: 120, + render: (text, record) => { + return record.instancePort ? ( + <div + style={{ + color: "#1890ff", + fontWeight: "bold", + }} + > + {text || "----"} + </div> + ) : ( + <div style={{ color: "#260033", fontWeight: "bold" }}> + {text || "----"} + </div> + ); + }, + }, + { + align: "center", + title: getIntlContent("SHENYU.INSTANCE.SELECT.TYPE"), + dataIndex: "instanceType", + ellipsis: true, + key: "instanceType", + width: 120, + sorter: (a, b) => (a.instanceType > b.instanceType ? 1 : -1), + render: (text) => { + return <div style={{ color: "#1f640a" }}>{text || "----"}</div>; + }, + }, + { + align: "center", + title: getIntlContent("SHENYU.INSTANCE.INFO"), + dataIndex: "instanceInfo", + key: "instanceInfo", + ellipsis: true, + render: (text, record) => { + const tag = ( + <div> + <Tag color="#9dd3a8">{record.instanceType}</Tag> + <Tag color="#CCCC99">{record.instanceIp}</Tag> + <Tag color="#DCDC17">{record.instancePort}</Tag> + </div> + ); + const t = JSON.stringify( + JSON.parse(text !== null && text.length > 0 ? text : "{}"), + null, + 4, + ); + const content = ( + <div> + <Text type="secondary">{`${getIntlContent("SHENYU.SYSTEM.CREATETIME")}: ${record.dateCreated ? new Date(record.dateCreated).toLocaleString() : "----"}`}</Text> + <br /> + <Text type="secondary">{`${getIntlContent("SHENYU.SYSTEM.UPDATETIME")}: ${record.dateUpdated ? new Date(record.dateUpdated).toLocaleString() : "----"}`}</Text> + <hr /> + <div style={{ fontWeight: "bold" }}> + <pre> + <code>{t}</code> + </pre> + </div> + </div> + ); + return ( + <Popover content={content} title={tag}> + <div>{text || "----"}</div> + </Popover> + ); + }, + }, + ], + }); + } + + render() { + const { instance, loading } = this.props; + const { instanceList, total } = instance; + const { currentPage, pageSize, selectedRowKeys, instanceIp, instanceType } = + this.state; + const columns = this.state.columns.map((col, index) => ({ + ...col, + onHeaderCell: (column) => ({ + width: column.width, + onResize: this.handleResize(index), + }), + })); + const rowSelection = { + selectedRowKeys, + onChange: this.onSelectChange, + }; + + return ( + <div className="plug-content-wrap"> + <div style={{ display: "flex" }}> + <Input + allowClear + value={instanceIp} + onChange={this.instanceIpOnchange} + placeholder={getIntlContent("SHENYU.INSTANCE.IP")} + style={{ width: 240 }} + /> + <Select + value={instanceType != null ? instanceType : undefined} + onChange={this.instanceTypeOnchange} + placeholder={getIntlContent("SHENYU.INSTANCE.SELECT.TYPE")} + style={{ width: 150, marginLeft: 20 }} + allowClear + > + <Option value="bootstrap"> + {getIntlContent("SHENYU.INSTANCE.SELECT.TYPE.BOOTSTRAP")} + </Option> + <Option value="client"> + {getIntlContent("SHENYU.INSTANCE.SELECT.TYPE.CLIENT")} + </Option> + </Select> + <AuthButton perms="system:instance:list"> + <Button + type="primary" + style={{ marginLeft: 20 }} + onClick={this.searchClick} + > + {getIntlContent("SHENYU.SYSTEM.SEARCH")} + </Button> + </AuthButton> + </div> + + <Table + size="small" + components={this.components} + style={{ marginTop: 30 }} + bordered + loading={loading} + columns={columns} + dataSource={instanceList} + rowSelection={rowSelection} + pagination={{ + total, + showTotal: (showTotal) => `${showTotal}`, + showSizeChanger: true, + pageSizeOptions: ["12", "20", "50", "100"], + current: currentPage, + pageSize, + onShowSizeChange: this.onShowSizeChange, + onChange: this.pageOnchange, + }} + /> + </div> + ); + } +} diff --git a/src/services/api.js b/src/services/api.js index 92b6e5b0..2b6ad181 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -1355,3 +1355,41 @@ export async function asyncNamespacePlugin(params) { body: params, }); } + +/* getInstancesByNamespace */ +export async function getInstancesByNamespace(params) { + return request(`${baseUrl}/instance?${stringify(params)}`, { + method: `GET`, + }); +} + +/* findInstance */ +export async function findInstance(params) { + return request(`${baseUrl}/instance/${params.id}`, { + method: `GET`, + }); +} + +/* addInstance */ +export async function addInstance(params) { + return request(`${baseUrl}/instance`, { + method: `POST`, + body: params, + }); +} + +/* updateInstance */ +export async function updateInstance(params) { + return request(`${baseUrl}/instance/${params.id}`, { + method: `PUT`, + body: params, + }); +} + +/* deleteInstance */ +export async function deleteInstance(params) { + return request(`${baseUrl}/instance/batch`, { + method: `DELETE`, + body: [...params.list], + }); +}