diff --git a/site/src/components/DeleteWorkspaceDialog/DeleteWorkspaceDialog.stories.tsx b/site/src/components/DeleteWorkspaceDialog/DeleteWorkspaceDialog.stories.tsx new file mode 100644 index 0000000000000..edafb6498e25a --- /dev/null +++ b/site/src/components/DeleteWorkspaceDialog/DeleteWorkspaceDialog.stories.tsx @@ -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 + +const Template: Story = (args) => + +export const Example = Template.bind({}) +Example.args = { + isOpen: true, +} diff --git a/site/src/components/DeleteWorkspaceDialog/DeleteWorkspaceDialog.tsx b/site/src/components/DeleteWorkspaceDialog/DeleteWorkspaceDialog.tsx new file mode 100644 index 0000000000000..9e518d4be7fe7 --- /dev/null +++ b/site/src/components/DeleteWorkspaceDialog/DeleteWorkspaceDialog.tsx @@ -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 = ({ + isOpen, + handleCancel, + handleConfirm, +}) => ( + +) diff --git a/site/src/components/Workspace/Workspace.tsx b/site/src/components/Workspace/Workspace.tsx index 1194d024acb48..1cb14486ae141 100644 --- a/site/src/components/Workspace/Workspace.tsx +++ b/site/src/components/Workspace/Workspace.tsx @@ -14,6 +14,7 @@ import { WorkspaceStats } from "../WorkspaceStats/WorkspaceStats" export interface WorkspaceProps { handleStart: () => void handleStop: () => void + handleDelete: () => void handleUpdate: () => void handleCancel: () => void workspace: TypesGen.Workspace @@ -28,6 +29,7 @@ export interface WorkspaceProps { export const Workspace: React.FC = ({ handleStart, handleStop, + handleDelete, handleUpdate, handleCancel, workspace, @@ -55,6 +57,7 @@ export const Workspace: React.FC = ({ workspace={workspace} handleStart={handleStart} handleStop={handleStop} + handleDelete={handleDelete} handleUpdate={handleUpdate} handleCancel={handleCancel} /> diff --git a/site/src/components/WorkspaceActions/WorkspaceActions.tsx b/site/src/components/WorkspaceActions/WorkspaceActions.tsx index cc9f51cd19ad7..b0912084f90a8 100644 --- a/site/src/components/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/components/WorkspaceActions/WorkspaceActions.tsx @@ -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 React 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: React.FC = ({ workspace, handleStart, handleStop, + handleDelete, handleUpdate, handleCancel, }) => { @@ -74,6 +82,14 @@ export const WorkspaceActions: React.FC = ({ label={Language.stop} /> )} + {canDelete(workspaceStatus) && ( + } + onClick={handleDelete} + label={Language.delete} + /> + )} {canCancelJobs(workspaceStatus) && ( { 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) => { diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index dcb0068d4301c..7129fd99dee7b 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -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 ( - workspaceSend("START")} - handleStop={() => workspaceSend("STOP")} - handleUpdate={() => workspaceSend("UPDATE")} - handleCancel={() => workspaceSend("CANCEL")} - resources={resources} - getResourcesError={getResourcesError instanceof Error ? getResourcesError : undefined} - builds={builds} - /> + <> + 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} + /> + workspaceSend("CANCEL_DELETE")} + handleConfirm={() => { + workspaceSend("DELETE") + navigate("/workspaces") + }} + /> + ) diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index 075475b45a9d0..9c2beea1b3876 100644 --- a/site/src/xServices/workspace/workspaceXService.ts +++ b/site/src/xServices/workspace/workspaceXService.ts @@ -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: { @@ -428,6 +453,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) 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