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 */

Reply via email to