refactor: 全面重构前端 Dashboard 为 TDesign + TanStack Query 分组表格布局
- 卡片式布局改为分组 PrimaryTable,Modal 改为 Drawer - 手写 hooks 替换为 TanStack Query(轮询/缓存/条件查询) - CSS 607行精简至73行,颜色迁移至 TDesign tokens - 可用率进度条颜色按 10% 一档红→绿渐变 - 新增纯函数测试 34 项全通过(排序/筛选/色阶阈值) - 同步更新主 specs 并归档变更文档
This commit is contained in:
95
tests/web/constants/color-threshold.test.ts
Normal file
95
tests/web/constants/color-threshold.test.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import { getAvailabilityProgressColor, getLatencyColor } from "../../../src/web/constants/color-threshold";
|
||||
|
||||
describe("color-threshold", () => {
|
||||
describe("getAvailabilityProgressColor", () => {
|
||||
test("0-10% 返回第一档颜色", () => {
|
||||
expect(getAvailabilityProgressColor(0)).toBe("#d54941");
|
||||
expect(getAvailabilityProgressColor(5)).toBe("#d54941");
|
||||
expect(getAvailabilityProgressColor(9.99)).toBe("#d54941");
|
||||
});
|
||||
|
||||
test("10-20% 返回第二档颜色", () => {
|
||||
expect(getAvailabilityProgressColor(10)).toBe("#d96241");
|
||||
expect(getAvailabilityProgressColor(15)).toBe("#d96241");
|
||||
expect(getAvailabilityProgressColor(19.99)).toBe("#d96241");
|
||||
});
|
||||
|
||||
test("20-30% 返回第三档颜色", () => {
|
||||
expect(getAvailabilityProgressColor(20)).toBe("#e37318");
|
||||
expect(getAvailabilityProgressColor(25)).toBe("#e37318");
|
||||
});
|
||||
|
||||
test("30-40% 返回第四档颜色", () => {
|
||||
expect(getAvailabilityProgressColor(30)).toBe("#e89318");
|
||||
expect(getAvailabilityProgressColor(35)).toBe("#e89318");
|
||||
});
|
||||
|
||||
test("40-50% 返回第五档颜色", () => {
|
||||
expect(getAvailabilityProgressColor(40)).toBe("#d9a818");
|
||||
expect(getAvailabilityProgressColor(45)).toBe("#d9a818");
|
||||
});
|
||||
|
||||
test("50-60% 返回第六档颜色", () => {
|
||||
expect(getAvailabilityProgressColor(50)).toBe("#b8b020");
|
||||
expect(getAvailabilityProgressColor(55)).toBe("#b8b020");
|
||||
});
|
||||
|
||||
test("60-70% 返回第七档颜色", () => {
|
||||
expect(getAvailabilityProgressColor(60)).toBe("#8dba30");
|
||||
expect(getAvailabilityProgressColor(65)).toBe("#8dba30");
|
||||
});
|
||||
|
||||
test("70-80% 返回第八档颜色", () => {
|
||||
expect(getAvailabilityProgressColor(70)).toBe("#6dba3f");
|
||||
expect(getAvailabilityProgressColor(75)).toBe("#6dba3f");
|
||||
});
|
||||
|
||||
test("80-90% 返回第九档颜色", () => {
|
||||
expect(getAvailabilityProgressColor(80)).toBe("#4dba50");
|
||||
expect(getAvailabilityProgressColor(85)).toBe("#4dba50");
|
||||
});
|
||||
|
||||
test("90-100% 返回第十档颜色", () => {
|
||||
expect(getAvailabilityProgressColor(90)).toBe("#3dba60");
|
||||
expect(getAvailabilityProgressColor(95)).toBe("#3dba60");
|
||||
expect(getAvailabilityProgressColor(99.9)).toBe("#3dba60");
|
||||
expect(getAvailabilityProgressColor(100)).toBe("#3dba60");
|
||||
});
|
||||
|
||||
test("边界值", () => {
|
||||
expect(getAvailabilityProgressColor(9.999)).toBe("#d54941");
|
||||
expect(getAvailabilityProgressColor(10)).toBe("#d96241");
|
||||
expect(getAvailabilityProgressColor(19.999)).toBe("#d96241");
|
||||
expect(getAvailabilityProgressColor(20)).toBe("#e37318");
|
||||
expect(getAvailabilityProgressColor(89.999)).toBe("#4dba50");
|
||||
expect(getAvailabilityProgressColor(90)).toBe("#3dba60");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getLatencyColor", () => {
|
||||
test("<=100ms 返回 success 色", () => {
|
||||
expect(getLatencyColor(0)).toBe("var(--td-success-color)");
|
||||
expect(getLatencyColor(50)).toBe("var(--td-success-color)");
|
||||
expect(getLatencyColor(100)).toBe("var(--td-success-color)");
|
||||
});
|
||||
|
||||
test("100-500ms 返回 warning 色", () => {
|
||||
expect(getLatencyColor(101)).toBe("var(--td-warning-color)");
|
||||
expect(getLatencyColor(250)).toBe("var(--td-warning-color)");
|
||||
expect(getLatencyColor(500)).toBe("var(--td-warning-color)");
|
||||
});
|
||||
|
||||
test(">500ms 返回 error 色", () => {
|
||||
expect(getLatencyColor(501)).toBe("var(--td-error-color)");
|
||||
expect(getLatencyColor(1000)).toBe("var(--td-error-color)");
|
||||
});
|
||||
|
||||
test("边界值", () => {
|
||||
expect(getLatencyColor(100)).toBe("var(--td-success-color)");
|
||||
expect(getLatencyColor(100.01)).toBe("var(--td-warning-color)");
|
||||
expect(getLatencyColor(500)).toBe("var(--td-warning-color)");
|
||||
expect(getLatencyColor(500.01)).toBe("var(--td-error-color)");
|
||||
});
|
||||
});
|
||||
});
|
||||
28
tests/web/constants/target-table-filters.test.ts
Normal file
28
tests/web/constants/target-table-filters.test.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import { statusFilter, typeFilter } from "../../../src/web/constants/target-table-filters";
|
||||
|
||||
describe("target-table-filters", () => {
|
||||
describe("statusFilter", () => {
|
||||
test("包含全部选项", () => {
|
||||
expect(statusFilter).toBeDefined();
|
||||
expect(statusFilter!.type).toBe("single");
|
||||
const list = statusFilter!.list!;
|
||||
expect(list).toHaveLength(3);
|
||||
expect(list[0]!.label).toBe("全部");
|
||||
expect(list[1]!.label).toBe("UP");
|
||||
expect(list[2]!.label).toBe("DOWN");
|
||||
});
|
||||
});
|
||||
|
||||
describe("typeFilter", () => {
|
||||
test("包含全部选项", () => {
|
||||
expect(typeFilter).toBeDefined();
|
||||
expect(typeFilter!.type).toBe("single");
|
||||
const list = typeFilter!.list!;
|
||||
expect(list).toHaveLength(3);
|
||||
expect(list[0]!.label).toBe("全部");
|
||||
expect(list[1]!.label).toBe("HTTP");
|
||||
expect(list[2]!.label).toBe("CMD");
|
||||
});
|
||||
});
|
||||
});
|
||||
111
tests/web/constants/target-table-sorters.test.ts
Normal file
111
tests/web/constants/target-table-sorters.test.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import {
|
||||
statusSorter,
|
||||
availabilitySorter,
|
||||
latencySorter,
|
||||
nameSorter,
|
||||
} from "../../../src/web/constants/target-table-sorters";
|
||||
import type { TargetStatus } from "../../../src/shared/api";
|
||||
|
||||
function makeTarget(overrides: Partial<TargetStatus> = {}): TargetStatus {
|
||||
return {
|
||||
id: 1,
|
||||
name: "test",
|
||||
type: "http",
|
||||
target: "https://example.com",
|
||||
group: "default",
|
||||
interval: "5s",
|
||||
latestCheck: null,
|
||||
stats: { totalChecks: 0, availability: 100 },
|
||||
recentSamples: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("statusSorter", () => {
|
||||
test("DOWN 排在 UP 前面", () => {
|
||||
const up = makeTarget({
|
||||
latestCheck: { timestamp: "", matched: true, durationMs: 10, statusDetail: null, failure: null },
|
||||
});
|
||||
const down = makeTarget({
|
||||
latestCheck: { timestamp: "", matched: false, durationMs: 10, statusDetail: null, failure: null },
|
||||
});
|
||||
expect(statusSorter(down, up)).toBeLessThan(0);
|
||||
expect(statusSorter(up, down)).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("相同状态返回 0", () => {
|
||||
const a = makeTarget({
|
||||
latestCheck: { timestamp: "", matched: true, durationMs: 10, statusDetail: null, failure: null },
|
||||
});
|
||||
const b = makeTarget({
|
||||
latestCheck: { timestamp: "", matched: true, durationMs: 20, statusDetail: null, failure: null },
|
||||
});
|
||||
expect(statusSorter(a, b)).toBe(0);
|
||||
});
|
||||
|
||||
test("无 latestCheck 的目标排在最后", () => {
|
||||
const noCheck = makeTarget();
|
||||
const up = makeTarget({
|
||||
latestCheck: { timestamp: "", matched: true, durationMs: 10, statusDetail: null, failure: null },
|
||||
});
|
||||
expect(statusSorter(noCheck, up)).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("availabilitySorter", () => {
|
||||
test("低可用率排前面", () => {
|
||||
const low = makeTarget({ stats: { totalChecks: 100, availability: 95 } });
|
||||
const high = makeTarget({ stats: { totalChecks: 100, availability: 99.9 } });
|
||||
expect(availabilitySorter(low, high)).toBeLessThan(0);
|
||||
});
|
||||
|
||||
test("相同可用率返回 0", () => {
|
||||
const a = makeTarget({ stats: { totalChecks: 100, availability: 99.9 } });
|
||||
const b = makeTarget({ stats: { totalChecks: 50, availability: 99.9 } });
|
||||
expect(availabilitySorter(a, b)).toBe(0);
|
||||
});
|
||||
|
||||
test("无 stats 按 0 处理", () => {
|
||||
const noStats = makeTarget({ stats: undefined as unknown as TargetStatus["stats"] });
|
||||
const high = makeTarget({ stats: { totalChecks: 100, availability: 99.9 } });
|
||||
expect(availabilitySorter(noStats, high)).toBeLessThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("latencySorter", () => {
|
||||
test("低延迟排前面", () => {
|
||||
const fast = makeTarget({
|
||||
latestCheck: { timestamp: "", matched: true, durationMs: 50, statusDetail: null, failure: null },
|
||||
});
|
||||
const slow = makeTarget({
|
||||
latestCheck: { timestamp: "", matched: true, durationMs: 200, statusDetail: null, failure: null },
|
||||
});
|
||||
expect(latencySorter(fast, slow)).toBeLessThan(0);
|
||||
});
|
||||
|
||||
test("无延迟排最后", () => {
|
||||
const noLatency = makeTarget({
|
||||
latestCheck: { timestamp: "", matched: true, durationMs: null, statusDetail: null, failure: null },
|
||||
});
|
||||
const hasLatency = makeTarget({
|
||||
latestCheck: { timestamp: "", matched: true, durationMs: 100, statusDetail: null, failure: null },
|
||||
});
|
||||
expect(latencySorter(noLatency, hasLatency)).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("nameSorter", () => {
|
||||
test("按名称字母排序", () => {
|
||||
const a = makeTarget({ name: "Alpha" });
|
||||
const b = makeTarget({ name: "Beta" });
|
||||
expect(nameSorter(a, b)).toBeLessThan(0);
|
||||
});
|
||||
|
||||
test("中文名称排序", () => {
|
||||
const a = makeTarget({ name: "百度" });
|
||||
const b = makeTarget({ name: "谷歌" });
|
||||
const result = nameSorter(a, b);
|
||||
expect(typeof result).toBe("number");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user