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

@@ -9,6 +9,7 @@ function makeTarget(
return {
type: "command",
name: "test-cmd",
group: "default",
command: {
exec: "echo",
args: ["hello"],

View File

@@ -479,4 +479,53 @@ targets:
expect(t.command.env.PATH).toBeDefined();
}
});
test("解析 group 字段", async () => {
const configPath = join(tempDir, "group.yaml");
await writeFile(
configPath,
`targets:
- name: "grouped"
type: http
group: "搜索引擎"
http:
url: "http://example.com"
`,
);
const config = await loadConfig(configPath);
expect(config.targets[0]!.group).toBe("搜索引擎");
});
test("group 字段默认为 default", async () => {
const configPath = join(tempDir, "no-group.yaml");
await writeFile(
configPath,
`targets:
- name: "no-group"
type: http
http:
url: "http://example.com"
`,
);
const config = await loadConfig(configPath);
expect(config.targets[0]!.group).toBe("default");
});
test("非法 group 类型抛出错误", async () => {
const configPath = join(tempDir, "bad-group.yaml");
await writeFile(
configPath,
`targets:
- name: "test"
type: http
group: 123
http:
url: "http://example.com"
`,
);
await expect(loadConfig(configPath)).rejects.toThrow("group 字段必须为字符串");
});
});

View File

@@ -19,6 +19,7 @@ function createMockStore(targetNames: string[]) {
interval_ms: 60000,
timeout_ms: 5000,
expect: null,
grp: "default",
}));
},
insertCheckResult(result: Record<string, unknown>) {
@@ -32,6 +33,7 @@ function makeCommandTarget(name: string, overrides?: Partial<ResolvedCommandTarg
return {
type: "command",
name,
group: "default",
command: {
exec: "echo",
args: ["hello"],
@@ -175,6 +177,7 @@ describe("ProbeEngine", () => {
const httpTarget: ResolvedHttpTarget = {
type: "http",
name: "http-test",
group: "default",
http: {
url: `http://localhost:${httpServer.port}/`,
method: "GET",

View File

@@ -61,6 +61,7 @@ describe("runHttpCheck 集成", () => {
return {
type: "http" as const,
name: "test-http",
group: "default",
http: {
url: overrides.url ?? `${baseUrl}/ok`,
method: overrides.method ?? "GET",

View File

@@ -8,6 +8,7 @@ import { tmpdir } from "node:os";
const httpTarget: ResolvedTarget = {
type: "http",
name: "test-http",
group: "default",
http: {
url: "https://example.com/health",
method: "GET",
@@ -22,6 +23,7 @@ const httpTarget: ResolvedTarget = {
const commandTarget: ResolvedTarget = {
type: "command",
name: "test-cmd",
group: "default",
command: {
exec: "ping",
args: ["-c", "1", "localhost"],
@@ -166,9 +168,9 @@ describe("ProbeStore", () => {
failure,
});
const history = store.getHistory(t1Id, 10);
expect(history).toHaveLength(3);
expect(history[0]!.timestamp).toBe("2025-01-01T00:01:00.000Z");
const history = store.getHistory(t1Id, "2025-01-01T00:00:00.000Z", "2025-12-31T23:59:59.999Z", 1, 10);
expect(history.items).toHaveLength(3);
expect(history.items[0]!.timestamp).toBe("2025-01-01T00:01:00.000Z");
const latest = store.getLatestCheck(t1Id)!;
expect(latest.success).toBe(0);
@@ -195,8 +197,8 @@ describe("ProbeStore", () => {
});
}
const history = store.getHistory(t1Id);
expect(history).toHaveLength(20);
const history = store.getHistory(t1Id, "2025-01-01T00:00:00.000Z", "2025-12-31T23:59:59.999Z");
expect(history.items).toHaveLength(20);
});
test("getTargetStats 计算可用率和 duration", () => {
@@ -207,8 +209,6 @@ describe("ProbeStore", () => {
expect(stats.totalChecks).toBeGreaterThan(0);
expect(stats.availability).toBeGreaterThanOrEqual(0);
expect(stats.availability).toBeLessThanOrEqual(100);
expect(stats.avgDurationMs).not.toBeNull();
expect(typeof stats.avgDurationMs).toBe("number");
});
test("无记录目标的 stats", () => {
@@ -218,8 +218,6 @@ describe("ProbeStore", () => {
const stats = store.getTargetStats(t2Id);
expect(stats.totalChecks).toBe(0);
expect(stats.availability).toBe(0);
expect(stats.avgDurationMs).toBeNull();
expect(stats.p99DurationMs).toBeNull();
});
test("getSummary 返回总览统计", () => {
@@ -227,14 +225,13 @@ describe("ProbeStore", () => {
expect(summary.total).toBe(2);
expect(summary.up + summary.down).toBe(2);
expect(summary.lastCheckTime).not.toBeNull();
expect(summary.avgDurationMs).not.toBeNull();
});
test("getTrend 返回趋势数据", () => {
const targets = store.getTargets();
const t1Id = targets[0]!.id;
const trend = store.getTrend(t1Id, 24);
const trend = store.getTrend(t1Id, "2024-01-01T00:00:00.000Z", "2026-12-31T23:59:59.999Z");
expect(Array.isArray(trend)).toBe(true);
if (trend.length > 0) {
expect(trend[0]!.hour).toBeDefined();
@@ -244,15 +241,17 @@ describe("ProbeStore", () => {
}
});
test("getSparkline 返回 duration 数组", () => {
test("getRecentSamples 返回最近采样数据", () => {
const targets = store.getTargets();
const t1Id = targets[0]!.id;
const sparkline = store.getSparkline(t1Id);
expect(Array.isArray(sparkline)).toBe(true);
expect(sparkline.length).toBeGreaterThan(0);
for (const val of sparkline) {
expect(typeof val).toBe("number");
const samples = store.getRecentSamples(t1Id, 10);
expect(Array.isArray(samples)).toBe(true);
expect(samples.length).toBeGreaterThan(0);
for (const sample of samples) {
expect(typeof sample.timestamp).toBe("string");
expect(typeof sample.success).toBe("number");
expect(typeof sample.matched).toBe("number");
}
});