feat: add dashboard fields for configuring autostop reminder (#26762)

This commit is contained in:
Jon Ayers
2026-06-29 18:28:23 -05:00
committed by GitHub
parent 1a07bd65d4
commit 538eddb731
5 changed files with 145 additions and 0 deletions
@@ -59,6 +59,48 @@ export const ActivityBumpHelperText = (props: {
);
};
export const AutostopReminderHelperText = (props: {
lead?: number;
defaultTTL?: number;
autostopRequirementDaysOfWeek?: string;
}) => {
const { lead = 0, defaultTTL = 0, autostopRequirementDaysOfWeek } = props;
const hasAutostopRequirement =
Boolean(autostopRequirementDaysOfWeek) &&
autostopRequirementDaysOfWeek !== "off";
if (!defaultTTL && !hasAutostopRequirement) {
return (
<span>
Autostop reminders only apply when an autostop deadline is configured.
Set a default TTL or an autostop requirement above to enable reminders.
</span>
);
}
// Error will show once field is considered touched
if (lead < 0) {
return null;
}
if (lead === 0) {
return (
<span>
Workspace owners will not be reminded before their workspace is
automatically stopped.
</span>
);
}
return (
<span>
Workspace owners will be reminded {lead} {hours(lead)} before their
workspace is automatically stopped.
</span>
);
};
export const FailureTTLHelperText = (props: { ttl?: number }) => {
const { ttl = 0 } = props;
@@ -37,6 +37,7 @@ import { ScheduleDialog } from "./ScheduleDialog";
import { TemplateScheduleAutostart } from "./TemplateScheduleAutostart";
import {
ActivityBumpHelperText,
AutostopReminderHelperText,
DefaultTTLHelperText,
DormancyAutoDeletionTTLHelperText,
DormancyTTLHelperText,
@@ -84,6 +85,8 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
// on display, convert from ms => hours
default_ttl_ms: template.default_ttl_ms / MS_HOUR_CONVERSION,
activity_bump_ms: template.activity_bump_ms / MS_HOUR_CONVERSION,
time_til_autostop_notify_ms:
template.time_til_autostop_notify_ms / MS_HOUR_CONVERSION,
failure_ttl_ms: template.failure_ttl_ms,
time_til_dormant_ms: template.time_til_dormant_ms,
time_til_dormant_autodelete_ms: template.time_til_dormant_autodelete_ms,
@@ -207,6 +210,10 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
form.values.default_ttl_ms && form.values.activity_bump_ms
? form.values.activity_bump_ms * MS_HOUR_CONVERSION
: undefined,
// 0 disables the reminder, so always send an explicit value.
time_til_autostop_notify_ms: form.values.time_til_autostop_notify_ms
? form.values.time_til_autostop_notify_ms * MS_HOUR_CONVERSION
: 0,
failure_ttl_ms: form.values.failure_ttl_ms,
time_til_dormant_ms: form.values.time_til_dormant_ms,
time_til_dormant_autodelete_ms:
@@ -317,6 +324,25 @@ export const TemplateScheduleForm: FC<TemplateScheduleForm> = ({
type="number"
/>
<TextField
{...getFieldHelpers("time_til_autostop_notify_ms", {
helperText: (
<AutostopReminderHelperText
lead={form.values.time_til_autostop_notify_ms}
defaultTTL={form.values.default_ttl_ms}
autostopRequirementDaysOfWeek={
form.values.autostop_requirement_days_of_week
}
/>
),
})}
disabled={isSubmitting}
fullWidth
inputProps={{ min: 0, step: 1 }}
label="Autostop reminder (hours)"
type="number"
/>
<div className="flex flex-row gap-4 w-full">
<TextField
{...getFieldHelpers("autostop_requirement_days_of_week", {
@@ -18,6 +18,7 @@ import TemplateSchedulePage from "./TemplateSchedulePage";
const validFormValues: TemplateScheduleFormValues = {
default_ttl_ms: 1,
activity_bump_ms: 1,
time_til_autostop_notify_ms: 1,
failure_ttl_ms: 7,
time_til_dormant_ms: 180,
time_til_dormant_autodelete_ms: 30,
@@ -82,3 +82,71 @@ export const SubmitClearsActivityBumpWhenDefaultTTLIsZero: Story = {
});
},
};
export const SubmitAutostopReminderConvertsHoursToMs: Story = {
args: {
...defaultArgs,
template: {
...MockTemplate,
time_til_autostop_notify_ms: 0,
},
onSubmit: fn(),
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const user = userEvent.setup();
const reminderField = await canvas.findByLabelText(
"Autostop reminder (hours)",
);
await user.clear(reminderField);
await user.type(reminderField, "2");
const submitButton = canvas.getByRole("button", { name: /save/i });
await user.click(submitButton);
await waitFor(() => {
expect(args.onSubmit).toHaveBeenCalledWith(
expect.objectContaining({
time_til_autostop_notify_ms: 2 * 60 * 60 * 1000,
}),
);
});
},
};
export const SubmitClearsAutostopReminder: Story = {
args: {
...defaultArgs,
template: {
...MockTemplate,
// Start with a non-zero autostop reminder so we can verify
// it gets cleared to 0 when the field is emptied.
time_til_autostop_notify_ms: 2 * 60 * 60 * 1000,
},
onSubmit: fn(),
},
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const user = userEvent.setup();
const reminderField = await canvas.findByLabelText(
"Autostop reminder (hours)",
);
await user.clear(reminderField);
await user.type(reminderField, "0");
const submitButton = canvas.getByRole("button", { name: /save/i });
await user.click(submitButton);
await waitFor(() => {
expect(args.onSubmit).toHaveBeenCalledWith(
expect.objectContaining({
time_til_autostop_notify_ms: 0,
}),
);
});
},
};
@@ -38,6 +38,14 @@ export const getValidationSchema = (): Yup.AnyObjectSchema =>
24 * MAX_TTL_DAYS /* 30 days in hours */,
"Please enter an activity bump duration that is less than or equal to 720 hours (30 days).",
),
time_til_autostop_notify_ms: Yup.number()
.integer("Autostop reminder must be an integer.")
.required()
.min(0, "Autostop reminder must not be less than 0.")
.max(
24 * MAX_TTL_DAYS /* 30 days in hours */,
"Autostop reminder must not exceed 720 hours (30 days).",
),
failure_ttl_ms: Yup.number()
.integer("Failure cleanup days must be an integer.")
.required()