mirror of
https://github.com/supabase/supabase.git
synced 2026-10-08 19:05:06 +03:00
* 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 commitcfd7886460. * undo charthover changes, fix data aggregation * Revert "fix tests" This reverts commite5200a2035. * 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>
275 lines
8.9 KiB
TypeScript
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,
|
|
}
|
|
}
|