diff --git a/frontend/app/(app)/dashboard/page.test.tsx b/frontend/app/(app)/dashboard/page.test.tsx
index adbed04..97c8445 100644
--- a/frontend/app/(app)/dashboard/page.test.tsx
+++ b/frontend/app/(app)/dashboard/page.test.tsx
@@ -273,6 +273,20 @@ describe("DashboardPage", () => {
);
});
+ it("lets the user type a timeout when creating an HTTP check", async () => {
+ renderDashboard("SIGNAL");
+
+ fireEvent.click(
+ (await screen.findAllByRole("button", { name: /new check/i }))[0],
+ );
+ fireEvent.click(await screen.findByRole("button", { name: /^http$/i }));
+
+ const timeout = await screen.findByLabelText("Timeout");
+ expect(timeout).toHaveValue(5);
+ fireEvent.change(timeout, { target: { value: "8" } });
+ expect(timeout).toHaveValue(8);
+ });
+
it("uses the SIGNAL creator floor when a SOLO collaborator creates a check", async () => {
context.activeOrg.plan = "SIGNAL";
renderDashboard("SOLO");
diff --git a/frontend/app/(app)/dashboard/page.tsx b/frontend/app/(app)/dashboard/page.tsx
index 6495553..2c06295 100644
--- a/frontend/app/(app)/dashboard/page.tsx
+++ b/frontend/app/(app)/dashboard/page.tsx
@@ -34,6 +34,7 @@ import {
} from "@/components/ui/select";
import { CircleGauge, Plus, Pause, Play } from "lucide-react";
import { Slider } from "@/components/ui/slider";
+import { DurationSliderField } from "@/components/app/duration-slider-field";
import { formatDuration } from "@/lib/format";
import { usePollWhenVisible } from "@/lib/use-poll-when-visible";
import { CHECK_POLL_INTERVAL_MS } from "@/lib/polling";
@@ -460,15 +461,15 @@ function CreateCheckDialog({
value={[parseInt(intervalSeconds || String(floor), 10)]}
onValueChange={(v) => setIntervalSeconds(String(Array.isArray(v) ? v[0] : v))} />
-
-
-
- {Math.round(parseInt(timeoutMs || "0", 10) / 1000)} s
-
-
setTimeoutMs(String(Array.isArray(v) ? v[0] : v))} />
-
+ setTimeoutMs(String(ms))}
+ minMs={1000}
+ maxMs={60000}
+ stepMs={500}
+ />
{checkType === "HTTP" && (
-
- Timeout (ms)
- setTimeoutMs(e.target.value)}
- required
- />
-
+ setTimeoutMs(String(ms))}
+ minMs={1000}
+ maxMs={60000}
+ stepMs={500}
+ />
{type === "HTTP" && (
<>
diff --git a/frontend/test/edit-check-dialog.test.tsx b/frontend/test/edit-check-dialog.test.tsx
index 1e132c3..849f7cd 100644
--- a/frontend/test/edit-check-dialog.test.tsx
+++ b/frontend/test/edit-check-dialog.test.tsx
@@ -203,6 +203,32 @@ describe("EditCheckDialog", () => {
expect(screen.queryByLabelText(/period/i)).not.toBeInTheDocument();
});
+ it("lets the user type timeout in seconds or set it with a slider", () => {
+ renderDialog(HTTP);
+
+ const input = screen.getByLabelText("Timeout");
+ expect(input).toHaveValue(5);
+ expect(screen.getByRole("slider", { hidden: true })).toHaveValue("5000");
+
+ fireEvent.change(input, { target: { value: "10" } });
+ expect(input).toHaveValue(10);
+ expect(screen.getByRole("slider", { hidden: true })).toHaveValue("10000");
+ });
+
+ it("submits typed timeout seconds as milliseconds", async () => {
+ const { captured } = renderDialogCapturingVariables(HTTP);
+ await waitForPlan();
+
+ fireEvent.change(screen.getByLabelText("Timeout"), {
+ target: { value: "10" },
+ });
+ submitForm();
+
+ await waitFor(() => expect(captured).toHaveLength(1));
+ const input = captured[0].input as Record;
+ expect(input.timeoutMs).toBe(10000);
+ });
+
it("uses the paid plan floor for heartbeat periods and active intervals", async () => {
const { unmount } = renderDialog(HEARTBEAT);
await waitFor(() =>