Skip to content

Commit ece0275

Browse files
authored
[DESIGN-301] Project Homepage: refactor to logflare endpoint (supabase#40124)
1 parent 7b7eb3b commit ece0275

File tree

5 files changed

+300
-111
lines changed

5 files changed

+300
-111
lines changed

apps/studio/components/interfaces/HomeNew/ProjectUsageSection.tsx

Lines changed: 9 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { useParams } from 'common'
88
import NoDataPlaceholder from 'components/ui/Charts/NoDataPlaceholder'
99
import { InlineLink } from 'components/ui/InlineLink'
1010
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
11-
import useProjectUsageStats from 'hooks/analytics/useProjectUsageStats'
1211
import { useCurrentOrgPlan } from 'hooks/misc/useCurrentOrgPlan'
1312
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
1413
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
@@ -33,9 +32,9 @@ import {
3332
import { Row } from 'ui-patterns'
3433
import { LogsBarChart } from 'ui-patterns/LogsBarChart'
3534
import { useServiceStats } from './ProjectUsageSection.utils'
35+
import type { StatsLike } from './ProjectUsageSection.utils'
3636
import type { LogsBarChartDatum } from './ProjectUsage.metrics'
3737
import {
38-
toLogsBarChartData,
3938
sumTotal,
4039
sumWarnings,
4140
sumErrors,
@@ -68,7 +67,7 @@ const CHART_INTERVALS: ChartIntervals[] = [
6867
label: 'Last 7 days',
6968
startValue: 7,
7069
startUnit: 'day',
71-
format: 'MMM D',
70+
format: 'MMM D, ha',
7271
availableIn: ['pro', 'team', 'enterprise'],
7372
},
7473
]
@@ -90,7 +89,7 @@ type ServiceComputed = ServiceEntry & {
9089
total: number
9190
warn: number
9291
err: number
93-
stats: ReturnType<typeof useProjectUsageStats>
92+
stats: StatsLike
9493
}
9594

9695
export const ProjectUsageSection = () => {
@@ -109,37 +108,12 @@ export const ProjectUsageSection = () => {
109108

110109
const selectedInterval = CHART_INTERVALS.find((i) => i.key === interval) || CHART_INTERVALS[1]
111110

112-
const { timestampStart, timestampEnd, datetimeFormat } = useMemo(() => {
113-
const startDateLocal = dayjs().subtract(
114-
selectedInterval.startValue,
115-
selectedInterval.startUnit as dayjs.ManipulateType
116-
)
117-
const endDateLocal = dayjs()
111+
const { datetimeFormat } = useMemo(() => {
118112
const format = selectedInterval.format || 'MMM D, ha'
113+
return { datetimeFormat: format }
114+
}, [selectedInterval])
119115

120-
return {
121-
timestampStart: startDateLocal.toISOString(),
122-
timestampEnd: endDateLocal.toISOString(),
123-
datetimeFormat: format,
124-
}
125-
}, [selectedInterval]) // Only recalculate when interval changes
126-
127-
const { previousStart, previousEnd } = useMemo(() => {
128-
const currentStart = dayjs(timestampStart)
129-
const currentEnd = dayjs(timestampEnd)
130-
const durationMs = currentEnd.diff(currentStart)
131-
const prevEnd = currentStart
132-
const prevStart = currentStart.subtract(durationMs, 'millisecond')
133-
return { previousStart: prevStart.toISOString(), previousEnd: prevEnd.toISOString() }
134-
}, [timestampStart, timestampEnd])
135-
136-
const statsByService = useServiceStats(
137-
projectRef as string,
138-
timestampStart,
139-
timestampEnd,
140-
previousStart,
141-
previousEnd
142-
)
116+
const statsByService = useServiceStats(projectRef!, interval)
143117

144118
const serviceBase: ServiceEntry[] = useMemo(
145119
() => [
@@ -185,7 +159,7 @@ export const ProjectUsageSection = () => {
185159
() =>
186160
serviceBase.map((s) => {
187161
const currentStats = statsByService[s.key].current
188-
const data = toLogsBarChartData(currentStats.eventChartData)
162+
const data = currentStats.eventChartData
189163
const total = sumTotal(data)
190164
const warn = sumWarnings(data)
191165
const err = sumErrors(data)
@@ -239,7 +213,7 @@ export const ProjectUsageSection = () => {
239213
() =>
240214
serviceBase.map((s) => {
241215
const previousStats = statsByService[s.key].previous
242-
const data = toLogsBarChartData(previousStats.eventChartData)
216+
const data = previousStats.eventChartData
243217
return {
244218
enabled: s.enabled,
245219
total: sumTotal(data),
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { describe, it, expect, vi } from 'vitest'
2+
3+
import { toServiceStatsMap } from './ProjectUsageSection.utils'
4+
import type { ProjectMetricsRow } from 'data/analytics/project-metrics-query'
5+
6+
const mkRow = (
7+
n: number,
8+
service: ProjectMetricsRow['service'],
9+
time_window: ProjectMetricsRow['time_window']
10+
): ProjectMetricsRow => ({
11+
timestamp: (1700000000000 + n * 60000) * 1000, // microseconds
12+
service,
13+
time_window,
14+
ok_count: n,
15+
warning_count: 0,
16+
error_count: 0,
17+
})
18+
19+
const emptyRows: ProjectMetricsRow[] = []
20+
21+
describe('toServiceStatsMap', () => {
22+
it('returns empty arrays when no data', () => {
23+
const onRefresh = vi.fn()
24+
const map = toServiceStatsMap({
25+
data: emptyRows,
26+
isLoading: false,
27+
error: undefined,
28+
onRefresh,
29+
})
30+
31+
expect(map.db.current.eventChartData).toEqual([])
32+
expect(map.functions.previous.eventChartData).toEqual([])
33+
expect(map.auth.current.isLoading).toBe(false)
34+
expect(map.storage.current.error).toBeNull()
35+
36+
map.realtime.current.refresh()
37+
expect(onRefresh).toHaveBeenCalledTimes(1)
38+
})
39+
40+
it('maps data rows through for each service', () => {
41+
const rows: ProjectMetricsRow[] = [
42+
mkRow(1, 'db', 'current'),
43+
mkRow(2, 'db', 'current'),
44+
mkRow(0, 'db', 'previous'),
45+
]
46+
const map = toServiceStatsMap({
47+
data: rows,
48+
isLoading: true,
49+
error: undefined,
50+
onRefresh: () => {},
51+
})
52+
53+
expect(map.db.current.eventChartData.length).toBe(2)
54+
expect(map.db.previous.eventChartData.length).toBe(1)
55+
expect(map.db.current.isLoading).toBe(true)
56+
})
57+
58+
it('propagates errors to all services', () => {
59+
const err = new Error('boom')
60+
const map = toServiceStatsMap({
61+
data: emptyRows,
62+
isLoading: false,
63+
error: err,
64+
onRefresh: () => {},
65+
})
66+
67+
expect(map.db.current.error).toBe(err)
68+
expect(map.functions.previous.error).toBe(err)
69+
})
70+
})
Lines changed: 105 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,92 +1,121 @@
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'
33

44
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+
518
type ServiceStatsMap = Record<
619
ServiceKey,
720
{
8-
current: ReturnType<typeof useProjectUsageStats>
9-
previous: ReturnType<typeof useProjectUsageStats>
21+
current: StatsLike
22+
previous: StatsLike
1023
}
1124
>
1225

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
3244

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+
}
4552

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: [] }
5854

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+
}
7165

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
8488

8589
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+
},
91104
}
92105
}
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+
}

apps/studio/data/analytics/keys.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ export const analyticsKeys = {
121121
'infra-monitoring',
122122
{ attribute, startDate, endDate, interval, databaseIdentifier },
123123
] as const,
124+
projectMetrics: (projectRef: string | undefined, { interval }: { interval?: string }) =>
125+
['projects', projectRef, 'project.metrics', { interval }] as const,
124126
usageApiCounts: (projectRef: string | undefined, interval: string | undefined) =>
125127
['projects', projectRef, 'usage.api-counts', interval] as const,
126128

0 commit comments

Comments
 (0)