From 12a5b4741a7ffa53ea64e4fcd8039cbcf5e3fff6 Mon Sep 17 00:00:00 2001 From: Presley Date: Thu, 26 May 2022 21:12:39 +0000 Subject: [PATCH 1/5] Add delete button --- site/src/components/Workspace/Workspace.tsx | 3 +++ .../WorkspaceActions/WorkspaceActions.tsx | 16 +++++++++++++ .../WorkspacePage/WorkspacePage.test.tsx | 4 ++++ .../src/pages/WorkspacePage/WorkspacePage.tsx | 1 + .../xServices/workspace/workspaceXService.ts | 24 +++++++++++++++++++ 5 files changed, 48 insertions(+) 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", async () => { + const deleteWorkspaceMock = jest.spyOn(api, "deleteWorkspace").mockResolvedValueOnce(MockWorkspaceBuild) + await testButton(Language.delete, deleteWorkspaceMock) + }) 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..6231bb916e125 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -36,6 +36,7 @@ export const WorkspacePage: React.FC = () => { workspace={workspace} handleStart={() => workspaceSend("START")} handleStop={() => workspaceSend("STOP")} + handleDelete={() => workspaceSend("DELETE")} handleUpdate={() => workspaceSend("UPDATE")} handleCancel={() => workspaceSend("CANCEL")} resources={resources} diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index 075475b45a9d0..7ff246eba31ce 100644 --- a/site/src/xServices/workspace/workspaceXService.ts +++ b/site/src/xServices/workspace/workspaceXService.ts @@ -40,6 +40,7 @@ export type WorkspaceEvent = | { type: "GET_WORKSPACE"; workspaceId: string } | { type: "START" } | { type: "STOP" } + | { type: "DELETE" } | { type: "UPDATE" } | { type: "CANCEL" } | { type: "LOAD_MORE_BUILDS" } @@ -136,6 +137,7 @@ export const workspaceMachine = createMachine( on: { START: "requestingStart", STOP: "requestingStop", + DELETE: "requestingDelete", UPDATE: "refreshingTemplate", CANCEL: "requestingCancel", }, @@ -170,6 +172,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 +445,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) From e4bf288aa76dae5d327575de061331de2b79015b Mon Sep 17 00:00:00 2001 From: Presley Date: Thu, 26 May 2022 22:13:03 +0000 Subject: [PATCH 2/5] Add confirmation dialog --- .../src/pages/WorkspacePage/WorkspacePage.tsx | 25 ++++++++++++++++++- .../xServices/workspace/workspaceXService.ts | 10 +++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index 6231bb916e125..e6264eb267317 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 { ConfirmDialog } from "../../components/ConfirmDialog/ConfirmDialog" import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary" import { FullScreenLoader } from "../../components/Loader/FullScreenLoader" import { Margins } from "../../components/Margins/Margins" @@ -9,6 +10,12 @@ import { Workspace } from "../../components/Workspace/Workspace" import { firstOrItem } from "../../util/array" import { workspaceMachine } from "../../xServices/workspace/workspaceXService" +const Language = { + deleteDialogTitle: "Delete workspace?", + confirmDelete: "Yes, delete", + deleteDialogMessage: "Deleting your workspace is irreversible. Are you sure?" +} + export const WorkspacePage: React.FC = () => { const { workspace: workspaceQueryParam } = useParams() const workspaceId = firstOrItem(workspaceQueryParam, null) @@ -32,17 +39,33 @@ export const WorkspacePage: React.FC = () => { return ( + <> workspaceSend("START")} handleStop={() => workspaceSend("STOP")} - handleDelete={() => workspaceSend("DELETE")} + handleDelete={() => workspaceSend("ASK_DELETE")} handleUpdate={() => workspaceSend("UPDATE")} handleCancel={() => workspaceSend("CANCEL")} resources={resources} getResourcesError={getResourcesError instanceof Error ? getResourcesError : undefined} builds={builds} /> + { + workspaceSend({ type: "DELETE" }) + }} + onClose={() => { + workspaceSend({ type: "CANCEL_DELETE" }) + }} + description={<>{Language.deleteDialogMessage}} + /> + ) diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index 7ff246eba31ce..efe6e891642ad 100644 --- a/site/src/xServices/workspace/workspaceXService.ts +++ b/site/src/xServices/workspace/workspaceXService.ts @@ -40,7 +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" } @@ -137,11 +139,17 @@ export const workspaceMachine = createMachine( on: { START: "requestingStart", STOP: "requestingStop", - DELETE: "requestingDelete", + ASK_DELETE: "askingDelete", UPDATE: "refreshingTemplate", CANCEL: "requestingCancel", }, }, + askingDelete: { + on: { + DELETE: "requestingDelete", + CANCEL_DELETE: "idle" + } + }, requestingStart: { entry: "clearBuildError", invoke: { From 44240de048c419dfcb97e8a3215d3630ebf94269 Mon Sep 17 00:00:00 2001 From: Presley Date: Thu, 26 May 2022 22:46:04 +0000 Subject: [PATCH 3/5] Extract dialog, storybook it, and test it --- .../DeleteWorkspaceDialog.stories.tsx | 30 ++++++++++++ .../DeleteWorkspaceDialog.tsx | 29 +++++++++++ .../WorkspacePage/WorkspacePage.test.tsx | 12 +++-- .../src/pages/WorkspacePage/WorkspacePage.tsx | 49 +++++++------------ .../xServices/workspace/workspaceXService.ts | 4 +- 5 files changed, 87 insertions(+), 37 deletions(-) create mode 100644 site/src/components/DeleteWorkspaceDialog/DeleteWorkspaceDialog.stories.tsx create mode 100644 site/src/components/DeleteWorkspaceDialog/DeleteWorkspaceDialog.tsx 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..9ffcef41de1e6 --- /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, +}) => ( + {Language.deleteDialogMessage}} + /> +) diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index 920528c121a7d..d9bc1d362210d 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -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 React from "react" import * as api from "../../api/api" @@ -76,9 +76,15 @@ 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", async () => { + it("requests a delete job when the user presses Delete and confirms", async () => { const deleteWorkspaceMock = jest.spyOn(api, "deleteWorkspace").mockResolvedValueOnce(MockWorkspaceBuild) - await testButton(Language.delete, deleteWorkspaceMock) + 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( diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index e6264eb267317..46e733fe003b3 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -1,7 +1,7 @@ import { useMachine } from "@xstate/react" import React, { useEffect } from "react" import { useParams } from "react-router-dom" -import { ConfirmDialog } from "../../components/ConfirmDialog/ConfirmDialog" +import { DeleteWorkspaceDialog } from "../../components/DeleteWorkspaceDialog/DeleteWorkspaceDialog" import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary" import { FullScreenLoader } from "../../components/Loader/FullScreenLoader" import { Margins } from "../../components/Margins/Margins" @@ -10,12 +10,6 @@ import { Workspace } from "../../components/Workspace/Workspace" import { firstOrItem } from "../../util/array" import { workspaceMachine } from "../../xServices/workspace/workspaceXService" -const Language = { - deleteDialogTitle: "Delete workspace?", - confirmDelete: "Yes, delete", - deleteDialogMessage: "Deleting your workspace is irreversible. Are you sure?" -} - export const WorkspacePage: React.FC = () => { const { workspace: workspaceQueryParam } = useParams() const workspaceId = firstOrItem(workspaceQueryParam, null) @@ -40,31 +34,22 @@ export const WorkspacePage: React.FC = () => { <> - 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({ type: "DELETE" }) - }} - onClose={() => { - workspaceSend({ type: "CANCEL_DELETE" }) - }} - description={<>{Language.deleteDialogMessage}} - /> + 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("ASK_DELETE")} + handleConfirm={() => workspaceSend("DELETE")} + /> diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index efe6e891642ad..9c2beea1b3876 100644 --- a/site/src/xServices/workspace/workspaceXService.ts +++ b/site/src/xServices/workspace/workspaceXService.ts @@ -147,8 +147,8 @@ export const workspaceMachine = createMachine( askingDelete: { on: { DELETE: "requestingDelete", - CANCEL_DELETE: "idle" - } + CANCEL_DELETE: "idle", + }, }, requestingStart: { entry: "clearBuildError", From a9f18c11ea4b45a98bf695a96716f17d009148a6 Mon Sep 17 00:00:00 2001 From: Presley Date: Thu, 26 May 2022 22:59:05 +0000 Subject: [PATCH 4/5] Fix cancel and redirect --- site/src/pages/WorkspacePage/WorkspacePage.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index 46e733fe003b3..7129fd99dee7b 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.tsx @@ -1,6 +1,6 @@ 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" @@ -12,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) @@ -47,8 +48,11 @@ export const WorkspacePage: React.FC = () => { /> workspaceSend("ASK_DELETE")} - handleConfirm={() => workspaceSend("DELETE")} + handleCancel={() => workspaceSend("CANCEL_DELETE")} + handleConfirm={() => { + workspaceSend("DELETE") + navigate("/workspaces") + }} /> From b9a23558ed48129bffd80d33a12d5a945d796cbc Mon Sep 17 00:00:00 2001 From: Presley Date: Tue, 31 May 2022 14:32:27 +0000 Subject: [PATCH 5/5] Remove fragment --- .../components/DeleteWorkspaceDialog/DeleteWorkspaceDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/components/DeleteWorkspaceDialog/DeleteWorkspaceDialog.tsx b/site/src/components/DeleteWorkspaceDialog/DeleteWorkspaceDialog.tsx index 9ffcef41de1e6..9e518d4be7fe7 100644 --- a/site/src/components/DeleteWorkspaceDialog/DeleteWorkspaceDialog.tsx +++ b/site/src/components/DeleteWorkspaceDialog/DeleteWorkspaceDialog.tsx @@ -24,6 +24,6 @@ export const DeleteWorkspaceDialog: React.FC = ({ title={Language.deleteDialogTitle} onConfirm={handleConfirm} onClose={handleCancel} - description={<>{Language.deleteDialogMessage}} + description={Language.deleteDialogMessage} /> ) 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