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:
Charis authored and GitHub committed 2026-08-14 13:45:41 +07:00
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(),
])
+2 -2
View File
@@ -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
},