mirror of
https://github.com/coder/coder.git
synced 2026-09-22 05:05:20 +08:00
feat: add dashboard fields for configuring autostop reminder (#26762)
This commit is contained in:
@@ -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,
|
||||
|
||||
+68
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user