1
0

feat: 重构 Dashboard 为卡片式分组布局

表格布局替换为按分组展示的卡片式布局,新增 group 字段配置和 TargetBoard/TargetCard 等组件。模态框详情页支持时间范围筛选和分页,SummaryCards 减为 3 个。API 端点变更:trend/history 改用 from/to 参数,history 支持分页。recentSampleCount 硬编码为 30。
This commit is contained in:
2026-05-11 08:54:21 +08:00
parent b8810f1182
commit 548b44d28e
44 changed files with 1676 additions and 557 deletions

View File

@@ -1,7 +1,7 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { createFetchHandler, type StaticAssets } from "../../src/server/app";
import { ProbeStore } from "../../src/server/checker/store";
import type { SummaryResponse, TargetStatus, HealthResponse } from "../../src/shared/api";
import type { HistoryResponse, SummaryResponse, TargetStatus, HealthResponse } from "../../src/shared/api";
import { mkdir, rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
@@ -28,6 +28,7 @@ describe("API 路由", () => {
{
type: "http",
name: "test-a",
group: "default",
http: {
url: "http://a.com",
method: "GET",
@@ -40,6 +41,7 @@ describe("API 路由", () => {
{
type: "command",
name: "test-b",
group: "default",
command: {
exec: "echo",
args: ["hello"],
@@ -104,7 +106,7 @@ describe("API 路由", () => {
expect(body.up).toBeGreaterThanOrEqual(0);
expect(body.down).toBeGreaterThanOrEqual(0);
expect(body.up + body.down).toBe(2);
expect(body.avgDurationMs).toBeDefined();
expect(body.lastCheckTime).not.toBeNull();
});
test("/api/targets 返回目标列表", async () => {
@@ -117,14 +119,15 @@ describe("API 路由", () => {
const tA = body.find((t) => t.name === "test-a")!;
expect(tA.type).toBe("http");
expect(tA.target).toBe("http://a.com");
expect(tA.group).toBe("default");
expect(tA.latestCheck).not.toBeNull();
expect(tA.latestCheck!.success).toBe(false);
expect(tA.latestCheck!.matched).toBe(false);
expect(tA.latestCheck!.failure).not.toBeNull();
expect(tA.sparkline).toBeDefined();
expect(Array.isArray(tA.sparkline)).toBe(true);
expect(tA.stats.avgDurationMs).toBeDefined();
expect(tA.stats.p99DurationMs).toBeDefined();
expect(tA.recentSamples).toBeDefined();
expect(Array.isArray(tA.recentSamples)).toBe(true);
expect(tA.stats.totalChecks).toBeDefined();
expect(tA.stats.availability).toBeDefined();
const tB = body.find((t) => t.name === "test-b")!;
expect(tB.type).toBe("command");
@@ -134,27 +137,43 @@ describe("API 路由", () => {
test("/api/targets/:id/history 返回历史记录", async () => {
const targets = store.getTargets();
const response = await fetchHandler(new Request(`http://localhost/api/targets/${targets[0]!.id}/history`));
const body = await response.json();
const from = "2024-01-01T00:00:00.000Z";
const to = "2026-12-31T23:59:59.999Z";
const response = await fetchHandler(
new Request(`http://localhost/api/targets/${targets[0]!.id}/history?from=${from}&to=${to}`),
);
const body = (await response.json()) as HistoryResponse;
expect(response.status).toBe(200);
expect(body).toHaveLength(2);
expect(body[0].failure).not.toBeNull();
expect(body[0].failure.kind).toBe("error");
expect(body.items).toHaveLength(2);
expect(body.total).toBe(2);
expect(body.page).toBe(1);
expect(body.pageSize).toBe(20);
expect(body.items[0]!.failure).not.toBeNull();
expect(body.items[0]!.failure!.kind).toBe("error");
});
test("/api/targets/:id/history 支持 limit 参数", async () => {
test("/api/targets/:id/history 支持 page 参数", async () => {
const targets = store.getTargets();
const response = await fetchHandler(new Request(`http://localhost/api/targets/${targets[0]!.id}/history?limit=1`));
const body = await response.json();
const from = "2024-01-01T00:00:00.000Z";
const to = "2026-12-31T23:59:59.999Z";
const response = await fetchHandler(
new Request(`http://localhost/api/targets/${targets[0]!.id}/history?from=${from}&to=${to}&pageSize=1`),
);
const body = (await response.json()) as HistoryResponse;
expect(response.status).toBe(200);
expect(body).toHaveLength(1);
expect(body.items).toHaveLength(1);
expect(body.total).toBe(2);
});
test("/api/targets/:id/trend 返回趋势数据", async () => {
const targets = store.getTargets();
const response = await fetchHandler(new Request(`http://localhost/api/targets/${targets[0]!.id}/trend`));
const from = "2024-01-01T00:00:00.000Z";
const to = "2026-12-31T23:59:59.999Z";
const response = await fetchHandler(
new Request(`http://localhost/api/targets/${targets[0]!.id}/trend?from=${from}&to=${to}`),
);
const body = await response.json();
expect(response.status).toBe(200);
@@ -162,22 +181,33 @@ describe("API 路由", () => {
});
test("查询不存在的目标返回 404", async () => {
const response = await fetchHandler(new Request("http://localhost/api/targets/99999/history"));
const response = await fetchHandler(
new Request(
"http://localhost/api/targets/99999/history?from=2024-01-01T00:00:00.000Z&to=2026-12-31T23:59:59.999Z",
),
);
const body = await response.json();
expect(response.status).toBe(404);
expect(body.error).toBe("Target not found");
});
test("无效 limit 参数返回 400", async () => {
test("history 缺少 from/to 参数返回 400", async () => {
const targets = store.getTargets();
const response = await fetchHandler(
new Request(`http://localhost/api/targets/${targets[0]!.id}/history?limit=abc`),
);
const response = await fetchHandler(new Request(`http://localhost/api/targets/${targets[0]!.id}/history`));
const body = await response.json();
expect(response.status).toBe(400);
expect(body.error).toBe("Invalid limit parameter");
expect(body.error).toContain("from and to");
});
test("trend 缺少 from/to 参数返回 400", async () => {
const targets = store.getTargets();
const response = await fetchHandler(new Request(`http://localhost/api/targets/${targets[0]!.id}/trend`));
const body = await response.json();
expect(response.status).toBe(400);
expect(body.error).toContain("from and to");
});
test("无效目标 ID 返回 400", async () => {