Files
supabase/apps/studio/components/interfaces/Settings/Logs/Logs.datePickerHelpers.ts
Saxon FletcherandJoshen Lim 76c4f2b739 feat(studio): add time range to project logs filter bar (#50685)
<img width="1079" height="566" alt="image"
src="https://github.com/user-attachments/assets/b6f360e9-dfcf-4717-86db-1fc9acc4ae6a"
/>


## Problem

Project logs only exposed time-range selection through the sidebar.

## Solution

Add a Time range property to the filter bar using the sidebar’s picker,
preset labels, and date formatting. Keep it synchronized with the
sidebar and timeline, including retention checks.

## Review instructions

1. Open Project Logs and select **Time range** in the filter bar. Choose
a preset and confirm the sidebar and logs update.
2. Select a custom range and confirm its label matches the sidebar’s
date formatting.
3. Change the range in the sidebar or timeline and confirm the filter
bar updates. Remove the time-range pill and confirm the range resets
without removing other filters.
4. Select a range beyond the plan’s log retention and confirm the
upgrade prompt appears.

Validation: 67 focused tests passed, Studio typecheck passed, and
changed Studio files passed lint.

## Checklist

- [x] I have read
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
- [ ] Docs authoring skills (not applicable: no docs-site topic changes)

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

* **New Features**
* Added human-readable filter value labels while preserving the
underlying selections.
* Added dedicated log time-range filters with preset and custom ranges.
  * Added inline date-range picker support for flexible layouts.
* Added controls to show or hide filter properties based on
availability.
  * Added consistent date-range formatting and custom range support.

* **Bug Fixes**
  * Invalid or incomplete time-range filters are no longer applied.
  * Time-range filters remain separate from standard column filters.
* Clipboard interactions are limited to the popover date-picker variant.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
2026-09-24 09:29:59 +10:00

89 lines
2.9 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)
const exceedsDateRange = !dayjs().subtract(value, 'day').isValid()
if (!Number.isFinite(value) || value <= 0 || exceedsDateRange) 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
}
}
export const formatDateRange = (from: string, to: string): string =>
`${dayjs(from).format('DD MMM, HH:mm')} - ${dayjs(to || new Date()).format('DD MMM, HH:mm')}`
export const findMatchingDateHelper = (
from: Date,
to: Date,
helpers: DatetimeHelper[]
): DatetimeHelper | undefined =>
helpers.find((helper) => {
const tolerance = 60_000
return (
Math.abs(from.getTime() - new Date(helper.calcFrom()).getTime()) < tolerance &&
Math.abs(to.getTime() - new Date(helper.calcTo()).getTime()) < tolerance
)
})