diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TTLHelperText.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TTLHelperText.tsx index 90a1606d35..817c46ecc9 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TTLHelperText.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TTLHelperText.tsx @@ -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 ( + + Autostop reminders only apply when an autostop deadline is configured. + Set a default TTL or an autostop requirement above to enable reminders. + + ); + } + + // Error will show once field is considered touched + if (lead < 0) { + return null; + } + + if (lead === 0) { + return ( + + Workspace owners will not be reminded before their workspace is + automatically stopped. + + ); + } + + return ( + + Workspace owners will be reminded {lead} {hours(lead)} before their + workspace is automatically stopped. + + ); +}; + export const FailureTTLHelperText = (props: { ttl?: number }) => { const { ttl = 0 } = props; diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm.tsx index 2db8dafc2f..4c8e8892ab 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm.tsx @@ -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 = ({ // 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 = ({ 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 = ({ type="number" /> + + ), + })} + disabled={isSubmitting} + fullWidth + inputProps={{ min: 0, step: 1 }} + label="Autostop reminder (hours)" + type="number" + /> +
{ + 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, + }), + ); + }); + }, +}; diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/formHelpers.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/formHelpers.tsx index f948e1dd1d..e5b5399dec 100644 --- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/formHelpers.tsx +++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/formHelpers.tsx @@ -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()