Files
supabase/apps/studio/hooks/misc/useReportDateRange.ts
T
f65884f341 Reports consolidation 1: Edge Functions (#38129)
* wip

* add formatTimestamp for reports

* add tests for formatTimestamp

* fix type err

* rm old chart logic

* do not retry replication sources

* update report config schema

* use report config

* fix charts, simplify config

* sync charts

* rm comment

* usefilltimeseries

* move chart upsell to its own component

* cleanup v2 chart component

* fix chartheader function format bug

* make attributes dynamic

* move types to its own file

* update reportv2chart types

* update reportv2 chart type

* use new edgefn report

* rm mock

* rm old code

* fix type err

* fix type err

* rename hook

* return sql in dataprovider

* add button to open in log explorer in chart header

* fix type

* fix chart type toggle

* fix type error

* export data transformation for testing

* add tests for data transformation

* rm dupped code

* add report settings to edge fn report

* rm mocked data

* rename

* fix type err

* Apply suggestion from @joshenlim

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>

* rm unnecessary div

* rm unnecessary props from report upsell

* cleaner query keys

* move loadin/error/empty states to inside panel content to prevent layout shift

* use card instead of panel

* use buttontooltip in chartheader

* use buttontooltips in buttons with tooltips

* rm export default

* rm export defualt

* rm i from key

* fix status code transformation

* fix filltimeseries

* add tests for filltimeseries

* fix data transform

* pass xaxis key to chart

* refactor hover state to use timestamp not index

* fix tests

* fix areachart

* fix barchart

* fix chartheader

* fix composedchart

* fix stacked

* fix type missing xaxiskey

* type fix

* Revert "refactor hover state to use timestamp not index"

This reverts commit cfd7886460.

* undo charthover changes, fix data aggregation

* Revert "fix tests"

This reverts commit e5200a2035.

* move transforms to testable functions, add tests

* nit refactors and clean up

* feat: add aws nimbus cloud provider (#38312)

* feat: add aws nimbus cloud provider

* Fix return type of useCustomContent hook

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>

* add utc plugin to dayjs

* move plugins to vitest setup

* rm customparser plugin

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
Co-authored-by: Alaister Young <alaister@users.noreply.github.com>
2025-09-01 11:10:30 +02:00

275 lines
8.9 KiB
TypeScript

import { useCallback, useMemo, useState } from 'react'
import dayjs from 'dayjs'
import { createParser, useQueryState } from 'nuqs'
import { DatePickerValue } from 'components/interfaces/Settings/Logs/Logs.DatePickers'
import {
REPORTS_DATEPICKER_HELPERS,
ReportsDatetimeHelper,
REPORT_DATERANGE_HELPER_LABELS,
} from 'components/interfaces/Reports/Reports.constants'
import { useCurrentOrgPlan } from 'hooks/misc/useCurrentOrgPlan'
import { maybeShowUpgradePrompt } from 'components/interfaces/Settings/Logs/Logs.utils'
import { AnalyticsInterval } from 'data/analytics/constants'
export const DATERANGE_LIMITS: { [key: string]: number } = {
free: 1,
pro: 7,
team: 28,
enterprise: 90,
}
export interface ReportDateRange {
period_start: { date: string; time_period: string }
period_end: { date: string; time_period: string }
interval: AnalyticsInterval
}
// Create parsers for individual URL parameters
const stringWithDefault = (defaultValue: string) =>
createParser({
parse: (v) => v ?? defaultValue,
serialize: (v) => v || '',
})
const booleanWithDefault = (defaultValue: boolean) =>
createParser({
parse: (v) => {
if (v === 'true') return true
if (v === 'false') return false
return defaultValue
},
serialize: (v) => String(v),
})
export const useReportDateRange = (
defaultHelper:
| REPORT_DATERANGE_HELPER_LABELS
| string
| ReportsDatetimeHelper = REPORT_DATERANGE_HELPER_LABELS.LAST_60_MINUTES
) => {
const { plan: orgPlan, isLoading: isOrgPlanLoading } = useCurrentOrgPlan()
const [showUpgradePrompt, setShowUpgradePrompt] = useState(false)
// Get filtered date picker helpers based on organization plan
const datePickerHelpers: ReportsDatetimeHelper[] = useMemo(
() =>
REPORTS_DATEPICKER_HELPERS.map((helper) => ({
...helper,
disabled: false,
})),
[]
)
// Use nuqs for URL state management with individual parameters (empty defaults)
const [timestampStartValue, setTimestampStart] = useQueryState('its', stringWithDefault(''))
const [timestampEndValue, setTimestampEnd] = useQueryState('ite', stringWithDefault(''))
const [isHelperValue, setIsHelper] = useQueryState('isHelper', booleanWithDefault(false))
const [helperTextValue, setHelperText] = useQueryState('helperText', stringWithDefault(''))
const getDefaultHelper = useCallback(() => {
let targetHelper: ReportsDatetimeHelper | undefined
if (typeof defaultHelper === 'string') {
// Find helper by text (supports both enum values and direct strings)
targetHelper = REPORTS_DATEPICKER_HELPERS.find((helper) => helper.text === defaultHelper)
} else if (defaultHelper && typeof defaultHelper === 'object' && 'text' in defaultHelper) {
// Use the provided helper object directly
targetHelper = defaultHelper
}
// Check if the target helper is available for the current plan
if (targetHelper && targetHelper.availableIn?.includes(orgPlan?.id || 'free')) {
return {
start: targetHelper.calcFrom(),
end: targetHelper.calcTo(),
helper: { isHelper: true, text: targetHelper.text },
}
}
// Fallback: look for default helper marked in REPORTS_DATEPICKER_HELPERS
const fallbackHelper = REPORTS_DATEPICKER_HELPERS.find(
(helper) => helper.default && helper.availableIn?.includes(orgPlan?.id || 'free')
)
if (fallbackHelper) {
return {
start: fallbackHelper.calcFrom(),
end: fallbackHelper.calcTo(),
helper: { isHelper: true, text: fallbackHelper.text },
}
}
// Final fallback: use first available helper
const firstAvailable = REPORTS_DATEPICKER_HELPERS.find((helper) =>
helper.availableIn?.includes(orgPlan?.id || 'free')
)
if (firstAvailable) {
return {
start: firstAvailable.calcFrom(),
end: firstAvailable.calcTo(),
helper: { isHelper: true, text: firstAvailable.text },
}
}
// Ultimate fallback to manual date calculation (should rarely happen)
const defaultStart = dayjs().subtract(1, 'hour').toISOString()
const defaultEnd = dayjs().toISOString()
return {
start: defaultStart,
end: defaultEnd,
helper: { isHelper: false },
}
}, [defaultHelper, orgPlan?.id])
// Get current effective values (from URL or defaults, but don't set URL)
const timestampStart = useMemo(() => {
if (timestampStartValue) {
// Validate that stored date is reasonable
const storedFrom = dayjs(timestampStartValue)
const now = dayjs()
if (
storedFrom.isValid() &&
storedFrom.isAfter(now.subtract(90, 'day')) &&
storedFrom.isBefore(now.add(1, 'day'))
) {
return timestampStartValue
}
}
// Return default without setting URL
return getDefaultHelper().start
}, [timestampStartValue, getDefaultHelper])
const timestampEnd = useMemo(() => {
if (timestampEndValue) {
// Validate that stored date is reasonable
const storedTo = dayjs(timestampEndValue)
const now = dayjs()
if (
storedTo.isValid() &&
storedTo.isAfter(now.subtract(90, 'day')) &&
storedTo.isBefore(now.add(1, 'day'))
) {
return timestampEndValue
}
}
// Return default without setting URL
return getDefaultHelper().end
}, [timestampEndValue, getDefaultHelper])
// For helper state, use URL values or defaults from helper when no URL params exist
const isHelper = useMemo(() => {
if (timestampStartValue && timestampEndValue) {
return Boolean(isHelperValue)
}
// If no URL params, get default from helper
return getDefaultHelper().helper.isHelper
}, [timestampStartValue, timestampEndValue, isHelperValue, getDefaultHelper])
const helperText = useMemo(() => {
if (timestampStartValue && timestampEndValue) {
return helperTextValue || undefined
}
// If no URL params, get default from helper
return getDefaultHelper().helper.text
}, [timestampStartValue, timestampEndValue, helperTextValue, getDefaultHelper])
const handleIntervalGranularity = useCallback((from: string, to: string) => {
const diffInDays = dayjs(to).diff(from, 'day', true)
const diffInHours = dayjs(to).diff(from, 'hour', true)
const conditions = {
'1m': diffInHours < 1.1, // less than 1.1 hours
'5m': diffInHours < 3.1, // less than 3.1 hours
'10m': diffInHours < 6.1, // less than 6.1 hours
'30m': diffInHours < 25, // less than 25 hours
'1h': diffInDays < 10, // less than 10 days
'1d': diffInDays >= 10, // more than 10 days
}
switch (true) {
case conditions['1m']:
return '1m'
case conditions['5m']:
return '5m'
case conditions['10m']:
return '10m'
case conditions['30m']:
return '30m'
default:
return '1h'
}
}, [])
// Derive selectedDateRange from current values
const selectedDateRange: ReportDateRange = useMemo(
() => ({
period_start: { date: timestampStart, time_period: '1d' },
period_end: { date: timestampEnd, time_period: 'today' },
interval: handleIntervalGranularity(timestampStart, timestampEnd),
}),
[timestampStart, timestampEnd, handleIntervalGranularity]
)
const updateDateRange = useCallback(
(from: string, to: string) => {
setTimestampStart(from)
setTimestampEnd(to)
setIsHelper(false)
setHelperText('')
},
[setTimestampStart, setTimestampEnd, setIsHelper, setHelperText]
)
// For backward compatibility, provide a setter that updates the derived state
const setSelectedDateRange = useCallback(
(newRange: ReportDateRange) => {
setTimestampStart(newRange.period_start.date)
setTimestampEnd(newRange.period_end.date)
// When setting directly, assume it's not from a helper
setIsHelper(false)
setHelperText('')
},
[setTimestampStart, setTimestampEnd, setIsHelper, setHelperText]
)
// Convert to DatePickerValue format for LogsDatePicker
const datePickerValue: DatePickerValue = useMemo(
() => ({
from: timestampStart,
to: timestampEnd,
isHelper,
text: helperText,
}),
[timestampStart, timestampEnd, isHelper, helperText]
)
const handleDatePickerChange = (values: DatePickerValue) => {
const shouldShowUpgradePrompt = maybeShowUpgradePrompt(values.from, orgPlan?.id)
if (shouldShowUpgradePrompt) {
setShowUpgradePrompt(true)
return true
} else {
if (values.from && values.to) {
setTimestampStart(values.from)
setTimestampEnd(values.to)
setIsHelper(values.isHelper || false)
setHelperText(values.text || '')
}
return false
}
}
return {
selectedDateRange,
setSelectedDateRange,
updateDateRange,
datePickerValue,
datePickerHelpers,
isOrgPlanLoading,
orgPlan,
showUpgradePrompt,
setShowUpgradePrompt,
handleDatePickerChange,
}
}