Skip to content

Commit 73cd637

Browse files
presleypkylecarbs
authored andcommitted
feat: Delete workspace (#1822)
* Add delete button * Add confirmation dialog * Extract dialog, storybook it, and test it * Fix cancel and redirect * Remove fragment
1 parent e326cc8 commit 73cd637

File tree

7 files changed

+145
-12
lines changed

7 files changed

+145
-12
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { ComponentMeta, Story } from "@storybook/react"
2+
import React from "react"
3+
import { DeleteWorkspaceDialog, DeleteWorkspaceDialogProps } from "./DeleteWorkspaceDialog"
4+
5+
export default {
6+
title: "Components/DeleteWorkspaceDialog",
7+
component: DeleteWorkspaceDialog,
8+
argTypes: {
9+
onClose: {
10+
action: "onClose",
11+
},
12+
onConfirm: {
13+
action: "onConfirm",
14+
},
15+
open: {
16+
control: "boolean",
17+
defaultValue: true,
18+
},
19+
title: {
20+
defaultValue: "Confirm Dialog",
21+
},
22+
},
23+
} as ComponentMeta<typeof DeleteWorkspaceDialog>
24+
25+
const Template: Story<DeleteWorkspaceDialogProps> = (args) => <DeleteWorkspaceDialog {...args} />
26+
27+
export const Example = Template.bind({})
28+
Example.args = {
29+
isOpen: true,
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import React from "react"
2+
import { ConfirmDialog } from "../ConfirmDialog/ConfirmDialog"
3+
4+
const Language = {
5+
deleteDialogTitle: "Delete workspace?",
6+
deleteDialogMessage: "Deleting your workspace is irreversible. Are you sure?",
7+
}
8+
9+
export interface DeleteWorkspaceDialogProps {
10+
isOpen: boolean
11+
handleConfirm: () => void
12+
handleCancel: () => void
13+
}
14+
15+
export const DeleteWorkspaceDialog: React.FC<DeleteWorkspaceDialogProps> = ({
16+
isOpen,
17+
handleCancel,
18+
handleConfirm,
19+
}) => (
20+
<ConfirmDialog
21+
type="delete"
22+
hideCancel={false}
23+
open={isOpen}
24+
title={Language.deleteDialogTitle}
25+
onConfirm={handleConfirm}
26+
onClose={handleCancel}
27+
description={Language.deleteDialogMessage}
28+
/>
29+
)

site/src/components/Workspace/Workspace.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { WorkspaceStats } from "../WorkspaceStats/WorkspaceStats"
1515
export interface WorkspaceProps {
1616
handleStart: () => void
1717
handleStop: () => void
18+
handleDelete: () => void
1819
handleUpdate: () => void
1920
handleCancel: () => void
2021
workspace: TypesGen.Workspace
@@ -29,6 +30,7 @@ export interface WorkspaceProps {
2930
export const Workspace: FC<WorkspaceProps> = ({
3031
handleStart,
3132
handleStop,
33+
handleDelete,
3234
handleUpdate,
3335
handleCancel,
3436
workspace,
@@ -56,6 +58,7 @@ export const Workspace: FC<WorkspaceProps> = ({
5658
workspace={workspace}
5759
handleStart={handleStart}
5860
handleStop={handleStop}
61+
handleDelete={handleDelete}
5962
handleUpdate={handleUpdate}
6063
handleCancel={handleCancel}
6164
/>

site/src/components/WorkspaceActions/WorkspaceActions.tsx

+16
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Button from "@material-ui/core/Button"
22
import { makeStyles } from "@material-ui/core/styles"
33
import CancelIcon from "@material-ui/icons/Cancel"
44
import CloudDownloadIcon from "@material-ui/icons/CloudDownload"
5+
import DeleteIcon from "@material-ui/icons/Delete"
56
import PlayArrowRoundedIcon from "@material-ui/icons/PlayArrowRounded"
67
import StopIcon from "@material-ui/icons/Stop"
78
import { FC } from "react"
@@ -15,6 +16,8 @@ export const Language = {
1516
stopping: "Stopping workspace",
1617
start: "Start workspace",
1718
starting: "Starting workspace",
19+
delete: "Delete workspace",
20+
deleting: "Deleting workspace",
1821
cancel: "Cancel action",
1922
update: "Update workspace",
2023
}
@@ -38,10 +41,14 @@ const canStart = (workspaceStatus: WorkspaceStatus) => ["stopped", "canceled", "
3841

3942
const canStop = (workspaceStatus: WorkspaceStatus) => ["started", "canceled", "error"].includes(workspaceStatus)
4043

44+
const canDelete = (workspaceStatus: WorkspaceStatus) =>
45+
["started", "stopped", "canceled", "error"].includes(workspaceStatus)
46+
4147
export interface WorkspaceActionsProps {
4248
workspace: Workspace
4349
handleStart: () => void
4450
handleStop: () => void
51+
handleDelete: () => void
4552
handleUpdate: () => void
4653
handleCancel: () => void
4754
}
@@ -50,6 +57,7 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
5057
workspace,
5158
handleStart,
5259
handleStop,
60+
handleDelete,
5361
handleUpdate,
5462
handleCancel,
5563
}) => {
@@ -74,6 +82,14 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
7482
label={Language.stop}
7583
/>
7684
)}
85+
{canDelete(workspaceStatus) && (
86+
<WorkspaceActionButton
87+
className={styles.actionButton}
88+
icon={<DeleteIcon />}
89+
onClick={handleDelete}
90+
label={Language.delete}
91+
/>
92+
)}
7793
{canCancelJobs(workspaceStatus) && (
7894
<WorkspaceActionButton
7995
className={styles.actionButton}

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

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { fireEvent, screen, waitFor } from "@testing-library/react"
1+
import { fireEvent, screen, waitFor, within } from "@testing-library/react"
22
import { rest } from "msw"
33
import * as api from "../../api/api"
44
import { Workspace } from "../../api/typesGenerated"
@@ -75,6 +75,16 @@ describe("Workspace Page", () => {
7575
const stopWorkspaceMock = jest.spyOn(api, "stopWorkspace").mockResolvedValueOnce(MockWorkspaceBuild)
7676
await testButton(Language.stop, stopWorkspaceMock)
7777
})
78+
it("requests a delete job when the user presses Delete and confirms", async () => {
79+
const deleteWorkspaceMock = jest.spyOn(api, "deleteWorkspace").mockResolvedValueOnce(MockWorkspaceBuild)
80+
await renderWorkspacePage()
81+
const button = await screen.findByText(Language.delete)
82+
await waitFor(() => fireEvent.click(button))
83+
const confirmDialog = await screen.findByRole("dialog")
84+
const confirmButton = within(confirmDialog).getByText("Delete")
85+
await waitFor(() => fireEvent.click(confirmButton))
86+
expect(deleteWorkspaceMock).toBeCalled()
87+
})
7888
it("requests a start job when the user presses Start", async () => {
7989
server.use(
8090
rest.get(`/api/v2/workspaces/${MockWorkspace.id}`, (req, res, ctx) => {

site/src/pages/WorkspacePage/WorkspacePage.tsx

+24-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useMachine } from "@xstate/react"
22
import React, { useEffect } from "react"
3-
import { useParams } from "react-router-dom"
3+
import { useNavigate, useParams } from "react-router-dom"
4+
import { DeleteWorkspaceDialog } from "../../components/DeleteWorkspaceDialog/DeleteWorkspaceDialog"
45
import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary"
56
import { FullScreenLoader } from "../../components/Loader/FullScreenLoader"
67
import { Margins } from "../../components/Margins/Margins"
@@ -11,6 +12,7 @@ import { workspaceMachine } from "../../xServices/workspace/workspaceXService"
1112

1213
export const WorkspacePage: React.FC = () => {
1314
const { workspace: workspaceQueryParam } = useParams()
15+
const navigate = useNavigate()
1416
const workspaceId = firstOrItem(workspaceQueryParam, null)
1517

1618
const [workspaceState, workspaceSend] = useMachine(workspaceMachine)
@@ -32,16 +34,27 @@ export const WorkspacePage: React.FC = () => {
3234
return (
3335
<Margins>
3436
<Stack spacing={4}>
35-
<Workspace
36-
workspace={workspace}
37-
handleStart={() => workspaceSend("START")}
38-
handleStop={() => workspaceSend("STOP")}
39-
handleUpdate={() => workspaceSend("UPDATE")}
40-
handleCancel={() => workspaceSend("CANCEL")}
41-
resources={resources}
42-
getResourcesError={getResourcesError instanceof Error ? getResourcesError : undefined}
43-
builds={builds}
44-
/>
37+
<>
38+
<Workspace
39+
workspace={workspace}
40+
handleStart={() => workspaceSend("START")}
41+
handleStop={() => workspaceSend("STOP")}
42+
handleDelete={() => workspaceSend("ASK_DELETE")}
43+
handleUpdate={() => workspaceSend("UPDATE")}
44+
handleCancel={() => workspaceSend("CANCEL")}
45+
resources={resources}
46+
getResourcesError={getResourcesError instanceof Error ? getResourcesError : undefined}
47+
builds={builds}
48+
/>
49+
<DeleteWorkspaceDialog
50+
isOpen={workspaceState.matches({ ready: { build: "askingDelete" } })}
51+
handleCancel={() => workspaceSend("CANCEL_DELETE")}
52+
handleConfirm={() => {
53+
workspaceSend("DELETE")
54+
navigate("/workspaces")
55+
}}
56+
/>
57+
</>
4558
</Stack>
4659
</Margins>
4760
)

site/src/xServices/workspace/workspaceXService.ts

+32
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ export type WorkspaceEvent =
4040
| { type: "GET_WORKSPACE"; workspaceId: string }
4141
| { type: "START" }
4242
| { type: "STOP" }
43+
| { type: "ASK_DELETE" }
44+
| { type: "DELETE" }
45+
| { type: "CANCEL_DELETE" }
4346
| { type: "UPDATE" }
4447
| { type: "CANCEL" }
4548
| { type: "LOAD_MORE_BUILDS" }
@@ -136,10 +139,17 @@ export const workspaceMachine = createMachine(
136139
on: {
137140
START: "requestingStart",
138141
STOP: "requestingStop",
142+
ASK_DELETE: "askingDelete",
139143
UPDATE: "refreshingTemplate",
140144
CANCEL: "requestingCancel",
141145
},
142146
},
147+
askingDelete: {
148+
on: {
149+
DELETE: "requestingDelete",
150+
CANCEL_DELETE: "idle",
151+
},
152+
},
143153
requestingStart: {
144154
entry: "clearBuildError",
145155
invoke: {
@@ -170,6 +180,21 @@ export const workspaceMachine = createMachine(
170180
},
171181
},
172182
},
183+
requestingDelete: {
184+
entry: "clearBuildError",
185+
invoke: {
186+
id: "deleteWorkspace",
187+
src: "deleteWorkspace",
188+
onDone: {
189+
target: "idle",
190+
actions: ["assignBuild", "refreshTimeline"],
191+
},
192+
onError: {
193+
target: "idle",
194+
actions: ["assignBuildError", "displayBuildError"],
195+
},
196+
},
197+
},
173198
requestingCancel: {
174199
entry: "clearCancellationMessage",
175200
invoke: {
@@ -429,6 +454,13 @@ export const workspaceMachine = createMachine(
429454
throw Error("Cannot stop workspace without workspace id")
430455
}
431456
},
457+
deleteWorkspace: async (context) => {
458+
if (context.workspace) {
459+
return await API.deleteWorkspace(context.workspace.id)
460+
} else {
461+
throw Error("Cannot delete workspace without workspace id")
462+
}
463+
},
432464
cancelWorkspace: async (context) => {
433465
if (context.workspace) {
434466
return await API.cancelWorkspaceBuild(context.workspace.latest_build.id)

0 commit comments

Comments
 (0)