Skip to content

Commit b7a7c17

Browse files
committed
Improve the UX and style of the custom duration option
1 parent 97ecf38 commit b7a7c17

1 file changed

Lines changed: 104 additions & 89 deletions

File tree

apps/webapp/app/components/runs/v3/SharedFilters.tsx

Lines changed: 104 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,10 @@ import { startTransition, useCallback, useEffect, useState } from "react";
66
import { AppliedFilter } from "~/components/primitives/AppliedFilter";
77
import { DateField } from "~/components/primitives/DateField";
88
import { DateTime } from "~/components/primitives/DateTime";
9-
import { Input } from "~/components/primitives/Input";
109
import { Label } from "~/components/primitives/Label";
1110
import { ComboboxProvider, SelectPopover, SelectProvider } from "~/components/primitives/Select";
12-
import {
13-
Select,
14-
SelectContent,
15-
SelectItem,
16-
SelectTrigger,
17-
SelectValue,
18-
} from "~/components/primitives/SimpleSelect";
1911
import { useSearchParams } from "~/hooks/useSearchParam";
12+
import { cn } from "~/utils/cn";
2013
import { Button } from "../../primitives/Buttons";
2114
import { filterIcon } from "./RunFilters";
2215

@@ -104,9 +97,9 @@ const timePeriods = [
10497
];
10598

10699
const timeUnits = [
107-
{ label: "minutes", value: "m", singular: "minute" },
108-
{ label: "hours", value: "h", singular: "hour" },
109-
{ label: "days", value: "d", singular: "day" },
100+
{ label: "minutes", value: "m", singular: "minute", shortLabel: "mins" },
101+
{ label: "hours", value: "h", singular: "hour", shortLabel: "hours" },
102+
{ label: "days", value: "d", singular: "day", shortLabel: "days" },
110103
];
111104

112105
// Parse a period string (e.g., "90m", "2h", "7d") into value and unit
@@ -307,34 +300,44 @@ export function TimeDropdown({
307300
const [fromValue, setFromValue] = useState(from);
308301
const [toValue, setToValue] = useState(to);
309302

310-
// Custom duration state
303+
// Selection state: preset value, "custom", or null (for date range mode)
311304
const initialCustom = getInitialCustomDuration(period);
305+
const isInitialCustom =
306+
period && !timePeriods.some((p) => p.value === period) && initialCustom.value !== "";
307+
const [selectedPeriod, setSelectedPeriod] = useState<string | null>(
308+
isInitialCustom ? "custom" : period ?? defaultPeriod
309+
);
310+
311+
// Custom duration state
312312
const [customValue, setCustomValue] = useState(initialCustom.value);
313313
const [customUnit, setCustomUnit] = useState(initialCustom.unit);
314314

315-
// Sync custom duration state when period prop changes
315+
// Sync state when period prop changes
316316
useEffect(() => {
317317
const parsed = getInitialCustomDuration(period);
318318
setCustomValue(parsed.value);
319319
setCustomUnit(parsed.unit);
320+
321+
const isCustom = period && !timePeriods.some((p) => p.value === period) && parsed.value !== "";
322+
setSelectedPeriod(isCustom ? "custom" : period ?? defaultPeriod);
320323
}, [period]);
321324

322-
const applyDateRange = useCallback(() => {
323-
replace({
324-
period: undefined,
325-
cursor: undefined,
326-
direction: undefined,
327-
from: fromValue?.getTime().toString(),
328-
to: toValue?.getTime().toString(),
329-
});
325+
const applySelection = useCallback(() => {
326+
// If a period is selected (preset or custom), apply it
327+
if (selectedPeriod) {
328+
let periodToApply = selectedPeriod;
330329

331-
setOpen(false);
332-
}, [fromValue, toValue, replace]);
330+
// Build custom period string if custom is selected
331+
if (selectedPeriod === "custom") {
332+
const value = parseInt(customValue, 10);
333+
if (isNaN(value) || value <= 0) {
334+
return;
335+
}
336+
periodToApply = `${value}${customUnit}`;
337+
}
333338

334-
const handlePeriodClick = useCallback(
335-
(period: string) => {
336339
replace({
337-
period,
340+
period: periodToApply,
338341
cursor: undefined,
339342
direction: undefined,
340343
from: undefined,
@@ -343,26 +346,33 @@ export function TimeDropdown({
343346

344347
setFromValue(undefined);
345348
setToValue(undefined);
346-
347349
setOpen(false);
348-
},
349-
[replace]
350-
);
351-
352-
const applyCustomDuration = useCallback(() => {
353-
const value = parseInt(customValue, 10);
354-
if (isNaN(value) || value <= 0) {
355350
return;
356351
}
357-
const periodString = `${value}${customUnit}`;
358-
handlePeriodClick(periodString);
359-
}, [customValue, customUnit, handlePeriodClick]);
352+
353+
// Otherwise apply date range
354+
replace({
355+
period: undefined,
356+
cursor: undefined,
357+
direction: undefined,
358+
from: fromValue?.getTime().toString(),
359+
to: toValue?.getTime().toString(),
360+
});
361+
362+
setOpen(false);
363+
}, [selectedPeriod, customValue, customUnit, fromValue, toValue, replace]);
360364

361365
const isCustomDurationValid = (() => {
362366
const value = parseInt(customValue, 10);
363367
return !isNaN(value) && value > 0;
364368
})();
365369

370+
// Determine if Apply button should be enabled
371+
const canApply =
372+
(selectedPeriod && selectedPeriod !== "custom") ||
373+
(selectedPeriod === "custom" && isCustomDurationValid) ||
374+
(!selectedPeriod && (fromValue || toValue));
375+
366376
return (
367377
<SelectProvider virtualFocus={true} open={open} setOpen={setOpen}>
368378
{trigger}
@@ -374,86 +384,88 @@ export function TimeDropdown({
374384
>
375385
<div className="flex flex-col gap-6 p-3">
376386
<div className="flex flex-col gap-1">
377-
<Label>Runs created in the last</Label>
387+
<Label className="mb-2">Runs created in the last</Label>
378388
<div className="grid grid-cols-4 gap-2">
379389
{timePeriods.map((p) => (
380390
<Button
381391
key={p.value}
382392
variant="secondary/small"
383393
className={
384-
p.value === period
394+
p.value === selectedPeriod
385395
? "border-indigo-500 group-hover/button:border-indigo-500"
386396
: undefined
387397
}
388398
onClick={(e) => {
389399
e.preventDefault();
390-
handlePeriodClick(p.value);
400+
setSelectedPeriod(p.value);
401+
// Clear custom value when selecting a preset
402+
setCustomValue("");
391403
}}
392404
fullWidth
393405
type="button"
394406
>
395407
{p.label}
396408
</Button>
397409
))}
398-
</div>
399-
</div>
400-
401-
<div className="flex flex-col gap-1">
402-
<Label>Custom duration</Label>
403-
<div className="flex items-center gap-2">
404-
<Input
405-
type="number"
406-
min="1"
407-
step="1"
408-
placeholder="e.g. 90"
409-
value={customValue}
410-
onChange={(e) => setCustomValue(e.target.value)}
411-
onKeyDown={(e) => {
412-
if (e.key === "Enter" && isCustomDurationValid) {
413-
e.preventDefault();
414-
applyCustomDuration();
415-
}
416-
}}
417-
variant="small"
418-
fullWidth={false}
419-
containerClassName="w-20"
420-
/>
421-
<Select value={customUnit} onValueChange={setCustomUnit}>
422-
<SelectTrigger size="secondary/small">
423-
<SelectValue />
424-
</SelectTrigger>
425-
<SelectContent>
410+
{/* Custom duration row */}
411+
<div
412+
className={cn(
413+
"col-span-4 flex h-[1.8rem] w-full items-center gap-2 rounded border py-0.5 pl-0 pr-2 transition-colors",
414+
selectedPeriod === "custom"
415+
? "border-indigo-500 bg-charcoal-750"
416+
: "border-charcoal-650 bg-charcoal-750 hover:border-charcoal-600"
417+
)}
418+
>
419+
<input
420+
type="number"
421+
min="1"
422+
step="1"
423+
placeholder="Custom"
424+
value={customValue}
425+
onChange={(e) => {
426+
setCustomValue(e.target.value);
427+
setSelectedPeriod("custom");
428+
}}
429+
onFocus={() => setSelectedPeriod("custom")}
430+
className="h-full w-full translate-y-px border-none bg-transparent py-0 pl-2 pr-0 text-xs leading-none text-text-bright outline-none placeholder:text-text-dimmed focus:outline-none focus:ring-0 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
431+
/>
432+
<div className="flex items-center gap-2">
426433
{timeUnits.map((unit) => (
427-
<SelectItem key={unit.value} value={unit.value}>
428-
{unit.label}
429-
</SelectItem>
434+
<button
435+
key={unit.value}
436+
type="button"
437+
onClick={() => {
438+
setCustomUnit(unit.value);
439+
setSelectedPeriod("custom");
440+
}}
441+
className={cn(
442+
"text-xs transition-colors",
443+
customUnit === unit.value
444+
? "text-indigo-500"
445+
: "text-text-dimmed hover:text-text-bright"
446+
)}
447+
>
448+
{unit.shortLabel}
449+
</button>
430450
))}
431-
</SelectContent>
432-
</Select>
433-
<Button
434-
variant="secondary/small"
435-
disabled={!isCustomDurationValid}
436-
onClick={(e) => {
437-
e.preventDefault();
438-
applyCustomDuration();
439-
}}
440-
type="button"
441-
>
442-
Apply
443-
</Button>
451+
</div>
452+
</div>
444453
</div>
445454
</div>
446455

447456
<div className="flex flex-col gap-4 border-t border-grid-bright pt-4">
448-
<Label className="text-text-dimmed">Or specify exact time range</Label>
457+
<Label className="text-text-bright">Or specify exact time range</Label>
449458
<div className="flex flex-col gap-1">
450459
<Label>
451460
From <span className="text-text-dimmed">(local time)</span>
452461
</Label>
453462
<DateField
454463
label="From time"
455464
defaultValue={fromValue}
456-
onValueChange={setFromValue}
465+
onValueChange={(value) => {
466+
setFromValue(value);
467+
if (value) setSelectedPeriod(null);
468+
}}
457469
granularity="second"
458470
showNowButton
459471
showClearButton
@@ -467,7 +479,10 @@ export function TimeDropdown({
467479
<DateField
468480
label="To time"
469481
defaultValue={toValue}
470-
onValueChange={setToValue}
482+
onValueChange={(value) => {
483+
setToValue(value);
484+
if (value) setSelectedPeriod(null);
485+
}}
471486
granularity="second"
472487
showNowButton
473488
showClearButton
@@ -494,10 +509,10 @@ export function TimeDropdown({
494509
key: "Enter",
495510
enabledOnInputElements: true,
496511
}}
497-
disabled={!fromValue && !toValue}
512+
disabled={!canApply}
498513
onClick={(e) => {
499514
e.preventDefault();
500-
applyDateRange();
515+
applySelection();
501516
}}
502517
type="button"
503518
>

0 commit comments

Comments
 (0)