useReportDateRange.ts 9.8 KB

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