fix(frontend): 补齐 analysis 构建依赖与共享组件搬迁
补 commit hooks/图表组件、路由与 i18n domains、logs metrics API; StatCard/TimeRangePicker/ingress-access-log 抽到共享路径,干净 checkout 可编过。
This commit is contained in:
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 }
|
||||
}
|
||||
Reference in new issue
Block a user