mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
refactor(studio): borrow the wire schema's time range in the query-source registry (#49070)
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>
This commit is contained in:
1 parent
18dc7e971d
commit
c0e109f662
14 files changed
+124
-186
No files matched your search
@@ -56,7 +56,7 @@ describe('ExplorerQuerySourceMenu', () => {
|
||||
source={{
|
||||
id: 'logs',
|
||||
type: 'logs',
|
||||
parameters: { time_range: { type: 'relative', amount: 1, unit: 'hour' } },
|
||||
parameters: { time_range: { _tag: 'relative_time_range', amount: 1, unit: 'hour' } },
|
||||
}}
|
||||
onSourceChange={onSourceChange}
|
||||
/>
|
||||
@@ -80,7 +80,7 @@ describe('ExplorerQuerySourceMenu', () => {
|
||||
source={{
|
||||
id: 'logs',
|
||||
type: 'logs',
|
||||
parameters: { time_range: { type: 'relative', amount: 1, unit: 'hour' } },
|
||||
parameters: { time_range: { _tag: 'relative_time_range', amount: 1, unit: 'hour' } },
|
||||
}}
|
||||
onSourceChange={onSourceChange}
|
||||
/>
|
||||
@@ -93,7 +93,7 @@ describe('ExplorerQuerySourceMenu', () => {
|
||||
expect(onSourceChange).toHaveBeenCalledWith({
|
||||
id: 'logs',
|
||||
type: 'logs',
|
||||
parameters: { time_range: { type: 'relative', amount: 3, unit: 'hour' } },
|
||||
parameters: { time_range: { _tag: 'relative_time_range', amount: 3, unit: 'hour' } },
|
||||
})
|
||||
})
|
||||
|
||||
@@ -113,7 +113,7 @@ describe('ExplorerQuerySourceMenu', () => {
|
||||
{
|
||||
id: 'logs',
|
||||
type: 'logs',
|
||||
parameters: { time_range: { type: 'relative', amount: 1, unit: 'hour' } },
|
||||
parameters: { time_range: { _tag: 'relative_time_range', amount: 1, unit: 'hour' } },
|
||||
},
|
||||
{ sqlEditorLogsSource: false, otelLegacyLogs: false }
|
||||
)
|
||||
|
||||
@@ -48,7 +48,7 @@ const createDraft = (
|
||||
id: 'logs'
|
||||
type: 'logs'
|
||||
parameters: {
|
||||
time_range: { type: 'relative'; amount: number; unit: 'hour' }
|
||||
time_range: { _tag: 'relative_time_range'; amount: number; unit: 'hour' }
|
||||
}
|
||||
}
|
||||
) => {
|
||||
@@ -85,7 +85,7 @@ describe('QueryTab execution', () => {
|
||||
createDraft({
|
||||
id: 'logs',
|
||||
type: 'logs',
|
||||
parameters: { time_range: { type: 'relative', amount: 1, unit: 'hour' } },
|
||||
parameters: { time_range: { _tag: 'relative_time_range', amount: 1, unit: 'hour' } },
|
||||
})
|
||||
const requests: Request[] = []
|
||||
addAPIMock({
|
||||
@@ -154,7 +154,7 @@ describe('QueryTab execution', () => {
|
||||
createDraft({
|
||||
id: 'logs',
|
||||
type: 'logs',
|
||||
parameters: { time_range: { type: 'relative', amount: 2, unit: 'hour' } },
|
||||
parameters: { time_range: { _tag: 'relative_time_range', amount: 2, unit: 'hour' } },
|
||||
})
|
||||
const bodies: Array<{ iso_timestamp_start: string; iso_timestamp_end: string }> = []
|
||||
addAPIMock({
|
||||
|
||||
@@ -9,10 +9,12 @@ import {
|
||||
resolveLogTimeRange,
|
||||
} from './LogTimeRange.utils'
|
||||
import { generateDynamicHelper } from '@/components/interfaces/Settings/Logs/Logs.datePickerHelpers'
|
||||
import {
|
||||
DEFAULT_LOG_TIME_RANGE,
|
||||
type LogTimeRange,
|
||||
} from '@/data/query-sources/query-source-registry'
|
||||
import { timeRangeSchema, type TimeRange } from '@/data/content/notebooks/notebook-schema'
|
||||
import { DEFAULT_LOG_TIME_RANGE } from '@/data/query-sources/query-source-registry'
|
||||
|
||||
/** Absolute bounds are branded ISO strings, so build them through the schema. */
|
||||
const absolute = (start: string, end: string): TimeRange =>
|
||||
timeRangeSchema.parse({ _tag: 'absolute_time_range', start, end })
|
||||
|
||||
describe('LogTimeRange.utils', () => {
|
||||
beforeEach(() => {
|
||||
@@ -34,12 +36,22 @@ describe('LogTimeRange.utils', () => {
|
||||
isHelper: true,
|
||||
text: helper.text,
|
||||
}
|
||||
const range: LogTimeRange = { type: 'relative', amount, unit }
|
||||
const range: TimeRange = { _tag: 'relative_time_range', amount, unit }
|
||||
|
||||
expect(datePickerValueToLogTimeRange(pickerValue)).toEqual(range)
|
||||
expect(logTimeRangeToDatePickerValue(range)).toEqual(pickerValue)
|
||||
})
|
||||
|
||||
it('renders a relative unit the picker has no preset for as a resolved absolute range', () => {
|
||||
expect(
|
||||
logTimeRangeToDatePickerValue({ _tag: 'relative_time_range', amount: 2, unit: 'month' })
|
||||
).toEqual({
|
||||
from: dayjs().subtract(2, 'month').toISOString(),
|
||||
to: dayjs().toISOString(),
|
||||
isHelper: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('falls back to the default when a custom value has no valid start', () => {
|
||||
expect(datePickerValueToLogTimeRange({ from: '', to: '', isHelper: false })).toEqual(
|
||||
DEFAULT_LOG_TIME_RANGE
|
||||
@@ -55,47 +67,46 @@ describe('LogTimeRange.utils', () => {
|
||||
text: 'Custom',
|
||||
})
|
||||
).toEqual({
|
||||
type: 'absolute',
|
||||
from: '2025-01-01T00:00:00.000Z',
|
||||
to: '2025-01-08T12:00:00.000Z',
|
||||
_tag: 'absolute_time_range',
|
||||
start: '2025-01-01T00:00:00.000Z',
|
||||
end: '2025-01-08T12:00:00.000Z',
|
||||
})
|
||||
})
|
||||
|
||||
it('compares relative and absolute ranges structurally', () => {
|
||||
expect(
|
||||
logTimeRangesEqual(
|
||||
{ type: 'relative', amount: 1, unit: 'hour' },
|
||||
{ type: 'relative', amount: 1, unit: 'hour' }
|
||||
{ _tag: 'relative_time_range', amount: 1, unit: 'hour' },
|
||||
{ _tag: 'relative_time_range', amount: 1, unit: 'hour' }
|
||||
)
|
||||
).toBe(true)
|
||||
expect(
|
||||
logTimeRangesEqual(
|
||||
{ type: 'relative', amount: 1, unit: 'hour' },
|
||||
{ type: 'relative', amount: 1, unit: 'day' }
|
||||
{ _tag: 'relative_time_range', amount: 1, unit: 'hour' },
|
||||
{ _tag: 'relative_time_range', amount: 1, unit: 'day' }
|
||||
)
|
||||
).toBe(false)
|
||||
expect(
|
||||
logTimeRangesEqual(
|
||||
{ type: 'absolute', from: '2025-01-01T00:00:00.000Z', to: '2025-01-02T00:00:00.000Z' },
|
||||
{ type: 'absolute', from: '2025-01-01T00:00:00.000Z', to: '2025-01-02T00:00:00.000Z' }
|
||||
absolute('2025-01-01T00:00:00.000Z', '2025-01-02T00:00:00.000Z'),
|
||||
absolute('2025-01-01T00:00:00.000Z', '2025-01-02T00:00:00.000Z')
|
||||
)
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('resolves a relative range against the current time', () => {
|
||||
expect(resolveLogTimeRange({ type: 'relative', amount: 2, unit: 'day' })).toEqual({
|
||||
expect(resolveLogTimeRange({ _tag: 'relative_time_range', amount: 2, unit: 'day' })).toEqual({
|
||||
from: dayjs().subtract(2, 'day').toISOString(),
|
||||
to: dayjs().toISOString(),
|
||||
})
|
||||
})
|
||||
|
||||
it('passes an absolute range through unchanged', () => {
|
||||
const range: LogTimeRange = {
|
||||
type: 'absolute',
|
||||
const range = absolute('2025-01-01T00:00:00.000Z', '2025-01-02T00:00:00.000Z')
|
||||
expect(resolveLogTimeRange(range)).toEqual({
|
||||
from: '2025-01-01T00:00:00.000Z',
|
||||
to: '2025-01-02T00:00:00.000Z',
|
||||
}
|
||||
expect(resolveLogTimeRange(range)).toEqual({ from: range.from, to: range.to })
|
||||
})
|
||||
})
|
||||
|
||||
it('clamps a custom range ending today to now', () => {
|
||||
@@ -106,9 +117,9 @@ describe('LogTimeRange.utils', () => {
|
||||
to: new Date('2025-01-08T09:00:00.000Z'),
|
||||
})
|
||||
).toEqual({
|
||||
type: 'absolute',
|
||||
from: dayjs(from).startOf('day').toISOString(),
|
||||
to: '2025-01-08T12:00:00.000Z',
|
||||
_tag: 'absolute_time_range',
|
||||
start: dayjs(from).startOf('day').toISOString(),
|
||||
end: '2025-01-08T12:00:00.000Z',
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,17 +1,32 @@
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
import { generateDynamicHelper } from '@/components/interfaces/Settings/Logs/Logs.datePickerHelpers'
|
||||
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 {
|
||||
DEFAULT_LOG_TIME_RANGE,
|
||||
type LogTimeRange,
|
||||
} from '@/data/query-sources/query-source-registry'
|
||||
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<LogTimeRange, { type: 'relative' }>['unit']
|
||||
export type RelativeTimeUnit = Extract<TimeRange, { _tag: 'relative_time_range' }>['unit']
|
||||
|
||||
const nowIsoDateTime = (): IsoDateTimeString => dayjs().toISOString() as IsoDateTimeString
|
||||
/**
|
||||
* 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
|
||||
@@ -31,20 +46,22 @@ function parseRelativeHelperLabel(
|
||||
return { amount, unit }
|
||||
}
|
||||
|
||||
export function datePickerValueToLogTimeRange(value: DatePickerValue): LogTimeRange {
|
||||
export function datePickerValueToLogTimeRange(value: DatePickerValue): TimeRange {
|
||||
if (value.isHelper) {
|
||||
const relative = parseRelativeHelperLabel(value.text)
|
||||
if (relative) return { type: 'relative', ...relative }
|
||||
if (relative) {
|
||||
return { _tag: 'relative_time_range', amount: relative.amount, unit: relative.unit }
|
||||
}
|
||||
}
|
||||
|
||||
const from = isoDateTimeString(value.from)
|
||||
if (from === null) return DEFAULT_LOG_TIME_RANGE
|
||||
const to = isoDateTimeString(value.to) ?? nowIsoDateTime()
|
||||
return { type: 'absolute', from, to }
|
||||
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: LogTimeRange): DatePickerValue {
|
||||
if (range.type === 'relative') {
|
||||
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(),
|
||||
@@ -53,7 +70,9 @@ export function logTimeRangeToDatePickerValue(range: LogTimeRange): DatePickerVa
|
||||
text: helper.text,
|
||||
}
|
||||
}
|
||||
return { from: range.from, to: range.to, isHelper: false }
|
||||
|
||||
const resolved = resolveLogTimeRange(range)
|
||||
return { from: resolved.from, to: resolved.to, isHelper: false }
|
||||
}
|
||||
|
||||
export function customDateRangeToLogTimeRange({
|
||||
@@ -64,34 +83,34 @@ export function customDateRangeToLogTimeRange({
|
||||
from: Date
|
||||
to: Date
|
||||
now?: Date
|
||||
}): Extract<LogTimeRange, { type: 'absolute' }> {
|
||||
}): Extract<TimeRange, { _tag: 'absolute_time_range' }> {
|
||||
const nowValue = dayjs(now)
|
||||
const requestedTo = dayjs(to).endOf('day')
|
||||
const requestedEnd = dayjs(to).endOf('day')
|
||||
|
||||
return {
|
||||
type: 'absolute',
|
||||
from: dayjs(from).startOf('day').toISOString(),
|
||||
to: requestedTo.isAfter(nowValue) ? nowValue.toISOString() : requestedTo.toISOString(),
|
||||
_tag: 'absolute_time_range',
|
||||
start: toIsoDateTime(dayjs(from).startOf('day')),
|
||||
end: toIsoDateTime(requestedEnd.isAfter(nowValue) ? nowValue : requestedEnd),
|
||||
}
|
||||
}
|
||||
|
||||
export function logTimeRangesEqual(a: LogTimeRange, b: LogTimeRange): boolean {
|
||||
if (a.type === 'relative' && b.type === 'relative') {
|
||||
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.type === 'absolute' && b.type === 'absolute') {
|
||||
return a.from === b.from && a.to === b.to
|
||||
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: LogTimeRange): ResolvedLogDateRange {
|
||||
if (range.type === 'relative') {
|
||||
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.from, to: range.to }
|
||||
return { from: range.start, to: range.end }
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from 'ui'
|
||||
import { describe, expect, it, vi, type Mock } from 'vitest'
|
||||
|
||||
import { LogsTimeRangeSubMenu } from './LogsTimeRangeSubMenu'
|
||||
import type { LogTimeRange } from '@/data/query-sources/query-source-registry'
|
||||
import { type TimeRange } from '@/data/content/notebooks/notebook-schema'
|
||||
import { customRender } from '@/tests/lib/custom-render'
|
||||
|
||||
mockAnimationsApi()
|
||||
@@ -15,15 +15,15 @@ vi.mock('@/hooks/misc/useCheckEntitlements', () => ({
|
||||
}))
|
||||
|
||||
const renderSubMenu = ({
|
||||
onRangeChange = vi.fn<(range: LogTimeRange) => void>(),
|
||||
onRangeChange = vi.fn<(range: TimeRange) => void>(),
|
||||
onOpenCustomRange = vi.fn<() => void>(),
|
||||
onShowUpgrade = vi.fn<() => void>(),
|
||||
range = { type: 'relative', amount: 1, unit: 'hour' } as LogTimeRange,
|
||||
range = { _tag: 'relative_time_range', amount: 1, unit: 'hour' } as TimeRange,
|
||||
}: {
|
||||
onRangeChange?: Mock<(range: LogTimeRange) => void>
|
||||
onRangeChange?: Mock<(range: TimeRange) => void>
|
||||
onOpenCustomRange?: Mock<() => void>
|
||||
onShowUpgrade?: Mock<() => void>
|
||||
range?: LogTimeRange
|
||||
range?: TimeRange
|
||||
} = {}) => {
|
||||
customRender(
|
||||
<DropdownMenu defaultOpen>
|
||||
@@ -63,7 +63,7 @@ describe('LogsTimeRangeSubMenu', () => {
|
||||
})
|
||||
|
||||
it('marks the structurally matching preset as selected', async () => {
|
||||
renderSubMenu({ range: { type: 'relative', amount: 3, unit: 'hour' } })
|
||||
renderSubMenu({ range: { _tag: 'relative_time_range', amount: 3, unit: 'hour' } })
|
||||
|
||||
await userEvent.hover(await screen.findByText('Time range'))
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { datePickerValueToLogTimeRange, logTimeRangesEqual } from './LogTimeRange.utils'
|
||||
import { EXPLORER_DATEPICKER_HELPERS } from '@/components/interfaces/Settings/Logs/Logs.constants'
|
||||
import { maybeShowUpgradePromptIfNotEntitled } from '@/components/interfaces/Settings/Logs/Logs.utils'
|
||||
import type { LogTimeRange } from '@/data/query-sources/query-source-registry'
|
||||
import type { TimeRange } from '@/data/content/notebooks/notebook-schema'
|
||||
import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
|
||||
|
||||
export const LogsTimeRangeSubMenu = ({
|
||||
@@ -20,15 +20,15 @@ export const LogsTimeRangeSubMenu = ({
|
||||
onOpenCustomRange,
|
||||
onShowUpgrade,
|
||||
}: {
|
||||
range: LogTimeRange
|
||||
onRangeChange: (range: LogTimeRange) => void
|
||||
range: TimeRange
|
||||
onRangeChange: (range: TimeRange) => void
|
||||
onOpenCustomRange: () => void
|
||||
onShowUpgrade: () => void
|
||||
}) => {
|
||||
const { getEntitlementNumericValue } = useCheckEntitlements('log.retention_days')
|
||||
const entitledToLogDays = getEntitlementNumericValue()
|
||||
|
||||
const isCustomRange = range.type === 'absolute'
|
||||
const isCustomRange = range._tag === 'absolute_time_range'
|
||||
const presets = EXPLORER_DATEPICKER_HELPERS.map((helper) => ({
|
||||
helper,
|
||||
range: datePickerValueToLogTimeRange({
|
||||
@@ -47,7 +47,7 @@ export const LogsTimeRangeSubMenu = ({
|
||||
<span>Time range</span>
|
||||
<span className="text-foreground-lighter text-xs">
|
||||
{isCustomRange
|
||||
? `${dayjs(range.from).format('DD MMM, HH:mm')} - ${dayjs(range.to).format('DD MMM, HH:mm')}`
|
||||
? `${dayjs(range.start).format('DD MMM, HH:mm')} - ${dayjs(range.end).format('DD MMM, HH:mm')}`
|
||||
: (selectedPreset?.helper.text ?? 'Custom range')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -2,13 +2,13 @@ import { useState } from 'react'
|
||||
|
||||
import { customDateRangeToLogTimeRange } from './LogTimeRange.utils'
|
||||
import { maybeShowUpgradePromptIfNotEntitled } from '@/components/interfaces/Settings/Logs/Logs.utils'
|
||||
import type { LogTimeRange } from '@/data/query-sources/query-source-registry'
|
||||
import type { TimeRange } from '@/data/content/notebooks/notebook-schema'
|
||||
import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
|
||||
|
||||
export function useLogsCustomRange({
|
||||
onRangeChange,
|
||||
}: {
|
||||
onRangeChange: (range: LogTimeRange) => void
|
||||
onRangeChange: (range: TimeRange) => void
|
||||
}) {
|
||||
const [isCustomRangeOpen, setIsCustomRangeOpen] = useState(false)
|
||||
const [showUpgradePrompt, setShowUpgradePrompt] = useState(false)
|
||||
@@ -17,7 +17,7 @@ export function useLogsCustomRange({
|
||||
|
||||
const handleApplyCustomRange = ({ from, to }: { from: Date; to: Date }) => {
|
||||
const range = customDateRangeToLogTimeRange({ from, to })
|
||||
if (maybeShowUpgradePromptIfNotEntitled(range.from, entitledToLogDays)) {
|
||||
if (maybeShowUpgradePromptIfNotEntitled(range.start, entitledToLogDays)) {
|
||||
setShowUpgradePrompt(true)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { type TimeRange } from '@/data/content/notebooks/notebook-schema'
|
||||
import type { Snippet } from '@/data/content/sql-folders-query'
|
||||
import { type LogTimeRange, type QuerySourceId } from '@/data/query-sources/query-source-registry'
|
||||
import { type QuerySourceId } from '@/data/query-sources/query-source-registry'
|
||||
|
||||
/**
|
||||
* Domain view of where a snippet's query runs. Derived from the content TYPE:
|
||||
@@ -61,4 +62,4 @@ export function resolveSnippetSource(
|
||||
* time range (session state, re-resolved at every run); a database run needs
|
||||
* nothing beyond the connection the execution pipeline already resolves.
|
||||
*/
|
||||
export type QuerySource = { type: 'database' } | { type: 'logs'; dateRange: LogTimeRange }
|
||||
export type QuerySource = { type: 'database' } | { type: 'logs'; dateRange: TimeRange }
|
||||
@@ -99,7 +99,7 @@ describe('useLogsSqlExecution', () => {
|
||||
it('resolves a relative session range to a from/to window around now', async () => {
|
||||
const captured = mockLogsAllOtel([])
|
||||
sqlEditorSessionState.setLogRange(SNIPPET_ID, {
|
||||
type: 'relative',
|
||||
_tag: 'relative_time_range',
|
||||
amount: 2,
|
||||
unit: 'hour',
|
||||
})
|
||||
|
||||
@@ -37,17 +37,13 @@ describe('useRunSource', () => {
|
||||
it('resolves a logs snippet to its session-stored range when one is set', () => {
|
||||
const id = 'logs-snippet-custom-range'
|
||||
seedSnippet({ id, source: 'logs' })
|
||||
sqlEditorSessionState.setLogRange(id, {
|
||||
type: 'relative',
|
||||
amount: 2,
|
||||
unit: 'hour',
|
||||
})
|
||||
sqlEditorSessionState.setLogRange(id, { _tag: 'relative_time_range', amount: 2, unit: 'hour' })
|
||||
|
||||
const { result } = renderSqlEditorHook(() => useRunSource(id))
|
||||
|
||||
expect(result.current).toEqual({
|
||||
type: 'logs',
|
||||
dateRange: { type: 'relative', amount: 2, unit: 'hour' },
|
||||
dateRange: { _tag: 'relative_time_range', amount: 2, unit: 'hour' },
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
cellSourceSchema,
|
||||
createDefaultCellSource,
|
||||
getQuerySource,
|
||||
logTimeRangeSchema,
|
||||
QUERY_SOURCES,
|
||||
} from './query-source-registry'
|
||||
|
||||
@@ -25,7 +24,7 @@ describe('query source registry', () => {
|
||||
id: 'logs',
|
||||
type: 'logs',
|
||||
parameters: {
|
||||
time_range: { type: 'relative', amount: 1, unit: 'hour' },
|
||||
time_range: { _tag: 'relative_time_range', amount: 1, unit: 'hour' },
|
||||
},
|
||||
})
|
||||
expect(first.parameters.time_range).not.toBe(second.parameters.time_range)
|
||||
@@ -49,7 +48,7 @@ describe('query source registry', () => {
|
||||
cellSourceSchema.parse({
|
||||
id: 'database',
|
||||
type: 'database',
|
||||
parameters: { time_range: { type: 'relative', amount: 1, unit: 'hour' } },
|
||||
parameters: { time_range: { _tag: 'relative_time_range', amount: 1, unit: 'hour' } },
|
||||
})
|
||||
).toThrow()
|
||||
|
||||
@@ -57,60 +56,24 @@ describe('query source registry', () => {
|
||||
cellSourceSchema.parse({
|
||||
id: 'logs',
|
||||
type: 'logs',
|
||||
parameters: { time_range: { type: 'relative', amount: 2, unit: 'week' } },
|
||||
parameters: { time_range: { _tag: 'relative_time_range', amount: 2, unit: 'fortnight' } },
|
||||
})
|
||||
).toThrow()
|
||||
})
|
||||
|
||||
it('rejects absolute ranges that do not move forward in time', () => {
|
||||
expect(
|
||||
logTimeRangeSchema.safeParse({
|
||||
type: 'absolute',
|
||||
from: '2025-01-01T00:00:00.000Z',
|
||||
to: '2025-01-02T00:00:00.000Z',
|
||||
}).success
|
||||
).toBe(true)
|
||||
|
||||
const equal = logTimeRangeSchema.safeParse({
|
||||
type: 'absolute',
|
||||
from: '2025-01-01T00:00:00.000Z',
|
||||
to: '2025-01-01T00:00:00.000Z',
|
||||
})
|
||||
expect(equal.success).toBe(false)
|
||||
expect(equal.error?.issues[0].path).toEqual(['to'])
|
||||
|
||||
expect(
|
||||
logTimeRangeSchema.safeParse({
|
||||
type: 'absolute',
|
||||
from: '2025-01-02T00:00:00.000Z',
|
||||
to: '2025-01-01T00:00:00.000Z',
|
||||
}).success
|
||||
).toBe(false)
|
||||
|
||||
it('refuses a cell source carrying an invalid absolute range', () => {
|
||||
expect(() =>
|
||||
cellSourceSchema.parse({
|
||||
id: 'logs',
|
||||
type: 'logs',
|
||||
parameters: {
|
||||
time_range: {
|
||||
type: 'absolute',
|
||||
from: '2025-01-02T00:00:00.000Z',
|
||||
to: '2025-01-01T00:00:00.000Z',
|
||||
_tag: 'absolute_time_range',
|
||||
start: '2025-01-02T00:00:00.000Z',
|
||||
end: '2025-01-01T00:00:00.000Z',
|
||||
},
|
||||
},
|
||||
})
|
||||
).toThrow()
|
||||
})
|
||||
|
||||
it('reports an invalid endpoint against its own field rather than the ordering rule', () => {
|
||||
const result = logTimeRangeSchema.safeParse({
|
||||
type: 'absolute',
|
||||
from: 'not-a-date',
|
||||
to: '2025-01-01T00:00:00.000Z',
|
||||
})
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
expect(result.error?.issues).toHaveLength(1)
|
||||
expect(result.error?.issues[0].path).toEqual(['from'])
|
||||
})
|
||||
})
|
||||
@@ -1,20 +1,7 @@
|
||||
import dayjs from 'dayjs'
|
||||
import * as z from 'zod'
|
||||
|
||||
import { timeRangeSchema, type TimeRange } from '@/data/content/notebooks/notebook-schema'
|
||||
import { logsAllEndpointUrl } from '@/data/logs/logs-endpoint'
|
||||
import { isoDateTimeString } from '@/lib/iso-datetime'
|
||||
|
||||
export type LogTimeRange =
|
||||
| {
|
||||
type: 'relative'
|
||||
amount: number
|
||||
unit: 'minute' | 'hour' | 'day'
|
||||
}
|
||||
| {
|
||||
type: 'absolute'
|
||||
from: string
|
||||
to: string
|
||||
}
|
||||
|
||||
export type DatabaseSource = {
|
||||
id: 'database'
|
||||
@@ -35,7 +22,7 @@ export type LogsSource = {
|
||||
type: 'logs'
|
||||
endpoint: ReturnType<typeof logsAllEndpointUrl>
|
||||
parameters: {
|
||||
time_range: LogTimeRange
|
||||
time_range: TimeRange
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,8 +33,8 @@ export type CellSourceOf<S extends Source> = Pick<S, 'type' | 'id' | 'parameters
|
||||
|
||||
export type CellSource = CellSourceOf<DatabaseSource> | CellSourceOf<LogsSource>
|
||||
|
||||
export const DEFAULT_LOG_TIME_RANGE: LogTimeRange = {
|
||||
type: 'relative',
|
||||
export const DEFAULT_LOG_TIME_RANGE: TimeRange = {
|
||||
_tag: 'relative_time_range',
|
||||
amount: 1,
|
||||
unit: 'hour',
|
||||
}
|
||||
@@ -76,45 +63,6 @@ export const QUERY_SOURCE_LABELS: Record<QuerySourceId, string> = {
|
||||
logs: 'Logs',
|
||||
}
|
||||
|
||||
const isoDateTimeSchema = z.string().refine((value) => isoDateTimeString(value) !== null, {
|
||||
message: 'must be a valid ISO-8601 datetime',
|
||||
})
|
||||
|
||||
export const logTimeRangeSchema = z
|
||||
.discriminatedUnion('type', [
|
||||
z
|
||||
.object({
|
||||
type: z.literal('relative'),
|
||||
amount: z.number().int().positive(),
|
||||
unit: z.enum(['minute', 'hour', 'day']),
|
||||
})
|
||||
.strict(),
|
||||
z
|
||||
.object({
|
||||
type: z.literal('absolute'),
|
||||
from: isoDateTimeSchema,
|
||||
to: isoDateTimeSchema,
|
||||
})
|
||||
.strict(),
|
||||
])
|
||||
.refine(
|
||||
(range) => {
|
||||
if (range.type !== 'absolute') return true
|
||||
|
||||
const from = dayjs(range.from)
|
||||
const to = dayjs(range.to)
|
||||
// An unparseable endpoint is already reported against its own field; the
|
||||
// ordering rule stays quiet so it doesn't add a second, misleading issue.
|
||||
if (!from.isValid() || !to.isValid()) return true
|
||||
|
||||
return to.isAfter(from)
|
||||
},
|
||||
{
|
||||
message: 'must be later than the start of the range',
|
||||
path: ['to'],
|
||||
}
|
||||
)
|
||||
|
||||
export const cellSourceSchema = z.discriminatedUnion('type', [
|
||||
z
|
||||
.object({
|
||||
@@ -127,7 +75,7 @@ export const cellSourceSchema = z.discriminatedUnion('type', [
|
||||
.object({
|
||||
id: z.literal('logs'),
|
||||
type: z.literal('logs'),
|
||||
parameters: z.object({ time_range: logTimeRangeSchema }).strict(),
|
||||
parameters: z.object({ time_range: timeRangeSchema }).strict(),
|
||||
})
|
||||
.strict(),
|
||||
])
|
||||
|
||||
@@ -51,7 +51,7 @@ describe('explorer query drafts', () => {
|
||||
source: {
|
||||
id: 'logs',
|
||||
type: 'logs',
|
||||
parameters: { time_range: { type: 'relative', amount: 3, unit: 'hour' } },
|
||||
parameters: { time_range: { _tag: 'relative_time_range', amount: 3, unit: 'hour' } },
|
||||
},
|
||||
})
|
||||
|
||||
@@ -62,7 +62,7 @@ describe('explorer query drafts', () => {
|
||||
expect(restored.drafts['query-1'].source).toEqual({
|
||||
id: 'logs',
|
||||
type: 'logs',
|
||||
parameters: { time_range: { type: 'relative', amount: 3, unit: 'hour' } },
|
||||
parameters: { time_range: { _tag: 'relative_time_range', amount: 3, unit: 'hour' } },
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { proxy, ref, snapshot, useSnapshot } from 'valtio'
|
||||
|
||||
import type { LogTimeRange } from '@/data/query-sources/query-source-registry'
|
||||
import type { TimeRange } from '@/data/content/notebooks/notebook-schema'
|
||||
|
||||
/**
|
||||
* Ephemeral, per-session SQL editor state that is NOT persisted: query results,
|
||||
@@ -37,9 +37,9 @@ export const sqlEditorSessionState = proxy({
|
||||
* and resets on reload. An unset snippet has no entry; read sites fall back to
|
||||
* `DEFAULT_LOG_TIME_RANGE`.
|
||||
*/
|
||||
logRange: {} as { [snippetId: string]: LogTimeRange },
|
||||
logRange: {} as { [snippetId: string]: TimeRange },
|
||||
|
||||
setLogRange: (id: string, range: LogTimeRange) => {
|
||||
setLogRange: (id: string, range: TimeRange) => {
|
||||
sqlEditorSessionState.logRange[id] = range
|
||||
},
|
||||
|
||||
|
||||
Reference in new issue
Block a user