mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 09:55:06 +03:00
<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>
89 lines
2.9 KiB
TypeScript
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
|
|
)
|
|
})
|