mirror of
https://github.com/supabase/supabase.git
synced 2026-09-07 02:20:52 +08:00
## 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 -->
74 lines
2.5 KiB
TypeScript
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
|
|
}
|
|
}
|