From ff45d7124241ad36ef5e080299656fdcacd5fe7e Mon Sep 17 00:00:00 2001 From: Presley Date: Mon, 23 May 2022 23:50:42 +0000 Subject: [PATCH 1/8] Start hooking up cancel --- site/src/api/api.ts | 5 ++++ .../xServices/workspace/workspaceXService.ts | 26 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 8b763bb644595..a1439bf12eb8d 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -161,6 +161,11 @@ export const startWorkspace = postWorkspaceBuild("start") export const stopWorkspace = postWorkspaceBuild("stop") export const deleteWorkspace = postWorkspaceBuild("delete") +export const cancelWorkspaceBuild = async (workspaceBuildId: TypesGen.WorkspaceBuild["id"]): Promise => { + const response = await axios.patch(`/api/v2/workspacebuilds/${workspaceBuildId}/cancel`) + return response.data +} + export const createUser = async (user: TypesGen.CreateUserRequest): Promise => { const response = await axios.post("/api/v2/users", user) return response.data diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index c8b2fea620b41..6d8612562b736 100644 --- a/site/src/xServices/workspace/workspaceXService.ts +++ b/site/src/xServices/workspace/workspaceXService.ts @@ -1,3 +1,4 @@ +import { ContextExclusionPlugin } from "webpack" import { assign, createMachine, send } from "xstate" import { pure } from "xstate/lib/actions" import * as API from "../../api/api" @@ -68,6 +69,9 @@ export const workspaceMachine = createMachine( stopWorkspace: { data: TypesGen.WorkspaceBuild } + cancelWorkspace: { + data: TypesGen.WorkspaceBuild + } refreshWorkspace: { data: TypesGen.Workspace | undefined } @@ -208,6 +212,21 @@ export const workspaceMachine = createMachine( }, }, }, + requestingCancel: { + entry: "clearCancelError", + invoke: { + id: "cancelWorkspace", + src: "cancelWorkspace", + onDone: { + target: "idle", + actions: ["assignBuild", "refreshTimeline"], + }, + onError: { + target: "idle", + actions: ["assignBuildError", "displayBuildError"] + } + } + }, refreshingTemplate: { entry: "clearRefreshTemplateError", invoke: { @@ -459,6 +478,13 @@ export const workspaceMachine = createMachine( throw Error("Cannot stop workspace without workspace id") } }, + cancelWorkspace: async (context) => { + if (context.workspace) { + return await API.cancelWorkspaceBuild(context.workspace.latest_build.id) + } else { + throw Error("Cannot cancel workspace without build id") + } + }, refreshWorkspace: async (context) => { if (context.workspace) { return await API.getWorkspace(context.workspace.id) From a771f59885adfa9d18d2b08de893332b580c428c Mon Sep 17 00:00:00 2001 From: Presley Date: Tue, 24 May 2022 18:33:01 +0000 Subject: [PATCH 2/8] Update xservice --- .../xServices/workspace/workspaceXService.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index 3cb7b75f4e93e..bdfbafa3f6bcf 100644 --- a/site/src/xServices/workspace/workspaceXService.ts +++ b/site/src/xServices/workspace/workspaceXService.ts @@ -33,6 +33,7 @@ export interface WorkspaceContext { builds?: TypesGen.WorkspaceBuild[] getBuildsError?: Error | unknown loadMoreBuildsError?: Error | unknown + cancellationMessage: string } export type WorkspaceEvent = @@ -64,7 +65,7 @@ export const workspaceMachine = createMachine( data: TypesGen.WorkspaceBuild } cancelWorkspace: { - data: TypesGen.WorkspaceBuild + data: string } refreshWorkspace: { data: TypesGen.Workspace | undefined @@ -170,17 +171,17 @@ export const workspaceMachine = createMachine( }, }, requestingCancel: { - entry: "clearCancelError", + entry: "clearCancellationMessage", invoke: { id: "cancelWorkspace", src: "cancelWorkspace", onDone: { target: "idle", - actions: ["assignBuild", "refreshTimeline"], + actions: ["assignCancellationMessage", "refreshTimeline"], }, onError: { target: "idle", - actions: ["assignBuildError", "displayBuildError"] + actions: ["assignCancellationMessage", "displayCancellationError"] } } }, @@ -312,6 +313,15 @@ export const workspaceMachine = createMachine( assign({ buildError: undefined, }), + assignCancellationMessage: (_, event) => assign({ + cancellationMessage: event.data + }), + clearCancellationMessage: (_) => assign({ + cancellationMessage: undefined + }), + displayCancellationError: (context) => { + displayError(context.cancellationMessage) + }, assignRefreshWorkspaceError: (_, event) => assign({ refreshWorkspaceError: event.data, @@ -331,6 +341,7 @@ export const workspaceMachine = createMachine( assign({ refreshTemplateError: undefined, }), + // Resources assignResources: assign({ resources: (_, event) => event.data, }), From 5b5219e131e467900fa9c34ba1cf6bd8c3daabbd Mon Sep 17 00:00:00 2001 From: Presley Date: Wed, 25 May 2022 14:10:07 +0000 Subject: [PATCH 3/8] Render cancel Changes behavior of other buttons too --- site/src/components/Workspace/Workspace.tsx | 3 ++ .../WorkspaceActionButton.tsx | 4 +- .../WorkspaceActions/WorkspaceActions.tsx | 47 ++++++++++++++----- .../src/pages/WorkspacePage/WorkspacePage.tsx | 1 + site/src/util/workspace.ts | 4 +- .../xServices/workspace/workspaceXService.ts | 3 +- 6 files changed, 44 insertions(+), 18 deletions(-) diff --git a/site/src/components/Workspace/Workspace.tsx b/site/src/components/Workspace/Workspace.tsx index d773d1ee874eb..b4c978087cee7 100644 --- a/site/src/components/Workspace/Workspace.tsx +++ b/site/src/components/Workspace/Workspace.tsx @@ -16,6 +16,7 @@ export interface WorkspaceProps { handleStop: () => void handleRetry: () => void handleUpdate: () => void + handleCancel: () => void workspace: TypesGen.Workspace resources?: TypesGen.WorkspaceResource[] getResourcesError?: Error @@ -30,6 +31,7 @@ export const Workspace: React.FC = ({ handleStop, handleRetry, handleUpdate, + handleCancel, workspace, resources, getResourcesError, @@ -57,6 +59,7 @@ export const Workspace: React.FC = ({ handleStop={handleStop} handleRetry={handleRetry} handleUpdate={handleUpdate} + handleCancel={handleCancel} /> diff --git a/site/src/components/WorkspaceActionButton/WorkspaceActionButton.tsx b/site/src/components/WorkspaceActionButton/WorkspaceActionButton.tsx index 1188dec6a771c..970cf92132442 100644 --- a/site/src/components/WorkspaceActionButton/WorkspaceActionButton.tsx +++ b/site/src/components/WorkspaceActionButton/WorkspaceActionButton.tsx @@ -5,8 +5,8 @@ import React from "react" export interface WorkspaceActionButtonProps { label: string - loadingLabel: string - isLoading: boolean + loadingLabel?: string + isLoading?: boolean icon: JSX.Element onClick: () => void className?: string diff --git a/site/src/components/WorkspaceActions/WorkspaceActions.tsx b/site/src/components/WorkspaceActions/WorkspaceActions.tsx index 15d0d391297a0..4ca539661bcf9 100644 --- a/site/src/components/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/components/WorkspaceActions/WorkspaceActions.tsx @@ -5,6 +5,7 @@ import CloudDownloadIcon from "@material-ui/icons/CloudDownload" import PlayArrowRoundedIcon from "@material-ui/icons/PlayArrowRounded" import ReplayIcon from "@material-ui/icons/Replay" import StopIcon from "@material-ui/icons/Stop" +import CancelIcon from "@material-ui/icons/Cancel" import React from "react" import { Link as RouterLink } from "react-router-dom" import { Workspace } from "../../api/typesGenerated" @@ -18,6 +19,7 @@ export const Language = { start: "Start workspace", starting: "Starting workspace", retry: "Retry", + cancel: "Cancel action", update: "Update workspace", } @@ -28,12 +30,27 @@ export const Language = { const canAcceptJobs = (workspaceStatus: WorkspaceStatus) => ["started", "stopped", "deleted", "error", "canceled"].includes(workspaceStatus) +/** + * Jobs that are in progress (queued or pending) can be canceled. + * @param workspaceStatus WorkspaceStatus + * @returns boolean + */ +const canCancelJobs = (workspaceStatus: WorkspaceStatus) => + ["starting", "stopping", "deleting"].includes(workspaceStatus) + +const canStart = (workspaceStatus: WorkspaceStatus) => + ["stopped", "canceled", "error"].includes(workspaceStatus) + +const canStop = (workspaceStatus: WorkspaceStatus) => + ["started", "canceled", "error"].includes(workspaceStatus) + export interface WorkspaceActionsProps { workspace: Workspace handleStart: () => void handleStop: () => void handleRetry: () => void handleUpdate: () => void + handleCancel: () => void } export const WorkspaceActions: React.FC = ({ @@ -42,6 +59,7 @@ export const WorkspaceActions: React.FC = ({ handleStop, handleRetry, handleUpdate, + handleCancel }) => { const styles = useStyles() const workspaceStatus = getWorkspaceStatus(workspace.latest_build) @@ -51,7 +69,17 @@ export const WorkspaceActions: React.FC = ({ - {(workspaceStatus === "started" || workspaceStatus === "stopping") && ( + {canStart(workspaceStatus) && ( + } + onClick={handleStart} + label={Language.start} + loadingLabel={Language.starting} + isLoading={workspaceStatus === "starting"} + /> + )} + {canStop(workspaceStatus) && ( } @@ -61,21 +89,14 @@ export const WorkspaceActions: React.FC = ({ isLoading={workspaceStatus === "stopping"} /> )} - {(workspaceStatus === "stopped" || workspaceStatus === "starting") && ( + {canCancelJobs(workspaceStatus) && } - onClick={handleStart} - label={Language.start} - loadingLabel={Language.starting} - isLoading={workspaceStatus === "starting"} + icon={} + onClick={handleCancel} + label={Language.cancel} /> - )} - {workspaceStatus === "error" && ( - - )} + } {workspace.outdated && canAcceptJobs(workspaceStatus) && ( ) } - -const useStyles = makeStyles((theme) => ({ - spinner: { - color: theme.palette.text.disabled, - marginRight: theme.spacing(1), - }, -})) diff --git a/site/src/components/WorkspaceActions/WorkspaceActions.tsx b/site/src/components/WorkspaceActions/WorkspaceActions.tsx index 510fc59016569..23f179107d55a 100644 --- a/site/src/components/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/components/WorkspaceActions/WorkspaceActions.tsx @@ -69,8 +69,6 @@ export const WorkspaceActions: React.FC = ({ icon={} onClick={handleStart} label={Language.start} - loadingLabel={Language.starting} - isLoading={workspaceStatus === "starting"} /> )} {canStop(workspaceStatus) && ( @@ -79,8 +77,6 @@ export const WorkspaceActions: React.FC = ({ icon={} onClick={handleStop} label={Language.stop} - loadingLabel={Language.stopping} - isLoading={workspaceStatus === "stopping"} /> )} {canCancelJobs(workspaceStatus) && ( @@ -104,6 +100,6 @@ const useStyles = makeStyles((theme) => ({ actionButton: { // Set fixed width for the action buttons so they will not change the size // during the transitions - width: theme.spacing(30), + width: theme.spacing(27), }, })) From 12d0ebe4a6f88809a2fd95224dc1b37e6a9a4085 Mon Sep 17 00:00:00 2001 From: Presley Date: Wed, 25 May 2022 16:46:56 +0000 Subject: [PATCH 7/8] Fix type, extend tests --- site/src/api/api.ts | 3 ++- site/src/api/types.ts | 2 ++ .../pages/WorkspacePage/WorkspacePage.test.tsx | 15 +++++++++++++++ site/src/testHelpers/entities.ts | 4 ++++ site/src/testHelpers/handlers.ts | 3 +++ site/src/xServices/workspace/workspaceXService.ts | 3 ++- 6 files changed, 28 insertions(+), 2 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index a1439bf12eb8d..3cd0d3a7ec29c 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1,4 +1,5 @@ import axios, { AxiosRequestHeaders } from "axios" +import * as Types from "./types" import { WorkspaceBuildTransition } from "./types" import * as TypesGen from "./typesGenerated" @@ -161,7 +162,7 @@ export const startWorkspace = postWorkspaceBuild("start") export const stopWorkspace = postWorkspaceBuild("stop") export const deleteWorkspace = postWorkspaceBuild("delete") -export const cancelWorkspaceBuild = async (workspaceBuildId: TypesGen.WorkspaceBuild["id"]): Promise => { +export const cancelWorkspaceBuild = async (workspaceBuildId: TypesGen.WorkspaceBuild["id"]): Promise => { const response = await axios.patch(`/api/v2/workspacebuilds/${workspaceBuildId}/cancel`) return response.data } diff --git a/site/src/api/types.ts b/site/src/api/types.ts index 5ea8d8ff86285..daf4e451ac5e8 100644 --- a/site/src/api/types.ts +++ b/site/src/api/types.ts @@ -12,3 +12,5 @@ export interface ReconnectingPTYRequest { } export type WorkspaceBuildTransition = "start" | "stop" | "delete" + +export type Message = { message: string } diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index c4a5bd432808c..5340005dc1e07 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -6,6 +6,7 @@ import { Workspace } from "../../api/typesGenerated" import { Language } from "../../components/WorkspaceActions/WorkspaceActions" import { MockBuilds, + MockCanceledWorkspace, MockCancelingWorkspace, MockDeletedWorkspace, MockDeletingWorkspace, @@ -86,6 +87,17 @@ describe("Workspace Page", () => { .mockImplementation(() => Promise.resolve(MockWorkspaceBuild)) await testButton(Language.start, startWorkspaceMock) }) + it("requests cancellation when the user presses Cancel", async () => { + server.use( + rest.get(`/api/v2/workspaces/${MockWorkspace.id}`, (req, res, ctx) => { + return res(ctx.status(200), ctx.json(MockStartingWorkspace)) + }), + ) + const cancelWorkspaceMock = jest + .spyOn(api, "cancelWorkspaceBuild") + .mockImplementation(() => Promise.resolve({ message: "job canceled" })) + await testButton(Language.cancel, cancelWorkspaceMock) + }) it("requests a template when the user presses Update", async () => { const getTemplateMock = jest.spyOn(api, "getTemplate").mockResolvedValueOnce(MockTemplate) server.use( @@ -113,6 +125,9 @@ describe("Workspace Page", () => { it("shows the Canceling status when the workspace is canceling", async () => { await testStatus(MockCancelingWorkspace, DisplayStatusLanguage.canceling) }) + it("shows the Canceled status when the workspace is canceling", async () => { + await testStatus(MockCanceledWorkspace, DisplayStatusLanguage.canceled) + }) it("shows the Deleting status when the workspace is deleting", async () => { await testStatus(MockDeletingWorkspace, DisplayStatusLanguage.deleting) }) diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 39cec0c883645..c2b68ee72c43a 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -506,3 +506,7 @@ export const MockWorkspaceBuildLogs: TypesGen.ProvisionerJobLog[] = [ output: "", }, ] + +export const MockCancellationMessage = { + message: "Job successfully canceled", +} diff --git a/site/src/testHelpers/handlers.ts b/site/src/testHelpers/handlers.ts index 1870e7b4bfbdf..d5bc8c5dbc7d7 100644 --- a/site/src/testHelpers/handlers.ts +++ b/site/src/testHelpers/handlers.ts @@ -130,4 +130,7 @@ export const handlers = [ rest.get("/api/v2/workspacebuilds/:workspaceBuildId/logs", (req, res, ctx) => { return res(ctx.status(200), ctx.json(M.MockWorkspaceBuildLogs)) }), + rest.patch("/api/v2/workspacebuilds/:workspaceBuildId/cancel", (req, res, ctx) => { + return res(ctx.status(200), ctx.json(M.MockCancellationMessage)) + }), ] diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index fc7a8bcd0f42c..075475b45a9d0 100644 --- a/site/src/xServices/workspace/workspaceXService.ts +++ b/site/src/xServices/workspace/workspaceXService.ts @@ -1,6 +1,7 @@ import { assign, createMachine, send } from "xstate" import { pure } from "xstate/lib/actions" import * as API from "../../api/api" +import * as Types from "../../api/types" import * as TypesGen from "../../api/typesGenerated" import { displayError } from "../../components/GlobalSnackbar/utils" @@ -64,7 +65,7 @@ export const workspaceMachine = createMachine( data: TypesGen.WorkspaceBuild } cancelWorkspace: { - data: string + data: Types.Message } refreshWorkspace: { data: TypesGen.Workspace | undefined From 80b098000e9605b899c3d5924a578902ef045911 Mon Sep 17 00:00:00 2001 From: Presley Date: Wed, 25 May 2022 21:20:28 +0000 Subject: [PATCH 8/8] Update story --- .../WorkspaceActionButton.stories.tsx | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/site/src/components/WorkspaceActionButton/WorkspaceActionButton.stories.tsx b/site/src/components/WorkspaceActionButton/WorkspaceActionButton.stories.tsx index 0a3b9b315a83e..4995769fabe4a 100644 --- a/site/src/components/WorkspaceActionButton/WorkspaceActionButton.stories.tsx +++ b/site/src/components/WorkspaceActionButton/WorkspaceActionButton.stories.tsx @@ -14,14 +14,4 @@ export const Example = Template.bind({}) Example.args = { icon: , label: "Start workspace", - loadingLabel: "Starting workspace", - isLoading: false, -} - -export const Loading = Template.bind({}) -Loading.args = { - icon: , - label: "Start workspace", - loadingLabel: "Starting workspace", - isLoading: true, }