This is an automated email from the ASF dual-hosted git repository.
hefengen 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 9cba880d [feat]add instance status visualization management interface
(#540)
9cba880d is described below
commit 9cba880da485fc2cbcb895548a0b241284486e58
Author: xchoox <[email protected]>
AuthorDate: Tue Sep 30 09:03:31 2025 +0800
[feat]add instance status visualization management interface (#540)
* ui
* ui
* fix cr
---
package.json | 1 +
src/locales/en-US.json | 9 ++
src/locales/zh-CN.json | 9 ++
src/models/instance.js | 30 ++++-
src/routes/System/Instance/index.js | 243 +++++++++++++++++++++++++++++++++++-
src/services/api.js | 7 ++
6 files changed, 295 insertions(+), 4 deletions(-)
diff --git a/package.json b/package.json
index 4f7efdf8..7f9535af 100755
--- a/package.json
+++ b/package.json
@@ -27,6 +27,7 @@
"dayjs": "^1.8.17",
"dva": "^2.2.3",
"dva-loading": "^2.0.3",
+ "echarts": "^6.0.0",
"enquire-js": "^0.2.1",
"history": "^5.3.0",
"lodash": "^4.17.10",
diff --git a/src/locales/en-US.json b/src/locales/en-US.json
index 9d17af2a..70247968 100644
--- a/src/locales/en-US.json
+++ b/src/locales/en-US.json
@@ -381,6 +381,15 @@
"SHENYU.REGISTRY.GROUP": "Group",
"SHENYU.REGISTRY.MODAL.TITLE": "Add Registry Data",
"SHENYU.REGISTRY.PASSPORT": "Passport",
+ "SHENYU.INSTANCE.SELECT.LASTBEATTIME": "LastBeatTime",
+ "SHENYU.INSTANCE.SELECT.CREATETIME": "RegisterTime",
+ "SHENYU.INSTANCE.SELECT.STATE": "Instance State",
+ "SHENYU.INSTANCE.SELECT.STATE.UNKNOWN": "unKnown",
+ "SHENYU.INSTANCE.SELECT.STATE.ONLINE": "Online",
+ "SHENYU.INSTANCE.SELECT.STATE.OFFLINE": "Offline",
+ "SHENYU.INSTANCE.NO_DATA": "No Data",
+ "SHENYU.INSTANCE.PIE_DATA": "Service Live Status",
+ "SHENYU.INSTANCE.LINE_DATA": "Recent Service Live Status",
"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 c6f3510a..9da4fbc3 100644
--- a/src/locales/zh-CN.json
+++ b/src/locales/zh-CN.json
@@ -385,6 +385,15 @@
"SHENYU.REGISTRY.NAMESPACE": "命名空间",
"SHENYU.REGISTRY.GROUP": "分组",
"SHENYU.REGISTRY.PASSPORT": "密码",
+ "SHENYU.INSTANCE.SELECT.LASTBEATTIME": "最近心跳时间",
+ "SHENYU.INSTANCE.SELECT.CREATETIME": "注册时间",
+ "SHENYU.INSTANCE.SELECT.STATE": "服务状态",
+ "SHENYU.INSTANCE.SELECT.STATE.UNKNOWN": "未知",
+ "SHENYU.INSTANCE.SELECT.STATE.ONLINE": "在线",
+ "SHENYU.INSTANCE.SELECT.STATE.OFFLINE": "离线",
+ "SHENYU.INSTANCE.NO_DATA": "暂无数据",
+ "SHENYU.INSTANCE.PIE_DATA": "服务存活状态",
+ "SHENYU.INSTANCE.LINE_DATA": "服务近期存活状态",
"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
index 89a04937..9d833dc0 100644
--- a/src/models/instance.js
+++ b/src/models/instance.js
@@ -15,7 +15,11 @@
* limitations under the License.
*/
-import { getInstancesByNamespace, findInstance } from "../services/api";
+import {
+ getInstancesByNamespace,
+ findInstance,
+ findInstanceAnalysis,
+} from "../services/api";
export default {
namespace: "instance",
@@ -23,6 +27,8 @@ export default {
state: {
instanceList: [],
total: 0,
+ pieData: [],
+ lineList: [],
},
effects: {
@@ -44,6 +50,21 @@ export default {
});
}
},
+ *fetchAnalysis(params, { call, put }) {
+ const { payload } = params;
+ const json = yield call(findInstanceAnalysis, payload);
+
+ if (json.code === 200) {
+ let { pieData, lineData } = json.data;
+ yield put({
+ type: "saveInstancesAnalysis",
+ payload: {
+ pieData,
+ lineData,
+ },
+ });
+ }
+ },
*fetchItem(params, { call }) {
const { payload, callback } = params;
const json = yield call(findInstance, payload);
@@ -75,5 +96,12 @@ export default {
total: payload.total,
};
},
+ saveInstancesAnalysis(state, { payload }) {
+ return {
+ ...state,
+ pieData: payload.pieData,
+ lineData: payload.lineData,
+ };
+ },
},
};
diff --git a/src/routes/System/Instance/index.js
b/src/routes/System/Instance/index.js
index e368a7e6..774da9f4 100644
--- a/src/routes/System/Instance/index.js
+++ b/src/routes/System/Instance/index.js
@@ -16,8 +16,21 @@
*/
import React, { Component } from "react";
-import { Button, Input, Popover, Select, Table, Tag, Typography } from "antd";
+import {
+ Button,
+ Card,
+ Col,
+ Input,
+ Popover,
+ Row,
+ Select,
+ Table,
+ Tag,
+ Typography,
+} from "antd";
import { connect } from "dva";
+import { format } from "date-fns";
+import * as echarts from "echarts";
import { resizableComponents } from "../../../utils/resizable";
import { getCurrentLocale, getIntlContent } from "../../../utils/IntlUtils";
import AuthButton from "../../../utils/AuthButton";
@@ -53,10 +66,15 @@ export default class Instance extends Component {
componentDidMount() {
this.query();
this.initInstanceColumns();
+ this.queryAnalysis();
+ this.pieChartInstance =
echarts.init(document.getElementById("pieDataDiv"));
+ this.lineChartInstance = echarts.init(
+ document.getElementById("lineDataDiv"),
+ );
}
componentDidUpdate(prevProps) {
- const { language, currentNamespaceId } = this.props;
+ const { language, currentNamespaceId, instance } = this.props;
const { localeName } = this.state;
if (language !== localeName) {
this.initInstanceColumns();
@@ -65,6 +83,12 @@ export default class Instance extends Component {
if (prevProps.currentNamespaceId !== currentNamespaceId) {
this.query();
}
+ if (prevProps.instance.pieData !== instance.pieData) {
+ this.renderPieChart(instance.pieData);
+ }
+ if (prevProps.instance.lineData !== instance.lineData) {
+ this.renderLineChart(instance.lineData);
+ }
}
handleResize =
@@ -105,6 +129,137 @@ export default class Instance extends Component {
});
};
+ queryAnalysis = () => {
+ const { dispatch, currentNamespaceId } = this.props;
+ dispatch({
+ type: "instance/fetchAnalysis",
+ payload: {
+ namespaceId: currentNamespaceId,
+ },
+ });
+ };
+
+ renderPieChart = (pieData) => {
+ if (!pieData || !this.pieChartInstance) {
+ return;
+ }
+ const chartData =
+ pieData && pieData.length > 0
+ ? pieData
+ : [{ value: 0, name: getIntlContent("SHENYU.INSTANCE.NO_DATA") }];
+
+ const option = {
+ title: {
+ text: getIntlContent("SHENYU.INSTANCE.PIE_DATA"),
+ left: "center",
+ },
+ tooltip: {
+ trigger: "item",
+ formatter: "{a} <br/>{b}: {c} ({d}%)",
+ },
+ legend: {
+ orient: "vertical",
+ left: "left",
+ },
+ series: [
+ {
+ name: getIntlContent("SHENYU.INSTANCE.DISTRIBUTION"),
+ type: "pie",
+ radius: "50%",
+ data: chartData,
+ emphasis: {
+ itemStyle: {
+ shadowBlur: 10,
+ shadowOffsetX: 0,
+ shadowColor: "rgba(0, 0, 0, 0.5)",
+ },
+ },
+ },
+ ],
+ };
+
+ this.pieChartInstance.setOption(option);
+
+ window.addEventListener("resize", () => {
+ this.pieChartInstance.resize();
+ });
+ };
+
+ renderLineChart = (lineData) => {
+ if (!lineData || !this.lineChartInstance) {
+ return;
+ }
+
+ const chartData =
+ lineData && lineData.length > 0
+ ? lineData
+ : [
+ {
+ name: getIntlContent("SHENYU.INSTANCE.NO_DATA"),
+ type: "line",
+ data: [0, 0, 0, 0, 0],
+ },
+ ];
+
+ const formattedSeries = chartData.map((item) => ({
+ name: item.name || "Unknown",
+ type: "line",
+ data: Array.isArray(item.data) ? item.data : [],
+ }));
+
+ const option = {
+ title: {
+ text: getIntlContent("SHENYU.INSTANCE.LINE_DATA"),
+ },
+ tooltip: {
+ trigger: "axis",
+ },
+ legend: {
+ data: formattedSeries.map((series) => series.name),
+ },
+ grid: {
+ left: "3%",
+ right: "4%",
+ bottom: "10%",
+ containLabel: true,
+ },
+ toolbox: {
+ feature: {
+ saveAsImage: {},
+ },
+ },
+ xAxis: {
+ type: "category",
+ boundaryGap: false,
+ show: false,
+ },
+ yAxis: {
+ type: "value",
+ show: true,
+ min: 0,
+ max: 4,
+ minInterval: 1,
+ interval: 1,
+ name: "数量",
+ nameTextStyle: {
+ color: "#333",
+ fontSize: 12,
+ padding: [0, 0, 0, 0],
+ },
+ axisLabel: {
+ formatter: "{value}",
+ },
+ },
+ series: formattedSeries,
+ };
+
+ this.lineChartInstance.setOption(option);
+
+ window.addEventListener("resize", () => {
+ this.lineChartInstance.resize();
+ });
+ };
+
pageOnchange = (page) => {
this.setState({ currentPage: page }, this.query);
};
@@ -189,7 +344,7 @@ export default class Instance extends Component {
dataIndex: "instanceType",
ellipsis: true,
key: "instanceType",
- width: 120,
+ width: 150,
sorter: (a, b) => (a.instanceType > b.instanceType ? 1 : -1),
render: (text) => {
return <div style={{ color: "#1f640a" }}>{text || "----"}</div>;
@@ -201,6 +356,7 @@ export default class Instance extends Component {
dataIndex: "instanceInfo",
key: "instanceInfo",
ellipsis: true,
+ width: 200,
render: (text, record) => {
const tag = (
<div>
@@ -234,6 +390,68 @@ export default class Instance extends Component {
);
},
},
+ {
+ align: "center",
+ title: getIntlContent("SHENYU.INSTANCE.SELECT.LASTBEATTIME"),
+ dataIndex: "lastHeartBeatTime",
+ ellipsis: true,
+ key: "lastHeartBeatTime",
+ width: 120,
+ sorter: (a, b) => (a.instanceType > b.instanceType ? 1 : -1),
+ render: (text) => {
+ return (
+ <div style={{ color: "#1f640a" }}>
+ {format(new Date(text), "YYYY-MM-DD HH:mm:ss") || "----"}
+ </div>
+ );
+ },
+ },
+ {
+ align: "center",
+ title: getIntlContent("SHENYU.INSTANCE.SELECT.CREATETIME"),
+ dataIndex: "dateCreated",
+ ellipsis: true,
+ key: "dateCreated",
+ width: 120,
+ sorter: (a, b) => (a.instanceType > b.instanceType ? 1 : -1),
+ render: (text) => {
+ return (
+ <div style={{ color: "#1f640a" }}>
+ {format(new Date(text), "YYYY-MM-DD HH:mm:ss") || "----"}
+ </div>
+ );
+ },
+ },
+ {
+ align: "center",
+ title: getIntlContent("SHENYU.INSTANCE.SELECT.STATE"),
+ dataIndex: "instanceState",
+ ellipsis: true,
+ key: "instanceState",
+ width: 120,
+ sorter: (a, b) => (a.instanceType > b.instanceType ? 1 : -1),
+ render: (state) => {
+ if (state === 1) {
+ return (
+ <Tag color="green">
+ {getIntlContent("SHENYU.INSTANCE.SELECT.STATE.ONLINE")}
+ </Tag>
+ );
+ } else if (state === 0) {
+ return (
+ <Tag color="orange">
+ {getIntlContent("SHENYU.INSTANCE.SELECT.STATE.UNKNOWN")}
+ </Tag>
+ );
+ } else if (state === 2) {
+ return (
+ <Tag color="red">
+ {getIntlContent("SHENYU.INSTANCE.SELECT.STATE.OFFLINE")}
+ </Tag>
+ );
+ }
+ },
+ },
],
});
}
@@ -290,6 +508,25 @@ export default class Instance extends Component {
</AuthButton>
</div>
+ <Row gutter={16} style={{ marginBottom: 24 }}>
+ <Col span={12}>
+ <Card>
+ <div
+ id="pieDataDiv"
+ style={{ width: "800px", height: "400px" }}
+ />
+ </Card>
+ </Col>
+ <Col span={12}>
+ <Card>
+ <div
+ id="lineDataDiv"
+ style={{ width: "800px", height: "400px" }}
+ />
+ </Card>
+ </Col>
+ </Row>
+
<Table
size="small"
components={this.components}
diff --git a/src/services/api.js b/src/services/api.js
index 53cd67d8..c6ee1459 100644
--- a/src/services/api.js
+++ b/src/services/api.js
@@ -1438,6 +1438,13 @@ export async function importSwagger(params) {
});
}
+/* findInstance */
+export async function findInstanceAnalysis(params) {
+ return request(`${baseUrl}/instance/analysis/${params.namespaceId}`, {
+ method: `GET`,
+ });
+}
+
/* Registry Center Management APIs */
/* get registry list */