useReportDateRange.ts298 lines · main
1import dayjs from 'dayjs'
2import { createParser, useQueryState } from 'nuqs'
3import { useCallback, useMemo, useState } from 'react'
4
5import {
6 REPORT_DATERANGE_HELPER_LABELS,
7 REPORTS_DATEPICKER_HELPERS,
8 ReportsDatetimeHelper,
9} from '@/components/interfaces/Reports/Reports.constants'
10import { DatePickerValue } from '@/components/interfaces/Settings/Logs/Logs.DatePickers'
11import { maybeShowUpgradePromptIfNotEntitled } from '@/components/interfaces/Settings/Logs/Logs.utils'
12import { AnalyticsInterval } from '@/data/analytics/constants'
13import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
14import { useCurrentOrgPlan } from '@/hooks/misc/useCurrentOrgPlan'
15
16export const getIntervalGranularity = (from: string, to: string): AnalyticsInterval => {
17 const diffInDays = dayjs(to).diff(from, 'day', true)
18 const diffInHours = dayjs(to).diff(from, 'hour', true)
19
20 if (diffInHours <= 1) return '1m'
21 if (diffInHours <= 12) return '2m'
22 if (diffInHours <= 24) return '10m'
23 if (diffInDays <= 7) return '30m'
24 return '1d'
25}
26
27export const DATERANGE_LIMITS: { [key: string]: number } = {
28 free: 1,
29 pro: 7,
30 team: 28,
31 enterprise: 90,
32 platform: 1,
33}
34
35export interface ReportDateRange {
36 period_start: { date: string; time_period: string }
37 period_end: { date: string; time_period: string }
38 interval: AnalyticsInterval
39}
40
41// Create parsers for individual URL parameters
42const stringWithDefault = (defaultValue: string) =>
43 createParser({
44 parse: (v) => v ?? defaultValue,
45 serialize: (v) => v || '',
46 })
47
48const booleanWithDefault = (defaultValue: boolean) =>
49 createParser({
50 parse: (v) => {
51 if (v === 'true') return true
52 if (v === 'false') return false
53 return defaultValue
54 },
55 serialize: (v) => String(v),
56 })
57
58export const useReportDateRange = (
59 defaultHelper:
60 | REPORT_DATERANGE_HELPER_LABELS
61 | string
62 | ReportsDatetimeHelper = REPORT_DATERANGE_HELPER_LABELS.LAST_60_MINUTES
63) => {
64 const { plan: orgPlan, isLoading: isOrgPlanLoading } = useCurrentOrgPlan()
65 const { getEntitlementNumericValue } = useCheckEntitlements('log.retention_days')
66 const entitledToAuditLogDays = getEntitlementNumericValue()
67 const [showUpgradePrompt, setShowUpgradePrompt] = useState(false)
68
69 // Get filtered date picker helpers based on organization plan
70 const datePickerHelpers: ReportsDatetimeHelper[] = useMemo(
71 () =>
72 REPORTS_DATEPICKER_HELPERS.map((helper) => ({
73 ...helper,
74 disabled: false,
75 })),
76 []
77 )
78
79 // Use nuqs for URL state management with individual parameters (empty defaults)
80 const [timestampStartValue, setTimestampStart] = useQueryState('its', stringWithDefault(''))
81 const [timestampEndValue, setTimestampEnd] = useQueryState('ite', stringWithDefault(''))
82 const [isHelperValue, setIsHelper] = useQueryState('isHelper', booleanWithDefault(false))
83 const [helperTextValue, setHelperText] = useQueryState('helperText', stringWithDefault(''))
84
85 const hasAccessToHelper = useCallback(
86 (helper: ReportsDatetimeHelper) => {
87 if (!entitledToAuditLogDays) return true
88 const days = Math.abs(dayjs().diff(dayjs(helper.calcFrom()), 'day'))
89 return days <= entitledToAuditLogDays
90 },
91 [entitledToAuditLogDays]
92 )
93
94 const getDefaultHelper = useCallback(() => {
95 let targetHelper: ReportsDatetimeHelper | undefined
96
97 if (typeof defaultHelper === 'string') {
98 // Find helper by text (supports both enum values and direct strings)
99 targetHelper = REPORTS_DATEPICKER_HELPERS.find((helper) => helper.text === defaultHelper)
100 } else if (defaultHelper && typeof defaultHelper === 'object' && 'text' in defaultHelper) {
101 // Use the provided helper object directly
102 targetHelper = defaultHelper
103 }
104
105 // Check if the target helper is available for the current entitlement
106 if (targetHelper && hasAccessToHelper(targetHelper)) {
107 return {
108 start: targetHelper.calcFrom(),
109 end: targetHelper.calcTo(),
110 helper: { isHelper: true, text: targetHelper.text },
111 }
112 }
113
114 // Fallback: look for default helper marked in REPORTS_DATEPICKER_HELPERS
115 const fallbackHelper = REPORTS_DATEPICKER_HELPERS.find(
116 (helper) => helper.default && hasAccessToHelper(helper)
117 )
118
119 if (fallbackHelper) {
120 return {
121 start: fallbackHelper.calcFrom(),
122 end: fallbackHelper.calcTo(),
123 helper: { isHelper: true, text: fallbackHelper.text },
124 }
125 }
126
127 // Final fallback: use first available helper
128 const firstAvailable = REPORTS_DATEPICKER_HELPERS.find((helper) => hasAccessToHelper(helper))
129
130 if (firstAvailable) {
131 return {
132 start: firstAvailable.calcFrom(),
133 end: firstAvailable.calcTo(),
134 helper: { isHelper: true, text: firstAvailable.text },
135 }
136 }
137
138 // Ultimate fallback to manual date calculation (should rarely happen)
139 const defaultStart = dayjs().subtract(1, 'hour').toISOString()
140 const defaultEnd = dayjs().toISOString()
141 return {
142 start: defaultStart,
143 end: defaultEnd,
144 helper: { isHelper: false },
145 }
146 }, [defaultHelper, hasAccessToHelper])
147
148 // Get current effective values (from URL or defaults, but don't set URL)
149 const timestampStart = useMemo(() => {
150 if (timestampStartValue) {
151 // Validate that stored date is reasonable
152 const storedFrom = dayjs(timestampStartValue)
153 const now = dayjs()
154 if (
155 storedFrom.isValid() &&
156 storedFrom.isAfter(now.subtract(90, 'day')) &&
157 storedFrom.isBefore(now.add(1, 'day')) &&
158 !maybeShowUpgradePromptIfNotEntitled(timestampStartValue, entitledToAuditLogDays)
159 ) {
160 return timestampStartValue
161 }
162 }
163 // Return default without setting URL
164 return getDefaultHelper().start
165 }, [timestampStartValue, getDefaultHelper, entitledToAuditLogDays])
166
167 const timestampEnd = useMemo(() => {
168 if (timestampEndValue) {
169 // Validate that stored date is reasonable
170 const storedTo = dayjs(timestampEndValue)
171 const now = dayjs()
172 if (
173 storedTo.isValid() &&
174 storedTo.isAfter(now.subtract(90, 'day')) &&
175 storedTo.isBefore(now.add(1, 'day'))
176 ) {
177 return timestampEndValue
178 }
179 }
180 // Return default without setting URL
181 return getDefaultHelper().end
182 }, [timestampEndValue, getDefaultHelper])
183
184 // For helper state, use URL values or defaults from helper when no URL params exist
185 const isHelper = useMemo(() => {
186 if (timestampStartValue && timestampEndValue) {
187 return Boolean(isHelperValue)
188 }
189 // If no URL params, get default from helper
190 return getDefaultHelper().helper.isHelper
191 }, [timestampStartValue, timestampEndValue, isHelperValue, getDefaultHelper])
192
193 const helperText = useMemo(() => {
194 if (timestampStartValue && timestampEndValue) {
195 return helperTextValue || undefined
196 }
197 // If no URL params, get default from helper
198 return getDefaultHelper().helper.text
199 }, [timestampStartValue, timestampEndValue, helperTextValue, getDefaultHelper])
200
201 // Derive selectedDateRange from current values
202 const selectedDateRange: ReportDateRange = useMemo(
203 () => ({
204 period_start: { date: timestampStart, time_period: '1d' },
205 period_end: { date: timestampEnd, time_period: 'today' },
206 interval: getIntervalGranularity(timestampStart, timestampEnd),
207 }),
208 [timestampStart, timestampEnd]
209 )
210
211 const updateDateRange = useCallback(
212 (from: string, to: string) => {
213 setTimestampStart(from)
214 setTimestampEnd(to)
215 setIsHelper(false)
216 setHelperText('')
217 },
218 [setTimestampStart, setTimestampEnd, setIsHelper, setHelperText]
219 )
220
221 // For backward compatibility, provide a setter that updates the derived state
222 const setSelectedDateRange = useCallback(
223 (newRange: ReportDateRange) => {
224 setTimestampStart(newRange.period_start.date)
225 setTimestampEnd(newRange.period_end.date)
226 // When setting directly, assume it's not from a helper
227 setIsHelper(false)
228 setHelperText('')
229 },
230 [setTimestampStart, setTimestampEnd, setIsHelper, setHelperText]
231 )
232
233 // Convert to DatePickerValue format for LogsDatePicker
234 const datePickerValue: DatePickerValue = useMemo(
235 () => ({
236 from: timestampStart,
237 to: timestampEnd,
238 isHelper,
239 text: helperText,
240 }),
241 [timestampStart, timestampEnd, isHelper, helperText]
242 )
243
244 const handleDatePickerChange = (values: DatePickerValue) => {
245 const shouldShowUpgradePrompt = maybeShowUpgradePromptIfNotEntitled(
246 values.from,
247 entitledToAuditLogDays
248 )
249 if (shouldShowUpgradePrompt) {
250 setShowUpgradePrompt(true)
251 return true
252 } else {
253 if (values.from && values.to) {
254 setTimestampStart(values.from)
255 setTimestampEnd(values.to)
256 setIsHelper(values.isHelper || false)
257 setHelperText(values.text || '')
258 }
259 return false
260 }
261 }
262
263 return {
264 selectedDateRange,
265 setSelectedDateRange,
266 updateDateRange,
267 datePickerValue,
268 datePickerHelpers,
269 isOrgPlanLoading,
270 orgPlan,
271 showUpgradePrompt,
272 setShowUpgradePrompt,
273 handleDatePickerChange,
274 }
275}
276
277export function useRefreshHandler(
278 datePickerValue: DatePickerValue,
279 datePickerHelpers: ReportsDatetimeHelper[],
280 handleDatePickerChange: (value: DatePickerValue) => boolean | void,
281 refresh: () => void | Promise<void>
282): () => void | Promise<void> {
283 return useCallback(async () => {
284 if (datePickerValue.isHelper && datePickerValue.text) {
285 const selectedHelper = datePickerHelpers.find((h) => h.text === datePickerValue.text)
286 if (selectedHelper) {
287 handleDatePickerChange({
288 from: selectedHelper.calcFrom(),
289 to: selectedHelper.calcTo(),
290 isHelper: true,
291 text: selectedHelper.text,
292 })
293 }
294 }
295
296 await refresh()
297 }, [datePickerValue, datePickerHelpers, handleDatePickerChange, refresh])
298}