Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
d54909e
feat(webapp): add custom time interval input for run filters
claude Jan 8, 2026
74a4ca3
fix(webapp): sync custom duration state when period prop changes
github-actions[bot] Jan 8, 2026
97ecf38
style(webapp): use existing styled components for custom duration input
github-actions[bot] Jan 8, 2026
b7a7c17
Improve the UX and style of the custom duration option
samejr Jan 8, 2026
076436c
Improve the UX of the selected time range to be applied
samejr Jan 8, 2026
c0bfc95
Adds a new shadcn component for picking the date and time
samejr Jan 9, 2026
ff3eb2b
Don’t populate the custom duration field when timePeriods are selected
samejr Jan 9, 2026
db8e57d
Small layout improvements
samejr Jan 9, 2026
02a9c16
Show a tooltip when clearing
samejr Jan 9, 2026
2b36e26
Replace 1year with 90 days
samejr Jan 9, 2026
b54847b
Selecting timePeriods populates the custom duration field
samejr Jan 9, 2026
a00e9b7
Move custom duration field to top
samejr Jan 9, 2026
623d0bb
Fix type imports
samejr Jan 9, 2026
e83ec07
Layout tweaks
samejr Jan 9, 2026
1a988c4
Use the Button component
samejr Jan 9, 2026
c4e4882
Remove div wrapper
samejr Jan 9, 2026
99b830d
Show an optional inline label
samejr Jan 9, 2026
62d3f29
Selected time durations have an active state when clicked
samejr Jan 9, 2026
579493e
Nicer styling for the dateTimePicker
samejr Jan 9, 2026
489b9e1
Add “Last weekday” option
samejr Jan 9, 2026
a7cae5b
Style improvements
samejr Jan 9, 2026
e2e6b42
Fix ilegal DOM html
samejr Jan 9, 2026
2b89a4e
style improvement
samejr Jan 9, 2026
23f9f81
Better layout and adds shorthand month names
samejr Jan 9, 2026
f31e897
Default period exposed as a prop
samejr Jan 9, 2026
36e9987
style fix
samejr Jan 9, 2026
1340518
Calendar restructured to include nav buttons inline
samejr Jan 9, 2026
0cbed51
autofocus the input field
samejr Jan 9, 2026
c9a8d10
Merge remote-tracking branch 'origin/main' into claude/slack-add-cust…
samejr Jan 9, 2026
5d9abbb
pnpm lock file update
samejr Jan 9, 2026
2cf6a98
Adds defaultPeriod to the dep array
samejr Jan 9, 2026
68cd270
If the From date is after the To date, show an error
samejr Jan 9, 2026
1dd70d1
Reorder the quick date links
samejr Jan 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 126 additions & 0 deletions apps/webapp/app/components/primitives/Calendar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
"use client";

import * as React from "react";
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/20/solid";
import { format } from "date-fns";
import { DayPicker, useDayPicker } from "react-day-picker";
import { cn } from "~/utils/cn";

export type CalendarProps = React.ComponentProps<typeof DayPicker>;

const navButtonClass =
"size-7 rounded-[3px] bg-secondary border border-charcoal-600 text-text-bright hover:bg-charcoal-600 hover:border-charcoal-550 transition inline-flex items-center justify-center";

function CustomMonthCaption({ calendarMonth }: { calendarMonth: { date: Date } }) {
const { goToMonth, nextMonth, previousMonth } = useDayPicker();

return (
<div className="flex w-full items-center justify-between px-1">
<button
type="button"
className={navButtonClass}
disabled={!previousMonth}
onClick={() => previousMonth && goToMonth(previousMonth)}
aria-label="Go to previous month"
>
<ChevronLeftIcon className="size-4" />
</button>
<div className="flex items-center gap-2">
<select
className="rounded border border-charcoal-600 bg-charcoal-750 px-2 py-1 text-sm text-text-bright focus:border-charcoal-500 focus:outline-none"
value={calendarMonth.date.getMonth()}
onChange={(e) => {
const newDate = new Date(calendarMonth.date);
newDate.setMonth(parseInt(e.target.value));
goToMonth(newDate);
}}
>
{Array.from({ length: 12 }, (_, i) => (
<option key={i} value={i}>
{format(new Date(2000, i), "MMM")}
</option>
))}
</select>
<select
className="rounded border border-charcoal-600 bg-charcoal-750 px-2 py-1 text-sm text-text-bright focus:border-charcoal-500 focus:outline-none"
value={calendarMonth.date.getFullYear()}
onChange={(e) => {
const newDate = new Date(calendarMonth.date);
newDate.setFullYear(parseInt(e.target.value));
goToMonth(newDate);
}}
>
{Array.from({ length: 100 }, (_, i) => {
const year = new Date().getFullYear() - 50 + i;
return (
<option key={year} value={year}>
{year}
</option>
);
})}
</select>
</div>
<button
type="button"
className={navButtonClass}
disabled={!nextMonth}
onClick={() => nextMonth && goToMonth(nextMonth)}
aria-label="Go to next month"
>
<ChevronRightIcon className="size-4" />
</button>
</div>
);
}

export function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
weekStartsOn={1}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row gap-2",
month: "flex flex-col gap-4",
month_caption: "flex justify-center pt-1 relative items-center w-full",
caption_label: "sr-only",
nav: "hidden",
month_grid: "w-full border-collapse",
weekdays: "flex",
weekday: "text-text-dimmed rounded-md w-8 font-normal text-[0.8rem]",
week: "flex w-full mt-2",
day: "relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-charcoal-700 [&:has([aria-selected].day-outside)]:bg-charcoal-700/50 [&:has([aria-selected].day-range-end)]:rounded-r-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md",
day_button: cn(
"size-8 p-0 font-normal text-text-bright rounded-md",
"hover:bg-charcoal-700 hover:text-text-bright",
"focus:bg-charcoal-700 focus:text-text-bright focus:outline-none",
"aria-selected:opacity-100"
),
range_start: "day-range-start rounded-l-md",
range_end: "day-range-end rounded-r-md",
selected:
"bg-indigo-600 text-text-bright hover:bg-indigo-600 hover:text-text-bright focus:bg-indigo-600 focus:text-text-bright rounded-md",
today: "bg-charcoal-700 text-text-bright rounded-md",
outside:
"day-outside text-text-dimmed opacity-50 aria-selected:bg-charcoal-700/50 aria-selected:text-text-dimmed aria-selected:opacity-30",
disabled: "text-text-dimmed opacity-50",
range_middle: "aria-selected:bg-charcoal-700 aria-selected:text-text-bright",
hidden: "invisible",
dropdowns: "flex gap-2 items-center justify-center",
dropdown:
"bg-charcoal-750 border border-charcoal-600 rounded px-2 py-1 text-sm text-text-bright focus:outline-none focus:border-charcoal-500",
...classNames,
}}
components={{
MonthCaption: CustomMonthCaption,
}}
{...props}
/>
);
}
Calendar.displayName = "Calendar";
10 changes: 6 additions & 4 deletions apps/webapp/app/components/primitives/DateField.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { BellAlertIcon, XMarkIcon } from "@heroicons/react/20/solid";
import { CalendarDateTime, createCalendar } from "@internationalized/date";
import { useDateField, useDateSegment } from "@react-aria/datepicker";
import type { DateFieldState, DateSegment } from "@react-stately/datepicker";
import { useDateFieldState } from "@react-stately/datepicker";
import { Granularity } from "@react-types/datepicker";
import {
useDateFieldState,
type DateFieldState,
type DateSegment,
} from "@react-stately/datepicker";
import { type Granularity } from "@react-types/datepicker";
import { useEffect, useRef, useState } from "react";
import { cn } from "~/utils/cn";
import { Button } from "./Buttons";
Expand Down
145 changes: 145 additions & 0 deletions apps/webapp/app/components/primitives/DateTimePicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
"use client";

import * as React from "react";
import { ChevronUpDownIcon } from "@heroicons/react/20/solid";
import { format } from "date-fns";
import { Calendar } from "./Calendar";
import { Popover, PopoverContent, PopoverTrigger } from "./Popover";
import { Button } from "./Buttons";
import { cn } from "~/utils/cn";
import { SimpleTooltip } from "./Tooltip";
import { XIcon } from "lucide-react";

type DateTimePickerProps = {
label: string;
value?: Date;
onChange?: (date: Date | undefined) => void;
showSeconds?: boolean;
showNowButton?: boolean;
showClearButton?: boolean;
showInlineLabel?: boolean;
className?: string;
};

export function DateTimePicker({
label,
value,
onChange,
showSeconds = true,
showNowButton = false,
showClearButton = false,
showInlineLabel = false,
className,
}: DateTimePickerProps) {
const [open, setOpen] = React.useState(false);

// Extract time parts from value
const hours = value ? value.getHours().toString().padStart(2, "0") : "";
const minutes = value ? value.getMinutes().toString().padStart(2, "0") : "";
const seconds = value ? value.getSeconds().toString().padStart(2, "0") : "";
const timeValue = showSeconds ? `${hours}:${minutes}:${seconds}` : `${hours}:${minutes}`;

const handleDateSelect = (date: Date | undefined) => {
if (date) {
// Preserve the time from the current value if it exists
if (value) {
date.setHours(value.getHours());
date.setMinutes(value.getMinutes());
date.setSeconds(value.getSeconds());
}
onChange?.(date);
} else {
onChange?.(undefined);
}
setOpen(false);
};
Comment on lines +42 to +55
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid mutating the incoming date parameter.

The handleDateSelect function mutates the date object directly via setHours, setMinutes, and setSeconds. This can cause unexpected side effects if the caller retains a reference to the original date object.

🔧 Proposed fix: Create a new Date object
   const handleDateSelect = (date: Date | undefined) => {
     if (date) {
       // Preserve the time from the current value if it exists
       if (value) {
-        date.setHours(value.getHours());
-        date.setMinutes(value.getMinutes());
-        date.setSeconds(value.getSeconds());
+        const newDate = new Date(date);
+        newDate.setHours(value.getHours());
+        newDate.setMinutes(value.getMinutes());
+        newDate.setSeconds(value.getSeconds());
+        onChange?.(newDate);
+        setOpen(false);
+        return;
       }
       onChange?.(date);
     } else {
       onChange?.(undefined);
     }
     setOpen(false);
   };


const handleTimeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const timeString = e.target.value;
if (!timeString) return;

const [h, m, s] = timeString.split(":").map(Number);
const newDate = value ? new Date(value) : new Date();
newDate.setHours(h || 0);
newDate.setMinutes(m || 0);
newDate.setSeconds(s || 0);
onChange?.(newDate);
};

const handleNowClick = () => {
onChange?.(new Date());
};

const handleClearClick = () => {
onChange?.(undefined);
};

return (
<div className={cn("flex items-center gap-2", className)}>
{showInlineLabel && (
<span className="w-6 shrink-0 text-right text-xxs text-charcoal-500">{label}</span>
)}
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<button
type="button"
className={cn(
"flex h-[1.8rem] w-full items-center justify-between gap-2 whitespace-nowrap rounded border border-charcoal-650 bg-charcoal-750 px-2 text-xs tabular-nums transition hover:border-charcoal-600",
value ? "text-text-bright" : "text-text-dimmed"
)}
>
{value ? format(value, "yyyy/MM/dd") : "Select date"}
<ChevronUpDownIcon className="size-3.5 text-text-dimmed" />
</button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={value}
onSelect={handleDateSelect}
captionLayout="dropdown"
/>
</PopoverContent>
</Popover>
<input
type="time"
step={showSeconds ? "1" : "60"}
value={value ? timeValue : ""}
onChange={handleTimeChange}
className={cn(
"h-[1.8rem] rounded border border-charcoal-650 bg-charcoal-750 px-2 text-xs tabular-nums transition hover:border-charcoal-600",
value ? "text-text-bright" : "text-text-dimmed",
"focus:border-charcoal-500 focus:outline-none",
"[&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
)}
aria-label={`${label} time`}
/>
{showNowButton && (
<Button
type="button"
variant="secondary/small"
className="h-[1.8rem]"
onClick={handleNowClick}
>
Now
</Button>
)}
{showClearButton && (
<SimpleTooltip
button={
<button
type="button"
className="flex h-[1.8rem] items-center justify-center px-1 text-text-dimmed transition hover:text-text-bright"
onClick={handleClearClick}
>
<XIcon className="size-3.5" />
</button>
}
content="Clear"
disableHoverableContent
asChild
/>
)}
</div>
);
}
Loading