Skip to content

Commit 6f7b7f0

Browse files
authored
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 9b19dc9 commit 6f7b7f0

File tree

7 files changed

+145
-12
lines changed

7 files changed

+145
-12
lines changed
Lines changed: 30 additions & 0 deletions
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+
}
Lines changed: 29 additions & 0 deletions
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

Lines changed: 3 additions & 0 deletions
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

Lines changed: 16 additions & 0 deletions
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

Lines changed: 11 additions & 1 deletion
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

Lines changed: 24 additions & 11 deletions
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

Lines changed: 32 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy