ProjectUsageSection.tsx290 lines · main
1// @ts-nocheck
2import { useParams } from 'common'
3import dayjs from 'dayjs'
4import Link from 'next/link'
5import { useRouter } from 'next/router'
6import { useEffect, useMemo, useState } from 'react'
7import { Card, CardContent, CardHeader, CardTitle, Loading } from 'ui'
8import { Row } from 'ui-patterns'
9import { LogsBarChart } from 'ui-patterns/LogsBarChart'
10
11import NoDataPlaceholder from '@/components/ui/Charts/NoDataPlaceholder'
12import { ChartIntervalDropdown } from '@/components/ui/Logs/ChartIntervalDropdown'
13import { CHART_INTERVALS } from '@/components/ui/Logs/logs.utils'
14import { UsageApiCounts, useProjectLogStatsQuery } from '@/data/analytics/project-log-stats-query'
15import { useSendEventMutation } from '@/data/telemetry/send-event-mutation'
16import { useFillTimeseriesSorted } from '@/hooks/analytics/useFillTimeseriesSorted'
17import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
18import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
19import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
20
21type LogsBarChartDatum = {
22 timestamp: string
23 error_count: number
24 ok_count: number
25 warning_count: number
26}
27
28type ChartIntervalKey = '1hr' | '1day' | '7day'
29
30type ServiceKey = 'db' | 'auth' | 'storage' | 'realtime'
31
32type ServiceEntry = {
33 key: ServiceKey
34 title: string
35 href?: string
36 route: string
37 enabled: boolean
38}
39
40type ServiceComputed = ServiceEntry & {
41 data: LogsBarChartDatum[]
42 total: number
43 isLoading: boolean
44 error: unknown | null
45}
46
47export const ProjectUsageSection = () => {
48 const router = useRouter()
49 const { ref: projectRef } = useParams()
50 const { data: organization } = useSelectedOrganizationQuery()
51 const { mutate: sendEvent } = useSendEventMutation()
52 const { projectAuthAll: authEnabled, projectStorageAll: storageEnabled } = useIsFeatureEnabled([
53 'project_auth:all',
54 'project_storage:all',
55 ])
56 const { getEntitlementMax } = useCheckEntitlements('log.retention_days')
57 const retentionDays = getEntitlementMax()
58
59 const DEFAULT_INTERVAL: ChartIntervalKey =
60 retentionDays !== undefined && retentionDays < 7 ? '1hr' : '1day'
61 const [interval, setInterval] = useState<ChartIntervalKey>(DEFAULT_INTERVAL)
62
63 useEffect(() => {
64 setInterval(retentionDays !== undefined && retentionDays < 7 ? '1hr' : '1day')
65 }, [retentionDays])
66
67 const selectedInterval = CHART_INTERVALS.find((i) => i.key === interval) || CHART_INTERVALS[1]
68
69 const { datetimeFormat } = useMemo(() => {
70 const format = selectedInterval.format || 'MMM D, ha'
71 return { datetimeFormat: format }
72 }, [selectedInterval])
73
74 // Use V1 data fetching
75 const { data: logStatsData, isPending: isLoading } = useProjectLogStatsQuery({
76 projectRef,
77 interval,
78 })
79
80 // Calculate date range for gap filling
81 const startDateLocal = dayjs().subtract(
82 selectedInterval.startValue,
83 selectedInterval.startUnit as dayjs.ManipulateType
84 )
85 const endDateLocal = dayjs()
86
87 // Fill gaps in timeseries data
88 const { data: filledCharts } = useFillTimeseriesSorted({
89 data: logStatsData?.result ?? [],
90 timestampKey: 'timestamp',
91 valueKey: [
92 'total_auth_requests',
93 'total_rest_requests',
94 'total_storage_requests',
95 'total_realtime_requests',
96 ],
97 defaultValue: 0,
98 startDate: startDateLocal.toISOString(),
99 endDate: endDateLocal.toISOString(),
100 minPointsToFill: 5,
101 })
102
103 const serviceBase: ServiceEntry[] = useMemo(
104 () => [
105 {
106 key: 'db',
107 title: 'Database requests',
108 href: `/project/${projectRef}/editor`,
109 route: '/logs/postgres-logs',
110 enabled: true,
111 },
112 {
113 key: 'auth',
114 title: 'Auth requests',
115 href: `/project/${projectRef}/auth/users`,
116 route: '/logs/auth-logs',
117 enabled: authEnabled,
118 },
119 {
120 key: 'storage',
121 title: 'Storage requests',
122 href: `/project/${projectRef}/storage/buckets`,
123 route: '/logs/storage-logs',
124 enabled: storageEnabled,
125 },
126 {
127 key: 'realtime',
128 title: 'Realtime requests',
129 route: '/logs/realtime-logs',
130 enabled: true,
131 },
132 ],
133 [projectRef, authEnabled, storageEnabled]
134 )
135
136 const services: ServiceComputed[] = useMemo(
137 () =>
138 serviceBase.map((s) => {
139 // Map service keys to V1 data field names
140 const dataKeyMap: Record<ServiceKey, keyof UsageApiCounts> = {
141 db: 'total_rest_requests',
142 auth: 'total_auth_requests',
143 storage: 'total_storage_requests',
144 realtime: 'total_realtime_requests',
145 }
146
147 const dataKey = dataKeyMap[s.key]
148
149 // Transform V1 data to LogsBarChart format
150 // Since V1 doesn't have error/warning breakdown, we show everything as "ok"
151 const transformedData: LogsBarChartDatum[] = (filledCharts || []).map((item) => ({
152 timestamp: item.timestamp,
153 error_count: 0,
154 warning_count: 0,
155 ok_count: Number(item[dataKey]) || 0,
156 }))
157
158 // Calculate total from filled data
159 const total = transformedData.reduce((sum, item) => sum + item.ok_count, 0)
160
161 return {
162 ...s,
163 data: transformedData,
164 total,
165 isLoading,
166 error: null,
167 }
168 }),
169 [serviceBase, filledCharts, isLoading]
170 )
171
172 const handleBarClick =
173 (logRoute: string, serviceKey: ServiceKey) => (datum: LogsBarChartDatum) => {
174 if (!datum?.timestamp) return
175
176 const datumTimestamp = dayjs(datum.timestamp).toISOString()
177 const start = dayjs(datumTimestamp).subtract(1, 'minute').toISOString()
178 const end = dayjs(datumTimestamp).add(1, 'minute').toISOString()
179
180 const queryParams = new URLSearchParams({
181 iso_timestamp_start: start,
182 iso_timestamp_end: end,
183 })
184
185 router.push(`/project/${projectRef}${logRoute}?${queryParams.toString()}`)
186
187 if (projectRef && organization?.slug) {
188 sendEvent({
189 action: 'home_project_usage_chart_clicked',
190 properties: {
191 service_type: serviceKey,
192 bar_timestamp: datum.timestamp,
193 },
194 groups: {
195 project: projectRef,
196 organization: organization.slug,
197 },
198 })
199 }
200 }
201
202 const enabledServices = services.filter((s) => s.enabled)
203 const totalRequests = enabledServices.reduce((sum, s) => sum + (s.total || 0), 0)
204
205 return (
206 <div className="space-y-6">
207 <div className="flex flex-row justify-between items-center gap-x-2">
208 <div className="flex items-start gap-2 heading-section text-foreground-light">
209 <span className="text-foreground">{totalRequests.toLocaleString()}</span>
210 <span>Total Requests</span>
211 </div>
212 <ChartIntervalDropdown
213 value={interval}
214 onChange={(interval) => setInterval(interval as ChartIntervalKey)}
215 organizationSlug={organization?.slug}
216 dropdownAlign="end"
217 tooltipSide="left"
218 />
219 </div>
220 <Row maxColumns={4} minWidth={280}>
221 {enabledServices.map((s) => (
222 <Card key={s.key} className="mb-0 md:mb-0 h-full flex flex-col h-64">
223 <CardHeader className="flex flex-row items-end justify-between gap-2 space-y-0 pb-0 border-b-0">
224 <div className="flex items-center gap-2">
225 <div className="flex flex-col">
226 <CardTitle className="text-xs font-mono uppercase text-foreground-light">
227 {s.href ? (
228 <Link
229 href={s.href}
230 onClick={() => {
231 if (projectRef && organization?.slug) {
232 sendEvent({
233 action: 'home_project_usage_service_clicked',
234 properties: {
235 service_type: s.key,
236 total_requests: s.total || 0,
237 },
238 groups: {
239 project: projectRef,
240 organization: organization.slug,
241 },
242 })
243 }
244 }}
245 >
246 {s.title}
247 </Link>
248 ) : (
249 s.title
250 )}
251 </CardTitle>
252 <span className="text-foreground text-xl">{(s.total || 0).toLocaleString()}</span>
253 </div>
254 </div>
255 </CardHeader>
256 <CardContent className="p-card flex-1 h-full overflow-hidden">
257 <Loading isFullHeight active={isLoading}>
258 <LogsBarChart
259 isFullHeight
260 data={s.data}
261 DateTimeFormat={datetimeFormat}
262 onBarClick={handleBarClick(s.route, s.key)}
263 hideZeroValues={true}
264 chartConfig={{
265 error_count: {
266 label: 'Errors',
267 },
268 warning_count: {
269 label: 'Warnings',
270 },
271 ok_count: {
272 label: 'Requests',
273 },
274 }}
275 EmptyState={
276 <NoDataPlaceholder
277 size="small"
278 message="No data for selected period"
279 isFullHeight
280 />
281 }
282 />
283 </Loading>
284 </CardContent>
285 </Card>
286 ))}
287 </Row>
288 </div>
289 )
290}