|
1 | | -import useProjectUsageStats from 'hooks/analytics/useProjectUsageStats' |
2 | | -import { LogsTableName } from '../Settings/Logs/Logs.constants' |
| 1 | +import { useProjectMetricsQuery } from 'data/analytics/project-metrics-query' |
| 2 | +import type { ProjectMetricsRow } from 'data/analytics/project-metrics-query' |
3 | 3 |
|
4 | 4 | type ServiceKey = 'db' | 'functions' | 'auth' | 'storage' | 'realtime' |
| 5 | + |
| 6 | +export type StatsLike = { |
| 7 | + error: unknown | null |
| 8 | + isLoading: boolean |
| 9 | + eventChartData: Array<{ |
| 10 | + timestamp: string |
| 11 | + ok_count: number |
| 12 | + warning_count: number |
| 13 | + error_count: number |
| 14 | + }> |
| 15 | + refresh: () => void |
| 16 | +} |
| 17 | + |
5 | 18 | type ServiceStatsMap = Record< |
6 | 19 | ServiceKey, |
7 | 20 | { |
8 | | - current: ReturnType<typeof useProjectUsageStats> |
9 | | - previous: ReturnType<typeof useProjectUsageStats> |
| 21 | + current: StatsLike |
| 22 | + previous: StatsLike |
10 | 23 | } |
11 | 24 | > |
12 | 25 |
|
13 | | -export const useServiceStats = ( |
14 | | - projectRef: string, |
15 | | - timestampStart: string, |
16 | | - timestampEnd: string, |
17 | | - previousStart: string, |
18 | | - previousEnd: string |
19 | | -): ServiceStatsMap => { |
20 | | - const dbCurrent = useProjectUsageStats({ |
21 | | - projectRef, |
22 | | - table: LogsTableName.POSTGRES, |
23 | | - timestampStart, |
24 | | - timestampEnd, |
25 | | - }) |
26 | | - const dbPrevious = useProjectUsageStats({ |
27 | | - projectRef, |
28 | | - table: LogsTableName.POSTGRES, |
29 | | - timestampStart: previousStart, |
30 | | - timestampEnd: previousEnd, |
31 | | - }) |
| 26 | +/** |
| 27 | + * Transform backend project metrics into a UI-friendly structure with consistent |
| 28 | + * loading/error/refresh state per service. |
| 29 | + * |
| 30 | + * Why this exists |
| 31 | + * - Backend returns flat rows: one record per (time_window, service, bucket_ts). |
| 32 | + * - UI needs per-service objects with two series (current/previous) to drive 5 cards and compute per-service totals. |
| 33 | + * - Charts expect ISO string timestamps; backend gives TIMESTAMP (coming to client as microseconds). We convert to ISO. |
| 34 | + * - We also need stable sorting and consistent empty arrays when a series has no points. |
| 35 | + * - We attach loading/error/refresh per service to keep UI simple. |
| 36 | + */ |
| 37 | +export const toServiceStatsMap = (args: { |
| 38 | + data?: ProjectMetricsRow[] |
| 39 | + isLoading: boolean |
| 40 | + error?: unknown |
| 41 | + onRefresh: () => void |
| 42 | +}): ServiceStatsMap => { |
| 43 | + const { data, isLoading, error, onRefresh } = args |
32 | 44 |
|
33 | | - const fnCurrent = useProjectUsageStats({ |
34 | | - projectRef, |
35 | | - table: LogsTableName.FN_EDGE, |
36 | | - timestampStart, |
37 | | - timestampEnd, |
38 | | - }) |
39 | | - const fnPrevious = useProjectUsageStats({ |
40 | | - projectRef, |
41 | | - table: LogsTableName.FN_EDGE, |
42 | | - timestampStart: previousStart, |
43 | | - timestampEnd: previousEnd, |
44 | | - }) |
| 45 | + const base = { |
| 46 | + error: error ?? null, |
| 47 | + isLoading, |
| 48 | + refresh: () => { |
| 49 | + onRefresh() |
| 50 | + }, |
| 51 | + } |
45 | 52 |
|
46 | | - const authCurrent = useProjectUsageStats({ |
47 | | - projectRef, |
48 | | - table: LogsTableName.AUTH, |
49 | | - timestampStart, |
50 | | - timestampEnd, |
51 | | - }) |
52 | | - const authPrevious = useProjectUsageStats({ |
53 | | - projectRef, |
54 | | - table: LogsTableName.AUTH, |
55 | | - timestampStart: previousStart, |
56 | | - timestampEnd: previousEnd, |
57 | | - }) |
| 53 | + const empty: StatsLike = { ...base, eventChartData: [] } |
58 | 54 |
|
59 | | - const storageCurrent = useProjectUsageStats({ |
60 | | - projectRef, |
61 | | - table: LogsTableName.STORAGE, |
62 | | - timestampStart, |
63 | | - timestampEnd, |
64 | | - }) |
65 | | - const storagePrevious = useProjectUsageStats({ |
66 | | - projectRef, |
67 | | - table: LogsTableName.STORAGE, |
68 | | - timestampStart: previousStart, |
69 | | - timestampEnd: previousEnd, |
70 | | - }) |
| 55 | + const grouped: Record< |
| 56 | + ServiceKey, |
| 57 | + { current: StatsLike['eventChartData']; previous: StatsLike['eventChartData'] } |
| 58 | + > = { |
| 59 | + db: { current: [], previous: [] }, |
| 60 | + functions: { current: [], previous: [] }, |
| 61 | + auth: { current: [], previous: [] }, |
| 62 | + storage: { current: [], previous: [] }, |
| 63 | + realtime: { current: [], previous: [] }, |
| 64 | + } |
71 | 65 |
|
72 | | - const realtimeCurrent = useProjectUsageStats({ |
73 | | - projectRef, |
74 | | - table: LogsTableName.REALTIME, |
75 | | - timestampStart, |
76 | | - timestampEnd, |
77 | | - }) |
78 | | - const realtimePrevious = useProjectUsageStats({ |
79 | | - projectRef, |
80 | | - table: LogsTableName.REALTIME, |
81 | | - timestampStart: previousStart, |
82 | | - timestampEnd: previousEnd, |
83 | | - }) |
| 66 | + const toIso = (microseconds: number) => new Date(microseconds / 1000).toISOString() |
| 67 | + |
| 68 | + for (const r of data ?? []) { |
| 69 | + const bucket = grouped[r.service as ServiceKey] |
| 70 | + const target = r.time_window === 'current' ? bucket.current : bucket.previous |
| 71 | + target.push({ |
| 72 | + timestamp: toIso(r.timestamp), |
| 73 | + ok_count: r.ok_count, |
| 74 | + warning_count: r.warning_count, |
| 75 | + error_count: r.error_count, |
| 76 | + }) |
| 77 | + } |
| 78 | + |
| 79 | + const byTime = (a: { timestamp: string }, b: { timestamp: string }) => |
| 80 | + Date.parse(a.timestamp) - Date.parse(b.timestamp) |
| 81 | + for (const key of Object.keys(grouped) as ServiceKey[]) { |
| 82 | + grouped[key].current.sort(byTime) |
| 83 | + grouped[key].previous.sort(byTime) |
| 84 | + } |
| 85 | + |
| 86 | + const toStats = (rows: StatsLike['eventChartData'] | undefined): StatsLike => |
| 87 | + rows ? { ...base, eventChartData: rows } : empty |
84 | 88 |
|
85 | 89 | return { |
86 | | - db: { current: dbCurrent, previous: dbPrevious }, |
87 | | - functions: { current: fnCurrent, previous: fnPrevious }, |
88 | | - auth: { current: authCurrent, previous: authPrevious }, |
89 | | - storage: { current: storageCurrent, previous: storagePrevious }, |
90 | | - realtime: { current: realtimeCurrent, previous: realtimePrevious }, |
| 90 | + db: { current: toStats(grouped.db.current), previous: toStats(grouped.db.previous) }, |
| 91 | + functions: { |
| 92 | + current: toStats(grouped.functions.current), |
| 93 | + previous: toStats(grouped.functions.previous), |
| 94 | + }, |
| 95 | + auth: { current: toStats(grouped.auth.current), previous: toStats(grouped.auth.previous) }, |
| 96 | + storage: { |
| 97 | + current: toStats(grouped.storage.current), |
| 98 | + previous: toStats(grouped.storage.previous), |
| 99 | + }, |
| 100 | + realtime: { |
| 101 | + current: toStats(grouped.realtime.current), |
| 102 | + previous: toStats(grouped.realtime.previous), |
| 103 | + }, |
91 | 104 | } |
92 | 105 | } |
| 106 | + |
| 107 | +export const useServiceStats = ( |
| 108 | + projectRef: string, |
| 109 | + interval: '1hr' | '1day' | '7day' |
| 110 | +): ServiceStatsMap => { |
| 111 | + const { data, isLoading, error, refetch } = useProjectMetricsQuery({ projectRef, interval }) |
| 112 | + |
| 113 | + return toServiceStatsMap({ |
| 114 | + data, |
| 115 | + isLoading, |
| 116 | + error, |
| 117 | + onRefresh: () => { |
| 118 | + void refetch() |
| 119 | + }, |
| 120 | + }) |
| 121 | +} |
0 commit comments