mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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) => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user