diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TTLHelperText.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TTLHelperText.tsx
index 90a1606d35b..f58e56a7408 100644
--- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TTLHelperText.tsx
+++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TTLHelperText.tsx
@@ -25,14 +25,20 @@ export const DefaultTTLHelperText = (props: { ttl?: number }) => {
export const ActivityBumpHelperText = (props: {
bump?: number;
defaultTTL?: number;
+ allowUserAutostop?: boolean;
}) => {
- const { bump = 0, defaultTTL = 0 } = props;
+ const { bump = 0, defaultTTL = 0, allowUserAutostop = false } = props;
- if (!defaultTTL) {
+ // Activity bump extends a workspace's scheduled stop time. If there is no
+ // default TTL AND users cannot set their own autostop, there is no stop
+ // time to bump, so the field has no effect.
+ if (!defaultTTL && !allowUserAutostop) {
return (
- Activity bump only applies when a default TTL is configured. Set a
- default TTL above to enable activity bumping.
+ Activity bump only applies when "Default autostop" is configured or
+ users are allowed to customize autostop. Set "Default autostop" above or
+ check "Allow users to customize autostop duration for workspaces" below
+ to enable activity bumping.
);
}
diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm.tsx
index 2db8dafc2f9..d4eda81d32d 100644
--- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm.tsx
+++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateScheduleForm.tsx
@@ -201,10 +201,12 @@ export const TemplateScheduleForm: FC = ({
default_ttl_ms: form.values.default_ttl_ms
? form.values.default_ttl_ms * MS_HOUR_CONVERSION
: undefined,
- // Activity bump has no effect without a default TTL, so
- // discard any stale value when default autostop is off.
+ // Activity bump has no effect without a scheduled stop time, so
+ // discard any stale value when there is no default TTL AND users
+ // cannot customize autostop on their workspaces.
activity_bump_ms:
- form.values.default_ttl_ms && form.values.activity_bump_ms
+ (form.values.default_ttl_ms || form.values.allow_user_autostop) &&
+ form.values.activity_bump_ms
? form.values.activity_bump_ms * MS_HOUR_CONVERSION
: undefined,
failure_ttl_ms: form.values.failure_ttl_ms,
@@ -307,10 +309,14 @@ export const TemplateScheduleForm: FC = ({
),
})}
- disabled={isSubmitting || !form.values.default_ttl_ms}
+ disabled={
+ isSubmitting ||
+ (!form.values.default_ttl_ms && !form.values.allow_user_autostop)
+ }
fullWidth
inputProps={{ min: 0, step: 1 }}
label="Activity bump (hours)"
diff --git a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx
index 21837080e62..d2edb82598f 100644
--- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx
+++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage.test.tsx
@@ -349,64 +349,4 @@ 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 5ba34480bfb..d3b4686ee5e 100644
--- a/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePageView.stories.tsx
+++ b/site/src/pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePageView.stories.tsx
@@ -53,6 +53,10 @@ export const SubmitClearsActivityBumpWhenDefaultTTLIsZero: Story = {
// 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,
+ // Disable user autostop so the guard applies (activity bump
+ // remains editable when either default TTL or user autostop is
+ // enabled).
+ allow_user_autostop: false,
},
onSubmit: fn(),
},
@@ -70,6 +74,13 @@ export const SubmitClearsActivityBumpWhenDefaultTTLIsZero: Story = {
await expect(activityBumpField).toBeDisabled();
+ // Helper text explains why the field is disabled.
+ await expect(
+ canvas.getByText(
+ /activity bump only applies when "default autostop" is configured or users are allowed to customize autostop/i,
+ ),
+ ).toBeInTheDocument();
+
const submitButton = canvas.getByRole("button", { name: /save/i });
await user.click(submitButton);
@@ -82,3 +93,105 @@ export const SubmitClearsActivityBumpWhenDefaultTTLIsZero: Story = {
});
},
};
+
+export const SubmitPreservesActivityBumpWhenAllowUserAutostopIsEnabled: Story =
+ {
+ args: {
+ ...defaultArgs,
+ template: {
+ ...MockTemplate,
+ activity_bump_ms: 3 * 60 * 60 * 1000,
+ // Users can customize autostop on their workspaces, so activity
+ // bump still has meaning even without a default TTL.
+ allow_user_autostop: true,
+ },
+ 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");
+
+ // Activity bump stays enabled because allow_user_autostop is on.
+ await expect(activityBumpField).not.toBeDisabled();
+
+ const submitButton = canvas.getByRole("button", { name: /save/i });
+ await user.click(submitButton);
+
+ await waitFor(() => {
+ expect(args.onSubmit).toHaveBeenCalledWith(
+ expect.objectContaining({
+ activity_bump_ms: 3 * 60 * 60 * 1000,
+ }),
+ );
+ });
+ },
+ };
+
+export const ReEnablesActivityBumpWhenDefaultTTLIsSetBack: Story = {
+ args: {
+ ...defaultArgs,
+ template: {
+ ...MockTemplate,
+ allow_user_autostop: false,
+ },
+ },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const user = userEvent.setup();
+
+ const defaultTtlField = await canvas.findByLabelText(
+ "Default autostop (hours)",
+ );
+ const activityBumpField = canvas.getByLabelText("Activity bump (hours)");
+
+ // Guard fires: default TTL is 0 and user autostop is off.
+ await user.clear(defaultTtlField);
+ await user.type(defaultTtlField, "0");
+ await expect(activityBumpField).toBeDisabled();
+
+ // Setting default TTL back to a non-zero value re-enables the field.
+ await user.clear(defaultTtlField);
+ await user.type(defaultTtlField, "8");
+ await expect(activityBumpField).not.toBeDisabled();
+ },
+};
+
+export const ReEnablesActivityBumpWhenAllowUserAutostopIsToggledOn: Story = {
+ args: {
+ ...defaultArgs,
+ template: {
+ ...MockTemplate,
+ allow_user_autostop: false,
+ },
+ },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const user = userEvent.setup();
+
+ const defaultTtlField = await canvas.findByLabelText(
+ "Default autostop (hours)",
+ );
+ const activityBumpField = canvas.getByLabelText("Activity bump (hours)");
+ const allowUserAutostopCheckbox = canvas.getByRole("checkbox", {
+ name: /allow users to customize autostop/i,
+ });
+
+ // Guard fires: default TTL is 0 and user autostop is off.
+ await user.clear(defaultTtlField);
+ await user.type(defaultTtlField, "0");
+ await expect(activityBumpField).toBeDisabled();
+
+ // Toggling user autostop back on re-enables the field without
+ // touching the default TTL.
+ await user.click(allowUserAutostopCheckbox);
+ await expect(activityBumpField).not.toBeDisabled();
+ },
+};