useReportDateRange.ts298 lines · main
| 1 | import dayjs from 'dayjs' |
| 2 | import { createParser, useQueryState } from 'nuqs' |
| 3 | import { useCallback, useMemo, useState } from 'react' |
| 4 | |
| 5 | import { |
| 6 | REPORT_DATERANGE_HELPER_LABELS, |
| 7 | REPORTS_DATEPICKER_HELPERS, |
| 8 | ReportsDatetimeHelper, |
| 9 | } from '@/components/interfaces/Reports/Reports.constants' |
| 10 | import { DatePickerValue } from '@/components/interfaces/Settings/Logs/Logs.DatePickers' |
| 11 | import { maybeShowUpgradePromptIfNotEntitled } from '@/components/interfaces/Settings/Logs/Logs.utils' |
| 12 | import { AnalyticsInterval } from '@/data/analytics/constants' |
| 13 | import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements' |
| 14 | import { useCurrentOrgPlan } from '@/hooks/misc/useCurrentOrgPlan' |
| 15 | |
| 16 | export 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 | |
| 27 | export const DATERANGE_LIMITS: { [key: string]: number } = { |
| 28 | free: 1, |
| 29 | pro: 7, |
| 30 | team: 28, |
| 31 | enterprise: 90, |
| 32 | platform: 1, |
| 33 | } |
| 34 | |
| 35 | export 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 |
| 42 | const stringWithDefault = (defaultValue: string) => |
| 43 | createParser({ |
| 44 | parse: (v) => v ?? defaultValue, |
| 45 | serialize: (v) => v || '', |
| 46 | }) |
| 47 | |
| 48 | const 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 | |
| 58 | export 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 | |
| 277 | export 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 | } |