Files
supabase/apps/studio/components/interfaces/Settings/Logs/Logs.datePickerHelpers.ts
Charis d5436ae826 feat(studio): log date range domain + session logRange state (#48401)
## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

Feature (+ a small refactor and a docs/convention note). PR 4 of the
stacked SQL-editor query-source series (Database vs Logs).

## What is the current behavior?

The SQL editor has no representation of a logs query's time range:
`querySource.ts` only knows how to map a snippet type to a source
(`getSnippetSource`), and session state (`sql-editor-session-state.ts`)
tracks results and the row limit but not a per-snippet time range. The
Logs date picker's pure range helpers (`parseCustomInput`,
`generateDynamicHelper`, the `Unit` type) are trapped inside the
`Logs.DatePickers.tsx` React component.

## What is the new behavior?

- **Logs time-range domain** in `querySource.ts`: branded
`IsoDateTimeString` + `isoDateTimeString()`, `RelativeTimeUnit`, a
`LogDateRange` discriminated union (relative/absolute),
`DEFAULT_LOG_DATE_RANGE`, a single date-picker parser
(`datePickerValueToLogDateRange` / `logDateRangeToDatePickerValue` —
handles the five presets *and* dynamic `2h`/`30m` helpers; `calcTo ===
''` means "now"; unparseable helpers degrade to absolute), and
`resolveLogRunRange` which re-resolves relative ranges against `now` at
run time (reusing the existing `ResolvedLogDateRange` shape).
- **Session state**: per-snippet `logRange` + `setLogRange` —
session-only, never written to snippet content, so it works on read-only
shared snippets and is cleaned up in `clearForSnippet`.
- **Refactor**: extracted the picker's framework-free helpers into a new
pure `Logs.datePickerHelpers.ts`; the logs domain now shares the `Unit`
type and reuses `generateDynamicHelper` instead of duplicating them.
Importers point at the new module directly (no re-export shim). Hardened
the amount parse against `NaN`.
- **Full unit coverage** in `querySource.test.ts`. Recorded the no-shim
refactoring convention in the `studio-best-practices` skill.

Verification: `pnpm typecheck` clean, lint ratchet improved, 43 tests
pass (querySource + Logs.Datepickers), Prettier clean.

## Additional context

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Added robust Logs date-range modeling with support for relative (e.g.,
last N units) and absolute time periods.
  - SQL Editor sessions now remember log date ranges per snippet.
- **Bug Fixes**
- Safer handling of invalid or missing date inputs, with sensible
fallback to default/current time.
- **Tests**
- Added/expanded automated coverage for date-range conversion, helper
parsing, and resolution behavior.
- **Refactor**
- Centralized date-picker helper utilities for reuse across the Logs and
SQL query experience.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-28 13:49:11 -04:00

74 lines
2.5 KiB
TypeScript

import dayjs from 'dayjs'
import type { DatetimeHelper } from './Logs.types'
/**
* Framework-free date-range helpers shared by the Logs date picker
* (`Logs.DatePickers.tsx`) and the SQL editor's logs query-source domain
* (`SQLEditor/querySource.ts`). Kept in a pure `.ts` module — depending only on
* dayjs — so both a React component and a valtio-free domain module can import it
* without pulling in the picker's UI.
*/
/** The relative-range units the picker and the logs domain both speak. */
export type Unit = 'minute' | 'hour' | 'day'
export type ParsedCustomInput =
| { type: 'number'; value: number }
| { type: 'unit'; value: number; unit: Unit }
| { type: 'invalid' }
export const parseCustomInput = (input: string): ParsedCustomInput => {
const trimmed = input.trim().toLowerCase()
if (!trimmed) return { type: 'invalid' }
// Try to match "number + optional space + unit prefix"
const match = trimmed.match(/^(\d+)\s*([a-z]*)$/)
if (!match) return { type: 'invalid' }
const [, numStr, unitStr] = match
const value = Number.parseInt(numStr, 10)
// Only finite positive values may reach generateDynamicHelper(): Number.isFinite
// rejects NaN and Infinity outright, and the <= 0 guard keeps out non-positive.
if (!Number.isFinite(value) || value <= 0) return { type: 'invalid' }
if (!unitStr) {
return { type: 'number', value }
}
// Match if unitStr is a prefix of any unit name or its first letter
const units: Unit[] = ['minute', 'hour', 'day']
const matchedUnit = units.find((u) => u.startsWith(unitStr) || u[0] === unitStr)
if (!matchedUnit) return { type: 'invalid' }
return { type: 'unit', value, unit: matchedUnit }
}
export const generateDynamicHelper = (value: number, unit: Unit): DatetimeHelper => {
return {
text: `Last ${value} ${unit}${value === 1 ? '' : 's'}`,
calcFrom: () => dayjs().subtract(value, unit).toISOString(),
calcTo: () => dayjs().toISOString(),
}
}
export const generateDynamicHelpers = (value: number): DatetimeHelper[] => {
const units: Unit[] = ['minute', 'hour', 'day']
return units.map((unit) => generateDynamicHelper(value, unit))
}
export const generateHelpersFromInput = (input: string): DatetimeHelper[] | null => {
const parsed = parseCustomInput(input)
switch (parsed.type) {
case 'number':
return generateDynamicHelpers(parsed.value)
case 'unit':
return [generateDynamicHelper(parsed.value, parsed.unit)]
case 'invalid':
return null
}
}