diff --git a/apps/webapp/app/components/runs/v3/SharedFilters.tsx b/apps/webapp/app/components/runs/v3/SharedFilters.tsx index 14edfa3c7d..52f1181072 100644 --- a/apps/webapp/app/components/runs/v3/SharedFilters.tsx +++ b/apps/webapp/app/components/runs/v3/SharedFilters.tsx @@ -2,12 +2,20 @@ import * as Ariakit from "@ariakit/react"; import type { RuntimeEnvironment } from "@trigger.dev/database"; import parse from "parse-duration"; import type { ReactNode } from "react"; -import { startTransition, useCallback, useState } from "react"; +import { startTransition, useCallback, useEffect, useState } from "react"; import { AppliedFilter } from "~/components/primitives/AppliedFilter"; import { DateField } from "~/components/primitives/DateField"; import { DateTime } from "~/components/primitives/DateTime"; +import { Input } from "~/components/primitives/Input"; import { Label } from "~/components/primitives/Label"; import { ComboboxProvider, SelectPopover, SelectProvider } from "~/components/primitives/Select"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "~/components/primitives/SimpleSelect"; import { useSearchParams } from "~/hooks/useSearchParam"; import { Button } from "../../primitives/Buttons"; import { filterIcon } from "./RunFilters"; @@ -95,6 +103,21 @@ const timePeriods = [ }, ]; +const timeUnits = [ + { label: "minutes", value: "m", singular: "minute" }, + { label: "hours", value: "h", singular: "hour" }, + { label: "days", value: "d", singular: "day" }, +]; + +// Parse a period string (e.g., "90m", "2h", "7d") into value and unit +function parsePeriodString(period: string): { value: number; unit: string } | null { + const match = period.match(/^(\d+)([mhd])$/); + if (match) { + return { value: parseInt(match[1], 10), unit: match[2] }; + } + return null; +} + const defaultPeriod = "7d"; const defaultPeriodMs = parse(defaultPeriod); if (!defaultPeriodMs) { @@ -175,9 +198,29 @@ export function timeFilterRenderValues({ let valueLabel: ReactNode; switch (rangeType) { - case "period": - valueLabel = timePeriods.find((t) => t.value === period)?.label ?? period ?? defaultPeriod; + case "period": { + // First check if it's a preset period + const preset = timePeriods.find((t) => t.value === period); + if (preset) { + valueLabel = preset.label; + } else if (period) { + // Parse custom period and format nicely (e.g., "90m" -> "90 mins") + const parsed = parsePeriodString(period); + if (parsed) { + const unit = timeUnits.find((u) => u.value === parsed.unit); + if (unit) { + valueLabel = `${parsed.value} ${parsed.value === 1 ? unit.singular : unit.label}`; + } else { + valueLabel = period; + } + } else { + valueLabel = period; + } + } else { + valueLabel = defaultPeriod; + } break; + } case "range": valueLabel = ( @@ -237,6 +280,17 @@ export function TimeFilter() { ); } +// Get initial custom duration state from a period string +function getInitialCustomDuration(period?: string): { value: string; unit: string } { + if (period) { + const parsed = parsePeriodString(period); + if (parsed) { + return { value: parsed.value.toString(), unit: parsed.unit }; + } + } + return { value: "", unit: "m" }; +} + export function TimeDropdown({ trigger, period, @@ -253,7 +307,19 @@ export function TimeDropdown({ const [fromValue, setFromValue] = useState(from); const [toValue, setToValue] = useState(to); - const apply = useCallback(() => { + // Custom duration state + const initialCustom = getInitialCustomDuration(period); + const [customValue, setCustomValue] = useState(initialCustom.value); + const [customUnit, setCustomUnit] = useState(initialCustom.unit); + + // Sync custom duration state when period prop changes + useEffect(() => { + const parsed = getInitialCustomDuration(period); + setCustomValue(parsed.value); + setCustomUnit(parsed.unit); + }, [period]); + + const applyDateRange = useCallback(() => { replace({ period: undefined, cursor: undefined, @@ -283,6 +349,20 @@ export function TimeDropdown({ [replace] ); + const applyCustomDuration = useCallback(() => { + const value = parseInt(customValue, 10); + if (isNaN(value) || value <= 0) { + return; + } + const periodString = `${value}${customUnit}`; + handlePeriodClick(periodString); + }, [customValue, customUnit, handlePeriodClick]); + + const isCustomDurationValid = (() => { + const value = parseInt(customValue, 10); + return !isNaN(value) && value > 0; + })(); + return ( {trigger} @@ -318,7 +398,54 @@ export function TimeDropdown({ -
+
+ +
+ setCustomValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter" && isCustomDurationValid) { + e.preventDefault(); + applyCustomDuration(); + } + }} + variant="small" + fullWidth={false} + containerClassName="w-20" + /> + + +
+
+ +
+