feat: Delete workspace (#1822)

* Add delete button

* Add confirmation dialog

* Extract dialog, storybook it, and test it

* Fix cancel and redirect

* Remove fragment
This commit is contained in:
Presley Pizzo
2022-05-31 10:43:31 -04:00
committed by GitHub
parent 9b19dc9154
commit 6f7b7f0248
7 changed files with 145 additions and 12 deletions
@@ -0,0 +1,30 @@
import { ComponentMeta, Story } from "@storybook/react"
import React from "react"
import { DeleteWorkspaceDialog, DeleteWorkspaceDialogProps } from "./DeleteWorkspaceDialog"
export default {
title: "Components/DeleteWorkspaceDialog",
component: DeleteWorkspaceDialog,
argTypes: {
onClose: {
action: "onClose",
},
onConfirm: {
action: "onConfirm",
},
open: {
control: "boolean",
defaultValue: true,
},
title: {
defaultValue: "Confirm Dialog",
},
},
} as ComponentMeta<typeof DeleteWorkspaceDialog>
const Template: Story<DeleteWorkspaceDialogProps> = (args) => <DeleteWorkspaceDialog {...args} />
export const Example = Template.bind({})
Example.args = {
isOpen: true,
}
@@ -0,0 +1,29 @@
import React from "react"
import { ConfirmDialog } from "../ConfirmDialog/ConfirmDialog"
const Language = {
deleteDialogTitle: "Delete workspace?",
deleteDialogMessage: "Deleting your workspace is irreversible. Are you sure?",
}
export interface DeleteWorkspaceDialogProps {
isOpen: boolean
handleConfirm: () => void
handleCancel: () => void
}
export const DeleteWorkspaceDialog: React.FC<DeleteWorkspaceDialogProps> = ({
isOpen,
handleCancel,
handleConfirm,
}) => (
<ConfirmDialog
type="delete"
hideCancel={false}
open={isOpen}
title={Language.deleteDialogTitle}
onConfirm={handleConfirm}
onClose={handleCancel}
description={Language.deleteDialogMessage}
/>
)
@@ -15,6 +15,7 @@ import { WorkspaceStats } from "../WorkspaceStats/WorkspaceStats"
export interface WorkspaceProps {
handleStart: () => void
handleStop: () => void
handleDelete: () => void
handleUpdate: () => void
handleCancel: () => void
workspace: TypesGen.Workspace
@@ -29,6 +30,7 @@ export interface WorkspaceProps {
export const Workspace: FC<WorkspaceProps> = ({
handleStart,
handleStop,
handleDelete,
handleUpdate,
handleCancel,
workspace,
@@ -56,6 +58,7 @@ export const Workspace: FC<WorkspaceProps> = ({
workspace={workspace}
handleStart={handleStart}
handleStop={handleStop}
handleDelete={handleDelete}
handleUpdate={handleUpdate}
handleCancel={handleCancel}
/>
@@ -2,6 +2,7 @@ import Button from "@material-ui/core/Button"
import { makeStyles } from "@material-ui/core/styles"
import CancelIcon from "@material-ui/icons/Cancel"
import CloudDownloadIcon from "@material-ui/icons/CloudDownload"
import DeleteIcon from "@material-ui/icons/Delete"
import PlayArrowRoundedIcon from "@material-ui/icons/PlayArrowRounded"
import StopIcon from "@material-ui/icons/Stop"
import { FC } from "react"
@@ -15,6 +16,8 @@ export const Language = {
stopping: "Stopping workspace",
start: "Start workspace",
starting: "Starting workspace",
delete: "Delete workspace",
deleting: "Deleting workspace",
cancel: "Cancel action",
update: "Update workspace",
}
@@ -38,10 +41,14 @@ const canStart = (workspaceStatus: WorkspaceStatus) => ["stopped", "canceled", "
const canStop = (workspaceStatus: WorkspaceStatus) => ["started", "canceled", "error"].includes(workspaceStatus)
const canDelete = (workspaceStatus: WorkspaceStatus) =>
["started", "stopped", "canceled", "error"].includes(workspaceStatus)
export interface WorkspaceActionsProps {
workspace: Workspace
handleStart: () => void
handleStop: () => void
handleDelete: () => void
handleUpdate: () => void
handleCancel: () => void
}
@@ -50,6 +57,7 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
workspace,
handleStart,
handleStop,
handleDelete,
handleUpdate,
handleCancel,
}) => {
@@ -74,6 +82,14 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
label={Language.stop}
/>
)}
{canDelete(workspaceStatus) && (
<WorkspaceActionButton
className={styles.actionButton}
icon={<DeleteIcon />}
onClick={handleDelete}
label={Language.delete}
/>
)}
{canCancelJobs(workspaceStatus) && (
<WorkspaceActionButton
className={styles.actionButton}
@@ -1,4 +1,4 @@
import { fireEvent, screen, waitFor } from "@testing-library/react"
import { fireEvent, screen, waitFor, within } from "@testing-library/react"
import { rest } from "msw"
import * as api from "../../api/api"
import { Workspace } from "../../api/typesGenerated"
@@ -75,6 +75,16 @@ describe("Workspace Page", () => {
const stopWorkspaceMock = jest.spyOn(api, "stopWorkspace").mockResolvedValueOnce(MockWorkspaceBuild)
await testButton(Language.stop, stopWorkspaceMock)
})
it("requests a delete job when the user presses Delete and confirms", async () => {
const deleteWorkspaceMock = jest.spyOn(api, "deleteWorkspace").mockResolvedValueOnce(MockWorkspaceBuild)
await renderWorkspacePage()
const button = await screen.findByText(Language.delete)
await waitFor(() => fireEvent.click(button))
const confirmDialog = await screen.findByRole("dialog")
const confirmButton = within(confirmDialog).getByText("Delete")
await waitFor(() => fireEvent.click(confirmButton))
expect(deleteWorkspaceMock).toBeCalled()
})
it("requests a start job when the user presses Start", async () => {
server.use(
rest.get(`/api/v2/workspaces/${MockWorkspace.id}`, (req, res, ctx) => {
+24 -11
View File
@@ -1,6 +1,7 @@
import { useMachine } from "@xstate/react"
import React, { useEffect } from "react"
import { useParams } from "react-router-dom"
import { useNavigate, useParams } from "react-router-dom"
import { DeleteWorkspaceDialog } from "../../components/DeleteWorkspaceDialog/DeleteWorkspaceDialog"
import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary"
import { FullScreenLoader } from "../../components/Loader/FullScreenLoader"
import { Margins } from "../../components/Margins/Margins"
@@ -11,6 +12,7 @@ import { workspaceMachine } from "../../xServices/workspace/workspaceXService"
export const WorkspacePage: React.FC = () => {
const { workspace: workspaceQueryParam } = useParams()
const navigate = useNavigate()
const workspaceId = firstOrItem(workspaceQueryParam, null)
const [workspaceState, workspaceSend] = useMachine(workspaceMachine)
@@ -32,16 +34,27 @@ export const WorkspacePage: React.FC = () => {
return (
<Margins>
<Stack spacing={4}>
<Workspace
workspace={workspace}
handleStart={() => workspaceSend("START")}
handleStop={() => workspaceSend("STOP")}
handleUpdate={() => workspaceSend("UPDATE")}
handleCancel={() => workspaceSend("CANCEL")}
resources={resources}
getResourcesError={getResourcesError instanceof Error ? getResourcesError : undefined}
builds={builds}
/>
<>
<Workspace
workspace={workspace}
handleStart={() => workspaceSend("START")}
handleStop={() => workspaceSend("STOP")}
handleDelete={() => workspaceSend("ASK_DELETE")}
handleUpdate={() => workspaceSend("UPDATE")}
handleCancel={() => workspaceSend("CANCEL")}
resources={resources}
getResourcesError={getResourcesError instanceof Error ? getResourcesError : undefined}
builds={builds}
/>
<DeleteWorkspaceDialog
isOpen={workspaceState.matches({ ready: { build: "askingDelete" } })}
handleCancel={() => workspaceSend("CANCEL_DELETE")}
handleConfirm={() => {
workspaceSend("DELETE")
navigate("/workspaces")
}}
/>
</>
</Stack>
</Margins>
)
@@ -40,6 +40,9 @@ export type WorkspaceEvent =
| { type: "GET_WORKSPACE"; workspaceId: string }
| { type: "START" }
| { type: "STOP" }
| { type: "ASK_DELETE" }
| { type: "DELETE" }
| { type: "CANCEL_DELETE" }
| { type: "UPDATE" }
| { type: "CANCEL" }
| { type: "LOAD_MORE_BUILDS" }
@@ -136,10 +139,17 @@ export const workspaceMachine = createMachine(
on: {
START: "requestingStart",
STOP: "requestingStop",
ASK_DELETE: "askingDelete",
UPDATE: "refreshingTemplate",
CANCEL: "requestingCancel",
},
},
askingDelete: {
on: {
DELETE: "requestingDelete",
CANCEL_DELETE: "idle",
},
},
requestingStart: {
entry: "clearBuildError",
invoke: {
@@ -170,6 +180,21 @@ export const workspaceMachine = createMachine(
},
},
},
requestingDelete: {
entry: "clearBuildError",
invoke: {
id: "deleteWorkspace",
src: "deleteWorkspace",
onDone: {
target: "idle",
actions: ["assignBuild", "refreshTimeline"],
},
onError: {
target: "idle",
actions: ["assignBuildError", "displayBuildError"],
},
},
},
requestingCancel: {
entry: "clearCancellationMessage",
invoke: {
@@ -429,6 +454,13 @@ export const workspaceMachine = createMachine(
throw Error("Cannot stop workspace without workspace id")
}
},
deleteWorkspace: async (context) => {
if (context.workspace) {
return await API.deleteWorkspace(context.workspace.id)
} else {
throw Error("Cannot delete workspace without workspace id")
}
},
cancelWorkspace: async (context) => {
if (context.workspace) {
return await API.cancelWorkspaceBuild(context.workspace.latest_build.id)