+
= ({
label="Weeks between required stops"
type="number"
/>
-
-
- {
- await form.setFieldValue("allow_user_autostop", checked);
- }}
- name="allow_user_autostop"
- checked={form.values.allow_user_autostop}
- />
- }
- label={
+
+
+
+ {
+ await form.setFieldValue(
+ "allow_user_autostop",
+ checked === true,
+ );
+ }}
+ name="allow_user_autostop"
+ checked={form.values.allow_user_autostop}
+ />
+
Allow users to customize autostop duration for workspaces.
@@ -417,8 +390,8 @@ export const TemplateScheduleForm: FC = ({
Autostop timers on their workspaces or turn off the timer.
- }
- />
+
+
@@ -426,29 +399,26 @@ export const TemplateScheduleForm: FC = ({
title="Autostart"
description="Allow users to set custom autostart and autostop scheduling options for workspaces created from this template."
>
-
- {
- await form.setFieldValue(
- "allow_user_autostart",
- !form.values.allow_user_autostart,
- );
- }}
- name="allow_user_autostart"
- checked={form.values.allow_user_autostart}
- />
- }
- label={
+
+
+ {
+ await form.setFieldValue(
+ "allow_user_autostart",
+ checked === true,
+ );
+ }}
+ name="allow_user_autostart"
+ checked={form.values.allow_user_autostart}
+ />
+
Allow users to automatically start workspaces on a schedule.
- }
- />
+
+
{allowAdvancedScheduling && (
= ({
}}
/>
)}
-
+
{allowAdvancedScheduling && (
@@ -474,18 +444,18 @@ export const TemplateScheduleForm: FC = ({
description="When enabled, Coder will mark workspaces as dormant after a period of time with no connections. Dormant workspaces can be auto-deleted (see below) or manually reviewed by the workspace owner or admins."
>
-
-
- }
- label={Enable Dormancy Threshold }
- />
+
+
+
+
+ Enable Dormancy Threshold
+
+
= ({
isSubmitting || !form.values.inactivity_cleanup_enabled
}
/>
-
-
-
-
- }
- label={
+
+
+
+
+
+
Enable Dormancy Auto-Deletion
@@ -525,8 +493,8 @@ export const TemplateScheduleForm: FC = ({
- }
- />
+
+
= ({
!form.values.dormant_autodeletion_cleanup_enabled
}
/>
-
-
-
-
- }
- label={
+
+
+
+
+
+
Enable Failure Cleanup
@@ -565,8 +531,8 @@ export const TemplateScheduleForm: FC = ({
are in a failed state after a period of time.
- }
- />
+
+
= ({
onChange={(v) => form.setFieldValue("failure_ttl_ms", v)}
disabled={isSubmitting || !form.values.failure_cleanup_enabled}
/>
-
+
)}
@@ -646,12 +612,3 @@ export const TemplateScheduleForm: FC = ({
);
};
-
-const styles = {
- ttlFields: {
- width: "100%",
- },
- dayButtons: {
- borderRadius: "0px",
- },
-};
diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx
index 5ce54cc1178..c270ddb10bc 100644
--- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx
+++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx
@@ -75,27 +75,24 @@ const fillAndSubmitForm = async ({
}
if (failure_ttl_ms) {
- const failureTtlField = screen.getByRole("checkbox", {
+ const failureTtlField = screen.getByRole("switch", {
name: /Failure Cleanup/i,
});
- await user.type(failureTtlField, failure_ttl_ms.toString());
+ await user.click(failureTtlField);
}
if (time_til_dormant_ms) {
- const inactivityTtlField = screen.getByRole("checkbox", {
+ const inactivityTtlField = screen.getByRole("switch", {
name: /Dormancy Threshold/i,
});
- await user.type(inactivityTtlField, time_til_dormant_ms.toString());
+ await user.click(inactivityTtlField);
}
if (time_til_dormant_autodelete_ms) {
- const dormancyAutoDeletionField = screen.getByRole("checkbox", {
+ const dormancyAutoDeletionField = screen.getByRole("switch", {
name: /Dormancy Auto-Deletion/i,
});
- await user.type(
- dormancyAutoDeletionField,
- time_til_dormant_autodelete_ms.toString(),
- );
+ await user.click(dormancyAutoDeletionField);
}
const submitButton = screen.getByRole("button", {
@@ -352,4 +349,64 @@ describe("TemplateSchedulePage", () => {
const validate = () => getValidationSchema().validateSync(values);
expect(validate).toThrowError();
});
+
+ it("disables activity bump field when default TTL is 0", async () => {
+ await renderTemplateSchedulePage();
+ const user = userEvent.setup();
+
+ const defaultTtlField = await screen.findByLabelText(
+ "Default autostop (hours)",
+ );
+ const activityBumpField = screen.getByLabelText("Activity bump (hours)");
+
+ // Activity bump should be enabled when default TTL is non-zero.
+ expect(activityBumpField).not.toBeDisabled();
+
+ // Set default TTL to 0.
+ await user.clear(defaultTtlField);
+ await user.type(defaultTtlField, "0");
+
+ // Activity bump should now be disabled.
+ expect(activityBumpField).toBeDisabled();
+ });
+
+ it("shows helper text on activity bump when default TTL is 0", async () => {
+ await renderTemplateSchedulePage();
+ const user = userEvent.setup();
+
+ const defaultTtlField = await screen.findByLabelText(
+ "Default autostop (hours)",
+ );
+
+ // Set default TTL to 0.
+ await user.clear(defaultTtlField);
+ await user.type(defaultTtlField, "0");
+
+ // Should show the explanatory helper text.
+ expect(
+ screen.getByText(
+ /activity bump only applies when a default TTL is configured/i,
+ ),
+ ).toBeInTheDocument();
+ });
+
+ it("re-enables activity bump field when default TTL is set back to non-zero", async () => {
+ await renderTemplateSchedulePage();
+ const user = userEvent.setup();
+
+ const defaultTtlField = await screen.findByLabelText(
+ "Default autostop (hours)",
+ );
+ const activityBumpField = screen.getByLabelText("Activity bump (hours)");
+
+ // Set default TTL to 0.
+ await user.clear(defaultTtlField);
+ await user.type(defaultTtlField, "0");
+ expect(activityBumpField).toBeDisabled();
+
+ // Set default TTL back to a non-zero value.
+ await user.clear(defaultTtlField);
+ await user.type(defaultTtlField, "8");
+ expect(activityBumpField).not.toBeDisabled();
+ });
});
diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePageView.stories.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePageView.stories.tsx
index 364fadbd7ee..44f3f0e248a 100644
--- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePageView.stories.tsx
+++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePageView.stories.tsx
@@ -2,6 +2,7 @@ import { MockTemplate } from "testHelpers/entities";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { QueryClient, QueryClientProvider } from "react-query";
import { action } from "storybook/actions";
+import { expect, fn, userEvent, waitFor, within } from "storybook/test";
import { TemplateSchedulePageView } from "./TemplateSchedulePageView";
const queryClient = new QueryClient({
@@ -43,3 +44,41 @@ export const Example: Story = {
export const CantSetMaxTTL: Story = {
args: { ...defaultArgs, allowAdvancedScheduling: false },
};
+
+export const SubmitClearsActivityBumpWhenDefaultTTLIsZero: Story = {
+ args: {
+ ...defaultArgs,
+ template: {
+ ...MockTemplate,
+ // Start with a non-zero activity bump so we can verify
+ // it gets discarded when default TTL is set to 0.
+ activity_bump_ms: 3 * 60 * 60 * 1000,
+ },
+ onSubmit: fn(),
+ },
+ play: async ({ canvasElement, args }) => {
+ const canvas = within(canvasElement);
+ const user = userEvent.setup();
+
+ const defaultTtlField = await canvas.findByLabelText(
+ "Default autostop (hours)",
+ );
+ const activityBumpField = canvas.getByLabelText("Activity bump (hours)");
+
+ await user.clear(defaultTtlField);
+ await user.type(defaultTtlField, "0");
+
+ await expect(activityBumpField).toBeDisabled();
+
+ const submitButton = canvas.getByRole("button", { name: /save/i });
+ await user.click(submitButton);
+
+ await waitFor(() => {
+ expect(args.onSubmit).toHaveBeenCalledWith(
+ expect.objectContaining({
+ activity_bump_ms: undefined,
+ }),
+ );
+ });
+ },
+};