diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.stories.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.stories.tsx index d6a2fbcf82d..1af29059047 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.stories.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; -import { expect, spyOn, userEvent, within } from "storybook/test"; +import { expect, spyOn, userEvent, waitFor, within } from "storybook/test"; import { reactRouterOutlet, reactRouterParameters, @@ -73,7 +73,58 @@ export const EnablingAutostopUsesTemplateDefault: Story = { }, }; -export const ChangingAutostopShowsRestartDialog: Story = { +export const EnablingAutostopShowsRestartDialog: Story = { + parameters: { + reactRouter: workspaceRouterParameters(autostopDisabledWorkspace), + queries: workspaceQueries(autostopDisabledWorkspace), + }, + beforeEach: () => { + spyOn(API, "getWorkspaceByOwnerAndName").mockResolvedValue( + autostopDisabledWorkspace, + ); + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const body = within(document.body); + const user = userEvent.setup(); + await user.click(await canvas.findByLabelText("Enable Autostop")); + await user.click(await canvas.findByRole("button", { name: /save/i })); + await body.findByText( + `Schedule for workspace "${MockWorkspace.name}" updated successfully.`, + ); + await body.findByText("Restart workspace?"); + }, +}; + +export const ApplyLaterKeepsUserOnSchedulePage: Story = { + parameters: { + reactRouter: workspaceRouterParameters(autostopDisabledWorkspace), + queries: workspaceQueries(autostopDisabledWorkspace), + }, + beforeEach: () => { + spyOn(API, "getWorkspaceByOwnerAndName").mockResolvedValue( + autostopDisabledWorkspace, + ); + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const body = within(document.body); + const user = userEvent.setup(); + const restartSpy = spyOn(API, "restartWorkspace"); + await user.click(await canvas.findByLabelText("Enable Autostop")); + await user.click(await canvas.findByRole("button", { name: /save/i })); + await body.findByText("Restart workspace?"); + await user.click(await body.findByRole("button", { name: /apply later/i })); + // The dialog closes without restarting or leaving the schedule page. + await waitFor(() => + expect(body.queryByText("Restart workspace?")).not.toBeInTheDocument(), + ); + expect(restartSpy).not.toHaveBeenCalled(); + await canvas.findByLabelText("Enable Autostop"); + }, +}; + +export const ChangingAutostopValueShowsRestartDialog: Story = { parameters: { reactRouter: workspaceRouterParameters(MockWorkspace), queries: workspaceQueries(MockWorkspace), @@ -85,7 +136,11 @@ export const ChangingAutostopShowsRestartDialog: Story = { const canvas = within(canvasElement); const body = within(document.body); const user = userEvent.setup(); - await user.click(await canvas.findByLabelText("Enable Autostop")); + const ttlInput = await canvas.findByLabelText( + "Time until shutdown (hours)", + ); + await user.clear(ttlInput); + await user.type(ttlInput, "4"); await user.click(await canvas.findByRole("button", { name: /save/i })); await body.findByText( `Schedule for workspace "${MockWorkspace.name}" updated successfully.`, @@ -94,19 +149,41 @@ export const ChangingAutostopShowsRestartDialog: Story = { }, }; -const stoppedWorkspace: Workspace = { - ...MockWorkspace, +export const DisablingAutostopSkipsRestartDialog: Story = { + parameters: { + reactRouter: workspaceRouterParameters(MockWorkspace), + queries: workspaceQueries(MockWorkspace), + }, + beforeEach: () => { + spyOn(API, "getWorkspaceByOwnerAndName").mockResolvedValue(MockWorkspace); + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const body = within(document.body); + const user = userEvent.setup(); + // MockWorkspace has autostop enabled, so clicking the toggle disables it. + await user.click(await canvas.findByLabelText("Enable Autostop")); + await user.click(await canvas.findByRole("button", { name: /save/i })); + await body.findByText( + `Schedule for workspace "${MockWorkspace.name}" updated successfully.`, + ); + expect(body.queryByText("Restart workspace?")).not.toBeInTheDocument(); + }, +}; + +const stoppedAutostopDisabledWorkspace: Workspace = { + ...autostopDisabledWorkspace, latest_build: { ...MockWorkspaceBuild, status: "stopped" }, }; -export const ChangingAutostopWhileStoppedSkipsDialog: Story = { +export const EnablingAutostopWhileStoppedSkipsDialog: Story = { parameters: { - reactRouter: workspaceRouterParameters(stoppedWorkspace), - queries: workspaceQueries(stoppedWorkspace), + reactRouter: workspaceRouterParameters(stoppedAutostopDisabledWorkspace), + queries: workspaceQueries(stoppedAutostopDisabledWorkspace), }, beforeEach: () => { spyOn(API, "getWorkspaceByOwnerAndName").mockResolvedValue( - stoppedWorkspace, + stoppedAutostopDisabledWorkspace, ); }, play: async ({ canvasElement }) => { diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx index 5ef83950494..ed12a6b9e13 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx @@ -140,9 +140,19 @@ const WorkspaceSchedulePage: FC = () => { await submitScheduleMutation.mutateAsync(data); + // A running build's autostop deadline is calculated when the + // build starts, so updating the TTL does not retroactively + // change it. Prompt the user to restart so the new value takes + // effect immediately, but only when all of the following hold: + // - autostop actually changed (toggled or new TTL value), + // - autostop is enabled after the change; disabling clears the + // running build's deadline server-side, so no restart is + // needed, and + // - the workspace is running; a stopped workspace picks up the + // new value on its next start. if ( data.autostopChanged && - getAutostop(workspace).autostopEnabled && + values.autostopEnabled && workspace.latest_build.status === "running" ) { setIsConfirmingApply(true); @@ -163,7 +173,9 @@ const WorkspaceSchedulePage: FC = () => { navigate(`/@${username}/${workspaceName}`); }} onClose={() => { - navigate(`/@${username}/${workspaceName}`); + // Keep the user on the schedule page; the saved value still + // applies on the next workspace start. + setIsConfirmingApply(false); }} />