fix(frontend): 补齐 analysis 构建依赖与共享组件搬迁

补 commit hooks/图表组件、路由与 i18n domains、logs metrics API;
StatCard/TimeRangePicker/ingress-access-log 抽到共享路径,干净 checkout 可编过。
This commit is contained in:
Codex committed 2026-10-28 22:00:30 +08:00
1 parent f993d5b420
commit 11d0fff764
22 files changed
+552 -313

No files matched your search

@@ -0,0 +1,117 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { logsApi } from '@/api/logs'
export interface DailyAppRow {
app: string
namespace: string
/** 每个自然日(本地时区)的日志总量,与 days 一一对应 */
totals: number[]
total: number
}
export interface DailyVolumeData {
days: string[]
apps: DailyAppRow[]
}
const DAY_MS = 86400000
const RANGE_DAYS = 7
const ns = (ms: number) => Math.round(ms).toString() + '000000'
const dayLabel = (ts: number) => new Date(ts).toLocaleDateString([], { month: '2-digit', day: '2-digit' })
/**
* 应用每日日志量(近 7 天,Top 10)— 独立于页面时间范围筛选:
* 一条 `sum by (app, namespace)(count_over_time(sel[$step]))` 查询(7 天跨度后端自动选 1h 步长),
* 客户端把小时桶按本地日历日聚合,避免为每天单独发查询。
*/
export function useDailyVolume({ sourceId, namespace, appLabel }: {
sourceId: number
namespace: string
appLabel: string
}) {
const [data, setData] = useState<DailyVolumeData | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [refreshTick, setRefreshTick] = useState(0)
const abortRef = useRef<AbortController | null>(null)
const versionRef = useRef(0)
const refresh = useCallback(() => setRefreshTick(tick => tick + 1), [])
useEffect(() => {
if (!sourceId) return
const end = Date.now()
// 从今天往前取整 7 个自然日(首尾两天可能是不完整天)
const today = Math.floor(end / DAY_MS) * DAY_MS
const days = Array.from({ length: RANGE_DAYS }, (_, index) => today - (RANGE_DAYS - 1 - index) * DAY_MS)
const start = days[0]
abortRef.current?.abort()
const controller = new AbortController()
abortRef.current = controller
const version = ++versionRef.current
const selector = namespace ? `{namespace="${namespace}"}` : '{namespace=~".+"}'
void (async () => {
setLoading(true)
setError('')
try {
const series = await logsApi.metrics(
{ sourceId, start: Number(ns(start)), end: Number(ns(end)), query: `sum by (${appLabel}, namespace) (count_over_time(${selector}[$step]))` },
{ signal: controller.signal },
)
if (version !== versionRef.current) return
// 小时桶 → 本地日历日分桶(app × 天 累加,并记录各 app 落在哪个命名空间)
const perDay = days.map(() => new Map<string, { total: number; perNs: Map<string, number> }>())
const dayIndexOf = (timestamp: string) => {
const day = Math.floor(new Date(timestamp).getTime() / DAY_MS) * DAY_MS
return days.indexOf(day)
}
for (const item of series) {
const app = item.labels[appLabel] ?? ''
const nsLabel = item.labels.namespace ?? ''
for (const point of item.points) {
const index = dayIndexOf(point.timestamp)
if (index < 0) continue
const bucket = perDay[index]
const entry = bucket.get(app) ?? { total: 0, perNs: new Map<string, number>() }
entry.total += point.value
entry.perNs.set(nsLabel, (entry.perNs.get(nsLabel) ?? 0) + point.value)
bucket.set(app, entry)
}
}
const merged = new Map<string, { totals: number[]; total: number; perNs: Map<string, number> }>()
perDay.forEach((bucket, index) => {
for (const [app, entry] of bucket) {
const row = merged.get(app) ?? { totals: days.map(() => 0), total: 0, perNs: new Map<string, number>() }
row.totals[index] += entry.total
row.total += entry.total
for (const [nsName, count] of entry.perNs) row.perNs.set(nsName, (row.perNs.get(nsName) ?? 0) + count)
merged.set(app, row)
}
})
const apps: DailyAppRow[] = [...merged.entries()]
.map(([app, row]) => ({
app,
namespace: [...row.perNs.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? '',
totals: row.totals,
total: row.total,
}))
.filter(row => row.total > 0)
.sort((a, b) => b.total - a.total)
.slice(0, 10)
setData({ days: days.map(dayLabel), apps })
} catch (err) {
if (version !== versionRef.current) return
if ((err as Error).name === 'AbortError') return
setError(err instanceof Error ? err.message : 'metrics failed')
} finally {
if (version === versionRef.current) setLoading(false)
}
})()
return () => { controller.abort() }
}, [sourceId, namespace, appLabel, refreshTick])
return { data, loading, error, refresh }
}