refactor(site): modernize DurationField for agent settings (#23532)

This commit is contained in:
Danielle Maywood
2026-03-24 21:43:16 +00:00
committed by GitHub
parent 4bc79d7413
commit 4f3c7c8719
3 changed files with 234 additions and 1 deletions
@@ -16,7 +16,6 @@ import { user } from "api/queries/users";
import type * as TypesGen from "api/typesGenerated";
import { AvatarData } from "components/Avatar/AvatarData";
import { Button } from "components/Button/Button";
import { DurationField } from "components/DurationField/DurationField";
import { Link } from "components/Link/Link";
import { PaginationAmount } from "components/PaginationWidget/PaginationAmount";
import { PaginationWidgetBase } from "components/PaginationWidget/PaginationWidgetBase";
@@ -58,6 +57,7 @@ import {
DateRangePicker,
type DateRangeValue,
} from "./components/DateRangePicker/DateRangePicker";
import { DurationField } from "./components/DurationField/DurationField";
import { InsightsContent } from "./components/InsightsContent";
import { LimitsTab } from "./components/LimitsTab";
import { MCPServerAdminPanel } from "./components/MCPServerAdminPanel";
@@ -0,0 +1,101 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { useState } from "react";
import { expect, userEvent, within } from "storybook/test";
import { DurationField } from "./DurationField";
const meta: Meta<typeof DurationField> = {
title: "pages/AgentsPage/DurationField",
component: DurationField,
args: {
label: "Duration",
},
render: function RenderComponent(args) {
const [value, setValue] = useState<number>(args.valueMs);
return (
<DurationField
{...args}
valueMs={value}
onChange={(value) => setValue(value)}
/>
);
},
};
export default meta;
type Story = StoryObj<typeof DurationField>;
export const Hours: Story = {
args: {
valueMs: hoursToMs(16),
},
};
export const Days: Story = {
args: {
valueMs: daysToMs(2),
},
};
export const TypeOnlyNumbers: Story = {
args: {
valueMs: 0,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const input = canvas.getByLabelText("Duration");
await userEvent.clear(input);
await userEvent.type(input, "abcd_.?/48.0");
await expect(input).toHaveValue("480");
},
};
export const ChangeUnit: Story = {
args: {
valueMs: daysToMs(2),
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const input = canvas.getByLabelText("Duration");
const unitTrigger = canvas.getByLabelText("Time unit");
await userEvent.click(unitTrigger);
const hoursOption = await within(document.body).findByText("Hours");
await userEvent.click(hoursOption);
await expect(input).toHaveValue("48");
},
};
export const ConvertSmallHoursToDays: Story = {
args: {
valueMs: hoursToMs(2),
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const input = canvas.getByLabelText("Duration");
await expect(input).toHaveValue("2");
const unitTrigger = canvas.getByLabelText("Time unit");
await userEvent.click(unitTrigger);
const daysOption = await within(document.body).findByText("Days");
await userEvent.click(daysOption);
await expect(input).toHaveValue("1");
},
};
export const WithError: Story = {
args: {
valueMs: hoursToMs(1),
error: true,
helperText: "Duration must be greater than zero.",
},
};
function hoursToMs(hours: number): number {
return hours * 60 * 60 * 1000;
}
function daysToMs(days: number): number {
return days * 24 * 60 * 60 * 1000;
}
@@ -0,0 +1,132 @@
import dayjs from "dayjs";
import { type FC, type ReactNode, useState } from "react";
import { Input } from "#/components/Input/Input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "#/components/Select/Select";
import { cn } from "#/utils/cn";
import {
durationInDays,
durationInHours,
suggestedTimeUnit,
type TimeUnit,
} from "#/utils/time";
type DurationFieldProps = {
valueMs: number;
onChange: (value: number) => void;
label?: string;
disabled?: boolean;
error?: boolean;
helperText?: ReactNode;
className?: string;
};
function toMs(value: string, unit: TimeUnit): number {
const n = Number.parseInt(value, 10);
if (Number.isNaN(n)) {
return 0;
}
return unit === "hours"
? dayjs.duration(n, "hours").asMilliseconds()
: dayjs.duration(n, "days").asMilliseconds();
}
function toDisplayValue(ms: number, unit: TimeUnit): string {
return unit === "hours"
? durationInHours(ms).toString()
: durationInDays(ms).toString();
}
export const DurationField: FC<DurationFieldProps> = ({
valueMs,
onChange,
label,
disabled,
error,
helperText,
className,
}) => {
const [unit, setUnit] = useState<TimeUnit>(() => suggestedTimeUnit(valueMs));
const [text, setText] = useState(() => toDisplayValue(valueMs, unit));
// Adjust local state when the parent value diverges from ours.
const localMs = toMs(text, unit);
if (valueMs !== localMs) {
const newUnit = suggestedTimeUnit(valueMs);
setUnit(newUnit);
setText(toDisplayValue(valueMs, newUnit));
}
const handleTextChange = (raw: string) => {
const digits = raw.replace(/\D/g, "");
setText(digits);
const ms = toMs(digits, unit);
if (ms !== valueMs) {
onChange(ms);
}
};
const handleUnitChange = (newUnit: TimeUnit) => {
const currentMs = toMs(text, unit);
let newMs: number;
if (newUnit === "hours") {
newMs = currentMs;
} else {
const days = Math.ceil(durationInDays(currentMs));
newMs = dayjs.duration(days, "days").asMilliseconds();
}
setUnit(newUnit);
setText(toDisplayValue(newMs, newUnit));
if (newMs !== valueMs) {
onChange(newMs);
}
};
return (
<div className={cn("space-y-1", className)}>
<div className="flex gap-2">
<Input
value={text}
onChange={(e) => handleTextChange(e.currentTarget.value)}
aria-label={label}
aria-invalid={error}
disabled={disabled}
className="flex-1"
/>
<Select
value={unit}
onValueChange={(v: string) => handleUnitChange(v as TimeUnit)}
disabled={disabled}
>
<SelectTrigger className="w-[120px]" aria-label="Time unit">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="hours">Hours</SelectItem>
<SelectItem value="days">Days</SelectItem>
</SelectContent>
</Select>
</div>
{helperText && (
<p
className={cn(
"m-0 text-xs",
error ? "text-content-destructive" : "text-content-secondary",
)}
>
{helperText}
</p>
)}
</div>
);
};