mirror of
https://github.com/supabase/supabase.git
synced 2026-09-06 09:59:03 +08:00
Second of a stack. **Stacked on #49069** — review that one first; this PR's diff only makes sense on top of it. Base will retarget to `master` automatically when #49069 merges. Net `-72` lines. No behavior change beyond the one noted at the bottom. ## The problem The query-source registry carried its own `LogTimeRange` type and `logTimeRangeSchema`, which had drifted from the notebook wire schema's copy in four ways: | | wire schema | registry | |---|---|---| | discriminant | `_tag: 'relative_time_range'` | `type: 'relative'` | | absolute bounds | `start` / `end` | `from` / `to` | | relative units | minute…year | minute, hour, day | | validation | none | positive int, end-after-start | Two definitions of one concept, neither convertible to the other without a lossy mapping — and the notebook query cell was papering over it by discarding a log cell's persisted range and substituting a default. ## What changed #49069 moved the validations onto the wire schema's `timeRangeSchema` and exported it. This PR deletes the registry's copy and points every consumer at `TimeRange`. The registry keeps what is genuinely runtime: endpoints, labels, availability, defaults. The field renames ripple mechanically through the logs date-picker helpers, the time-range submenu, `useLogsCustomRange`, the SQL editor's session state, and their tests. Coverage for the absolute-range and unit rules moved to `notebook-schema.test.ts` in #49069, alongside the schema that now owns them. `ExplorerQuerySourceMenu` also drops its hand-rolled custom-range construction in favor of `customDateRangeToLogTimeRange`, which already existed and does the same clamping. ## One behavior change `logTimeRangeToDatePickerValue` now renders a range whose unit has no picker preset (week, month, year — allowed by the wire schema, not offered in the UI) as a resolved absolute range, instead of trying and failing to build a helper for it. Previously unreachable, since the registry's narrower unit set made those ranges unrepresentable. ## Verification Typecheck, Prettier, and the lint ratchet clean. 401 tests pass across the notebook schema, query sources, the logs source components, the SQL editor, and the Explorer surfaces. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved log time-range handling across Explorer and SQL Editor. * Custom date ranges now display and resolve correctly, including clamping invalid ranges. * Unsupported relative time units are converted to compatible absolute date-picker values. * **Refactor** * Standardized log queries on a shared time-range format for more consistent validation and behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
117 lines
3.9 KiB
TypeScript
117 lines
3.9 KiB
TypeScript
import dayjs from 'dayjs'
|
|
|
|
import {
|
|
generateDynamicHelper,
|
|
type Unit,
|
|
} from '@/components/interfaces/Settings/Logs/Logs.datePickerHelpers'
|
|
import type { DatePickerValue } from '@/components/interfaces/Settings/Logs/Logs.DatePickers'
|
|
import type { ResolvedLogDateRange } from '@/components/interfaces/Settings/Logs/logsDateRange'
|
|
import { type TimeRange } from '@/data/content/notebooks/notebook-schema'
|
|
import { DEFAULT_LOG_TIME_RANGE } from '@/data/query-sources/query-source-registry'
|
|
import { isoDateTimeString, type IsoDateTimeString } from '@/lib/iso-datetime'
|
|
|
|
export type RelativeTimeUnit = Extract<TimeRange, { _tag: 'relative_time_range' }>['unit']
|
|
|
|
/**
|
|
* The relative units the logs date picker can render as a helper preset. The wire schema
|
|
* allows coarser units (week, month, year) that the picker has no preset for; a range
|
|
* using one of those is shown as a resolved absolute range instead.
|
|
*/
|
|
const PICKER_TIME_UNITS: readonly Unit[] = ['minute', 'hour', 'day']
|
|
|
|
const isPickerUnit = (unit: RelativeTimeUnit): unit is Unit =>
|
|
(PICKER_TIME_UNITS as readonly RelativeTimeUnit[]).includes(unit)
|
|
|
|
/** `toISOString()` is valid ISO-8601 by construction, so it needs no re-validation. */
|
|
const toIsoDateTime = (value: dayjs.Dayjs): IsoDateTimeString =>
|
|
value.toISOString() as IsoDateTimeString
|
|
|
|
const nowIsoDateTime = (): IsoDateTimeString => toIsoDateTime(dayjs())
|
|
|
|
function parseRelativeHelperLabel(
|
|
text: string | undefined
|
|
): { amount: number; unit: RelativeTimeUnit } | null {
|
|
if (!text) return null
|
|
const match = text
|
|
.trim()
|
|
.toLowerCase()
|
|
.match(/^last\s+(?:(\d+)\s+)?(minute|hour|day)s?$/)
|
|
if (!match) return null
|
|
|
|
const amount = match[1] ? parseInt(match[1], 10) : 1
|
|
if (!Number.isFinite(amount) || amount <= 0) return null
|
|
|
|
const unit = match[2]
|
|
if (unit !== 'minute' && unit !== 'hour' && unit !== 'day') return null
|
|
return { amount, unit }
|
|
}
|
|
|
|
export function datePickerValueToLogTimeRange(value: DatePickerValue): TimeRange {
|
|
if (value.isHelper) {
|
|
const relative = parseRelativeHelperLabel(value.text)
|
|
if (relative) {
|
|
return { _tag: 'relative_time_range', amount: relative.amount, unit: relative.unit }
|
|
}
|
|
}
|
|
|
|
const start = isoDateTimeString(value.from)
|
|
if (start === null) return DEFAULT_LOG_TIME_RANGE
|
|
const end = isoDateTimeString(value.to) ?? nowIsoDateTime()
|
|
return { _tag: 'absolute_time_range', start, end }
|
|
}
|
|
|
|
export function logTimeRangeToDatePickerValue(range: TimeRange): DatePickerValue {
|
|
if (range._tag === 'relative_time_range' && isPickerUnit(range.unit)) {
|
|
const helper = generateDynamicHelper(range.amount, range.unit)
|
|
return {
|
|
from: helper.calcFrom(),
|
|
to: helper.calcTo(),
|
|
isHelper: true,
|
|
text: helper.text,
|
|
}
|
|
}
|
|
|
|
const resolved = resolveLogTimeRange(range)
|
|
return { from: resolved.from, to: resolved.to, isHelper: false }
|
|
}
|
|
|
|
export function customDateRangeToLogTimeRange({
|
|
from,
|
|
to,
|
|
now = new Date(),
|
|
}: {
|
|
from: Date
|
|
to: Date
|
|
now?: Date
|
|
}): Extract<TimeRange, { _tag: 'absolute_time_range' }> {
|
|
const nowValue = dayjs(now)
|
|
const requestedEnd = dayjs(to).endOf('day')
|
|
|
|
return {
|
|
_tag: 'absolute_time_range',
|
|
start: toIsoDateTime(dayjs(from).startOf('day')),
|
|
end: toIsoDateTime(requestedEnd.isAfter(nowValue) ? nowValue : requestedEnd),
|
|
}
|
|
}
|
|
|
|
export function logTimeRangesEqual(a: TimeRange, b: TimeRange): boolean {
|
|
if (a._tag === 'relative_time_range' && b._tag === 'relative_time_range') {
|
|
return a.amount === b.amount && a.unit === b.unit
|
|
}
|
|
if (a._tag === 'absolute_time_range' && b._tag === 'absolute_time_range') {
|
|
return a.start === b.start && a.end === b.end
|
|
}
|
|
return false
|
|
}
|
|
|
|
export function resolveLogTimeRange(range: TimeRange): ResolvedLogDateRange {
|
|
if (range._tag === 'relative_time_range') {
|
|
const now = dayjs()
|
|
return {
|
|
from: now.subtract(range.amount, range.unit).toISOString(),
|
|
to: now.toISOString(),
|
|
}
|
|
}
|
|
return { from: range.start, to: range.end }
|
|
}
|