Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 774fc2a

Browse files
presleypkylecarbs
authored andcommitted
feat: UI for canceling workspace builds (#1735)
* Start hooking up cancel * Update xservice * Render cancel Changes behavior of other buttons too * Make outdated workspace story show max buttons * Remove retry code * Remove loading button state * Fix type, extend tests * Update story
1 parent 90ea37c commit 774fc2a

File tree

13 files changed

+106
-103
lines changed

13 files changed

+106
-103
lines changed

site/src/api/api.ts

+6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import axios, { AxiosRequestHeaders } from "axios"
2+
import * as Types from "./types"
23
import { WorkspaceBuildTransition } from "./types"
34
import * as TypesGen from "./typesGenerated"
45

@@ -161,6 +162,11 @@ export const startWorkspace = postWorkspaceBuild("start")
161162
export const stopWorkspace = postWorkspaceBuild("stop")
162163
export const deleteWorkspace = postWorkspaceBuild("delete")
163164

165+
export const cancelWorkspaceBuild = async (workspaceBuildId: TypesGen.WorkspaceBuild["id"]): Promise<Types.Message> => {
166+
const response = await axios.patch(`/api/v2/workspacebuilds/${workspaceBuildId}/cancel`)
167+
return response.data
168+
}
169+
164170
export const createUser = async (user: TypesGen.CreateUserRequest): Promise<TypesGen.User> => {
165171
const response = await axios.post<TypesGen.User>("/api/v2/users", user)
166172
return response.data

site/src/api/types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,5 @@ export interface ReconnectingPTYRequest {
1212
}
1313

1414
export type WorkspaceBuildTransition = "start" | "stop" | "delete"
15+
16+
export type Message = { message: string }

site/src/components/Workspace/Workspace.stories.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ Started.args = {
3131
workspace: MockWorkspace,
3232
handleStart: action("start"),
3333
handleStop: action("stop"),
34-
handleRetry: action("retry"),
3534
resources: [MockWorkspaceResource, MockWorkspaceResource2],
3635
builds: [MockWorkspaceBuild],
3736
}

site/src/components/Workspace/Workspace.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import { WorkspaceStats } from "../WorkspaceStats/WorkspaceStats"
1414
export interface WorkspaceProps {
1515
handleStart: () => void
1616
handleStop: () => void
17-
handleRetry: () => void
1817
handleUpdate: () => void
18+
handleCancel: () => void
1919
workspace: TypesGen.Workspace
2020
resources?: TypesGen.WorkspaceResource[]
2121
getResourcesError?: Error
@@ -28,8 +28,8 @@ export interface WorkspaceProps {
2828
export const Workspace: React.FC<WorkspaceProps> = ({
2929
handleStart,
3030
handleStop,
31-
handleRetry,
3231
handleUpdate,
32+
handleCancel,
3333
workspace,
3434
resources,
3535
getResourcesError,
@@ -55,8 +55,8 @@ export const Workspace: React.FC<WorkspaceProps> = ({
5555
workspace={workspace}
5656
handleStart={handleStart}
5757
handleStop={handleStop}
58-
handleRetry={handleRetry}
5958
handleUpdate={handleUpdate}
59+
handleCancel={handleCancel}
6060
/>
6161
</div>
6262
</div>

site/src/components/WorkspaceActionButton/WorkspaceActionButton.stories.tsx

-10
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,4 @@ export const Example = Template.bind({})
1414
Example.args = {
1515
icon: <PlayArrowRoundedIcon />,
1616
label: "Start workspace",
17-
loadingLabel: "Starting workspace",
18-
isLoading: false,
19-
}
20-
21-
export const Loading = Template.bind({})
22-
Loading.args = {
23-
icon: <PlayArrowRoundedIcon />,
24-
label: "Start workspace",
25-
loadingLabel: "Starting workspace",
26-
isLoading: true,
2717
}
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,17 @@
11
import Button from "@material-ui/core/Button"
2-
import CircularProgress from "@material-ui/core/CircularProgress"
3-
import { makeStyles } from "@material-ui/core/styles"
42
import React from "react"
53

64
export interface WorkspaceActionButtonProps {
75
label: string
8-
loadingLabel: string
9-
isLoading: boolean
106
icon: JSX.Element
117
onClick: () => void
128
className?: string
139
}
1410

15-
export const WorkspaceActionButton: React.FC<WorkspaceActionButtonProps> = ({
16-
label,
17-
loadingLabel,
18-
isLoading,
19-
icon,
20-
onClick,
21-
className,
22-
}) => {
23-
const styles = useStyles()
24-
11+
export const WorkspaceActionButton: React.FC<WorkspaceActionButtonProps> = ({ label, icon, onClick, className }) => {
2512
return (
26-
<Button
27-
className={className}
28-
startIcon={isLoading ? <CircularProgress size={12} className={styles.spinner} /> : icon}
29-
onClick={onClick}
30-
disabled={isLoading}
31-
>
32-
{isLoading ? loadingLabel : label}
13+
<Button className={className} startIcon={icon} onClick={onClick}>
14+
{label}
3315
</Button>
3416
)
3517
}
36-
37-
const useStyles = makeStyles((theme) => ({
38-
spinner: {
39-
color: theme.palette.text.disabled,
40-
marginRight: theme.spacing(1),
41-
},
42-
}))
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import Button from "@material-ui/core/Button"
22
import Link from "@material-ui/core/Link"
33
import { makeStyles } from "@material-ui/core/styles"
4+
import CancelIcon from "@material-ui/icons/Cancel"
45
import CloudDownloadIcon from "@material-ui/icons/CloudDownload"
56
import PlayArrowRoundedIcon from "@material-ui/icons/PlayArrowRounded"
6-
import ReplayIcon from "@material-ui/icons/Replay"
77
import StopIcon from "@material-ui/icons/Stop"
88
import React from "react"
99
import { Link as RouterLink } from "react-router-dom"
@@ -17,7 +17,7 @@ export const Language = {
1717
stopping: "Stopping workspace",
1818
start: "Start workspace",
1919
starting: "Starting workspace",
20-
retry: "Retry",
20+
cancel: "Cancel action",
2121
update: "Update workspace",
2222
}
2323

@@ -28,20 +28,32 @@ export const Language = {
2828
const canAcceptJobs = (workspaceStatus: WorkspaceStatus) =>
2929
["started", "stopped", "deleted", "error", "canceled"].includes(workspaceStatus)
3030

31+
/**
32+
* Jobs that are in progress (queued or pending) can be canceled.
33+
* @param workspaceStatus WorkspaceStatus
34+
* @returns boolean
35+
*/
36+
const canCancelJobs = (workspaceStatus: WorkspaceStatus) =>
37+
["starting", "stopping", "deleting"].includes(workspaceStatus)
38+
39+
const canStart = (workspaceStatus: WorkspaceStatus) => ["stopped", "canceled", "error"].includes(workspaceStatus)
40+
41+
const canStop = (workspaceStatus: WorkspaceStatus) => ["started", "canceled", "error"].includes(workspaceStatus)
42+
3143
export interface WorkspaceActionsProps {
3244
workspace: Workspace
3345
handleStart: () => void
3446
handleStop: () => void
35-
handleRetry: () => void
3647
handleUpdate: () => void
48+
handleCancel: () => void
3749
}
3850

3951
export const WorkspaceActions: React.FC<WorkspaceActionsProps> = ({
4052
workspace,
4153
handleStart,
4254
handleStop,
43-
handleRetry,
4455
handleUpdate,
56+
handleCancel,
4557
}) => {
4658
const styles = useStyles()
4759
const workspaceStatus = getWorkspaceStatus(workspace.latest_build)
@@ -51,31 +63,30 @@ export const WorkspaceActions: React.FC<WorkspaceActionsProps> = ({
5163
<Link underline="none" component={RouterLink} to="edit">
5264
<Button variant="outlined">Settings</Button>
5365
</Link>
54-
{(workspaceStatus === "started" || workspaceStatus === "stopping") && (
66+
{canStart(workspaceStatus) && (
67+
<WorkspaceActionButton
68+
className={styles.actionButton}
69+
icon={<PlayArrowRoundedIcon />}
70+
onClick={handleStart}
71+
label={Language.start}
72+
/>
73+
)}
74+
{canStop(workspaceStatus) && (
5575
<WorkspaceActionButton
5676
className={styles.actionButton}
5777
icon={<StopIcon />}
5878
onClick={handleStop}
5979
label={Language.stop}
60-
loadingLabel={Language.stopping}
61-
isLoading={workspaceStatus === "stopping"}
6280
/>
6381
)}
64-
{(workspaceStatus === "stopped" || workspaceStatus === "starting") && (
82+
{canCancelJobs(workspaceStatus) && (
6583
<WorkspaceActionButton
6684
className={styles.actionButton}
67-
icon={<PlayArrowRoundedIcon />}
68-
onClick={handleStart}
69-
label={Language.start}
70-
loadingLabel={Language.starting}
71-
isLoading={workspaceStatus === "starting"}
85+
icon={<CancelIcon />}
86+
onClick={handleCancel}
87+
label={Language.cancel}
7288
/>
7389
)}
74-
{workspaceStatus === "error" && (
75-
<Button className={styles.actionButton} startIcon={<ReplayIcon />} onClick={handleRetry}>
76-
{Language.retry}
77-
</Button>
78-
)}
7990
{workspace.outdated && canAcceptJobs(workspaceStatus) && (
8091
<Button className={styles.actionButton} startIcon={<CloudDownloadIcon />} onClick={handleUpdate}>
8192
{Language.update}
@@ -89,6 +100,6 @@ const useStyles = makeStyles((theme) => ({
89100
actionButton: {
90101
// Set fixed width for the action buttons so they will not change the size
91102
// during the transitions
92-
width: theme.spacing(30),
103+
width: theme.spacing(27),
93104
},
94105
}))

site/src/pages/WorkspacePage/WorkspacePage.test.tsx

+10-35
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Workspace } from "../../api/typesGenerated"
66
import { Language } from "../../components/WorkspaceActions/WorkspaceActions"
77
import {
88
MockBuilds,
9+
MockCanceledWorkspace,
910
MockCancelingWorkspace,
1011
MockDeletedWorkspace,
1112
MockDeletingWorkspace,
@@ -86,45 +87,16 @@ describe("Workspace Page", () => {
8687
.mockImplementation(() => Promise.resolve(MockWorkspaceBuild))
8788
await testButton(Language.start, startWorkspaceMock)
8889
})
89-
it("requests a start job when the user presses Retry after trying to start", async () => {
90-
// Use a workspace that failed during start
90+
it("requests cancellation when the user presses Cancel", async () => {
9191
server.use(
9292
rest.get(`/api/v2/workspaces/${MockWorkspace.id}`, (req, res, ctx) => {
93-
return res(
94-
ctx.status(200),
95-
ctx.json({
96-
...MockFailedWorkspace,
97-
latest_build: {
98-
...MockFailedWorkspace.latest_build,
99-
transition: "start",
100-
},
101-
}),
102-
)
93+
return res(ctx.status(200), ctx.json(MockStartingWorkspace))
10394
}),
10495
)
105-
const startWorkSpaceMock = jest.spyOn(api, "startWorkspace").mockResolvedValueOnce(MockWorkspaceBuild)
106-
await testButton(Language.retry, startWorkSpaceMock)
107-
})
108-
it("requests a stop job when the user presses Retry after trying to stop", async () => {
109-
// Use a workspace that failed during stop
110-
server.use(
111-
rest.get(`/api/v2/workspaces/${MockWorkspace.id}`, (req, res, ctx) => {
112-
return res(
113-
ctx.status(200),
114-
ctx.json({
115-
...MockFailedWorkspace,
116-
latest_build: {
117-
...MockFailedWorkspace.latest_build,
118-
transition: "stop",
119-
},
120-
}),
121-
)
122-
}),
123-
)
124-
const stopWorkspaceMock = jest
125-
.spyOn(api, "stopWorkspace")
126-
.mockImplementation(() => Promise.resolve(MockWorkspaceBuild))
127-
await testButton(Language.retry, stopWorkspaceMock)
96+
const cancelWorkspaceMock = jest
97+
.spyOn(api, "cancelWorkspaceBuild")
98+
.mockImplementation(() => Promise.resolve({ message: "job canceled" }))
99+
await testButton(Language.cancel, cancelWorkspaceMock)
128100
})
129101
it("requests a template when the user presses Update", async () => {
130102
const getTemplateMock = jest.spyOn(api, "getTemplate").mockResolvedValueOnce(MockTemplate)
@@ -153,6 +125,9 @@ describe("Workspace Page", () => {
153125
it("shows the Canceling status when the workspace is canceling", async () => {
154126
await testStatus(MockCancelingWorkspace, DisplayStatusLanguage.canceling)
155127
})
128+
it("shows the Canceled status when the workspace is canceling", async () => {
129+
await testStatus(MockCanceledWorkspace, DisplayStatusLanguage.canceled)
130+
})
156131
it("shows the Deleting status when the workspace is deleting", async () => {
157132
await testStatus(MockDeletingWorkspace, DisplayStatusLanguage.deleting)
158133
})

site/src/pages/WorkspacePage/WorkspacePage.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ export const WorkspacePage: React.FC = () => {
3636
workspace={workspace}
3737
handleStart={() => workspaceSend("START")}
3838
handleStop={() => workspaceSend("STOP")}
39-
handleRetry={() => workspaceSend("RETRY")}
4039
handleUpdate={() => workspaceSend("UPDATE")}
40+
handleCancel={() => workspaceSend("CANCEL")}
4141
resources={resources}
4242
getResourcesError={getResourcesError instanceof Error ? getResourcesError : undefined}
4343
builds={builds}

site/src/testHelpers/entities.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ export const MockDeletingWorkspace: TypesGen.Workspace = {
182182
}
183183
export const MockDeletedWorkspace: TypesGen.Workspace = { ...MockWorkspace, latest_build: MockWorkspaceBuildDelete }
184184

185-
export const MockOutdatedWorkspace: TypesGen.Workspace = { ...MockWorkspace, outdated: true }
185+
export const MockOutdatedWorkspace: TypesGen.Workspace = { ...MockFailedWorkspace, outdated: true }
186186

187187
export const MockWorkspaceAgent: TypesGen.WorkspaceAgent = {
188188
architecture: "amd64",
@@ -506,3 +506,7 @@ export const MockWorkspaceBuildLogs: TypesGen.ProvisionerJobLog[] = [
506506
output: "",
507507
},
508508
]
509+
510+
export const MockCancellationMessage = {
511+
message: "Job successfully canceled",
512+
}

site/src/testHelpers/handlers.ts

+3
Original file line numberDiff line numberDiff line change
@@ -130,4 +130,7 @@ export const handlers = [
130130
rest.get("/api/v2/workspacebuilds/:workspaceBuildId/logs", (req, res, ctx) => {
131131
return res(ctx.status(200), ctx.json(M.MockWorkspaceBuildLogs))
132132
}),
133+
rest.patch("/api/v2/workspacebuilds/:workspaceBuildId/cancel", (req, res, ctx) => {
134+
return res(ctx.status(200), ctx.json(M.MockCancellationMessage))
135+
}),
133136
]

site/src/util/workspace.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ export const DisplayStatusLanguage = {
5858
stopped: "Stopped",
5959
deleting: "Deleting",
6060
deleted: "Deleted",
61-
canceling: "Canceling",
62-
canceled: "Canceled",
61+
canceling: "Canceling action",
62+
canceled: "Canceled action",
6363
failed: "Failed",
6464
queued: "Queued",
6565
}

0 commit comments

Comments
 (0)