This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new bdb8fdea feat(metrics): bundle Grafana dashboards with view and export 
(#926)
bdb8fdea is described below

commit bdb8fdea315fc225c44f3dbdab2da181dc8cab0e
Author: zhaohai <[email protected]>
AuthorDate: Tue Aug 4 18:03:46 2026 +0800

    feat(metrics): bundle Grafana dashboards with view and export (#926)
    
    * feat(metrics): bundle Grafana dashboards with view/export (METRICS-01)
    
    Ship 12 ready-to-use Grafana dashboard JSON assets for RocketMQ (overview,
    broker, producer, consumer, topic, tps, storage, jvm, thread-pool, dlq,
    latency, network) and expose them through the dashboard.
    
    Backend:
    - GrafanaDashboardInfo record + GrafanaDashboardService (loads 
classpath*:grafana/*.json)
    - GrafanaDashboardController: list / get model / export (attachment) 
endpoints
    - backend unit tests (8 passing)
    
    Frontend:
    - GrafanaDashboardList component + GrafanaDashboards page + nav entry + 
route
    - metrics api (grafana) + grafanaService + mock + i18n keys
    - frontend unit tests (14 passing)
    
    * fix(ci): resolve tsc -b type errors in grafana dashboard frontend
    
    - GrafanaDashboardList.tsx: drop unused antd `message` import (used only via
      App.useApp()); remove unused default `React` import (automatic JSX 
runtime).
    - grafanaService.test.ts: widen the vi.hoisted isMockMode mock to `() => 
boolean`
      so switching it to `() => false` in the real-mode case no longer fails 
with
      TS2322 under `tsc -b`.
    
    Co-Authored-By: WorkBuddy <[email protected]>
    
    ---------
    
    Co-authored-by: WorkBuddy <[email protected]>
---
 server/scripts/gen_grafana_dashboards.py           | 293 +++++++++++++++++++
 .../grafana/GrafanaDashboardController.java        |  72 +++++
 .../metrics/grafana/GrafanaDashboardInfo.java      |  27 ++
 .../metrics/grafana/GrafanaDashboardService.java   | 153 ++++++++++
 .../main/resources/grafana/rocketmq-broker.json    | 234 +++++++++++++++
 .../main/resources/grafana/rocketmq-consumer.json  | 193 ++++++++++++
 .../src/main/resources/grafana/rocketmq-dlq.json   | 150 ++++++++++
 .../src/main/resources/grafana/rocketmq-jvm.json   | 234 +++++++++++++++
 .../main/resources/grafana/rocketmq-latency.json   | 150 ++++++++++
 .../main/resources/grafana/rocketmq-network.json   | 193 ++++++++++++
 .../main/resources/grafana/rocketmq-overview.json  | 322 +++++++++++++++++++++
 .../main/resources/grafana/rocketmq-producer.json  | 193 ++++++++++++
 .../main/resources/grafana/rocketmq-storage.json   | 192 ++++++++++++
 .../resources/grafana/rocketmq-threadpool.json     | 192 ++++++++++++
 .../src/main/resources/grafana/rocketmq-topic.json | 192 ++++++++++++
 .../src/main/resources/grafana/rocketmq-tps.json   | 192 ++++++++++++
 .../grafana/GrafanaDashboardControllerTest.java    |  87 ++++++
 .../grafana/GrafanaDashboardServiceTest.java       |  80 +++++
 web/src/App.tsx                                    |   2 +
 web/src/api/metrics.test.ts                        |  49 +++-
 web/src/api/metrics.ts                             |  28 ++
 web/src/components/GrafanaDashboardList.tsx        | 188 ++++++++++++
 .../__tests__/GrafanaDashboardList.test.tsx        | 141 +++++++++
 web/src/i18n/translations.ts                       |   9 +
 web/src/layouts/MainLayout.tsx                     |   3 +
 web/src/mock/grafanaDashboards.ts                  | 184 ++++++++++++
 web/src/pages/studio/GrafanaDashboards.tsx         |  44 +++
 web/src/services/grafanaService.test.ts            |  59 ++++
 web/src/services/grafanaService.ts                 |  39 +++
 29 files changed, 3894 insertions(+), 1 deletion(-)

diff --git a/server/scripts/gen_grafana_dashboards.py 
b/server/scripts/gen_grafana_dashboards.py
new file mode 100644
index 00000000..fe3a3c4e
--- /dev/null
+++ b/server/scripts/gen_grafana_dashboards.py
@@ -0,0 +1,293 @@
+#!/usr/bin/env python3
+"""Generate RocketMQ Grafana dashboard JSON assets for the dashboard project.
+
+Each dashboard is a standalone Grafana dashboard model (schemaVersion 39) that
+queries Prometheus-compatible RocketMQ metrics. The JSON files are written to
+server/src/main/resources/grafana/ and loaded at runtime by 
GrafanaDashboardService.
+"""
+import json
+import os
+
+OUT_DIR = os.path.join(os.path.dirname(__file__), "..", "src", "main", 
"resources", "grafana")
+os.makedirs(OUT_DIR, exist_ok=True)
+
+DS = "${DS_PROMETHEUS}"
+
+
+def ts_panel(panel_id, title, expr, grid, y, legend=None, unit="short"):
+    target = {
+        "datasource": {"type": "prometheus", "uid": DS},
+        "expr": expr,
+        "legendFormat": legend or "",
+        "refId": "A",
+    }
+    return {
+        "id": panel_id,
+        "title": title,
+        "type": "timeseries",
+        "datasource": {"type": "prometheus", "uid": DS},
+        "gridPos": {"h": 8, "w": grid, "x": 0, "y": y},
+        "fieldConfig": {
+            "defaults": {"custom": {"drawStyle": "line", "fillOpacity": 10}, 
"unit": unit},
+            "overrides": [],
+        },
+        "options": {"legend": {"displayMode": "list", "placement": "bottom"}},
+        "targets": [target],
+    }
+
+
+def stat_panel(panel_id, title, expr, grid, y, unit="short"):
+    return {
+        "id": panel_id,
+        "title": title,
+        "type": "stat",
+        "datasource": {"type": "prometheus", "uid": DS},
+        "gridPos": {"h": 6, "w": grid, "x": 0, "y": y},
+        "fieldConfig": {
+            "defaults": {"unit": unit, "custom": {"thresholdsStyle": {"mode": 
"none"}}},
+            "overrides": [],
+        },
+        "options": {"reduceOptions": {"calcs": ["lastNotNull"]}},
+        "targets": [{"datasource": {"type": "prometheus", "uid": DS}, "expr": 
expr, "refId": "A"}],
+    }
+
+
+def gauge_panel(panel_id, title, expr, grid, y, max_=100):
+    return {
+        "id": panel_id,
+        "title": title,
+        "type": "gauge",
+        "datasource": {"type": "prometheus", "uid": DS},
+        "gridPos": {"h": 8, "w": grid, "x": 0, "y": y},
+        "fieldConfig": {
+            "defaults": {
+                "unit": "percent",
+                "max": max_,
+                "custom": {"min": 0},
+            },
+            "overrides": [],
+        },
+        "options": {"reduceOptions": {"calcs": ["lastNotNull"]}},
+        "targets": [{"datasource": {"type": "prometheus", "uid": DS}, "expr": 
expr, "refId": "A"}],
+    }
+
+
+def template_vars(extra=None):
+    base = [
+        {
+            "name": "DS_PROMETHEUS",
+            "type": "datasource",
+            "label": "Prometheus",
+            "query": "prometheus",
+            "current": {},
+            "hide": 0,
+        },
+        {
+            "name": "cluster",
+            "type": "query",
+            "datasource": {"type": "prometheus", "uid": DS},
+            "query": "label_values(rocketmq_messages_in_total, cluster)",
+            "refresh": 2,
+            "current": {},
+            "hide": 0,
+        },
+        {
+            "name": "broker",
+            "type": "query",
+            "datasource": {"type": "prometheus", "uid": DS},
+            "query": 
"label_values(rocketmq_messages_in_total{cluster=\"$cluster\"}, broker)",
+            "refresh": 2,
+            "current": {},
+            "hide": 0,
+        },
+        {
+            "name": "topic",
+            "type": "query",
+            "datasource": {"type": "prometheus", "uid": DS},
+            "query": 
"label_values(rocketmq_messages_in_total{cluster=\"$cluster\"}, topic)",
+            "refresh": 2,
+            "current": {},
+            "hide": 0,
+        },
+    ]
+    if extra:
+        base.extend(extra)
+    return {"list": base}
+
+
+def dashboard(uid, title, description, panels, vars_=None):
+    return {
+        "uid": uid,
+        "title": title,
+        "description": description,
+        "tags": ["rocketmq"],
+        "schemaVersion": 39,
+        "timezone": "browser",
+        "editable": True,
+        "templating": vars_ or template_vars(),
+        "time": {"from": "now-6h", "to": "now"},
+        "refresh": "30s",
+        "panels": panels,
+    }
+
+
+def y_stack(panels):
+    """Assign gridPos y offsets sequentially (2 per row of w=12)."""
+    y = 0
+    for p in panels:
+        w = p["gridPos"]["w"]
+        p["gridPos"]["x"] = 0 if (panels.index(p) % 2 == 0 or w == 24) else 12
+        if w == 24:
+            p["gridPos"]["x"] = 0
+        p["gridPos"]["y"] = y
+        y += p["gridPos"]["h"]
+    return panels
+
+
+specs = []
+
+# 1. Overview
+specs.append((
+    "rocketmq-overview", "RocketMQ Cluster Overview",
+    "Cluster-wide throughput, topic/group counts and producer footprint.",
+    [
+        ts_panel(1, "Messages In TPS", 
"sum(rate(rocketmq_messages_in_total{cluster=\"$cluster\"}[1m]))", 12, 0, "by 
cluster", "ops"),
+        ts_panel(2, "Messages Out TPS", 
"sum(rate(rocketmq_messages_out_total{cluster=\"$cluster\"}[1m]))", 12, 0, "by 
cluster", "ops"),
+        stat_panel(3, "Total Topics", "count(count by (topic) 
(rocketmq_messages_in_total{cluster=\"$cluster\"}))", 6, 8),
+        stat_panel(4, "Total Consumer Groups", "count(count by (group) 
(rocketmq_messages_out_total{cluster=\"$cluster\"}))", 6, 8),
+        stat_panel(5, "Producer Count", 
"max(rocketmq_producer_count{cluster=\"$cluster\"})", 6, 8),
+        stat_panel(6, "Broker Count", 
"count(rocketmq_messages_in_total{cluster=\"$cluster\"})", 6, 8),
+    ],
+))
+
+# 2. Broker
+specs.append((
+    "rocketmq-broker", "RocketMQ Broker",
+    "Per-broker throughput, dispatch backlog and thread pool pressure.",
+    [
+        ts_panel(1, "Broker Messages In TPS", "sum by (broker) 
(rate(rocketmq_messages_in_total{cluster=\"$cluster\",broker=\"$broker\"}[1m]))",
 12, 0, "{{broker}}", "ops"),
+        ts_panel(2, "Broker Messages Out TPS", "sum by (broker) 
(rate(rocketmq_messages_out_total{cluster=\"$cluster\",broker=\"$broker\"}[1m]))",
 12, 0, "{{broker}}", "ops"),
+        ts_panel(3, "Dispatch Behind Bytes", 
"rocketmq_dispatch_behind_bytes{cluster=\"$cluster\",broker=\"$broker\"}", 12, 
8, "{{broker}}", "bytes"),
+        ts_panel(4, "Thread Pool Queue Size", 
"rocketmq_threadpool_queue_size{cluster=\"$cluster\",broker=\"$broker\"}", 12, 
8, "{{broker}}"),
+    ],
+))
+
+# 3. Producer
+specs.append((
+    "rocketmq-producer", "RocketMQ Producer",
+    "Producer presence, send size and per-topic ingress.",
+    [
+        stat_panel(1, "Producer Count", 
"max(rocketmq_producer_count{cluster=\"$cluster\"})", 6, 0),
+        ts_panel(2, "Producer Message Size", 
"rocketmq_producer_message_size{cluster=\"$cluster\"}", 12, 0, "{{topic}}", 
"bytes"),
+        ts_panel(3, "Messages In by Topic", "sum by (topic) 
(rate(rocketmq_messages_in_total{cluster=\"$cluster\"}[1m]))", 24, 8, 
"{{topic}}", "ops"),
+    ],
+))
+
+# 4. Consumer
+specs.append((
+    "rocketmq-consumer", "RocketMQ Consumer",
+    "Consumer group egress, lag and client footprint.",
+    [
+        stat_panel(1, "Consumer Count", 
"max(rocketmq_consumer_count{cluster=\"$cluster\"})", 6, 0),
+        ts_panel(2, "Messages Out TPS by Group", "sum by (group) 
(rate(rocketmq_messages_out_total{cluster=\"$cluster\",group=\"$group\"}[1m]))",
 12, 0, "{{group}}", "ops"),
+        ts_panel(3, "Consumer Message Size", 
"rocketmq_consumer_message_size{cluster=\"$cluster\"}", 12, 8, "{{group}}", 
"bytes"),
+    ],
+))
+
+# 5. Topic
+specs.append((
+    "rocketmq-topic", "RocketMQ Topic",
+    "Per-topic ingress/egress throughput and dispatch backlog.",
+    [
+        ts_panel(1, "Topic Messages In TPS", "sum by (topic) 
(rate(rocketmq_messages_in_total{cluster=\"$cluster\",topic=\"$topic\"}[1m]))", 
12, 0, "{{topic}}", "ops"),
+        ts_panel(2, "Topic Messages Out TPS", "sum by (topic) 
(rate(rocketmq_messages_out_total{cluster=\"$cluster\",topic=\"$topic\"}[1m]))",
 12, 0, "{{topic}}", "ops"),
+        ts_panel(3, "Topic Dispatch Behind Bytes", 
"rocketmq_dispatch_behind_bytes{cluster=\"$cluster\",topic=\"$topic\"}", 24, 8, 
"{{topic}}", "bytes"),
+    ],
+))
+
+# 6. TPS
+specs.append((
+    "rocketmq-tps", "RocketMQ TPS",
+    "Cluster and per-broker message throughput trends.",
+    [
+        ts_panel(1, "Cluster TPS In", 
"sum(rate(rocketmq_messages_in_total{cluster=\"$cluster\"}[1m]))", 12, 0, "", 
"ops"),
+        ts_panel(2, "Cluster TPS Out", 
"sum(rate(rocketmq_messages_out_total{cluster=\"$cluster\"}[1m]))", 12, 0, "", 
"ops"),
+        ts_panel(3, "Per-Broker TPS In", "sum by (broker) 
(rate(rocketmq_messages_in_total{cluster=\"$cluster\",broker=\"$broker\"}[1m]))",
 24, 8, "{{broker}}", "ops"),
+    ],
+))
+
+# 7. Storage
+specs.append((
+    "rocketmq-storage", "RocketMQ Storage",
+    "Broker disk usage and JVM heap footprint.",
+    [
+        gauge_panel(1, "Disk Use Ratio", 
"rocketmq_disk_use_ratio{cluster=\"$cluster\",broker=\"$broker\"}", 12, 0),
+        ts_panel(2, "JVM Heap Used", 
"jvm_memory_used_bytes{area=\"heap\",cluster=\"$cluster\",broker=\"$broker\"}", 
12, 0, "{{broker}}", "bytes"),
+        ts_panel(3, "Dispatch Behind Bytes", 
"rocketmq_dispatch_behind_bytes{cluster=\"$cluster\",broker=\"$broker\"}", 24, 
8, "{{broker}}", "bytes"),
+    ],
+))
+
+# 8. JVM
+specs.append((
+    "rocketmq-jvm", "RocketMQ Broker JVM",
+    "JVM memory, threads and garbage collection for brokers.",
+    [
+        ts_panel(1, "JVM Heap", 
"jvm_memory_used_bytes{area=\"heap\",cluster=\"$cluster\",broker=\"$broker\"}", 
12, 0, "{{broker}}", "bytes"),
+        ts_panel(2, "JVM Non-Heap", 
"jvm_memory_used_bytes{area=\"nonheap\",cluster=\"$cluster\",broker=\"$broker\"}",
 12, 0, "{{broker}}", "bytes"),
+        ts_panel(3, "Live Threads", 
"jvm_threads_live_threads{cluster=\"$cluster\",broker=\"$broker\"}", 12, 8, 
"{{broker}}"),
+        ts_panel(4, "GC Pause (1m rate)", 
"rate(jvm_gc_pause_seconds_sum{cluster=\"$cluster\",broker=\"$broker\"}[1m])", 
12, 8, "{{broker}}", "s"),
+    ],
+))
+
+# 9. Thread pool
+specs.append((
+    "rocketmq-threadpool", "RocketMQ Thread Pool",
+    "Broker thread pool queue depth, capacity and rejections.",
+    [
+        ts_panel(1, "Queue Size", 
"rocketmq_threadpool_queue_size{cluster=\"$cluster\",broker=\"$broker\"}", 12, 
0, "{{broker}}"),
+        ts_panel(2, "Queue Capacity", 
"rocketmq_threadpool_queue_capacity{cluster=\"$cluster\",broker=\"$broker\"}", 
12, 0, "{{broker}}"),
+        ts_panel(3, "Reject Count (1m)", 
"increase(rocketmq_threadpool_reject_count{cluster=\"$cluster\",broker=\"$broker\"}[1m])",
 24, 8, "{{broker}}"),
+    ],
+))
+
+# 10. DLQ
+specs.append((
+    "rocketmq-dlq", "RocketMQ DLQ & Retry",
+    "Dead-letter queue resend volume and latency.",
+    [
+        ts_panel(1, "DLQ Resend Count (1m)", 
"rate(rocketmq_dlq_resend_count{cluster=\"$cluster\"}[1m])", 12, 0, 
"{{topic}}"),
+        ts_panel(2, "DLQ Resend Latency", 
"rocketmq_dlq_resend_latency{cluster=\"$cluster\"}", 12, 0, "{{topic}}", "s"),
+    ],
+))
+
+# 11. Latency
+specs.append((
+    "rocketmq-latency", "RocketMQ Latency",
+    "Dispatch and client push latency percentiles.",
+    [
+        ts_panel(1, "Dispatch Latency p99", "histogram_quantile(0.99, sum by 
(le) (rate(rocketmq_dispatch_latency_bucket{cluster=\"$cluster\"}[5m])))", 12, 
0, "", "s"),
+        ts_panel(2, "Send To Client Latency p99", "histogram_quantile(0.99, 
sum by (le) 
(rate(rocketmq_send_to_client_latency_bucket{cluster=\"$cluster\"}[5m])))", 12, 
0, "", "s"),
+    ],
+))
+
+# 12. Network
+specs.append((
+    "rocketmq-network", "RocketMQ Network & Connections",
+    "Client connections and produced/consumed connection counts.",
+    [
+        stat_panel(1, "Client Connections", 
"rocketmq_producer_count{cluster=\"$cluster\"} + 
rocketmq_consumer_count{cluster=\"$cluster\"}", 6, 0),
+        ts_panel(2, "Connections by Broker", 
"rocketmq_connection_count{cluster=\"$cluster\",broker=\"$broker\"}", 12, 0, 
"{{broker}}"),
+        ts_panel(3, "Producer Connections", 
"rocketmq_producer_count{cluster=\"$cluster\"}", 12, 8, "{{broker}}"),
+    ],
+))
+
+for uid, title, desc, panels in specs:
+    panels = y_stack(panels)
+    doc = dashboard(uid, title, desc, panels)
+    path = os.path.join(OUT_DIR, f"{uid}.json")
+    with open(path, "w", encoding="utf-8") as f:
+        json.dump(doc, f, indent=2, ensure_ascii=False)
+        f.write("\n")
+    print(f"wrote {path} ({len(panels)} panels)")
+
+print(f"TOTAL dashboards: {len(specs)}")
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/grafana/GrafanaDashboardController.java
 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/grafana/GrafanaDashboardController.java
new file mode 100644
index 00000000..fd8c9197
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/grafana/GrafanaDashboardController.java
@@ -0,0 +1,72 @@
+/*
+ * 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.
+ */
+package org.apache.rocketmq.studio.cluster.metrics.grafana;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import lombok.RequiredArgsConstructor;
+import org.apache.rocketmq.studio.common.domain.Result;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/metrics/grafana")
+@RequiredArgsConstructor
+public class GrafanaDashboardController {
+
+    private final GrafanaDashboardService grafanaDashboardService;
+
+    @Operation(summary = "List bundled Grafana dashboards",
+            description = "Returns metadata for every RocketMQ Grafana 
dashboard shipped with the dashboard")
+    @ApiResponse(responseCode = "200", description = "Dashboards listed 
successfully", useReturnTypeSchema = true)
+    @GetMapping("/dashboards")
+    public Result<List<GrafanaDashboardInfo>> listDashboards() {
+        return Result.ok(grafanaDashboardService.listDashboards());
+    }
+
+    @Operation(summary = "Get a Grafana dashboard model",
+            description = "Returns the parsed Grafana dashboard JSON for the 
given uid")
+    @ApiResponse(responseCode = "200", description = "Dashboard returned 
successfully", useReturnTypeSchema = true)
+    @ApiResponse(responseCode = "404", description = "Dashboard uid is 
unknown")
+    @GetMapping("/dashboards/{uid}")
+    public Result<Map<String, Object>> getDashboard(@PathVariable("uid") 
String uid) {
+        return Result.ok(grafanaDashboardService.getDashboard(uid));
+    }
+
+    @Operation(summary = "Export a Grafana dashboard JSON",
+            description = "Returns the raw Grafana dashboard JSON as a 
downloadable attachment")
+    @ApiResponse(responseCode = "200", description = "Dashboard JSON returned")
+    @ApiResponse(responseCode = "404", description = "Dashboard uid is 
unknown")
+    @GetMapping("/dashboards/{uid}/export")
+    public ResponseEntity<byte[]> exportDashboard(@PathVariable("uid") String 
uid) {
+        String json = grafanaDashboardService.getDashboardJson(uid);
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_JSON);
+        headers.setContentDispositionFormData("attachment", uid + ".json");
+        return new ResponseEntity<>(json.getBytes(StandardCharsets.UTF_8), 
headers, HttpStatus.OK);
+    }
+}
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/grafana/GrafanaDashboardInfo.java
 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/grafana/GrafanaDashboardInfo.java
new file mode 100644
index 00000000..44f74ec1
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/grafana/GrafanaDashboardInfo.java
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ */
+package org.apache.rocketmq.studio.cluster.metrics.grafana;
+
+import java.util.List;
+
+/**
+ * Metadata describing a bundled Grafana dashboard. The {@code uid} matches the
+ * JSON file name (without the {@code .json} suffix) so dashboards can be 
looked
+ * up deterministically.
+ */
+public record GrafanaDashboardInfo(String uid, String title, String 
description, List<String> tags) {
+}
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/grafana/GrafanaDashboardService.java
 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/grafana/GrafanaDashboardService.java
new file mode 100644
index 00000000..d8913a11
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/cluster/metrics/grafana/GrafanaDashboardService.java
@@ -0,0 +1,153 @@
+/*
+ * 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.
+ */
+package org.apache.rocketmq.studio.cluster.metrics.grafana;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.rocketmq.studio.common.exception.BusinessException;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.core.io.support.ResourcePatternResolver;
+import org.springframework.stereotype.Service;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Loads the Grafana dashboard JSON assets bundled under {@code 
classpath*:grafana/*.json}
+ * and exposes them for listing, viewing and exporting.
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class GrafanaDashboardService {
+
+    private static final String LOCATION_PATTERN = "classpath*:grafana/*.json";
+
+    private final ObjectMapper objectMapper;
+    private final ResourcePatternResolver resourceResolver = new 
PathMatchingResourcePatternResolver();
+
+    /**
+     * Lists metadata for every bundled Grafana dashboard.
+     */
+    public List<GrafanaDashboardInfo> listDashboards() {
+        List<GrafanaDashboardInfo> infos = new ArrayList<>();
+        for (Resource resource : resolveResources()) {
+            String uid = uidOf(resource);
+            if (uid == null) {
+                continue;
+            }
+            try (InputStream in = resource.getInputStream()) {
+                JsonNode root = objectMapper.readTree(in);
+                String title = textOr(root, "title", uid);
+                String description = textOr(root, "description", "");
+                List<String> tags = parseTags(root);
+                infos.add(new GrafanaDashboardInfo(uid, title, description, 
tags));
+            } catch (IOException e) {
+                log.warn("Skipping unreadable Grafana dashboard resource {}: 
{}", resource, e.getMessage());
+            }
+        }
+        infos.sort((a, b) -> a.uid().compareTo(b.uid()));
+        return infos;
+    }
+
+    /**
+     * Returns the parsed dashboard model for the given uid.
+     *
+     * @throws BusinessException with code 404 when the uid is unknown
+     */
+    public Map<String, Object> getDashboard(String uid) {
+        Resource resource = findResource(uid);
+        if (resource == null) {
+            throw new BusinessException(404, "Grafana dashboard not found: " + 
uid);
+        }
+        try (InputStream in = resource.getInputStream()) {
+            @SuppressWarnings("unchecked")
+            Map<String, Object> model = objectMapper.readValue(in, Map.class);
+            return model;
+        } catch (IOException e) {
+            throw new BusinessException(500, "Failed to read Grafana 
dashboard: " + uid);
+        }
+    }
+
+    /**
+     * Returns the raw dashboard JSON for the given uid (used for export).
+     *
+     * @throws BusinessException with code 404 when the uid is unknown
+     */
+    public String getDashboardJson(String uid) {
+        Resource resource = findResource(uid);
+        if (resource == null) {
+            throw new BusinessException(404, "Grafana dashboard not found: " + 
uid);
+        }
+        try (InputStream in = resource.getInputStream()) {
+            return new String(in.readAllBytes());
+        } catch (IOException e) {
+            throw new BusinessException(500, "Failed to read Grafana 
dashboard: " + uid);
+        }
+    }
+
+    private Resource findResource(String uid) {
+        for (Resource resource : resolveResources()) {
+            if (uid.equals(uidOf(resource))) {
+                return resource;
+            }
+        }
+        return null;
+    }
+
+    private Resource[] resolveResources() {
+        try {
+            return resourceResolver.getResources(LOCATION_PATTERN);
+        } catch (IOException e) {
+            log.warn("Unable to resolve Grafana dashboard resources: {}", 
e.getMessage());
+            return new Resource[0];
+        }
+    }
+
+    private static String uidOf(Resource resource) {
+        String filename = resource.getFilename();
+        if (filename == null || !filename.endsWith(".json")) {
+            return null;
+        }
+        return filename.substring(0, filename.length() - ".json".length());
+    }
+
+    private static String textOr(JsonNode root, String field, String fallback) 
{
+        JsonNode node = root.get(field);
+        return node != null && node.isTextual() ? node.asText() : fallback;
+    }
+
+    private static List<String> parseTags(JsonNode root) {
+        JsonNode tags = root.get("tags");
+        if (tags == null || !tags.isArray()) {
+            return List.of();
+        }
+        List<String> result = new ArrayList<>();
+        tags.forEach(tag -> {
+            if (tag.isTextual()) {
+                result.add(tag.asText());
+            }
+        });
+        return result;
+    }
+}
diff --git a/server/src/main/resources/grafana/rocketmq-broker.json 
b/server/src/main/resources/grafana/rocketmq-broker.json
new file mode 100644
index 00000000..100dd31f
--- /dev/null
+++ b/server/src/main/resources/grafana/rocketmq-broker.json
@@ -0,0 +1,234 @@
+{
+  "uid": "rocketmq-broker",
+  "title": "RocketMQ Broker",
+  "description": "Per-broker throughput, dispatch backlog and thread pool 
pressure.",
+  "tags": [
+    "rocketmq"
+  ],
+  "schemaVersion": 39,
+  "timezone": "browser",
+  "editable": true,
+  "templating": {
+    "list": [
+      {
+        "name": "DS_PROMETHEUS",
+        "type": "datasource",
+        "label": "Prometheus",
+        "query": "prometheus",
+        "current": {},
+        "hide": 0
+      },
+      {
+        "name": "cluster",
+        "type": "query",
+        "datasource": {
+          "type": "prometheus",
+          "uid": "${DS_PROMETHEUS}"
+        },
+        "query": "label_values(rocketmq_messages_in_total, cluster)",
+        "refresh": 2,
+        "current": {},
+        "hide": 0
+      },
+      {
+        "name": "broker",
+        "type": "query",
+        "datasource": {
+          "type": "prometheus",
+          "uid": "${DS_PROMETHEUS}"
+        },
+        "query": 
"label_values(rocketmq_messages_in_total{cluster=\"$cluster\"}, broker)",
+        "refresh": 2,
+        "current": {},
+        "hide": 0
+      },
+      {
+        "name": "topic",
+        "type": "query",
+        "datasource": {
+          "type": "prometheus",
+          "uid": "${DS_PROMETHEUS}"
+        },
+        "query": 
"label_values(rocketmq_messages_in_total{cluster=\"$cluster\"}, topic)",
+        "refresh": 2,
+        "current": {},
+        "hide": 0
+      }
+    ]
+  },
+  "time": {
+    "from": "now-6h",
+    "to": "now"
+  },
+  "refresh": "30s",
+  "panels": [
+    {
+      "id": 1,
+      "title": "Broker Messages In TPS",
+      "type": "timeseries",
+      "datasource": {
+        "type": "prometheus",
+        "uid": "${DS_PROMETHEUS}"
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 0
+      },
+      "fieldConfig": {
+        "defaults": {
+          "custom": {
+            "drawStyle": "line",
+            "fillOpacity": 10
+          },
+          "unit": "ops"
+        },
+        "overrides": []
+      },
+      "options": {
+        "legend": {
+          "displayMode": "list",
+          "placement": "bottom"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "expr": "sum by (broker) 
(rate(rocketmq_messages_in_total{cluster=\"$cluster\",broker=\"$broker\"}[1m]))",
+          "legendFormat": "{{broker}}",
+          "refId": "A"
+        }
+      ]
+    },
+    {
+      "id": 2,
+      "title": "Broker Messages Out TPS",
+      "type": "timeseries",
+      "datasource": {
+        "type": "prometheus",
+        "uid": "${DS_PROMETHEUS}"
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 8
+      },
+      "fieldConfig": {
+        "defaults": {
+          "custom": {
+            "drawStyle": "line",
+            "fillOpacity": 10
+          },
+          "unit": "ops"
+        },
+        "overrides": []
+      },
+      "options": {
+        "legend": {
+          "displayMode": "list",
+          "placement": "bottom"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "expr": "sum by (broker) 
(rate(rocketmq_messages_out_total{cluster=\"$cluster\",broker=\"$broker\"}[1m]))",
+          "legendFormat": "{{broker}}",
+          "refId": "A"
+        }
+      ]
+    },
+    {
+      "id": 3,
+      "title": "Dispatch Behind Bytes",
+      "type": "timeseries",
+      "datasource": {
+        "type": "prometheus",
+        "uid": "${DS_PROMETHEUS}"
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 16
+      },
+      "fieldConfig": {
+        "defaults": {
+          "custom": {
+            "drawStyle": "line",
+            "fillOpacity": 10
+          },
+          "unit": "bytes"
+        },
+        "overrides": []
+      },
+      "options": {
+        "legend": {
+          "displayMode": "list",
+          "placement": "bottom"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "expr": 
"rocketmq_dispatch_behind_bytes{cluster=\"$cluster\",broker=\"$broker\"}",
+          "legendFormat": "{{broker}}",
+          "refId": "A"
+        }
+      ]
+    },
+    {
+      "id": 4,
+      "title": "Thread Pool Queue Size",
+      "type": "timeseries",
+      "datasource": {
+        "type": "prometheus",
+        "uid": "${DS_PROMETHEUS}"
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 24
+      },
+      "fieldConfig": {
+        "defaults": {
+          "custom": {
+            "drawStyle": "line",
+            "fillOpacity": 10
+          },
+          "unit": "short"
+        },
+        "overrides": []
+      },
+      "options": {
+        "legend": {
+          "displayMode": "list",
+          "placement": "bottom"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "expr": 
"rocketmq_threadpool_queue_size{cluster=\"$cluster\",broker=\"$broker\"}",
+          "legendFormat": "{{broker}}",
+          "refId": "A"
+        }
+      ]
+    }
+  ]
+}
diff --git a/server/src/main/resources/grafana/rocketmq-consumer.json 
b/server/src/main/resources/grafana/rocketmq-consumer.json
new file mode 100644
index 00000000..417052ad
--- /dev/null
+++ b/server/src/main/resources/grafana/rocketmq-consumer.json
@@ -0,0 +1,193 @@
+{
+  "uid": "rocketmq-consumer",
+  "title": "RocketMQ Consumer",
+  "description": "Consumer group egress, lag and client footprint.",
+  "tags": [
+    "rocketmq"
+  ],
+  "schemaVersion": 39,
+  "timezone": "browser",
+  "editable": true,
+  "templating": {
+    "list": [
+      {
+        "name": "DS_PROMETHEUS",
+        "type": "datasource",
+        "label": "Prometheus",
+        "query": "prometheus",
+        "current": {},
+        "hide": 0
+      },
+      {
+        "name": "cluster",
+        "type": "query",
+        "datasource": {
+          "type": "prometheus",
+          "uid": "${DS_PROMETHEUS}"
+        },
+        "query": "label_values(rocketmq_messages_in_total, cluster)",
+        "refresh": 2,
+        "current": {},
+        "hide": 0
+      },
+      {
+        "name": "broker",
+        "type": "query",
+        "datasource": {
+          "type": "prometheus",
+          "uid": "${DS_PROMETHEUS}"
+        },
+        "query": 
"label_values(rocketmq_messages_in_total{cluster=\"$cluster\"}, broker)",
+        "refresh": 2,
+        "current": {},
+        "hide": 0
+      },
+      {
+        "name": "topic",
+        "type": "query",
+        "datasource": {
+          "type": "prometheus",
+          "uid": "${DS_PROMETHEUS}"
+        },
+        "query": 
"label_values(rocketmq_messages_in_total{cluster=\"$cluster\"}, topic)",
+        "refresh": 2,
+        "current": {},
+        "hide": 0
+      }
+    ]
+  },
+  "time": {
+    "from": "now-6h",
+    "to": "now"
+  },
+  "refresh": "30s",
+  "panels": [
+    {
+      "id": 1,
+      "title": "Consumer Count",
+      "type": "stat",
+      "datasource": {
+        "type": "prometheus",
+        "uid": "${DS_PROMETHEUS}"
+      },
+      "gridPos": {
+        "h": 6,
+        "w": 6,
+        "x": 0,
+        "y": 0
+      },
+      "fieldConfig": {
+        "defaults": {
+          "unit": "short",
+          "custom": {
+            "thresholdsStyle": {
+              "mode": "none"
+            }
+          }
+        },
+        "overrides": []
+      },
+      "options": {
+        "reduceOptions": {
+          "calcs": [
+            "lastNotNull"
+          ]
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "expr": "max(rocketmq_consumer_count{cluster=\"$cluster\"})",
+          "refId": "A"
+        }
+      ]
+    },
+    {
+      "id": 2,
+      "title": "Messages Out TPS by Group",
+      "type": "timeseries",
+      "datasource": {
+        "type": "prometheus",
+        "uid": "${DS_PROMETHEUS}"
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 6
+      },
+      "fieldConfig": {
+        "defaults": {
+          "custom": {
+            "drawStyle": "line",
+            "fillOpacity": 10
+          },
+          "unit": "ops"
+        },
+        "overrides": []
+      },
+      "options": {
+        "legend": {
+          "displayMode": "list",
+          "placement": "bottom"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "expr": "sum by (group) 
(rate(rocketmq_messages_out_total{cluster=\"$cluster\",group=\"$group\"}[1m]))",
+          "legendFormat": "{{group}}",
+          "refId": "A"
+        }
+      ]
+    },
+    {
+      "id": 3,
+      "title": "Consumer Message Size",
+      "type": "timeseries",
+      "datasource": {
+        "type": "prometheus",
+        "uid": "${DS_PROMETHEUS}"
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 14
+      },
+      "fieldConfig": {
+        "defaults": {
+          "custom": {
+            "drawStyle": "line",
+            "fillOpacity": 10
+          },
+          "unit": "bytes"
+        },
+        "overrides": []
+      },
+      "options": {
+        "legend": {
+          "displayMode": "list",
+          "placement": "bottom"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "expr": "rocketmq_consumer_message_size{cluster=\"$cluster\"}",
+          "legendFormat": "{{group}}",
+          "refId": "A"
+        }
+      ]
+    }
+  ]
+}
diff --git a/server/src/main/resources/grafana/rocketmq-dlq.json 
b/server/src/main/resources/grafana/rocketmq-dlq.json
new file mode 100644
index 00000000..6fe5a12f
--- /dev/null
+++ b/server/src/main/resources/grafana/rocketmq-dlq.json
@@ -0,0 +1,150 @@
+{
+  "uid": "rocketmq-dlq",
+  "title": "RocketMQ DLQ & Retry",
+  "description": "Dead-letter queue resend volume and latency.",
+  "tags": [
+    "rocketmq"
+  ],
+  "schemaVersion": 39,
+  "timezone": "browser",
+  "editable": true,
+  "templating": {
+    "list": [
+      {
+        "name": "DS_PROMETHEUS",
+        "type": "datasource",
+        "label": "Prometheus",
+        "query": "prometheus",
+        "current": {},
+        "hide": 0
+      },
+      {
+        "name": "cluster",
+        "type": "query",
+        "datasource": {
+          "type": "prometheus",
+          "uid": "${DS_PROMETHEUS}"
+        },
+        "query": "label_values(rocketmq_messages_in_total, cluster)",
+        "refresh": 2,
+        "current": {},
+        "hide": 0
+      },
+      {
+        "name": "broker",
+        "type": "query",
+        "datasource": {
+          "type": "prometheus",
+          "uid": "${DS_PROMETHEUS}"
+        },
+        "query": 
"label_values(rocketmq_messages_in_total{cluster=\"$cluster\"}, broker)",
+        "refresh": 2,
+        "current": {},
+        "hide": 0
+      },
+      {
+        "name": "topic",
+        "type": "query",
+        "datasource": {
+          "type": "prometheus",
+          "uid": "${DS_PROMETHEUS}"
+        },
+        "query": 
"label_values(rocketmq_messages_in_total{cluster=\"$cluster\"}, topic)",
+        "refresh": 2,
+        "current": {},
+        "hide": 0
+      }
+    ]
+  },
+  "time": {
+    "from": "now-6h",
+    "to": "now"
+  },
+  "refresh": "30s",
+  "panels": [
+    {
+      "id": 1,
+      "title": "DLQ Resend Count (1m)",
+      "type": "timeseries",
+      "datasource": {
+        "type": "prometheus",
+        "uid": "${DS_PROMETHEUS}"
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 0
+      },
+      "fieldConfig": {
+        "defaults": {
+          "custom": {
+            "drawStyle": "line",
+            "fillOpacity": 10
+          },
+          "unit": "short"
+        },
+        "overrides": []
+      },
+      "options": {
+        "legend": {
+          "displayMode": "list",
+          "placement": "bottom"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "expr": "rate(rocketmq_dlq_resend_count{cluster=\"$cluster\"}[1m])",
+          "legendFormat": "{{topic}}",
+          "refId": "A"
+        }
+      ]
+    },
+    {
+      "id": 2,
+      "title": "DLQ Resend Latency",
+      "type": "timeseries",
+      "datasource": {
+        "type": "prometheus",
+        "uid": "${DS_PROMETHEUS}"
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 8
+      },
+      "fieldConfig": {
+        "defaults": {
+          "custom": {
+            "drawStyle": "line",
+            "fillOpacity": 10
+          },
+          "unit": "s"
+        },
+        "overrides": []
+      },
+      "options": {
+        "legend": {
+          "displayMode": "list",
+          "placement": "bottom"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "expr": "rocketmq_dlq_resend_latency{cluster=\"$cluster\"}",
+          "legendFormat": "{{topic}}",
+          "refId": "A"
+        }
+      ]
+    }
+  ]
+}
diff --git a/server/src/main/resources/grafana/rocketmq-jvm.json 
b/server/src/main/resources/grafana/rocketmq-jvm.json
new file mode 100644
index 00000000..9d9a69f7
--- /dev/null
+++ b/server/src/main/resources/grafana/rocketmq-jvm.json
@@ -0,0 +1,234 @@
+{
+  "uid": "rocketmq-jvm",
+  "title": "RocketMQ Broker JVM",
+  "description": "JVM memory, threads and garbage collection for brokers.",
+  "tags": [
+    "rocketmq"
+  ],
+  "schemaVersion": 39,
+  "timezone": "browser",
+  "editable": true,
+  "templating": {
+    "list": [
+      {
+        "name": "DS_PROMETHEUS",
+        "type": "datasource",
+        "label": "Prometheus",
+        "query": "prometheus",
+        "current": {},
+        "hide": 0
+      },
+      {
+        "name": "cluster",
+        "type": "query",
+        "datasource": {
+          "type": "prometheus",
+          "uid": "${DS_PROMETHEUS}"
+        },
+        "query": "label_values(rocketmq_messages_in_total, cluster)",
+        "refresh": 2,
+        "current": {},
+        "hide": 0
+      },
+      {
+        "name": "broker",
+        "type": "query",
+        "datasource": {
+          "type": "prometheus",
+          "uid": "${DS_PROMETHEUS}"
+        },
+        "query": 
"label_values(rocketmq_messages_in_total{cluster=\"$cluster\"}, broker)",
+        "refresh": 2,
+        "current": {},
+        "hide": 0
+      },
+      {
+        "name": "topic",
+        "type": "query",
+        "datasource": {
+          "type": "prometheus",
+          "uid": "${DS_PROMETHEUS}"
+        },
+        "query": 
"label_values(rocketmq_messages_in_total{cluster=\"$cluster\"}, topic)",
+        "refresh": 2,
+        "current": {},
+        "hide": 0
+      }
+    ]
+  },
+  "time": {
+    "from": "now-6h",
+    "to": "now"
+  },
+  "refresh": "30s",
+  "panels": [
+    {
+      "id": 1,
+      "title": "JVM Heap",
+      "type": "timeseries",
+      "datasource": {
+        "type": "prometheus",
+        "uid": "${DS_PROMETHEUS}"
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 0
+      },
+      "fieldConfig": {
+        "defaults": {
+          "custom": {
+            "drawStyle": "line",
+            "fillOpacity": 10
+          },
+          "unit": "bytes"
+        },
+        "overrides": []
+      },
+      "options": {
+        "legend": {
+          "displayMode": "list",
+          "placement": "bottom"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "expr": 
"jvm_memory_used_bytes{area=\"heap\",cluster=\"$cluster\",broker=\"$broker\"}",
+          "legendFormat": "{{broker}}",
+          "refId": "A"
+        }
+      ]
+    },
+    {
+      "id": 2,
+      "title": "JVM Non-Heap",
+      "type": "timeseries",
+      "datasource": {
+        "type": "prometheus",
+        "uid": "${DS_PROMETHEUS}"
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 8
+      },
+      "fieldConfig": {
+        "defaults": {
+          "custom": {
+            "drawStyle": "line",
+            "fillOpacity": 10
+          },
+          "unit": "bytes"
+        },
+        "overrides": []
+      },
+      "options": {
+        "legend": {
+          "displayMode": "list",
+          "placement": "bottom"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "expr": 
"jvm_memory_used_bytes{area=\"nonheap\",cluster=\"$cluster\",broker=\"$broker\"}",
+          "legendFormat": "{{broker}}",
+          "refId": "A"
+        }
+      ]
+    },
+    {
+      "id": 3,
+      "title": "Live Threads",
+      "type": "timeseries",
+      "datasource": {
+        "type": "prometheus",
+        "uid": "${DS_PROMETHEUS}"
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 16
+      },
+      "fieldConfig": {
+        "defaults": {
+          "custom": {
+            "drawStyle": "line",
+            "fillOpacity": 10
+          },
+          "unit": "short"
+        },
+        "overrides": []
+      },
+      "options": {
+        "legend": {
+          "displayMode": "list",
+          "placement": "bottom"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "expr": 
"jvm_threads_live_threads{cluster=\"$cluster\",broker=\"$broker\"}",
+          "legendFormat": "{{broker}}",
+          "refId": "A"
+        }
+      ]
+    },
+    {
+      "id": 4,
+      "title": "GC Pause (1m rate)",
+      "type": "timeseries",
+      "datasource": {
+        "type": "prometheus",
+        "uid": "${DS_PROMETHEUS}"
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 24
+      },
+      "fieldConfig": {
+        "defaults": {
+          "custom": {
+            "drawStyle": "line",
+            "fillOpacity": 10
+          },
+          "unit": "s"
+        },
+        "overrides": []
+      },
+      "options": {
+        "legend": {
+          "displayMode": "list",
+          "placement": "bottom"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "expr": 
"rate(jvm_gc_pause_seconds_sum{cluster=\"$cluster\",broker=\"$broker\"}[1m])",
+          "legendFormat": "{{broker}}",
+          "refId": "A"
+        }
+      ]
+    }
+  ]
+}
diff --git a/server/src/main/resources/grafana/rocketmq-latency.json 
b/server/src/main/resources/grafana/rocketmq-latency.json
new file mode 100644
index 00000000..028de8b5
--- /dev/null
+++ b/server/src/main/resources/grafana/rocketmq-latency.json
@@ -0,0 +1,150 @@
+{
+  "uid": "rocketmq-latency",
+  "title": "RocketMQ Latency",
+  "description": "Dispatch and client push latency percentiles.",
+  "tags": [
+    "rocketmq"
+  ],
+  "schemaVersion": 39,
+  "timezone": "browser",
+  "editable": true,
+  "templating": {
+    "list": [
+      {
+        "name": "DS_PROMETHEUS",
+        "type": "datasource",
+        "label": "Prometheus",
+        "query": "prometheus",
+        "current": {},
+        "hide": 0
+      },
+      {
+        "name": "cluster",
+        "type": "query",
+        "datasource": {
+          "type": "prometheus",
+          "uid": "${DS_PROMETHEUS}"
+        },
+        "query": "label_values(rocketmq_messages_in_total, cluster)",
+        "refresh": 2,
+        "current": {},
+        "hide": 0
+      },
+      {
+        "name": "broker",
+        "type": "query",
+        "datasource": {
+          "type": "prometheus",
+          "uid": "${DS_PROMETHEUS}"
+        },
+        "query": 
"label_values(rocketmq_messages_in_total{cluster=\"$cluster\"}, broker)",
+        "refresh": 2,
+        "current": {},
+        "hide": 0
+      },
+      {
+        "name": "topic",
+        "type": "query",
+        "datasource": {
+          "type": "prometheus",
+          "uid": "${DS_PROMETHEUS}"
+        },
+        "query": 
"label_values(rocketmq_messages_in_total{cluster=\"$cluster\"}, topic)",
+        "refresh": 2,
+        "current": {},
+        "hide": 0
+      }
+    ]
+  },
+  "time": {
+    "from": "now-6h",
+    "to": "now"
+  },
+  "refresh": "30s",
+  "panels": [
+    {
+      "id": 1,
+      "title": "Dispatch Latency p99",
+      "type": "timeseries",
+      "datasource": {
+        "type": "prometheus",
+        "uid": "${DS_PROMETHEUS}"
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 0
+      },
+      "fieldConfig": {
+        "defaults": {
+          "custom": {
+            "drawStyle": "line",
+            "fillOpacity": 10
+          },
+          "unit": "s"
+        },
+        "overrides": []
+      },
+      "options": {
+        "legend": {
+          "displayMode": "list",
+          "placement": "bottom"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "expr": "histogram_quantile(0.99, sum by (le) 
(rate(rocketmq_dispatch_latency_bucket{cluster=\"$cluster\"}[5m])))",
+          "legendFormat": "",
+          "refId": "A"
+        }
+      ]
+    },
+    {
+      "id": 2,
+      "title": "Send To Client Latency p99",
+      "type": "timeseries",
+      "datasource": {
+        "type": "prometheus",
+        "uid": "${DS_PROMETHEUS}"
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 8
+      },
+      "fieldConfig": {
+        "defaults": {
+          "custom": {
+            "drawStyle": "line",
+            "fillOpacity": 10
+          },
+          "unit": "s"
+        },
+        "overrides": []
+      },
+      "options": {
+        "legend": {
+          "displayMode": "list",
+          "placement": "bottom"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "expr": "histogram_quantile(0.99, sum by (le) 
(rate(rocketmq_send_to_client_latency_bucket{cluster=\"$cluster\"}[5m])))",
+          "legendFormat": "",
+          "refId": "A"
+        }
+      ]
+    }
+  ]
+}
diff --git a/server/src/main/resources/grafana/rocketmq-network.json 
b/server/src/main/resources/grafana/rocketmq-network.json
new file mode 100644
index 00000000..ddec7e09
--- /dev/null
+++ b/server/src/main/resources/grafana/rocketmq-network.json
@@ -0,0 +1,193 @@
+{
+  "uid": "rocketmq-network",
+  "title": "RocketMQ Network & Connections",
+  "description": "Client connections and produced/consumed connection counts.",
+  "tags": [
+    "rocketmq"
+  ],
+  "schemaVersion": 39,
+  "timezone": "browser",
+  "editable": true,
+  "templating": {
+    "list": [
+      {
+        "name": "DS_PROMETHEUS",
+        "type": "datasource",
+        "label": "Prometheus",
+        "query": "prometheus",
+        "current": {},
+        "hide": 0
+      },
+      {
+        "name": "cluster",
+        "type": "query",
+        "datasource": {
+          "type": "prometheus",
+          "uid": "${DS_PROMETHEUS}"
+        },
+        "query": "label_values(rocketmq_messages_in_total, cluster)",
+        "refresh": 2,
+        "current": {},
+        "hide": 0
+      },
+      {
+        "name": "broker",
+        "type": "query",
+        "datasource": {
+          "type": "prometheus",
+          "uid": "${DS_PROMETHEUS}"
+        },
+        "query": 
"label_values(rocketmq_messages_in_total{cluster=\"$cluster\"}, broker)",
+        "refresh": 2,
+        "current": {},
+        "hide": 0
+      },
+      {
+        "name": "topic",
+        "type": "query",
+        "datasource": {
+          "type": "prometheus",
+          "uid": "${DS_PROMETHEUS}"
+        },
+        "query": 
"label_values(rocketmq_messages_in_total{cluster=\"$cluster\"}, topic)",
+        "refresh": 2,
+        "current": {},
+        "hide": 0
+      }
+    ]
+  },
+  "time": {
+    "from": "now-6h",
+    "to": "now"
+  },
+  "refresh": "30s",
+  "panels": [
+    {
+      "id": 1,
+      "title": "Client Connections",
+      "type": "stat",
+      "datasource": {
+        "type": "prometheus",
+        "uid": "${DS_PROMETHEUS}"
+      },
+      "gridPos": {
+        "h": 6,
+        "w": 6,
+        "x": 0,
+        "y": 0
+      },
+      "fieldConfig": {
+        "defaults": {
+          "unit": "short",
+          "custom": {
+            "thresholdsStyle": {
+              "mode": "none"
+            }
+          }
+        },
+        "overrides": []
+      },
+      "options": {
+        "reduceOptions": {
+          "calcs": [
+            "lastNotNull"
+          ]
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "expr": "rocketmq_producer_count{cluster=\"$cluster\"} + 
rocketmq_consumer_count{cluster=\"$cluster\"}",
+          "refId": "A"
+        }
+      ]
+    },
+    {
+      "id": 2,
+      "title": "Connections by Broker",
+      "type": "timeseries",
+      "datasource": {
+        "type": "prometheus",
+        "uid": "${DS_PROMETHEUS}"
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 6
+      },
+      "fieldConfig": {
+        "defaults": {
+          "custom": {
+            "drawStyle": "line",
+            "fillOpacity": 10
+          },
+          "unit": "short"
+        },
+        "overrides": []
+      },
+      "options": {
+        "legend": {
+          "displayMode": "list",
+          "placement": "bottom"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "expr": 
"rocketmq_connection_count{cluster=\"$cluster\",broker=\"$broker\"}",
+          "legendFormat": "{{broker}}",
+          "refId": "A"
+        }
+      ]
+    },
+    {
+      "id": 3,
+      "title": "Producer Connections",
+      "type": "timeseries",
+      "datasource": {
+        "type": "prometheus",
+        "uid": "${DS_PROMETHEUS}"
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 14
+      },
+      "fieldConfig": {
+        "defaults": {
+          "custom": {
+            "drawStyle": "line",
+            "fillOpacity": 10
+          },
+          "unit": "short"
+        },
+        "overrides": []
+      },
+      "options": {
+        "legend": {
+          "displayMode": "list",
+          "placement": "bottom"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "expr": "rocketmq_producer_count{cluster=\"$cluster\"}",
+          "legendFormat": "{{broker}}",
+          "refId": "A"
+        }
+      ]
+    }
+  ]
+}
diff --git a/server/src/main/resources/grafana/rocketmq-overview.json 
b/server/src/main/resources/grafana/rocketmq-overview.json
new file mode 100644
index 00000000..d0c67694
--- /dev/null
+++ b/server/src/main/resources/grafana/rocketmq-overview.json
@@ -0,0 +1,322 @@
+{
+  "uid": "rocketmq-overview",
+  "title": "RocketMQ Cluster Overview",
+  "description": "Cluster-wide throughput, topic/group counts and producer 
footprint.",
+  "tags": [
+    "rocketmq"
+  ],
+  "schemaVersion": 39,
+  "timezone": "browser",
+  "editable": true,
+  "templating": {
+    "list": [
+      {
+        "name": "DS_PROMETHEUS",
+        "type": "datasource",
+        "label": "Prometheus",
+        "query": "prometheus",
+        "current": {},
+        "hide": 0
+      },
+      {
+        "name": "cluster",
+        "type": "query",
+        "datasource": {
+          "type": "prometheus",
+          "uid": "${DS_PROMETHEUS}"
+        },
+        "query": "label_values(rocketmq_messages_in_total, cluster)",
+        "refresh": 2,
+        "current": {},
+        "hide": 0
+      },
+      {
+        "name": "broker",
+        "type": "query",
+        "datasource": {
+          "type": "prometheus",
+          "uid": "${DS_PROMETHEUS}"
+        },
+        "query": 
"label_values(rocketmq_messages_in_total{cluster=\"$cluster\"}, broker)",
+        "refresh": 2,
+        "current": {},
+        "hide": 0
+      },
+      {
+        "name": "topic",
+        "type": "query",
+        "datasource": {
+          "type": "prometheus",
+          "uid": "${DS_PROMETHEUS}"
+        },
+        "query": 
"label_values(rocketmq_messages_in_total{cluster=\"$cluster\"}, topic)",
+        "refresh": 2,
+        "current": {},
+        "hide": 0
+      }
+    ]
+  },
+  "time": {
+    "from": "now-6h",
+    "to": "now"
+  },
+  "refresh": "30s",
+  "panels": [
+    {
+      "id": 1,
+      "title": "Messages In TPS",
+      "type": "timeseries",
+      "datasource": {
+        "type": "prometheus",
+        "uid": "${DS_PROMETHEUS}"
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 0
+      },
+      "fieldConfig": {
+        "defaults": {
+          "custom": {
+            "drawStyle": "line",
+            "fillOpacity": 10
+          },
+          "unit": "ops"
+        },
+        "overrides": []
+      },
+      "options": {
+        "legend": {
+          "displayMode": "list",
+          "placement": "bottom"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "expr": 
"sum(rate(rocketmq_messages_in_total{cluster=\"$cluster\"}[1m]))",
+          "legendFormat": "by cluster",
+          "refId": "A"
+        }
+      ]
+    },
+    {
+      "id": 2,
+      "title": "Messages Out TPS",
+      "type": "timeseries",
+      "datasource": {
+        "type": "prometheus",
+        "uid": "${DS_PROMETHEUS}"
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 8
+      },
+      "fieldConfig": {
+        "defaults": {
+          "custom": {
+            "drawStyle": "line",
+            "fillOpacity": 10
+          },
+          "unit": "ops"
+        },
+        "overrides": []
+      },
+      "options": {
+        "legend": {
+          "displayMode": "list",
+          "placement": "bottom"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "expr": 
"sum(rate(rocketmq_messages_out_total{cluster=\"$cluster\"}[1m]))",
+          "legendFormat": "by cluster",
+          "refId": "A"
+        }
+      ]
+    },
+    {
+      "id": 3,
+      "title": "Total Topics",
+      "type": "stat",
+      "datasource": {
+        "type": "prometheus",
+        "uid": "${DS_PROMETHEUS}"
+      },
+      "gridPos": {
+        "h": 6,
+        "w": 6,
+        "x": 0,
+        "y": 16
+      },
+      "fieldConfig": {
+        "defaults": {
+          "unit": "short",
+          "custom": {
+            "thresholdsStyle": {
+              "mode": "none"
+            }
+          }
+        },
+        "overrides": []
+      },
+      "options": {
+        "reduceOptions": {
+          "calcs": [
+            "lastNotNull"
+          ]
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "expr": "count(count by (topic) 
(rocketmq_messages_in_total{cluster=\"$cluster\"}))",
+          "refId": "A"
+        }
+      ]
+    },
+    {
+      "id": 4,
+      "title": "Total Consumer Groups",
+      "type": "stat",
+      "datasource": {
+        "type": "prometheus",
+        "uid": "${DS_PROMETHEUS}"
+      },
+      "gridPos": {
+        "h": 6,
+        "w": 6,
+        "x": 12,
+        "y": 22
+      },
+      "fieldConfig": {
+        "defaults": {
+          "unit": "short",
+          "custom": {
+            "thresholdsStyle": {
+              "mode": "none"
+            }
+          }
+        },
+        "overrides": []
+      },
+      "options": {
+        "reduceOptions": {
+          "calcs": [
+            "lastNotNull"
+          ]
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "expr": "count(count by (group) 
(rocketmq_messages_out_total{cluster=\"$cluster\"}))",
+          "refId": "A"
+        }
+      ]
+    },
+    {
+      "id": 5,
+      "title": "Producer Count",
+      "type": "stat",
+      "datasource": {
+        "type": "prometheus",
+        "uid": "${DS_PROMETHEUS}"
+      },
+      "gridPos": {
+        "h": 6,
+        "w": 6,
+        "x": 0,
+        "y": 28
+      },
+      "fieldConfig": {
+        "defaults": {
+          "unit": "short",
+          "custom": {
+            "thresholdsStyle": {
+              "mode": "none"
+            }
+          }
+        },
+        "overrides": []
+      },
+      "options": {
+        "reduceOptions": {
+          "calcs": [
+            "lastNotNull"
+          ]
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "expr": "max(rocketmq_producer_count{cluster=\"$cluster\"})",
+          "refId": "A"
+        }
+      ]
+    },
+    {
+      "id": 6,
+      "title": "Broker Count",
+      "type": "stat",
+      "datasource": {
+        "type": "prometheus",
+        "uid": "${DS_PROMETHEUS}"
+      },
+      "gridPos": {
+        "h": 6,
+        "w": 6,
+        "x": 12,
+        "y": 34
+      },
+      "fieldConfig": {
+        "defaults": {
+          "unit": "short",
+          "custom": {
+            "thresholdsStyle": {
+              "mode": "none"
+            }
+          }
+        },
+        "overrides": []
+      },
+      "options": {
+        "reduceOptions": {
+          "calcs": [
+            "lastNotNull"
+          ]
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "expr": "count(rocketmq_messages_in_total{cluster=\"$cluster\"})",
+          "refId": "A"
+        }
+      ]
+    }
+  ]
+}
diff --git a/server/src/main/resources/grafana/rocketmq-producer.json 
b/server/src/main/resources/grafana/rocketmq-producer.json
new file mode 100644
index 00000000..2d99ad97
--- /dev/null
+++ b/server/src/main/resources/grafana/rocketmq-producer.json
@@ -0,0 +1,193 @@
+{
+  "uid": "rocketmq-producer",
+  "title": "RocketMQ Producer",
+  "description": "Producer presence, send size and per-topic ingress.",
+  "tags": [
+    "rocketmq"
+  ],
+  "schemaVersion": 39,
+  "timezone": "browser",
+  "editable": true,
+  "templating": {
+    "list": [
+      {
+        "name": "DS_PROMETHEUS",
+        "type": "datasource",
+        "label": "Prometheus",
+        "query": "prometheus",
+        "current": {},
+        "hide": 0
+      },
+      {
+        "name": "cluster",
+        "type": "query",
+        "datasource": {
+          "type": "prometheus",
+          "uid": "${DS_PROMETHEUS}"
+        },
+        "query": "label_values(rocketmq_messages_in_total, cluster)",
+        "refresh": 2,
+        "current": {},
+        "hide": 0
+      },
+      {
+        "name": "broker",
+        "type": "query",
+        "datasource": {
+          "type": "prometheus",
+          "uid": "${DS_PROMETHEUS}"
+        },
+        "query": 
"label_values(rocketmq_messages_in_total{cluster=\"$cluster\"}, broker)",
+        "refresh": 2,
+        "current": {},
+        "hide": 0
+      },
+      {
+        "name": "topic",
+        "type": "query",
+        "datasource": {
+          "type": "prometheus",
+          "uid": "${DS_PROMETHEUS}"
+        },
+        "query": 
"label_values(rocketmq_messages_in_total{cluster=\"$cluster\"}, topic)",
+        "refresh": 2,
+        "current": {},
+        "hide": 0
+      }
+    ]
+  },
+  "time": {
+    "from": "now-6h",
+    "to": "now"
+  },
+  "refresh": "30s",
+  "panels": [
+    {
+      "id": 1,
+      "title": "Producer Count",
+      "type": "stat",
+      "datasource": {
+        "type": "prometheus",
+        "uid": "${DS_PROMETHEUS}"
+      },
+      "gridPos": {
+        "h": 6,
+        "w": 6,
+        "x": 0,
+        "y": 0
+      },
+      "fieldConfig": {
+        "defaults": {
+          "unit": "short",
+          "custom": {
+            "thresholdsStyle": {
+              "mode": "none"
+            }
+          }
+        },
+        "overrides": []
+      },
+      "options": {
+        "reduceOptions": {
+          "calcs": [
+            "lastNotNull"
+          ]
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "expr": "max(rocketmq_producer_count{cluster=\"$cluster\"})",
+          "refId": "A"
+        }
+      ]
+    },
+    {
+      "id": 2,
+      "title": "Producer Message Size",
+      "type": "timeseries",
+      "datasource": {
+        "type": "prometheus",
+        "uid": "${DS_PROMETHEUS}"
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 6
+      },
+      "fieldConfig": {
+        "defaults": {
+          "custom": {
+            "drawStyle": "line",
+            "fillOpacity": 10
+          },
+          "unit": "bytes"
+        },
+        "overrides": []
+      },
+      "options": {
+        "legend": {
+          "displayMode": "list",
+          "placement": "bottom"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "expr": "rocketmq_producer_message_size{cluster=\"$cluster\"}",
+          "legendFormat": "{{topic}}",
+          "refId": "A"
+        }
+      ]
+    },
+    {
+      "id": 3,
+      "title": "Messages In by Topic",
+      "type": "timeseries",
+      "datasource": {
+        "type": "prometheus",
+        "uid": "${DS_PROMETHEUS}"
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 24,
+        "x": 0,
+        "y": 14
+      },
+      "fieldConfig": {
+        "defaults": {
+          "custom": {
+            "drawStyle": "line",
+            "fillOpacity": 10
+          },
+          "unit": "ops"
+        },
+        "overrides": []
+      },
+      "options": {
+        "legend": {
+          "displayMode": "list",
+          "placement": "bottom"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "expr": "sum by (topic) 
(rate(rocketmq_messages_in_total{cluster=\"$cluster\"}[1m]))",
+          "legendFormat": "{{topic}}",
+          "refId": "A"
+        }
+      ]
+    }
+  ]
+}
diff --git a/server/src/main/resources/grafana/rocketmq-storage.json 
b/server/src/main/resources/grafana/rocketmq-storage.json
new file mode 100644
index 00000000..ca3a98e3
--- /dev/null
+++ b/server/src/main/resources/grafana/rocketmq-storage.json
@@ -0,0 +1,192 @@
+{
+  "uid": "rocketmq-storage",
+  "title": "RocketMQ Storage",
+  "description": "Broker disk usage and JVM heap footprint.",
+  "tags": [
+    "rocketmq"
+  ],
+  "schemaVersion": 39,
+  "timezone": "browser",
+  "editable": true,
+  "templating": {
+    "list": [
+      {
+        "name": "DS_PROMETHEUS",
+        "type": "datasource",
+        "label": "Prometheus",
+        "query": "prometheus",
+        "current": {},
+        "hide": 0
+      },
+      {
+        "name": "cluster",
+        "type": "query",
+        "datasource": {
+          "type": "prometheus",
+          "uid": "${DS_PROMETHEUS}"
+        },
+        "query": "label_values(rocketmq_messages_in_total, cluster)",
+        "refresh": 2,
+        "current": {},
+        "hide": 0
+      },
+      {
+        "name": "broker",
+        "type": "query",
+        "datasource": {
+          "type": "prometheus",
+          "uid": "${DS_PROMETHEUS}"
+        },
+        "query": 
"label_values(rocketmq_messages_in_total{cluster=\"$cluster\"}, broker)",
+        "refresh": 2,
+        "current": {},
+        "hide": 0
+      },
+      {
+        "name": "topic",
+        "type": "query",
+        "datasource": {
+          "type": "prometheus",
+          "uid": "${DS_PROMETHEUS}"
+        },
+        "query": 
"label_values(rocketmq_messages_in_total{cluster=\"$cluster\"}, topic)",
+        "refresh": 2,
+        "current": {},
+        "hide": 0
+      }
+    ]
+  },
+  "time": {
+    "from": "now-6h",
+    "to": "now"
+  },
+  "refresh": "30s",
+  "panels": [
+    {
+      "id": 1,
+      "title": "Disk Use Ratio",
+      "type": "gauge",
+      "datasource": {
+        "type": "prometheus",
+        "uid": "${DS_PROMETHEUS}"
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 0
+      },
+      "fieldConfig": {
+        "defaults": {
+          "unit": "percent",
+          "max": 100,
+          "custom": {
+            "min": 0
+          }
+        },
+        "overrides": []
+      },
+      "options": {
+        "reduceOptions": {
+          "calcs": [
+            "lastNotNull"
+          ]
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "expr": 
"rocketmq_disk_use_ratio{cluster=\"$cluster\",broker=\"$broker\"}",
+          "refId": "A"
+        }
+      ]
+    },
+    {
+      "id": 2,
+      "title": "JVM Heap Used",
+      "type": "timeseries",
+      "datasource": {
+        "type": "prometheus",
+        "uid": "${DS_PROMETHEUS}"
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 8
+      },
+      "fieldConfig": {
+        "defaults": {
+          "custom": {
+            "drawStyle": "line",
+            "fillOpacity": 10
+          },
+          "unit": "bytes"
+        },
+        "overrides": []
+      },
+      "options": {
+        "legend": {
+          "displayMode": "list",
+          "placement": "bottom"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "expr": 
"jvm_memory_used_bytes{area=\"heap\",cluster=\"$cluster\",broker=\"$broker\"}",
+          "legendFormat": "{{broker}}",
+          "refId": "A"
+        }
+      ]
+    },
+    {
+      "id": 3,
+      "title": "Dispatch Behind Bytes",
+      "type": "timeseries",
+      "datasource": {
+        "type": "prometheus",
+        "uid": "${DS_PROMETHEUS}"
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 24,
+        "x": 0,
+        "y": 16
+      },
+      "fieldConfig": {
+        "defaults": {
+          "custom": {
+            "drawStyle": "line",
+            "fillOpacity": 10
+          },
+          "unit": "bytes"
+        },
+        "overrides": []
+      },
+      "options": {
+        "legend": {
+          "displayMode": "list",
+          "placement": "bottom"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "expr": 
"rocketmq_dispatch_behind_bytes{cluster=\"$cluster\",broker=\"$broker\"}",
+          "legendFormat": "{{broker}}",
+          "refId": "A"
+        }
+      ]
+    }
+  ]
+}
diff --git a/server/src/main/resources/grafana/rocketmq-threadpool.json 
b/server/src/main/resources/grafana/rocketmq-threadpool.json
new file mode 100644
index 00000000..a5ea4784
--- /dev/null
+++ b/server/src/main/resources/grafana/rocketmq-threadpool.json
@@ -0,0 +1,192 @@
+{
+  "uid": "rocketmq-threadpool",
+  "title": "RocketMQ Thread Pool",
+  "description": "Broker thread pool queue depth, capacity and rejections.",
+  "tags": [
+    "rocketmq"
+  ],
+  "schemaVersion": 39,
+  "timezone": "browser",
+  "editable": true,
+  "templating": {
+    "list": [
+      {
+        "name": "DS_PROMETHEUS",
+        "type": "datasource",
+        "label": "Prometheus",
+        "query": "prometheus",
+        "current": {},
+        "hide": 0
+      },
+      {
+        "name": "cluster",
+        "type": "query",
+        "datasource": {
+          "type": "prometheus",
+          "uid": "${DS_PROMETHEUS}"
+        },
+        "query": "label_values(rocketmq_messages_in_total, cluster)",
+        "refresh": 2,
+        "current": {},
+        "hide": 0
+      },
+      {
+        "name": "broker",
+        "type": "query",
+        "datasource": {
+          "type": "prometheus",
+          "uid": "${DS_PROMETHEUS}"
+        },
+        "query": 
"label_values(rocketmq_messages_in_total{cluster=\"$cluster\"}, broker)",
+        "refresh": 2,
+        "current": {},
+        "hide": 0
+      },
+      {
+        "name": "topic",
+        "type": "query",
+        "datasource": {
+          "type": "prometheus",
+          "uid": "${DS_PROMETHEUS}"
+        },
+        "query": 
"label_values(rocketmq_messages_in_total{cluster=\"$cluster\"}, topic)",
+        "refresh": 2,
+        "current": {},
+        "hide": 0
+      }
+    ]
+  },
+  "time": {
+    "from": "now-6h",
+    "to": "now"
+  },
+  "refresh": "30s",
+  "panels": [
+    {
+      "id": 1,
+      "title": "Queue Size",
+      "type": "timeseries",
+      "datasource": {
+        "type": "prometheus",
+        "uid": "${DS_PROMETHEUS}"
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 0
+      },
+      "fieldConfig": {
+        "defaults": {
+          "custom": {
+            "drawStyle": "line",
+            "fillOpacity": 10
+          },
+          "unit": "short"
+        },
+        "overrides": []
+      },
+      "options": {
+        "legend": {
+          "displayMode": "list",
+          "placement": "bottom"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "expr": 
"rocketmq_threadpool_queue_size{cluster=\"$cluster\",broker=\"$broker\"}",
+          "legendFormat": "{{broker}}",
+          "refId": "A"
+        }
+      ]
+    },
+    {
+      "id": 2,
+      "title": "Queue Capacity",
+      "type": "timeseries",
+      "datasource": {
+        "type": "prometheus",
+        "uid": "${DS_PROMETHEUS}"
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 8
+      },
+      "fieldConfig": {
+        "defaults": {
+          "custom": {
+            "drawStyle": "line",
+            "fillOpacity": 10
+          },
+          "unit": "short"
+        },
+        "overrides": []
+      },
+      "options": {
+        "legend": {
+          "displayMode": "list",
+          "placement": "bottom"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "expr": 
"rocketmq_threadpool_queue_capacity{cluster=\"$cluster\",broker=\"$broker\"}",
+          "legendFormat": "{{broker}}",
+          "refId": "A"
+        }
+      ]
+    },
+    {
+      "id": 3,
+      "title": "Reject Count (1m)",
+      "type": "timeseries",
+      "datasource": {
+        "type": "prometheus",
+        "uid": "${DS_PROMETHEUS}"
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 24,
+        "x": 0,
+        "y": 16
+      },
+      "fieldConfig": {
+        "defaults": {
+          "custom": {
+            "drawStyle": "line",
+            "fillOpacity": 10
+          },
+          "unit": "short"
+        },
+        "overrides": []
+      },
+      "options": {
+        "legend": {
+          "displayMode": "list",
+          "placement": "bottom"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "expr": 
"increase(rocketmq_threadpool_reject_count{cluster=\"$cluster\",broker=\"$broker\"}[1m])",
+          "legendFormat": "{{broker}}",
+          "refId": "A"
+        }
+      ]
+    }
+  ]
+}
diff --git a/server/src/main/resources/grafana/rocketmq-topic.json 
b/server/src/main/resources/grafana/rocketmq-topic.json
new file mode 100644
index 00000000..651879c8
--- /dev/null
+++ b/server/src/main/resources/grafana/rocketmq-topic.json
@@ -0,0 +1,192 @@
+{
+  "uid": "rocketmq-topic",
+  "title": "RocketMQ Topic",
+  "description": "Per-topic ingress/egress throughput and dispatch backlog.",
+  "tags": [
+    "rocketmq"
+  ],
+  "schemaVersion": 39,
+  "timezone": "browser",
+  "editable": true,
+  "templating": {
+    "list": [
+      {
+        "name": "DS_PROMETHEUS",
+        "type": "datasource",
+        "label": "Prometheus",
+        "query": "prometheus",
+        "current": {},
+        "hide": 0
+      },
+      {
+        "name": "cluster",
+        "type": "query",
+        "datasource": {
+          "type": "prometheus",
+          "uid": "${DS_PROMETHEUS}"
+        },
+        "query": "label_values(rocketmq_messages_in_total, cluster)",
+        "refresh": 2,
+        "current": {},
+        "hide": 0
+      },
+      {
+        "name": "broker",
+        "type": "query",
+        "datasource": {
+          "type": "prometheus",
+          "uid": "${DS_PROMETHEUS}"
+        },
+        "query": 
"label_values(rocketmq_messages_in_total{cluster=\"$cluster\"}, broker)",
+        "refresh": 2,
+        "current": {},
+        "hide": 0
+      },
+      {
+        "name": "topic",
+        "type": "query",
+        "datasource": {
+          "type": "prometheus",
+          "uid": "${DS_PROMETHEUS}"
+        },
+        "query": 
"label_values(rocketmq_messages_in_total{cluster=\"$cluster\"}, topic)",
+        "refresh": 2,
+        "current": {},
+        "hide": 0
+      }
+    ]
+  },
+  "time": {
+    "from": "now-6h",
+    "to": "now"
+  },
+  "refresh": "30s",
+  "panels": [
+    {
+      "id": 1,
+      "title": "Topic Messages In TPS",
+      "type": "timeseries",
+      "datasource": {
+        "type": "prometheus",
+        "uid": "${DS_PROMETHEUS}"
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 0
+      },
+      "fieldConfig": {
+        "defaults": {
+          "custom": {
+            "drawStyle": "line",
+            "fillOpacity": 10
+          },
+          "unit": "ops"
+        },
+        "overrides": []
+      },
+      "options": {
+        "legend": {
+          "displayMode": "list",
+          "placement": "bottom"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "expr": "sum by (topic) 
(rate(rocketmq_messages_in_total{cluster=\"$cluster\",topic=\"$topic\"}[1m]))",
+          "legendFormat": "{{topic}}",
+          "refId": "A"
+        }
+      ]
+    },
+    {
+      "id": 2,
+      "title": "Topic Messages Out TPS",
+      "type": "timeseries",
+      "datasource": {
+        "type": "prometheus",
+        "uid": "${DS_PROMETHEUS}"
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 8
+      },
+      "fieldConfig": {
+        "defaults": {
+          "custom": {
+            "drawStyle": "line",
+            "fillOpacity": 10
+          },
+          "unit": "ops"
+        },
+        "overrides": []
+      },
+      "options": {
+        "legend": {
+          "displayMode": "list",
+          "placement": "bottom"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "expr": "sum by (topic) 
(rate(rocketmq_messages_out_total{cluster=\"$cluster\",topic=\"$topic\"}[1m]))",
+          "legendFormat": "{{topic}}",
+          "refId": "A"
+        }
+      ]
+    },
+    {
+      "id": 3,
+      "title": "Topic Dispatch Behind Bytes",
+      "type": "timeseries",
+      "datasource": {
+        "type": "prometheus",
+        "uid": "${DS_PROMETHEUS}"
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 24,
+        "x": 0,
+        "y": 16
+      },
+      "fieldConfig": {
+        "defaults": {
+          "custom": {
+            "drawStyle": "line",
+            "fillOpacity": 10
+          },
+          "unit": "bytes"
+        },
+        "overrides": []
+      },
+      "options": {
+        "legend": {
+          "displayMode": "list",
+          "placement": "bottom"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "expr": 
"rocketmq_dispatch_behind_bytes{cluster=\"$cluster\",topic=\"$topic\"}",
+          "legendFormat": "{{topic}}",
+          "refId": "A"
+        }
+      ]
+    }
+  ]
+}
diff --git a/server/src/main/resources/grafana/rocketmq-tps.json 
b/server/src/main/resources/grafana/rocketmq-tps.json
new file mode 100644
index 00000000..14acc158
--- /dev/null
+++ b/server/src/main/resources/grafana/rocketmq-tps.json
@@ -0,0 +1,192 @@
+{
+  "uid": "rocketmq-tps",
+  "title": "RocketMQ TPS",
+  "description": "Cluster and per-broker message throughput trends.",
+  "tags": [
+    "rocketmq"
+  ],
+  "schemaVersion": 39,
+  "timezone": "browser",
+  "editable": true,
+  "templating": {
+    "list": [
+      {
+        "name": "DS_PROMETHEUS",
+        "type": "datasource",
+        "label": "Prometheus",
+        "query": "prometheus",
+        "current": {},
+        "hide": 0
+      },
+      {
+        "name": "cluster",
+        "type": "query",
+        "datasource": {
+          "type": "prometheus",
+          "uid": "${DS_PROMETHEUS}"
+        },
+        "query": "label_values(rocketmq_messages_in_total, cluster)",
+        "refresh": 2,
+        "current": {},
+        "hide": 0
+      },
+      {
+        "name": "broker",
+        "type": "query",
+        "datasource": {
+          "type": "prometheus",
+          "uid": "${DS_PROMETHEUS}"
+        },
+        "query": 
"label_values(rocketmq_messages_in_total{cluster=\"$cluster\"}, broker)",
+        "refresh": 2,
+        "current": {},
+        "hide": 0
+      },
+      {
+        "name": "topic",
+        "type": "query",
+        "datasource": {
+          "type": "prometheus",
+          "uid": "${DS_PROMETHEUS}"
+        },
+        "query": 
"label_values(rocketmq_messages_in_total{cluster=\"$cluster\"}, topic)",
+        "refresh": 2,
+        "current": {},
+        "hide": 0
+      }
+    ]
+  },
+  "time": {
+    "from": "now-6h",
+    "to": "now"
+  },
+  "refresh": "30s",
+  "panels": [
+    {
+      "id": 1,
+      "title": "Cluster TPS In",
+      "type": "timeseries",
+      "datasource": {
+        "type": "prometheus",
+        "uid": "${DS_PROMETHEUS}"
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 0,
+        "y": 0
+      },
+      "fieldConfig": {
+        "defaults": {
+          "custom": {
+            "drawStyle": "line",
+            "fillOpacity": 10
+          },
+          "unit": "ops"
+        },
+        "overrides": []
+      },
+      "options": {
+        "legend": {
+          "displayMode": "list",
+          "placement": "bottom"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "expr": 
"sum(rate(rocketmq_messages_in_total{cluster=\"$cluster\"}[1m]))",
+          "legendFormat": "",
+          "refId": "A"
+        }
+      ]
+    },
+    {
+      "id": 2,
+      "title": "Cluster TPS Out",
+      "type": "timeseries",
+      "datasource": {
+        "type": "prometheus",
+        "uid": "${DS_PROMETHEUS}"
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 12,
+        "x": 12,
+        "y": 8
+      },
+      "fieldConfig": {
+        "defaults": {
+          "custom": {
+            "drawStyle": "line",
+            "fillOpacity": 10
+          },
+          "unit": "ops"
+        },
+        "overrides": []
+      },
+      "options": {
+        "legend": {
+          "displayMode": "list",
+          "placement": "bottom"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "expr": 
"sum(rate(rocketmq_messages_out_total{cluster=\"$cluster\"}[1m]))",
+          "legendFormat": "",
+          "refId": "A"
+        }
+      ]
+    },
+    {
+      "id": 3,
+      "title": "Per-Broker TPS In",
+      "type": "timeseries",
+      "datasource": {
+        "type": "prometheus",
+        "uid": "${DS_PROMETHEUS}"
+      },
+      "gridPos": {
+        "h": 8,
+        "w": 24,
+        "x": 0,
+        "y": 16
+      },
+      "fieldConfig": {
+        "defaults": {
+          "custom": {
+            "drawStyle": "line",
+            "fillOpacity": 10
+          },
+          "unit": "ops"
+        },
+        "overrides": []
+      },
+      "options": {
+        "legend": {
+          "displayMode": "list",
+          "placement": "bottom"
+        }
+      },
+      "targets": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "${DS_PROMETHEUS}"
+          },
+          "expr": "sum by (broker) 
(rate(rocketmq_messages_in_total{cluster=\"$cluster\",broker=\"$broker\"}[1m]))",
+          "legendFormat": "{{broker}}",
+          "refId": "A"
+        }
+      ]
+    }
+  ]
+}
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/cluster/metrics/grafana/GrafanaDashboardControllerTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/cluster/metrics/grafana/GrafanaDashboardControllerTest.java
new file mode 100644
index 00000000..09e828b8
--- /dev/null
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/cluster/metrics/grafana/GrafanaDashboardControllerTest.java
@@ -0,0 +1,87 @@
+/*
+ * 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.
+ */
+package org.apache.rocketmq.studio.cluster.metrics.grafana;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import 
org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.test.web.servlet.MockMvc;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.mockito.Mockito.when;
+import static 
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static 
org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static 
org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
+import static 
org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static 
org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@WebMvcTest(GrafanaDashboardController.class)
+@AutoConfigureMockMvc(addFilters = false)
+class GrafanaDashboardControllerTest {
+
+    @Autowired
+    private MockMvc mockMvc;
+
+    @MockBean
+    private GrafanaDashboardService grafanaDashboardService;
+
+    @Test
+    void listDashboardsShouldReturnMetadatas() throws Exception {
+        when(grafanaDashboardService.listDashboards()).thenReturn(List.of(
+                new GrafanaDashboardInfo("rocketmq-overview", "RocketMQ 
Cluster Overview",
+                        "Overview", List.of("rocketmq")),
+                new GrafanaDashboardInfo("rocketmq-broker", "RocketMQ Broker",
+                        "Broker", List.of("rocketmq"))
+        ));
+
+        mockMvc.perform(get("/api/metrics/grafana/dashboards"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data.length()").value(2))
+                
.andExpect(jsonPath("$.data[0].uid").value("rocketmq-overview"))
+                .andExpect(jsonPath("$.data[0].tags[0]").value("rocketmq"));
+    }
+
+    @Test
+    void getDashboardShouldReturnModel() throws Exception {
+        when(grafanaDashboardService.getDashboard("rocketmq-overview"))
+                .thenReturn(Map.of("uid", "rocketmq-overview", "title", 
"RocketMQ Cluster Overview"));
+
+        
mockMvc.perform(get("/api/metrics/grafana/dashboards/rocketmq-overview"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data.uid").value("rocketmq-overview"))
+                .andExpect(jsonPath("$.data.title").value("RocketMQ Cluster 
Overview"));
+    }
+
+    @Test
+    void exportDashboardShouldReturnAttachment() throws Exception {
+        when(grafanaDashboardService.getDashboardJson("rocketmq-overview"))
+                .thenReturn("{\"uid\":\"rocketmq-overview\"}");
+
+        
mockMvc.perform(get("/api/metrics/grafana/dashboards/rocketmq-overview/export"))
+                .andExpect(status().isOk())
+                .andExpect(header().string("Content-Type", "application/json"))
+                .andExpect(header().string("Content-Disposition",
+                        "form-data; name=\"attachment\"; 
filename=\"rocketmq-overview.json\""))
+                .andExpect(content().json("{\"uid\":\"rocketmq-overview\"}"));
+    }
+}
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/cluster/metrics/grafana/GrafanaDashboardServiceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/cluster/metrics/grafana/GrafanaDashboardServiceTest.java
new file mode 100644
index 00000000..6d423cbe
--- /dev/null
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/cluster/metrics/grafana/GrafanaDashboardServiceTest.java
@@ -0,0 +1,80 @@
+/*
+ * 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.
+ */
+package org.apache.rocketmq.studio.cluster.metrics.grafana;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.rocketmq.studio.common.exception.BusinessException;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class GrafanaDashboardServiceTest {
+
+    private final GrafanaDashboardService service = new 
GrafanaDashboardService(new ObjectMapper());
+
+    @Test
+    void listDashboardsShouldExposeBundledAssets() {
+        List<GrafanaDashboardInfo> dashboards = service.listDashboards();
+
+        assertFalse(dashboards.isEmpty(), "expected bundled dashboards to be 
present");
+        assertTrue(dashboards.size() >= 10, "expected at least 10 dashboards, 
got " + dashboards.size());
+
+        for (GrafanaDashboardInfo info : dashboards) {
+            assertFalse(info.uid().isBlank(), "dashboard uid must not be 
blank");
+            assertFalse(info.title().isBlank(), "dashboard title must not be 
blank");
+            assertTrue(info.tags().contains("rocketmq"), "dashboard should be 
tagged rocketmq");
+        }
+    }
+
+    @Test
+    void getDashboardShouldReturnParsedModel() {
+        Map<String, Object> model = service.getDashboard("rocketmq-overview");
+
+        assertEquals("rocketmq-overview", model.get("uid"));
+        assertEquals("RocketMQ Cluster Overview", model.get("title"));
+        assertTrue(model.containsKey("panels"), "dashboard should contain 
panels");
+    }
+
+    @Test
+    void getDashboardShouldThrowWhenUidUnknown() {
+        BusinessException exception = assertThrows(BusinessException.class,
+                () -> service.getDashboard("no-such-dashboard"));
+        assertEquals(404, exception.getCode());
+    }
+
+    @Test
+    void getDashboardJsonShouldReturnRawContent() {
+        String json = service.getDashboardJson("rocketmq-broker");
+
+        assertFalse(json.isBlank());
+        assertTrue(json.contains("\"uid\""));
+        assertTrue(json.contains("rocketmq-broker"));
+    }
+
+    @Test
+    void getDashboardJsonShouldThrowWhenUidUnknown() {
+        BusinessException exception = assertThrows(BusinessException.class,
+                () -> service.getDashboardJson("no-such-dashboard"));
+        assertEquals(404, exception.getCode());
+    }
+}
diff --git a/web/src/App.tsx b/web/src/App.tsx
index 86035f3d..cd9fba6c 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -48,6 +48,7 @@ const GroupManagementPage = lazy(() => 
import('./pages/studio/GroupManagement'))
 const BrokerClusterPage = lazy(() => import('./pages/studio/BrokerCluster'));
 const SslSettingsPage = lazy(() => import('./pages/studio/SslSettings'));
 const AlertManagementPage = lazy(() => 
import('./pages/studio/AlertManagement'));
+const GrafanaDashboardsPage = lazy(() => 
import('./pages/studio/GrafanaDashboards'));
 const ProducerPage = lazy(() => import('./pages/studio/Producer'));
 const OpsPage = lazy(() => import('./pages/studio/Ops'));
 
@@ -158,6 +159,7 @@ function App() {
             <Route path="cluster/certs" element={<K8sCertsPage />} />
             <Route path="cluster/clients" element={<ClientsPage />} />
             <Route path="ops/dashboard" element={<DashboardOpsPage />} />
+            <Route path="ops/grafana" element={<GrafanaDashboardsPage />} />
             <Route path="ops/alerts" element={<AlertsPage />} />
             <Route path="ops/system-alerts" element={<SystemAlertsPage />} />
             <Route path="ops/audit" element={<AuditPage />} />
diff --git a/web/src/api/metrics.test.ts b/web/src/api/metrics.test.ts
index 7333aee7..b517ae4e 100644
--- a/web/src/api/metrics.test.ts
+++ b/web/src/api/metrics.test.ts
@@ -18,7 +18,14 @@
 import MockAdapter from 'axios-mock-adapter';
 import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
 import client from './client';
-import { getDashboard, listMetricProfiles, queryMetrics } from './metrics';
+import {
+  getDashboard,
+  listGrafanaDashboards,
+  getGrafanaDashboard,
+  exportGrafanaDashboard,
+  listMetricProfiles,
+  queryMetrics,
+} from './metrics';
 
 const mock = new MockAdapter(client);
 const dashboard = {
@@ -103,4 +110,44 @@ describe('metrics API', () => {
 
     await expect(listMetricProfiles()).resolves.toEqual(profiles);
   });
+
+  it('lists bundled Grafana dashboards', async () => {
+    const dashboards = [
+      {
+        uid: 'rocketmq-overview',
+        title: 'RocketMQ Cluster Overview',
+        description: 'd',
+        tags: ['rocketmq'],
+      },
+      { uid: 'rocketmq-broker', title: 'RocketMQ Broker', description: 'd', 
tags: ['rocketmq'] },
+    ];
+
+    mock.onGet('/metrics/grafana/dashboards').reply(200, { code: 200, data: 
dashboards });
+
+    await expect(listGrafanaDashboards()).resolves.toEqual(dashboards);
+  });
+
+  it('loads a single Grafana dashboard model', async () => {
+    const model = {
+      uid: 'rocketmq-overview',
+      title: 'RocketMQ Cluster Overview',
+      schemaVersion: 39,
+    };
+
+    mock
+      .onGet('/metrics/grafana/dashboards/rocketmq-overview')
+      .reply(200, { code: 200, data: model });
+
+    await 
expect(getGrafanaDashboard('rocketmq-overview')).resolves.toEqual(model);
+  });
+
+  it('exports a Grafana dashboard as a blob', async () => {
+    const blob = new Blob(['{"uid":"rocketmq-overview"}'], { type: 
'application/json' });
+
+    
mock.onGet('/metrics/grafana/dashboards/rocketmq-overview/export').reply(200, 
blob);
+
+    const result = await exportGrafanaDashboard('rocketmq-overview');
+    expect(result).toBeInstanceOf(Blob);
+    await expect(result.text()).resolves.toContain('rocketmq-overview');
+  });
 });
diff --git a/web/src/api/metrics.ts b/web/src/api/metrics.ts
index b0665f08..8471b0af 100644
--- a/web/src/api/metrics.ts
+++ b/web/src/api/metrics.ts
@@ -107,3 +107,31 @@ export async function listMetricProfiles() {
   const res = await client.get<{ data: MetricProfile[] }>('/metrics/profiles');
   return res.data.data;
 }
+
+// ─── Grafana dashboards ─────────────────────────────────────────
+export interface GrafanaDashboardInfo {
+  uid: string;
+  title: string;
+  description: string;
+  tags: string[];
+}
+
+export async function listGrafanaDashboards(): Promise<GrafanaDashboardInfo[]> 
{
+  const res = await client.get<{ data: GrafanaDashboardInfo[] 
}>('/metrics/grafana/dashboards');
+  return res.data.data;
+}
+
+export async function getGrafanaDashboard(uid: string): Promise<Record<string, 
unknown>> {
+  const res = await client.get<{ data: Record<string, unknown> }>(
+    `/metrics/grafana/dashboards/${encodeURIComponent(uid)}`,
+  );
+  return res.data.data;
+}
+
+export async function exportGrafanaDashboard(uid: string): Promise<Blob> {
+  const res = await client.get<Blob>(
+    `/metrics/grafana/dashboards/${encodeURIComponent(uid)}/export`,
+    { responseType: 'blob' },
+  );
+  return res.data;
+}
diff --git a/web/src/components/GrafanaDashboardList.tsx 
b/web/src/components/GrafanaDashboardList.tsx
new file mode 100644
index 00000000..894fb22d
--- /dev/null
+++ b/web/src/components/GrafanaDashboardList.tsx
@@ -0,0 +1,188 @@
+/*
+ * 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 { useEffect, useState } from 'react';
+import { App, Button, Modal, Space, Table, Tag, Typography } from 'antd';
+import type { ColumnsType } from 'antd/es/table';
+import { DownloadSimple, Eye } from '@phosphor-icons/react';
+import { useLang } from '../i18n/LangContext';
+import {
+  getGrafanaDashboard,
+  exportGrafanaDashboard,
+  listGrafanaDashboards,
+} from '../services/grafanaService';
+import type { GrafanaDashboardInfo } from '../api/metrics';
+
+const { Paragraph, Text } = Typography;
+
+export const GrafanaDashboardList: React.FC = () => {
+  const { t } = useLang();
+  const { message } = App.useApp();
+  const [dashboards, setDashboards] = useState<GrafanaDashboardInfo[]>([]);
+  const [loading, setLoading] = useState(true);
+  const [viewing, setViewing] = useState<GrafanaDashboardInfo | null>(null);
+  const [viewContent, setViewContent] = useState('');
+  const [viewLoading, setViewLoading] = useState(false);
+  const [exportingUid, setExportingUid] = useState<string | null>(null);
+
+  useEffect(() => {
+    let cancelled = false;
+    const load = async () => {
+      try {
+        const data = await listGrafanaDashboards();
+        if (!cancelled) setDashboards(data);
+      } catch {
+        if (!cancelled) message.error(t('grafana.loadFailed'));
+      } finally {
+        if (!cancelled) setLoading(false);
+      }
+    };
+    void load();
+    return () => {
+      cancelled = true;
+    };
+  }, [t, message]);
+
+  const handleView = async (info: GrafanaDashboardInfo) => {
+    setViewing(info);
+    setViewLoading(true);
+    try {
+      const model = await getGrafanaDashboard(info.uid);
+      setViewContent(JSON.stringify(model, null, 2));
+    } catch {
+      message.error(t('grafana.loadFailed'));
+    } finally {
+      setViewLoading(false);
+    }
+  };
+
+  const triggerDownload = (uid: string, content: Blob | string) => {
+    const blob =
+      typeof content === 'string' ? new Blob([content], { type: 
'application/json' }) : content;
+    const url = URL.createObjectURL(blob);
+    const a = document.createElement('a');
+    a.href = url;
+    a.download = `${uid}.json`;
+    a.click();
+    URL.revokeObjectURL(url);
+  };
+
+  const handleExport = async (info: GrafanaDashboardInfo) => {
+    setExportingUid(info.uid);
+    try {
+      const blob = await exportGrafanaDashboard(info.uid);
+      triggerDownload(info.uid, blob);
+      message.success(t('grafana.exported'));
+    } catch {
+      message.error(t('grafana.exportFailed'));
+    } finally {
+      setExportingUid(null);
+    }
+  };
+
+  const columns: ColumnsType<GrafanaDashboardInfo> = [
+    {
+      title: t('grafana.title'),
+      dataIndex: 'title',
+      key: 'title',
+    },
+    {
+      title: t('grafana.description'),
+      dataIndex: 'description',
+      key: 'description',
+      ellipsis: true,
+    },
+    {
+      title: t('grafana.tags'),
+      dataIndex: 'tags',
+      key: 'tags',
+      width: 140,
+      render: (tags: string[]) => (
+        <Space size={[0, 4]} wrap>
+          {(tags || []).map((tag) => (
+            <Tag key={tag} color="blue">
+              {tag}
+            </Tag>
+          ))}
+        </Space>
+      ),
+    },
+    {
+      title: t('common.actions'),
+      key: 'actions',
+      width: 180,
+      render: (_: unknown, record: GrafanaDashboardInfo) => (
+        <Space size="small">
+          <Button size="small" icon={<Eye size={16} />} onClick={() => 
handleView(record)}>
+            {t('common.view')}
+          </Button>
+          <Button
+            size="small"
+            icon={<DownloadSimple size={16} />}
+            loading={exportingUid === record.uid}
+            onClick={() => handleExport(record)}
+          >
+            {t('common.export')}
+          </Button>
+        </Space>
+      ),
+    },
+  ];
+
+  return (
+    <div>
+      <Table
+        columns={columns}
+        dataSource={dashboards}
+        loading={loading}
+        rowKey="uid"
+        pagination={false}
+        size="small"
+      />
+
+      <Modal
+        title={viewing ? viewing.title : t('grafana.title')}
+        open={viewing !== null}
+        footer={<Button onClick={() => 
setViewing(null)}>{t('common.close')}</Button>}
+        onCancel={() => setViewing(null)}
+        width={760}
+        destroyOnHidden
+      >
+        {viewLoading ? (
+          <Text type="secondary">{t('common.loading')}</Text>
+        ) : (
+          <Paragraph>
+            <pre
+              style={{
+                maxHeight: 480,
+                overflow: 'auto',
+                background: '#f5f5f5',
+                padding: 16,
+                borderRadius: 6,
+                fontSize: 12,
+              }}
+            >
+              {viewContent}
+            </pre>
+          </Paragraph>
+        )}
+      </Modal>
+    </div>
+  );
+};
+
+export default GrafanaDashboardList;
diff --git a/web/src/components/__tests__/GrafanaDashboardList.test.tsx 
b/web/src/components/__tests__/GrafanaDashboardList.test.tsx
new file mode 100644
index 00000000..cc2c24f9
--- /dev/null
+++ b/web/src/components/__tests__/GrafanaDashboardList.test.tsx
@@ -0,0 +1,141 @@
+/*
+ * 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 { App } from 'antd';
+import { render, screen, waitFor, within } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 
'vitest';
+
+import { LangProvider } from '../../i18n/LangContext';
+import {
+  exportGrafanaDashboard,
+  getGrafanaDashboard,
+  listGrafanaDashboards,
+} from '../../services/grafanaService';
+import GrafanaDashboardList from '../GrafanaDashboardList';
+
+vi.mock('../../services/grafanaService', () => ({
+  listGrafanaDashboards: vi.fn(),
+  getGrafanaDashboard: vi.fn(),
+  exportGrafanaDashboard: vi.fn(),
+}));
+
+const dashboards = [
+  {
+    uid: 'rocketmq-overview',
+    title: 'RocketMQ Cluster Overview',
+    description: 'Overview',
+    tags: ['rocketmq'],
+  },
+  { uid: 'rocketmq-broker', title: 'RocketMQ Broker', description: 'Broker', 
tags: ['rocketmq'] },
+];
+
+const dashboardModel = {
+  uid: 'rocketmq-overview',
+  title: 'RocketMQ Cluster Overview',
+  schemaVersion: 39,
+  panels: [{ id: 1, title: 'Messages In TPS', type: 'timeseries' }],
+};
+
+beforeAll(() => {
+  Object.defineProperty(window, 'matchMedia', {
+    writable: true,
+    value: vi.fn().mockImplementation((query: string) => ({
+      matches: false,
+      media: query,
+      onchange: null,
+      addListener: vi.fn(),
+      removeListener: vi.fn(),
+      addEventListener: vi.fn(),
+      removeEventListener: vi.fn(),
+      dispatchEvent: vi.fn(),
+    })),
+  });
+});
+
+beforeEach(() => {
+  vi.mocked(listGrafanaDashboards).mockResolvedValue(dashboards);
+  vi.mocked(getGrafanaDashboard).mockResolvedValue(dashboardModel);
+  vi.mocked(exportGrafanaDashboard).mockResolvedValue(
+    new Blob([JSON.stringify(dashboardModel, null, 2)], { type: 
'application/json' }),
+  );
+});
+
+afterEach(() => {
+  vi.restoreAllMocks();
+});
+
+describe('GrafanaDashboardList', () => {
+  it('lists the configured dashboards', async () => {
+    render(
+      <App>
+        <LangProvider>
+          <GrafanaDashboardList />
+        </LangProvider>
+      </App>,
+    );
+
+    expect(await screen.findByText('RocketMQ Cluster 
Overview')).toBeInTheDocument();
+    expect(screen.getByText('RocketMQ Broker')).toBeInTheDocument();
+  });
+
+  it('opens the view modal and renders the dashboard JSON', async () => {
+    const user = userEvent.setup();
+    render(
+      <App>
+        <LangProvider>
+          <GrafanaDashboardList />
+        </LangProvider>
+      </App>,
+    );
+
+    await screen.findByText('RocketMQ Cluster Overview');
+    const viewButtons = screen.getAllByRole('button', { name: /View|查看/ });
+    await user.click(viewButtons[0]);
+
+    const dialog = await screen.findByRole('dialog');
+    await waitFor(() => 
expect(getGrafanaDashboard).toHaveBeenCalledWith('rocketmq-overview'));
+    expect(within(dialog).getByText(/"uid": 
"rocketmq-overview"/)).toBeInTheDocument();
+  });
+
+  it('exports a dashboard and triggers a download', async () => {
+    const user = userEvent.setup();
+    const createObjectURL = vi.fn().mockReturnValue('blob:grafana');
+    const revokeObjectURL = vi.fn();
+    const clickSpy = vi.spyOn(HTMLAnchorElement.prototype, 
'click').mockImplementation(() => {});
+    Object.defineProperty(URL, 'createObjectURL', { writable: true, value: 
createObjectURL });
+    Object.defineProperty(URL, 'revokeObjectURL', { writable: true, value: 
revokeObjectURL });
+
+    render(
+      <App>
+        <LangProvider>
+          <GrafanaDashboardList />
+        </LangProvider>
+      </App>,
+    );
+
+    await screen.findByText('RocketMQ Cluster Overview');
+    const exportButtons = screen.getAllByRole('button', { name: /Export|导出/ });
+    await user.click(exportButtons[0]);
+
+    await waitFor(() => 
expect(exportGrafanaDashboard).toHaveBeenCalledWith('rocketmq-overview'));
+    expect(createObjectURL).toHaveBeenCalledTimes(1);
+    expect(clickSpy).toHaveBeenCalled();
+
+    clickSpy.mockRestore();
+  });
+});
diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts
index 45161c42..9afb2e54 100644
--- a/web/src/i18n/translations.ts
+++ b/web/src/i18n/translations.ts
@@ -35,6 +35,7 @@ const translations: Record<string, Record<Lang, string>> = {
   'nav.alertEvents': { zh: '告警事件', en: 'Alert Events' },
   'nav.alertRules': { zh: '告警规则', en: 'Alert Rules' },
   'nav.audit': { zh: '审计日志', en: 'Audit Log' },
+  'nav.grafanaDashboards': { zh: 'Grafana 看板', en: 'Grafana Dashboards' },
   'nav.ai': { zh: 'AI 交互', en: 'AI Chat' },
   'nav.settings': { zh: '设置', en: 'Settings' },
 
@@ -733,6 +734,14 @@ const translations: Record<string, Record<Lang, string>> = 
{
   'alertMgmt.forDurationRequired': { zh: '持续时间为必填项', en: 'Duration is 
required' },
   'alertMgmt.summaryRequired': { zh: '摘要为必填项', en: 'Summary is required' },
 
+  // ─── Grafana dashboards ───
+  'grafana.title': { zh: 'Grafana 看板', en: 'Grafana Dashboards' },
+  'grafana.description': { zh: '说明', en: 'Description' },
+  'grafana.tags': { zh: '标签', en: 'Tags' },
+  'grafana.loadFailed': { zh: '加载看板失败', en: 'Failed to load dashboards' },
+  'grafana.exported': { zh: '看板已导出', en: 'Dashboard exported' },
+  'grafana.exportFailed': { zh: '导出看板失败', en: 'Failed to export dashboard' },
+
   // ─── Topic (detailed) ───
   'topic.subtitle': {
     zh: '管理 Topic 的创建、配置与删除',
diff --git a/web/src/layouts/MainLayout.tsx b/web/src/layouts/MainLayout.tsx
index 045c9d6f..d85024ad 100644
--- a/web/src/layouts/MainLayout.tsx
+++ b/web/src/layouts/MainLayout.tsx
@@ -30,6 +30,7 @@ import {
   ListDashes,
   UserGear,
   ChartBar,
+  ChartLine,
   Sun,
   Moon,
   ShieldCheck,
@@ -125,6 +126,7 @@ const MainLayout = () => {
       label: t('nav.clusterOps'),
       children: [
         { key: '/ops/dashboard', icon: <ChartBar size={16} />, label: 
t('nav.dashboard') },
+        { key: '/ops/grafana', icon: <ChartLine size={16} />, label: 
t('nav.grafanaDashboards') },
         { key: '/cluster/certs', icon: <ShieldCheck size={16} />, label: 
t('nav.certs') },
         { key: '/cluster', icon: <Database size={16} />, label: 
t('nav.rocketmqCluster') },
         { key: '/cluster/clients', icon: <PlugsConnected size={16} />, label: 
t('nav.clients') },
@@ -154,6 +156,7 @@ const MainLayout = () => {
     '/cluster/certs': t('nav.certs'),
     '/cluster/clients': t('nav.clients'),
     '/ops/dashboard': t('nav.dashboard'),
+    '/ops/grafana': t('nav.grafanaDashboards'),
     '/ops/system-alerts': t('nav.alertEvents'),
     '/ops/alerts': t('nav.alertRules'),
     '/ops/audit': t('nav.audit'),
diff --git a/web/src/mock/grafanaDashboards.ts 
b/web/src/mock/grafanaDashboards.ts
new file mode 100644
index 00000000..e8e1e598
--- /dev/null
+++ b/web/src/mock/grafanaDashboards.ts
@@ -0,0 +1,184 @@
+/*
+ * 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 type { GrafanaDashboardInfo } from '../api/metrics';
+
+export interface MockGrafanaDashboard extends GrafanaDashboardInfo {
+  model: Record<string, unknown>;
+}
+
+export const mockGrafanaDashboards: MockGrafanaDashboard[] = [
+  {
+    uid: 'rocketmq-overview',
+    title: 'RocketMQ Cluster Overview',
+    description: 'Cluster-wide throughput, topic/group counts and producer 
footprint.',
+    tags: ['rocketmq'],
+    model: {
+      uid: 'rocketmq-overview',
+      title: 'RocketMQ Cluster Overview',
+      schemaVersion: 39,
+      tags: ['rocketmq'],
+      panels: [
+        { id: 1, title: 'Messages In TPS', type: 'timeseries' },
+        { id: 2, title: 'Messages Out TPS', type: 'timeseries' },
+      ],
+    },
+  },
+  {
+    uid: 'rocketmq-broker',
+    title: 'RocketMQ Broker',
+    description: 'Per-broker throughput, dispatch backlog and thread pool 
pressure.',
+    tags: ['rocketmq'],
+    model: {
+      uid: 'rocketmq-broker',
+      title: 'RocketMQ Broker',
+      schemaVersion: 39,
+      tags: ['rocketmq'],
+      panels: [{ id: 1, title: 'Broker Messages In TPS', type: 'timeseries' }],
+    },
+  },
+  {
+    uid: 'rocketmq-producer',
+    title: 'RocketMQ Producer',
+    description: 'Producer presence, send size and per-topic ingress.',
+    tags: ['rocketmq'],
+    model: {
+      uid: 'rocketmq-producer',
+      title: 'RocketMQ Producer',
+      schemaVersion: 39,
+      tags: ['rocketmq'],
+      panels: [{ id: 1, title: 'Producer Count', type: 'stat' }],
+    },
+  },
+  {
+    uid: 'rocketmq-consumer',
+    title: 'RocketMQ Consumer',
+    description: 'Consumer group egress, lag and client footprint.',
+    tags: ['rocketmq'],
+    model: {
+      uid: 'rocketmq-consumer',
+      title: 'RocketMQ Consumer',
+      schemaVersion: 39,
+      tags: ['rocketmq'],
+      panels: [{ id: 1, title: 'Messages Out TPS by Group', type: 'timeseries' 
}],
+    },
+  },
+  {
+    uid: 'rocketmq-topic',
+    title: 'RocketMQ Topic',
+    description: 'Per-topic ingress/egress throughput and dispatch backlog.',
+    tags: ['rocketmq'],
+    model: {
+      uid: 'rocketmq-topic',
+      title: 'RocketMQ Topic',
+      schemaVersion: 39,
+      tags: ['rocketmq'],
+      panels: [{ id: 1, title: 'Topic Messages In TPS', type: 'timeseries' }],
+    },
+  },
+  {
+    uid: 'rocketmq-tps',
+    title: 'RocketMQ TPS',
+    description: 'Cluster and per-broker message throughput trends.',
+    tags: ['rocketmq'],
+    model: {
+      uid: 'rocketmq-tps',
+      title: 'RocketMQ TPS',
+      schemaVersion: 39,
+      tags: ['rocketmq'],
+      panels: [{ id: 1, title: 'Cluster TPS In', type: 'timeseries' }],
+    },
+  },
+  {
+    uid: 'rocketmq-storage',
+    title: 'RocketMQ Storage',
+    description: 'Broker disk usage and JVM heap footprint.',
+    tags: ['rocketmq'],
+    model: {
+      uid: 'rocketmq-storage',
+      title: 'RocketMQ Storage',
+      schemaVersion: 39,
+      tags: ['rocketmq'],
+      panels: [{ id: 1, title: 'Disk Use Ratio', type: 'gauge' }],
+    },
+  },
+  {
+    uid: 'rocketmq-jvm',
+    title: 'RocketMQ Broker JVM',
+    description: 'JVM memory, threads and garbage collection for brokers.',
+    tags: ['rocketmq'],
+    model: {
+      uid: 'rocketmq-jvm',
+      title: 'RocketMQ Broker JVM',
+      schemaVersion: 39,
+      tags: ['rocketmq'],
+      panels: [{ id: 1, title: 'JVM Heap', type: 'timeseries' }],
+    },
+  },
+  {
+    uid: 'rocketmq-threadpool',
+    title: 'RocketMQ Thread Pool',
+    description: 'Broker thread pool queue depth, capacity and rejections.',
+    tags: ['rocketmq'],
+    model: {
+      uid: 'rocketmq-threadpool',
+      title: 'RocketMQ Thread Pool',
+      schemaVersion: 39,
+      tags: ['rocketmq'],
+      panels: [{ id: 1, title: 'Queue Size', type: 'timeseries' }],
+    },
+  },
+  {
+    uid: 'rocketmq-dlq',
+    title: 'RocketMQ DLQ & Retry',
+    description: 'Dead-letter queue resend volume and latency.',
+    tags: ['rocketmq'],
+    model: {
+      uid: 'rocketmq-dlq',
+      title: 'RocketMQ DLQ & Retry',
+      schemaVersion: 39,
+      tags: ['rocketmq'],
+      panels: [{ id: 1, title: 'DLQ Resend Count', type: 'timeseries' }],
+    },
+  },
+  {
+    uid: 'rocketmq-latency',
+    title: 'RocketMQ Latency',
+    description: 'Dispatch and client push latency percentiles.',
+    tags: ['rocketmq'],
+    model: {
+      uid: 'rocketmq-latency',
+      title: 'RocketMQ Latency',
+      schemaVersion: 39,
+      tags: ['rocketmq'],
+      panels: [{ id: 1, title: 'Dispatch Latency p99', type: 'timeseries' }],
+    },
+  },
+  {
+    uid: 'rocketmq-network',
+    title: 'RocketMQ Network & Connections',
+    description: 'Client connections and produced/consumed connection counts.',
+    tags: ['rocketmq'],
+    model: {
+      uid: 'rocketmq-network',
+      title: 'RocketMQ Network & Connections',
+      schemaVersion: 39,
+      tags: ['rocketmq'],
+      panels: [{ id: 1, title: 'Client Connections', type: 'stat' }],
+    },
+  },
+];
diff --git a/web/src/pages/studio/GrafanaDashboards.tsx 
b/web/src/pages/studio/GrafanaDashboards.tsx
new file mode 100644
index 00000000..a92d5bd8
--- /dev/null
+++ b/web/src/pages/studio/GrafanaDashboards.tsx
@@ -0,0 +1,44 @@
+/*
+ * 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 from 'react';
+import { Card, Space } from 'antd';
+import { ChartLine } from '@phosphor-icons/react';
+import { useLang } from '../../i18n/LangContext';
+import GrafanaDashboardList from '../../components/GrafanaDashboardList';
+
+const GrafanaDashboardsPage: React.FC = () => {
+  const { t } = useLang();
+
+  return (
+    <div style={{ padding: '0 0 24px' }}>
+      <Card
+        size="small"
+        title={
+          <Space>
+            <ChartLine size={18} />
+            <span>{t('grafana.title')}</span>
+          </Space>
+        }
+      >
+        <GrafanaDashboardList />
+      </Card>
+    </div>
+  );
+};
+
+export default GrafanaDashboardsPage;
diff --git a/web/src/services/grafanaService.test.ts 
b/web/src/services/grafanaService.test.ts
new file mode 100644
index 00000000..b91bb4c2
--- /dev/null
+++ b/web/src/services/grafanaService.test.ts
@@ -0,0 +1,59 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.
+
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import * as grafanaApi from '../api/metrics';
+import {
+  exportGrafanaDashboard,
+  getGrafanaDashboard,
+  listGrafanaDashboards,
+} from './grafanaService';
+
+const isMockModeMock = vi.hoisted(() => ({ isMockMode: () => true as boolean 
}));
+vi.mock('./dataMode', () => isMockModeMock);
+
+describe('grafanaService', () => {
+  beforeEach(() => {
+    isMockModeMock.isMockMode = () => true;
+    vi.stubGlobal('localStorage', { getItem: vi.fn().mockReturnValue(null) });
+  });
+
+  afterEach(() => {
+    vi.restoreAllMocks();
+    vi.unstubAllGlobals();
+  });
+
+  it('lists at least 10 dashboards in mock mode', async () => {
+    const dashboards = await listGrafanaDashboards();
+    expect(dashboards.length).toBeGreaterThanOrEqual(10);
+    expect(dashboards.every((d) => d.uid && d.title)).toBe(true);
+  });
+
+  it('returns a dashboard model by uid in mock mode', async () => {
+    const model = await getGrafanaDashboard('rocketmq-overview');
+    expect(model.uid).toBe('rocketmq-overview');
+  });
+
+  it('throws for an unknown dashboard uid in mock mode', async () => {
+    await expect(getGrafanaDashboard('nope')).rejects.toThrow();
+  });
+
+  it('exports a dashboard as a blob in mock mode', async () => {
+    const blob = await exportGrafanaDashboard('rocketmq-overview');
+    expect(blob).toBeInstanceOf(Blob);
+    await expect(blob.text()).resolves.toContain('rocketmq-overview');
+  });
+
+  it('delegates to the api in real mode', async () => {
+    isMockModeMock.isMockMode = () => false;
+    const listSpy = vi
+      .spyOn(grafanaApi, 'listGrafanaDashboards')
+      .mockResolvedValue([
+        { uid: 'rocketmq-overview', title: 'Overview', description: '', tags: 
['rocketmq'] },
+      ]);
+
+    const result = await listGrafanaDashboards();
+    expect(listSpy).toHaveBeenCalledTimes(1);
+    expect(result[0].uid).toBe('rocketmq-overview');
+  });
+});
diff --git a/web/src/services/grafanaService.ts 
b/web/src/services/grafanaService.ts
new file mode 100644
index 00000000..1b8bb84b
--- /dev/null
+++ b/web/src/services/grafanaService.ts
@@ -0,0 +1,39 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.
+
+import { isMockMode } from './dataMode';
+import * as metricsApi from '../api/metrics';
+import type { GrafanaDashboardInfo } from '../api/metrics';
+import { mockGrafanaDashboards } from '../mock/grafanaDashboards';
+
+export async function listGrafanaDashboards(): Promise<GrafanaDashboardInfo[]> 
{
+  if (isMockMode()) {
+    return mockGrafanaDashboards.map(({ uid, title, description, tags }) => ({
+      uid,
+      title,
+      description,
+      tags,
+    }));
+  }
+  return metricsApi.listGrafanaDashboards();
+}
+
+export async function getGrafanaDashboard(uid: string): Promise<Record<string, 
unknown>> {
+  if (isMockMode()) {
+    const found = mockGrafanaDashboards.find((dashboard) => dashboard.uid === 
uid);
+    if (!found) {
+      throw new Error(`Grafana dashboard not found: ${uid}`);
+    }
+    return found.model;
+  }
+  return metricsApi.getGrafanaDashboard(uid);
+}
+
+export async function exportGrafanaDashboard(uid: string): Promise<Blob> {
+  if (isMockMode()) {
+    const found = mockGrafanaDashboards.find((dashboard) => dashboard.uid === 
uid);
+    const model = found ? found.model : { uid };
+    return new Blob([JSON.stringify(model, null, 2)], { type: 
'application/json' });
+  }
+  return metricsApi.exportGrafanaDashboard(uid);
+}

Reply via email to