diff --git a/site/src/components/DropdownButton/ActionCtas.tsx b/site/src/components/DropdownButton/ActionCtas.tsx index 0392985429b08..5be231e9793c9 100644 --- a/site/src/components/DropdownButton/ActionCtas.tsx +++ b/site/src/components/DropdownButton/ActionCtas.tsx @@ -17,6 +17,7 @@ export const Language = { delete: "Delete", cancel: "Cancel", update: "Update", + updating: "Updating", // these labels are used in WorkspaceActions.tsx starting: "Starting...", stopping: "Stopping...", diff --git a/site/src/components/Workspace/Workspace.tsx b/site/src/components/Workspace/Workspace.tsx index dfa22b4cbd7ca..e8c1a82a41b76 100644 --- a/site/src/components/Workspace/Workspace.tsx +++ b/site/src/components/Workspace/Workspace.tsx @@ -39,6 +39,7 @@ export interface WorkspaceProps { handleDelete: () => void handleUpdate: () => void handleCancel: () => void + isUpdating: boolean workspace: TypesGen.Workspace resources?: TypesGen.WorkspaceResource[] builds?: TypesGen.WorkspaceBuild[] @@ -61,6 +62,7 @@ export const Workspace: FC> = ({ handleUpdate, handleCancel, workspace, + isUpdating, resources, builds, canUpdateWorkspace, @@ -104,6 +106,7 @@ export const Workspace: FC> = ({ handleDelete={handleDelete} handleUpdate={handleUpdate} handleCancel={handleCancel} + isUpdating={isUpdating} /> } diff --git a/site/src/components/WorkspaceActions/WorkspaceActions.stories.tsx b/site/src/components/WorkspaceActions/WorkspaceActions.stories.tsx index 4b48a214ace08..ae9b72bfef04a 100644 --- a/site/src/components/WorkspaceActions/WorkspaceActions.stories.tsx +++ b/site/src/components/WorkspaceActions/WorkspaceActions.stories.tsx @@ -16,6 +16,7 @@ const defaultArgs = { handleDelete: action("delete"), handleUpdate: action("update"), handleCancel: action("cancel"), + isUpdating: false, } export const Starting = Template.bind({}) @@ -77,3 +78,10 @@ Errored.args = { ...defaultArgs, workspace: Mocks.MockFailedWorkspace, } + +export const Updating = Template.bind({}) +Updating.args = { + ...defaultArgs, + isUpdating: true, + workspace: Mocks.MockOutdatedWorkspace, +} diff --git a/site/src/components/WorkspaceActions/WorkspaceActions.test.tsx b/site/src/components/WorkspaceActions/WorkspaceActions.test.tsx index bb8df9087bb56..b2b0194563c80 100644 --- a/site/src/components/WorkspaceActions/WorkspaceActions.test.tsx +++ b/site/src/components/WorkspaceActions/WorkspaceActions.test.tsx @@ -14,6 +14,7 @@ const renderComponent = async (props: Partial = {}) => { handleDelete={jest.fn()} handleUpdate={jest.fn()} handleCancel={jest.fn()} + isUpdating={false} />, ) } @@ -27,6 +28,7 @@ const renderAndClick = async (props: Partial = {}) => { handleDelete={jest.fn()} handleUpdate={jest.fn()} handleCancel={jest.fn()} + isUpdating={false} />, ) const trigger = await screen.findByTestId("workspace-actions-button") diff --git a/site/src/components/WorkspaceActions/WorkspaceActions.tsx b/site/src/components/WorkspaceActions/WorkspaceActions.tsx index 48bb882f94189..ca543e01a7e1b 100644 --- a/site/src/components/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/components/WorkspaceActions/WorkspaceActions.tsx @@ -27,6 +27,7 @@ export interface WorkspaceActionsProps { handleDelete: () => void handleUpdate: () => void handleCancel: () => void + isUpdating: boolean children?: ReactNode } @@ -37,6 +38,7 @@ export const WorkspaceActions: FC = ({ handleDelete, handleUpdate, handleCancel, + isUpdating, }) => { const workspaceStatus: keyof typeof WorkspaceStateEnum = getWorkspaceStatus( workspace.latest_build, @@ -63,6 +65,7 @@ export const WorkspaceActions: FC = ({ // A mapping of button type to the corresponding React component const buttonMapping: ButtonMapping = { [ButtonTypesEnum.update]: , + [ButtonTypesEnum.updating]: , [ButtonTypesEnum.start]: , [ButtonTypesEnum.starting]: , [ButtonTypesEnum.stop]: , @@ -77,7 +80,9 @@ export const WorkspaceActions: FC = ({ return ( ({ diff --git a/site/src/components/WorkspaceActions/constants.ts b/site/src/components/WorkspaceActions/constants.ts index d27258ab17f20..ad3d47731ef5a 100644 --- a/site/src/components/WorkspaceActions/constants.ts +++ b/site/src/components/WorkspaceActions/constants.ts @@ -10,6 +10,7 @@ export enum ButtonTypesEnum { delete = "delete", deleting = "deleting", update = "update", + updating = "updating", // disabled buttons canceling = "canceling", disabled = "disabled", diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index 61e46f19821c7..390a3950350a5 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -157,7 +157,32 @@ describe("WorkspacePage", () => { return res(ctx.status(200), ctx.json(MockOutdatedWorkspace)) }), ) - testButton(Language.update, getTemplateMock) + + await renderWorkspacePage() + const button = await screen.findByText(Language.update, { exact: true }) + fireEvent.click(button) + + // getTemplate is called twice: once when the machine starts, and once after the user requests to update + expect(getTemplateMock).toBeCalledTimes(2) + }) + it("after an update postWorkspaceBuild is called with the latest template active version id", async () => { + jest.spyOn(api, "getTemplate").mockResolvedValueOnce(MockTemplate) // active_version_id = "test-template-version" + jest.spyOn(api, "startWorkspace").mockResolvedValueOnce({ + ...MockWorkspaceBuild, + }) + + server.use( + rest.get(`/api/v2/users/:userId/workspace/:workspaceName`, (req, res, ctx) => { + return res(ctx.status(200), ctx.json(MockOutdatedWorkspace)) + }), + ) + await renderWorkspacePage() + const button = await screen.findByText(Language.update, { exact: true }) + fireEvent.click(button) + + await waitFor(() => + expect(api.startWorkspace).toBeCalledWith("test-workspace", "test-template-version"), + ) }) it("shows the Stopping status when the workspace is stopping", async () => { await testStatus(MockStoppingWorkspace, DisplayStatusLanguage.stopping) diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index eb148f89a6ce3..20259771298e5 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -113,6 +113,7 @@ export const WorkspacePage: FC = () => { return canExtendDeadline(deadline, workspace, template) }, }} + isUpdating={workspaceState.hasTag("updating")} workspace={workspace} handleStart={() => workspaceSend("START")} handleStop={() => workspaceSend("STOP")} diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index 13bb3accb9ec3..7538a2453f0fa 100644 --- a/site/src/xServices/workspace/workspaceXService.ts +++ b/site/src/xServices/workspace/workspaceXService.ts @@ -257,7 +257,7 @@ export const workspaceMachine = createMachine( START: "requestingStart", STOP: "requestingStop", ASK_DELETE: "askingDelete", - UPDATE: "requestingStartWithLatestTemplate", + UPDATE: "updatingWorkspace", CANCEL: "requestingCancel", }, }, @@ -271,18 +271,37 @@ export const workspaceMachine = createMachine( }, }, }, - requestingStartWithLatestTemplate: { - entry: "clearBuildError", - invoke: { - id: "startWorkspaceWithLatestTemplate", - src: "startWorkspaceWithLatestTemplate", - onDone: { - target: "idle", - actions: ["assignBuild"], + updatingWorkspace: { + tags: "updating", + initial: "refreshingTemplate", + states: { + refreshingTemplate: { + invoke: { + id: "refreshTemplate", + src: "getTemplate", + onDone: { + target: "startingWithLatestTemplate", + actions: ["assignTemplate"], + }, + onError: { + target: "#workspaceState.ready.build.idle", + actions: ["assignGetTemplateWarning"], + }, + }, }, - onError: { - target: "idle", - actions: ["assignBuildError"], + startingWithLatestTemplate: { + invoke: { + id: "startWorkspaceWithLatestTemplate", + src: "startWorkspaceWithLatestTemplate", + onDone: { + target: "#workspaceState.ready.build.idle", + actions: ["assignBuild"], + }, + onError: { + target: "#workspaceState.ready.build.idle", + actions: ["assignBuildError"], + }, + }, }, }, },