@@ -95,6 +95,21 @@ const timePeriods = [
9595 } ,
9696] ;
9797
98+ const timeUnits = [
99+ { label : "minutes" , value : "m" , singular : "minute" } ,
100+ { label : "hours" , value : "h" , singular : "hour" } ,
101+ { label : "days" , value : "d" , singular : "day" } ,
102+ ] ;
103+
104+ // Parse a period string (e.g., "90m", "2h", "7d") into value and unit
105+ function parsePeriodString ( period : string ) : { value : number ; unit : string } | null {
106+ const match = period . match ( / ^ ( \d + ) ( [ m h d ] ) $ / ) ;
107+ if ( match ) {
108+ return { value : parseInt ( match [ 1 ] , 10 ) , unit : match [ 2 ] } ;
109+ }
110+ return null ;
111+ }
112+
98113const defaultPeriod = "7d" ;
99114const defaultPeriodMs = parse ( defaultPeriod ) ;
100115if ( ! defaultPeriodMs ) {
@@ -175,9 +190,29 @@ export function timeFilterRenderValues({
175190
176191 let valueLabel : ReactNode ;
177192 switch ( rangeType ) {
178- case "period" :
179- valueLabel = timePeriods . find ( ( t ) => t . value === period ) ?. label ?? period ?? defaultPeriod ;
193+ case "period" : {
194+ // First check if it's a preset period
195+ const preset = timePeriods . find ( ( t ) => t . value === period ) ;
196+ if ( preset ) {
197+ valueLabel = preset . label ;
198+ } else if ( period ) {
199+ // Parse custom period and format nicely (e.g., "90m" -> "90 mins")
200+ const parsed = parsePeriodString ( period ) ;
201+ if ( parsed ) {
202+ const unit = timeUnits . find ( ( u ) => u . value === parsed . unit ) ;
203+ if ( unit ) {
204+ valueLabel = `${ parsed . value } ${ parsed . value === 1 ? unit . singular : unit . label } ` ;
205+ } else {
206+ valueLabel = period ;
207+ }
208+ } else {
209+ valueLabel = period ;
210+ }
211+ } else {
212+ valueLabel = defaultPeriod ;
213+ }
180214 break ;
215+ }
181216 case "range" :
182217 valueLabel = (
183218 < span >
@@ -237,6 +272,17 @@ export function TimeFilter() {
237272 ) ;
238273}
239274
275+ // Get initial custom duration state from a period string
276+ function getInitialCustomDuration ( period ?: string ) : { value : string ; unit : string } {
277+ if ( period ) {
278+ const parsed = parsePeriodString ( period ) ;
279+ if ( parsed ) {
280+ return { value : parsed . value . toString ( ) , unit : parsed . unit } ;
281+ }
282+ }
283+ return { value : "" , unit : "m" } ;
284+ }
285+
240286export function TimeDropdown ( {
241287 trigger,
242288 period,
@@ -253,7 +299,12 @@ export function TimeDropdown({
253299 const [ fromValue , setFromValue ] = useState ( from ) ;
254300 const [ toValue , setToValue ] = useState ( to ) ;
255301
256- const apply = useCallback ( ( ) => {
302+ // Custom duration state
303+ const initialCustom = getInitialCustomDuration ( period ) ;
304+ const [ customValue , setCustomValue ] = useState ( initialCustom . value ) ;
305+ const [ customUnit , setCustomUnit ] = useState ( initialCustom . unit ) ;
306+
307+ const applyDateRange = useCallback ( ( ) => {
257308 replace ( {
258309 period : undefined ,
259310 cursor : undefined ,
@@ -283,6 +334,20 @@ export function TimeDropdown({
283334 [ replace ]
284335 ) ;
285336
337+ const applyCustomDuration = useCallback ( ( ) => {
338+ const value = parseInt ( customValue , 10 ) ;
339+ if ( isNaN ( value ) || value <= 0 ) {
340+ return ;
341+ }
342+ const periodString = `${ value } ${ customUnit } ` ;
343+ handlePeriodClick ( periodString ) ;
344+ } , [ customValue , customUnit , handlePeriodClick ] ) ;
345+
346+ const isCustomDurationValid = ( ( ) => {
347+ const value = parseInt ( customValue , 10 ) ;
348+ return ! isNaN ( value ) && value > 0 ;
349+ } ) ( ) ;
350+
286351 return (
287352 < SelectProvider virtualFocus = { true } open = { open } setOpen = { setOpen } >
288353 { trigger }
@@ -318,7 +383,50 @@ export function TimeDropdown({
318383 </ div >
319384 </ div >
320385
321- < div className = "flex flex-col gap-4" >
386+ < div className = "flex flex-col gap-1" >
387+ < Label > Custom duration</ Label >
388+ < div className = "flex items-center gap-2" >
389+ < input
390+ type = "number"
391+ min = "1"
392+ placeholder = "e.g. 90"
393+ value = { customValue }
394+ onChange = { ( e ) => setCustomValue ( e . target . value ) }
395+ onKeyDown = { ( e ) => {
396+ if ( e . key === "Enter" && isCustomDurationValid ) {
397+ e . preventDefault ( ) ;
398+ applyCustomDuration ( ) ;
399+ }
400+ } }
401+ className = "h-8 w-20 rounded border border-grid-bright bg-background-bright px-2 text-sm text-text-bright placeholder:text-text-dimmed focus:border-indigo-500 focus:outline-none"
402+ />
403+ < select
404+ value = { customUnit }
405+ onChange = { ( e ) => setCustomUnit ( e . target . value ) }
406+ className = "h-8 rounded border border-grid-bright bg-background-bright px-2 text-sm text-text-bright focus:border-indigo-500 focus:outline-none"
407+ >
408+ { timeUnits . map ( ( unit ) => (
409+ < option key = { unit . value } value = { unit . value } >
410+ { unit . label }
411+ </ option >
412+ ) ) }
413+ </ select >
414+ < Button
415+ variant = "secondary/small"
416+ disabled = { ! isCustomDurationValid }
417+ onClick = { ( e ) => {
418+ e . preventDefault ( ) ;
419+ applyCustomDuration ( ) ;
420+ } }
421+ type = "button"
422+ >
423+ Apply
424+ </ Button >
425+ </ div >
426+ </ div >
427+
428+ < div className = "flex flex-col gap-4 border-t border-grid-bright pt-4" >
429+ < Label className = "text-text-dimmed" > Or specify exact time range</ Label >
322430 < div className = "flex flex-col gap-1" >
323431 < Label >
324432 From < span className = "text-text-dimmed" > (local time)</ span >
@@ -370,7 +478,7 @@ export function TimeDropdown({
370478 disabled = { ! fromValue && ! toValue }
371479 onClick = { ( e ) => {
372480 e . preventDefault ( ) ;
373- apply ( ) ;
481+ applyDateRange ( ) ;
374482 } }
375483 type = "button"
376484 >
0 commit comments