feat: 前端指标体系增强 — Dashboard/Metrics API、2×4 统计区、趋势图面积+异常标记、连续状态列
- 新增 GET /api/dashboard 合并原 summary+targets 首屏接口 - 新增 GET /api/targets/:id/metrics 合并原 stats+trend 概览接口 - 后端指标纯函数:可用率、百分位、故障段分析、连续状态、UTC 小时分桶 - ProbeStore 窗口取数方法替代全量历史查询 - SummaryCards 扩展为 4 卡片(新增异常事件数)+ 数据新鲜度展示 - 表格新增「连续」列(Tag 渲染 capped 状态) - OverviewTab 重构为 2×4 Statistic 多维度布局 - TrendChart 改为延迟范围面积图 + 红色异常标记点 - 删除旧路由(summary/targets/trend)和 computeTrendStats - 同步 delta specs 到主 specs 并归档变更
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import type { MetaResponse, SummaryResponse, TargetStatus } from "../../shared/api";
|
||||
import type { DashboardResponse, MetaResponse, TargetMetricsResponse } from "../../shared/api";
|
||||
|
||||
const queryKeys = {
|
||||
dashboard: () => ["dashboard", "24h", 30] as const,
|
||||
meta: () => ["meta"] as const,
|
||||
summary: () => ["summary"] as const,
|
||||
targets: () => ["targets"] as const,
|
||||
metrics: (targetId: number, from: string, to: string, bucket: "1h") =>
|
||||
["metrics", targetId, from, to, bucket] as const,
|
||||
};
|
||||
|
||||
export async function fetchJson<T>(url: string): Promise<T> {
|
||||
@@ -14,6 +15,15 @@ export async function fetchJson<T>(url: string): Promise<T> {
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
export function useDashboard() {
|
||||
return useQuery({
|
||||
queryFn: () => fetchJson<DashboardResponse>("/api/dashboard?window=24h&recentLimit=30"),
|
||||
queryKey: queryKeys.dashboard(),
|
||||
refetchInterval: 8000,
|
||||
refetchIntervalInBackground: false,
|
||||
});
|
||||
}
|
||||
|
||||
export function useMeta() {
|
||||
return useQuery({
|
||||
queryFn: () => fetchJson<MetaResponse>("/api/meta"),
|
||||
@@ -22,20 +32,15 @@ export function useMeta() {
|
||||
});
|
||||
}
|
||||
|
||||
export function useSummary() {
|
||||
export function useTargetMetrics(targetId: null | number, from: string, to: string, bucket: "1h") {
|
||||
return useQuery({
|
||||
queryFn: () => fetchJson<SummaryResponse>("/api/summary"),
|
||||
queryKey: queryKeys.summary(),
|
||||
refetchInterval: 8000,
|
||||
refetchIntervalInBackground: false,
|
||||
});
|
||||
}
|
||||
|
||||
export function useTargets() {
|
||||
return useQuery({
|
||||
queryFn: () => fetchJson<TargetStatus[]>("/api/targets"),
|
||||
queryKey: queryKeys.targets(),
|
||||
refetchInterval: 8000,
|
||||
refetchIntervalInBackground: false,
|
||||
enabled: targetId !== null && !!from && !!to,
|
||||
queryFn: () => {
|
||||
if (targetId === null) throw new Error("未选择目标");
|
||||
return fetchJson<TargetMetricsResponse>(
|
||||
`/api/targets/${targetId}/metrics?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}&bucket=${bucket}`,
|
||||
);
|
||||
},
|
||||
queryKey: targetId !== null && from && to ? queryKeys.metrics(targetId, from, to, bucket) : ["metrics", "disabled"],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
import type { HistoryResponse, TargetStatus, TrendPoint } from "../../shared/api";
|
||||
import type { HistoryResponse, TargetStatus } from "../../shared/api";
|
||||
|
||||
import { subtractHours } from "../utils/time";
|
||||
import { fetchJson, useTargets } from "./use-queries";
|
||||
import { fetchJson, useDashboard, useTargetMetrics } from "./use-queries";
|
||||
|
||||
const detailQueryKeys = {
|
||||
history: (targetId: number, from: string, to: string, page: number) => ["history", targetId, from, to, page] as const,
|
||||
trend: (targetId: number, from: string, to: string) => ["trend", targetId, from, to] as const,
|
||||
};
|
||||
|
||||
export function useTargetDetail() {
|
||||
@@ -18,28 +17,22 @@ export function useTargetDetail() {
|
||||
const [timeTo, setTimeTo] = useState("");
|
||||
const [historyPage, setHistoryPage] = useState(1);
|
||||
|
||||
const { data: targetsData } = useTargets();
|
||||
const { data: dashboardData } = useDashboard();
|
||||
const selectedTarget =
|
||||
selectedTargetId !== null ? (targetsData?.find((target) => target.id === selectedTargetId) ?? null) : null;
|
||||
selectedTargetId !== null
|
||||
? (dashboardData?.targets.find((target) => target.id === selectedTargetId) ?? null)
|
||||
: null;
|
||||
|
||||
const trend = useQuery({
|
||||
enabled: selectedTargetId !== null && !!timeFrom && !!timeTo,
|
||||
queryFn: () =>
|
||||
fetchJson<TrendPoint[]>(
|
||||
`/api/targets/${selectedTargetId}/trend?from=${encodeURIComponent(timeFrom)}&to=${encodeURIComponent(timeTo)}`,
|
||||
),
|
||||
queryKey:
|
||||
selectedTargetId !== null && timeFrom && timeTo
|
||||
? detailQueryKeys.trend(selectedTargetId, timeFrom, timeTo)
|
||||
: ["trend", "disabled"],
|
||||
});
|
||||
const metrics = useTargetMetrics(selectedTargetId, timeFrom, timeTo, "1h");
|
||||
|
||||
const history = useQuery({
|
||||
enabled: selectedTargetId !== null && !!timeFrom && !!timeTo,
|
||||
queryFn: () =>
|
||||
fetchJson<HistoryResponse>(
|
||||
queryFn: () => {
|
||||
if (selectedTargetId === null) throw new Error("未选择目标");
|
||||
return fetchJson<HistoryResponse>(
|
||||
`/api/targets/${selectedTargetId}/history?from=${encodeURIComponent(timeFrom)}&to=${encodeURIComponent(timeTo)}&page=${historyPage}&pageSize=20`,
|
||||
),
|
||||
);
|
||||
},
|
||||
queryKey:
|
||||
selectedTargetId !== null && timeFrom && timeTo
|
||||
? detailQueryKeys.history(selectedTargetId, timeFrom, timeTo, historyPage)
|
||||
@@ -57,7 +50,7 @@ export function useTargetDetail() {
|
||||
|
||||
const closeDrawer = useCallback(() => {
|
||||
setSelectedTargetId(null);
|
||||
queryClient.removeQueries({ queryKey: ["trend"] });
|
||||
queryClient.removeQueries({ queryKey: ["metrics"] });
|
||||
queryClient.removeQueries({ queryKey: ["history"] });
|
||||
}, [queryClient]);
|
||||
|
||||
@@ -77,11 +70,11 @@ export function useTargetDetail() {
|
||||
handleTimeChange,
|
||||
historyData: history.data ?? { items: [], page: 1, pageSize: 20, total: 0 },
|
||||
historyLoading: history.isLoading,
|
||||
metricsData: metrics.data ?? null,
|
||||
metricsLoading: metrics.isLoading,
|
||||
openDrawer,
|
||||
selectedTarget,
|
||||
timeFrom,
|
||||
timeTo,
|
||||
trendData: trend.data ?? [],
|
||||
trendLoading: trend.isLoading,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user