From bb4777cbbac9f06c666de126020736d93d34c8d0 Mon Sep 17 00:00:00 2001 From: egeoztass Date: Thu, 16 Apr 2026 17:43:54 +0300 Subject: [PATCH 1/3] feat: add custom days input to time window picker Adds a "Last X days" input field to the time window dropdown, allowing users to quickly filter by any number of days (1-365) without opening the full calendar picker. The input calculates start/end dates and uses the existing custom range mechanism, so no backend changes are needed. Closes #168 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/time-window-picker.tsx | 54 +++++++++++++++++-- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/apps/start/src/components/time-window-picker.tsx b/apps/start/src/components/time-window-picker.tsx index e611e3e07..6c4249662 100644 --- a/apps/start/src/components/time-window-picker.tsx +++ b/apps/start/src/components/time-window-picker.tsx @@ -1,9 +1,10 @@ -import { timeWindows } from '@openpanel/constants'; +import { getDefaultIntervalByDates, timeWindows } from '@openpanel/constants'; import type { IChartRange, IInterval } from '@openpanel/validation'; import { bind } from 'bind-event-listener'; -import { endOfDay, format, startOfDay } from 'date-fns'; +import { endOfDay, format, startOfDay, subDays } from 'date-fns'; import { CalendarIcon } from 'lucide-react'; -import { useCallback, useEffect, useRef } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { Input } from '@/components/ui/input'; import { Button } from '@/components/ui/button'; import { DropdownMenu, @@ -44,6 +45,26 @@ export function TimeWindowPicker({ isDateRangerPickerOpen.current = open; }); const timeWindow = timeWindows[value ?? '30d']; + const [customDays, setCustomDays] = useState(''); + + const handleCustomDays = useCallback( + (days: number) => { + if (days < 1 || days > 365) return; + const end = endOfDay(new Date()); + const start = startOfDay(subDays(new Date(), days - 1)); + onStartDateChange(format(start, 'yyyy-MM-dd HH:mm:ss')); + onEndDateChange(format(end, 'yyyy-MM-dd HH:mm:ss')); + onChange('custom'); + const interval = getDefaultIntervalByDates( + start.toISOString(), + end.toISOString() + ); + if (interval) { + onIntervalChange(interval); + } + }, + [onChange, onStartDateChange, onEndDateChange, onIntervalChange] + ); const handleCustom = useCallback(() => { pushModal('DateRangerPicker', { @@ -194,6 +215,33 @@ export function TimeWindowPicker({ +
e.stopPropagation()} + onKeyDown={(e) => e.stopPropagation()} + > + Last + setCustomDays(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + const days = Number.parseInt(customDays, 10); + if (days >= 1 && days <= 365) { + handleCustomDays(days); + setCustomDays(''); + } + } + e.stopPropagation(); + }} + className="h-7 w-16 text-center" + /> + days +
handleCustom()}> {timeWindows.custom.label} From a7c7770759ca637d538742f77a78ed6c8a89e5fa Mon Sep 17 00:00:00 2001 From: egeoztass Date: Thu, 16 Apr 2026 17:51:12 +0300 Subject: [PATCH 2/3] fix: address CodeRabbit review feedback - Use single Date snapshot to avoid midnight drift - Add aria-label for accessibility - Add e.preventDefault() on Enter to prevent form submission Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/start/src/components/time-window-picker.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/start/src/components/time-window-picker.tsx b/apps/start/src/components/time-window-picker.tsx index 6c4249662..34b2e57e7 100644 --- a/apps/start/src/components/time-window-picker.tsx +++ b/apps/start/src/components/time-window-picker.tsx @@ -50,8 +50,9 @@ export function TimeWindowPicker({ const handleCustomDays = useCallback( (days: number) => { if (days < 1 || days > 365) return; - const end = endOfDay(new Date()); - const start = startOfDay(subDays(new Date(), days - 1)); + const now = new Date(); + const end = endOfDay(now); + const start = startOfDay(subDays(now, days - 1)); onStartDateChange(format(start, 'yyyy-MM-dd HH:mm:ss')); onEndDateChange(format(end, 'yyyy-MM-dd HH:mm:ss')); onChange('custom'); @@ -222,6 +223,7 @@ export function TimeWindowPicker({ > Last setCustomDays(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { + e.preventDefault(); const days = Number.parseInt(customDays, 10); if (days >= 1 && days <= 365) { handleCustomDays(days); From 81db11a8c3adda867c295c6e643c19a756de8f1e Mon Sep 17 00:00:00 2001 From: egeoztass Date: Thu, 16 Apr 2026 17:59:56 +0300 Subject: [PATCH 3/3] fix: use Number() instead of parseInt for robust input parsing Prevents truncation of edge-case inputs like 1e2 or 12.5 by using Number() with Number.isInteger() validation. Also adds step={1} to the input to hint integer-only values. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/start/src/components/time-window-picker.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/start/src/components/time-window-picker.tsx b/apps/start/src/components/time-window-picker.tsx index 34b2e57e7..3d0192ad8 100644 --- a/apps/start/src/components/time-window-picker.tsx +++ b/apps/start/src/components/time-window-picker.tsx @@ -227,14 +227,15 @@ export function TimeWindowPicker({ type="number" min={1} max={365} + step={1} placeholder="X" value={customDays} onChange={(e) => setCustomDays(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); - const days = Number.parseInt(customDays, 10); - if (days >= 1 && days <= 365) { + const days = Number(customDays); + if (Number.isInteger(days) && days >= 1 && days <= 365) { handleCustomDays(days); setCustomDays(''); }