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 9e0e2d1b feat: export all Grafana dashboards as an archive (#1320)
9e0e2d1b is described below
commit 9e0e2d1b7fd273676bdad544adea55fa9da51cd8
Author: yx9o <[email protected]>
AuthorDate: Mon Aug 10 20:30:43 2026 +0800
feat: export all Grafana dashboards as an archive (#1320)
---
docs/api-spec.md | 106 +++++++++++++++++++++
.../grafana/GrafanaDashboardController.java | 13 +++
.../metrics/grafana/GrafanaDashboardService.java | 27 ++++++
.../grafana/GrafanaDashboardControllerTest.java | 13 +++
.../grafana/GrafanaDashboardServiceTest.java | 30 ++++++
web/src/api/metrics.test.ts | 11 +++
web/src/api/metrics.ts | 7 ++
web/src/components/GrafanaDashboardList.tsx | 32 ++++++-
.../__tests__/GrafanaDashboardList.test.tsx | 39 +++++++-
web/src/i18n/translations.ts | 3 +
web/src/services/grafanaService.test.ts | 19 ++++
web/src/services/grafanaService.ts | 11 +++
12 files changed, 307 insertions(+), 4 deletions(-)
diff --git a/docs/api-spec.md b/docs/api-spec.md
index b75f4f0d..46062bec 100644
--- a/docs/api-spec.md
+++ b/docs/api-spec.md
@@ -114,6 +114,10 @@
| 71 | GET | `/api/ai/tools` | 可用工具列表 |
| 72 | POST | `/api/ai/tools/:name/execute` | 执行只读 AI 工具 |
| 73 | POST | `/api/metrics/query` | 查询监控指标数据 |
+| 74 | GET | `/api/metrics/grafana/dashboards` | Grafana 看板列表 |
+| 75 | GET | `/api/metrics/grafana/dashboards/:uid` | Grafana 看板 JSON 模型 |
+| 76 | GET | `/api/metrics/grafana/dashboards/:uid/export` | 导出单个 Grafana 看板
JSON |
+| 77 | GET | `/api/metrics/grafana/dashboards/export` | 打包导出全部 Grafana 看板 |
## 通用响应格式
@@ -1907,6 +1911,108 @@ studio:
| `503` | Prometheus 未配置或暂时不可用 |
| `504` | Prometheus 查询超时 |
+### 16.2 Grafana 看板列表
+
+```
+GET /api/metrics/grafana/dashboards
+```
+
+返回内置的 RocketMQ Grafana 看板资产元数据。服务端只返回可以解析为 JSON
+对象且包含有效 `uid` 的看板;无效资产会被跳过。
+
+**Response `data`:** `GrafanaDashboardInfo[]`
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `uid` | `string` | Grafana 看板 UID,同时用于详情和导出接口 |
+| `title` | `string` | 看板标题 |
+| `description` | `string` | 看板说明 |
+| `tags` | `string[]` | 看板标签 |
+
+**Response 示例:**
+
+```json
+[
+ {
+ "uid": "rocketmq-overview",
+ "title": "RocketMQ Cluster Overview",
+ "description": "Overview dashboard for RocketMQ cluster metrics",
+ "tags": ["rocketmq"]
+ }
+]
+```
+
+### 16.3 获取 Grafana 看板 JSON 模型
+
+```
+GET /api/metrics/grafana/dashboards/:uid
+```
+
+**Path Parameters:**
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `uid` | `string` | 是 | Grafana 看板 UID |
+
+**Response `data`:** `Record<string, any>`
+
+响应体是完整 Grafana dashboard JSON 模型,前端可用于预览或复制配置。
+
+**错误响应:**
+
+| HTTP 状态 | 场景 |
+|-----------|------|
+| `404` | 指定 UID 的内置看板不存在 |
+| `500` | 看板 JSON 读取失败 |
+
+### 16.4 导出单个 Grafana 看板
+
+```
+GET /api/metrics/grafana/dashboards/:uid/export
+```
+
+**Path Parameters:**
+
+| 参数 | 类型 | 必填 | 说明 |
+|------|------|------|------|
+| `uid` | `string` | 是 | Grafana 看板 UID |
+
+**Response:**
+
+- `Content-Type: application/json`
+- `Content-Disposition: attachment; filename="{uid}.json"`
+- Body 为原始 Grafana dashboard JSON 文件内容。
+
+**错误响应:**
+
+| HTTP 状态 | 场景 |
+|-----------|------|
+| `404` | 指定 UID 的内置看板不存在 |
+| `500` | 看板 JSON 读取失败 |
+
+### 16.5 打包导出全部 Grafana 看板
+
+```
+GET /api/metrics/grafana/dashboards/export
+```
+
+返回全部有效内置 Grafana 看板的 zip 压缩包。压缩包内每个条目按
+`{uid}.json` 命名,条目集合与 `GET /api/metrics/grafana/dashboards`
+返回的可见看板列表保持一致。
+
+**Response:**
+
+- `Content-Type: application/zip`
+- `Content-Disposition: attachment; filename="rocketmq-grafana-dashboards.zip"`
+- Body 为 zip 文件二进制内容。
+
+**错误响应:**
+
+| HTTP 状态 | 场景 |
+|-----------|------|
+| `404` | 没有可导出的有效内置看板 |
+| `500` | 看板 JSON 读取或 zip 打包失败 |
+
---
## 附录 A:枚举值速查
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
index c288e86b..9f7f4d13 100644
---
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
@@ -58,6 +58,19 @@ public class GrafanaDashboardController {
return Result.ok(grafanaDashboardService.getDashboard(uid));
}
+ @Operation(summary = "Export all Grafana dashboards",
+ description = "Returns all bundled RocketMQ Grafana dashboards as
a downloadable zip archive")
+ @ApiResponse(responseCode = "200", description = "Dashboard archive
returned")
+ @GetMapping("/dashboards/export")
+ public ResponseEntity<byte[]> exportDashboards() {
+ byte[] archive = grafanaDashboardService.getDashboardsArchive();
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.valueOf("application/zip"));
+ headers.setContentDisposition(
+
ContentDisposition.attachment().filename("rocketmq-grafana-dashboards.zip").build());
+ return new ResponseEntity<>(archive, headers, HttpStatus.OK);
+ }
+
@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")
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
index 180292f9..b37a6867 100644
---
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
@@ -26,12 +26,15 @@ import
org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.stereotype.Service;
+import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
/**
* Loads the Grafana dashboard JSON assets bundled under {@code
classpath*:grafana/*.json}
@@ -111,6 +114,30 @@ public class GrafanaDashboardService {
}
}
+ /**
+ * Returns all valid bundled dashboards as a zip archive. Invalid
dashboard assets are skipped
+ * the same way as {@link #listDashboards()} so the archive matches the
visible dashboard list.
+ */
+ public byte[] getDashboardsArchive() {
+ List<GrafanaDashboardInfo> dashboards = listDashboards();
+ if (dashboards.isEmpty()) {
+ throw new BusinessException(404, "No Grafana dashboards are
available");
+ }
+ try (ByteArrayOutputStream output = new ByteArrayOutputStream();
+ ZipOutputStream zip = new ZipOutputStream(output,
StandardCharsets.UTF_8)) {
+ for (GrafanaDashboardInfo dashboard : dashboards) {
+ ZipEntry entry = new ZipEntry(dashboard.uid() + ".json");
+ zip.putNextEntry(entry);
+
zip.write(getDashboardJson(dashboard.uid()).getBytes(StandardCharsets.UTF_8));
+ zip.closeEntry();
+ }
+ zip.finish();
+ return output.toByteArray();
+ } catch (IOException e) {
+ throw new BusinessException(500, "Failed to export Grafana
dashboards");
+ }
+ }
+
private Resource findResource(String uid) {
for (Resource resource : resolveResources()) {
if (uid.equals(uidOf(resource))) {
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
index e97c4781..905cf2ca 100644
---
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
@@ -84,4 +84,17 @@ class GrafanaDashboardControllerTest {
"attachment; filename=\"rocketmq-overview.json\""))
.andExpect(content().json("{\"uid\":\"rocketmq-overview\"}"));
}
+
+ @Test
+ void exportDashboardsShouldReturnZipAttachment() throws Exception {
+ byte[] archive = "zip-content".getBytes();
+
when(grafanaDashboardService.getDashboardsArchive()).thenReturn(archive);
+
+ mockMvc.perform(get("/api/metrics/grafana/dashboards/export"))
+ .andExpect(status().isOk())
+ .andExpect(header().string("Content-Type", "application/zip"))
+ .andExpect(header().string("Content-Disposition",
+ "attachment;
filename=\"rocketmq-grafana-dashboards.zip\""))
+ .andExpect(content().bytes(archive));
+ }
}
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
index 5513de9e..3bc0667a 100644
---
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
@@ -22,13 +22,16 @@ import org.junit.jupiter.api.Test;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
+import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
+import java.util.zip.ZipInputStream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -93,6 +96,33 @@ class GrafanaDashboardServiceTest {
assertEquals(404, exception.getCode());
}
+ @Test
+ void getDashboardsArchiveShouldIncludeAllVisibleDashboards() throws
Exception {
+ GrafanaDashboardService service = serviceWithResources(
+ resource("b.json",
"{\"uid\":\"b\",\"title\":\"B\",\"tags\":[\"rocketmq\"]}"),
+ resource("invalid.json", "[]"),
+ resource("a.json",
"{\"uid\":\"a\",\"title\":\"A\",\"tags\":[\"rocketmq\"]}"));
+
+ byte[] archive = service.getDashboardsArchive();
+
+ try (ZipInputStream zip = new ZipInputStream(new
ByteArrayInputStream(archive), StandardCharsets.UTF_8)) {
+ assertEquals("a.json", zip.getNextEntry().getName());
+ assertTrue(new String(zip.readAllBytes(),
StandardCharsets.UTF_8).contains("\"uid\":\"a\""));
+ assertEquals("b.json", zip.getNextEntry().getName());
+ assertTrue(new String(zip.readAllBytes(),
StandardCharsets.UTF_8).contains("\"uid\":\"b\""));
+ assertNull(zip.getNextEntry());
+ }
+ }
+
+ @Test
+ void getDashboardsArchiveShouldRejectEmptyDashboardSet() {
+ GrafanaDashboardService service =
serviceWithResources(resource("invalid.json", "[]"));
+
+ BusinessException exception = assertThrows(BusinessException.class,
service::getDashboardsArchive);
+
+ assertEquals(404, exception.getCode());
+ }
+
@Test
void listDashboardsShouldSkipEmptyAndNonObjectAssets() {
GrafanaDashboardService service = serviceWithResources(
diff --git a/web/src/api/metrics.test.ts b/web/src/api/metrics.test.ts
index 919bcf5f..adb098bc 100644
--- a/web/src/api/metrics.test.ts
+++ b/web/src/api/metrics.test.ts
@@ -23,6 +23,7 @@ import {
listGrafanaDashboards,
getGrafanaDashboard,
exportGrafanaDashboard,
+ exportGrafanaDashboards,
listMetricProfiles,
queryByDataSource,
queryMetrics,
@@ -191,4 +192,14 @@ describe('metrics API', () => {
expect(result).toBeInstanceOf(Blob);
await expect(result.text()).resolves.toContain('rocketmq-overview');
});
+
+ it('exports all Grafana dashboards as a blob', async () => {
+ const blob = new Blob(['zip-content'], { type: 'application/zip' });
+
+ mock.onGet('/metrics/grafana/dashboards/export').reply(200, blob);
+
+ const result = await exportGrafanaDashboards();
+ expect(result).toBeInstanceOf(Blob);
+ await expect(result.text()).resolves.toContain('zip-content');
+ });
});
diff --git a/web/src/api/metrics.ts b/web/src/api/metrics.ts
index 8179154b..0e48c65c 100644
--- a/web/src/api/metrics.ts
+++ b/web/src/api/metrics.ts
@@ -157,3 +157,10 @@ export async function exportGrafanaDashboard(uid: string):
Promise<Blob> {
);
return res.data;
}
+
+export async function exportGrafanaDashboards(): Promise<Blob> {
+ const res = await client.get<Blob>('/metrics/grafana/dashboards/export', {
+ responseType: 'blob',
+ });
+ return res.data;
+}
diff --git a/web/src/components/GrafanaDashboardList.tsx
b/web/src/components/GrafanaDashboardList.tsx
index 251ae6d3..2a08d542 100644
--- a/web/src/components/GrafanaDashboardList.tsx
+++ b/web/src/components/GrafanaDashboardList.tsx
@@ -23,6 +23,7 @@ import { useLang } from '../i18n/LangContext';
import {
getGrafanaDashboard,
exportGrafanaDashboard,
+ exportGrafanaDashboards,
listGrafanaDashboards,
} from '../services/grafanaService';
import type { GrafanaDashboardInfo } from '../api/metrics';
@@ -39,6 +40,7 @@ export const GrafanaDashboardList: React.FC = () => {
const [viewLoading, setViewLoading] = useState(false);
const viewRequestId = useRef(0);
const [exportingUids, setExportingUids] = useState<Set<string>>(() => new
Set());
+ const [exportingAll, setExportingAll] = useState(false);
useEffect(() => {
let cancelled = false;
@@ -87,13 +89,13 @@ export const GrafanaDashboardList: React.FC = () => {
setViewLoading(false);
};
- const triggerDownload = (uid: string, content: Blob | string) => {
+ const triggerDownload = (filename: 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.download = filename;
a.click();
URL.revokeObjectURL(url);
};
@@ -102,7 +104,7 @@ export const GrafanaDashboardList: React.FC = () => {
setExportingUids((current) => new Set(current).add(info.uid));
try {
const blob = await exportGrafanaDashboard(info.uid);
- triggerDownload(info.uid, blob);
+ triggerDownload(`${info.uid}.json`, blob);
message.success(t('grafana.exported'));
} catch {
message.error(t('grafana.exportFailed'));
@@ -115,6 +117,19 @@ export const GrafanaDashboardList: React.FC = () => {
}
};
+ const handleExportAll = async () => {
+ setExportingAll(true);
+ try {
+ const blob = await exportGrafanaDashboards();
+ triggerDownload('rocketmq-grafana-dashboards.zip', blob);
+ message.success(t('grafana.exportAllDone'));
+ } catch {
+ message.error(t('grafana.exportAllFailed'));
+ } finally {
+ setExportingAll(false);
+ }
+ };
+
const columns: ColumnsType<GrafanaDashboardInfo> = [
{
title: t('grafana.title'),
@@ -166,6 +181,17 @@ export const GrafanaDashboardList: React.FC = () => {
return (
<div>
+ <Space style={{ marginBottom: 12 }}>
+ <Button
+ icon={<DownloadSimple size={16} />}
+ loading={exportingAll}
+ disabled={loading || dashboards.length === 0}
+ onClick={handleExportAll}
+ >
+ {t('grafana.exportAll')}
+ </Button>
+ </Space>
+
<Table
columns={columns}
dataSource={dashboards}
diff --git a/web/src/components/__tests__/GrafanaDashboardList.test.tsx
b/web/src/components/__tests__/GrafanaDashboardList.test.tsx
index 618ecb4c..fd049ad7 100644
--- a/web/src/components/__tests__/GrafanaDashboardList.test.tsx
+++ b/web/src/components/__tests__/GrafanaDashboardList.test.tsx
@@ -23,6 +23,7 @@ import { afterEach, beforeAll, beforeEach, describe, expect,
it, vi } from 'vite
import { LangProvider } from '../../i18n/LangContext';
import {
exportGrafanaDashboard,
+ exportGrafanaDashboards,
getGrafanaDashboard,
listGrafanaDashboards,
} from '../../services/grafanaService';
@@ -32,6 +33,7 @@ vi.mock('../../services/grafanaService', () => ({
listGrafanaDashboards: vi.fn(),
getGrafanaDashboard: vi.fn(),
exportGrafanaDashboard: vi.fn(),
+ exportGrafanaDashboards: vi.fn(),
}));
const dashboards = [
@@ -73,6 +75,9 @@ beforeEach(() => {
vi.mocked(exportGrafanaDashboard).mockResolvedValue(
new Blob([JSON.stringify(dashboardModel, null, 2)], { type:
'application/json' }),
);
+ vi.mocked(exportGrafanaDashboards).mockResolvedValue(
+ new Blob(['zip-content'], { type: 'application/zip' }),
+ );
});
afterEach(() => {
@@ -189,7 +194,7 @@ describe('GrafanaDashboardList', () => {
await screen.findByText('RocketMQ Cluster Overview');
const exportButtons = screen.getAllByRole('button', { name: /Export|导出/ });
- await user.click(exportButtons[0]);
+ await user.click(exportButtons[1]);
await waitFor(() =>
expect(exportGrafanaDashboard).toHaveBeenCalledWith('rocketmq-overview'));
expect(createObjectURL).toHaveBeenCalledTimes(1);
@@ -197,4 +202,36 @@ describe('GrafanaDashboardList', () => {
clickSpy.mockRestore();
});
+
+ it('exports all dashboards and downloads the archive', async () => {
+ const user = userEvent.setup();
+ const createObjectURL = vi.fn().mockReturnValue('blob:grafana-all');
+ const revokeObjectURL = vi.fn();
+ let downloadedFilename = '';
+ const clickSpy = vi.spyOn(HTMLAnchorElement.prototype,
'click').mockImplementation(function (
+ this: HTMLAnchorElement,
+ ) {
+ downloadedFilename = this.download;
+ });
+ 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');
+ await user.click(screen.getByRole('button', { name: /Export all|导出全部/ }));
+
+ await waitFor(() =>
expect(exportGrafanaDashboards).toHaveBeenCalledTimes(1));
+ expect(downloadedFilename).toBe('rocketmq-grafana-dashboards.zip');
+ expect(createObjectURL).toHaveBeenCalledTimes(1);
+ expect(clickSpy).toHaveBeenCalled();
+
+ clickSpy.mockRestore();
+ });
});
diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts
index ba45a2f7..2199fe09 100644
--- a/web/src/i18n/translations.ts
+++ b/web/src/i18n/translations.ts
@@ -815,6 +815,9 @@ const translations: Record<string, Record<Lang, string>> = {
'grafana.loadFailed': { zh: '加载看板失败', en: 'Failed to load dashboards' },
'grafana.exported': { zh: '看板已导出', en: 'Dashboard exported' },
'grafana.exportFailed': { zh: '导出看板失败', en: 'Failed to export dashboard' },
+ 'grafana.exportAll': { zh: '导出全部', en: 'Export all' },
+ 'grafana.exportAllDone': { zh: '全部看板已导出', en: 'All dashboards exported' },
+ 'grafana.exportAllFailed': { zh: '导出全部看板失败', en: 'Failed to export
dashboards' },
// ─── Alert rule templates ───
'alertAssets.title': { zh: '告警规则模板', en: 'Alert Rule Templates' },
'alertAssets.name': { zh: '名称', en: 'Name' },
diff --git a/web/src/services/grafanaService.test.ts
b/web/src/services/grafanaService.test.ts
index b91bb4c2..40e835c2 100644
--- a/web/src/services/grafanaService.test.ts
+++ b/web/src/services/grafanaService.test.ts
@@ -5,6 +5,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from
'vitest';
import * as grafanaApi from '../api/metrics';
import {
exportGrafanaDashboard,
+ exportGrafanaDashboards,
getGrafanaDashboard,
listGrafanaDashboards,
} from './grafanaService';
@@ -44,6 +45,13 @@ describe('grafanaService', () => {
await expect(blob.text()).resolves.toContain('rocketmq-overview');
});
+ it('exports all dashboards as a blob in mock mode', async () => {
+ const blob = await exportGrafanaDashboards();
+ expect(blob).toBeInstanceOf(Blob);
+ expect(blob.type).toBe('application/zip');
+ await expect(blob.text()).resolves.toContain('rocketmq-overview.json');
+ });
+
it('delegates to the api in real mode', async () => {
isMockModeMock.isMockMode = () => false;
const listSpy = vi
@@ -56,4 +64,15 @@ describe('grafanaService', () => {
expect(listSpy).toHaveBeenCalledTimes(1);
expect(result[0].uid).toBe('rocketmq-overview');
});
+
+ it('delegates bulk export to the api in real mode', async () => {
+ isMockModeMock.isMockMode = () => false;
+ const exportSpy = vi
+ .spyOn(grafanaApi, 'exportGrafanaDashboards')
+ .mockResolvedValue(new Blob(['zip-content'], { type: 'application/zip'
}));
+
+ const result = await exportGrafanaDashboards();
+ expect(exportSpy).toHaveBeenCalledTimes(1);
+ expect(result.type).toBe('application/zip');
+ });
});
diff --git a/web/src/services/grafanaService.ts
b/web/src/services/grafanaService.ts
index 1b8bb84b..1568df8a 100644
--- a/web/src/services/grafanaService.ts
+++ b/web/src/services/grafanaService.ts
@@ -37,3 +37,14 @@ export async function exportGrafanaDashboard(uid: string):
Promise<Blob> {
}
return metricsApi.exportGrafanaDashboard(uid);
}
+
+export async function exportGrafanaDashboards(): Promise<Blob> {
+ if (isMockMode()) {
+ const bundle = mockGrafanaDashboards.map(({ uid, model }) => ({
+ filename: `${uid}.json`,
+ model,
+ }));
+ return new Blob([JSON.stringify(bundle, null, 2)], { type:
'application/zip' });
+ }
+ return metricsApi.exportGrafanaDashboards();
+}