Skip to content

Commit 18a6810

Browse files
Kira-Pilotkylecarbs
authored andcommitted
feat: show deleted workspace after delete action (#2208)
* added deleted workspace banner * x state pass * added include_deleted param * clean up x state * added teests * cleaning up unneeded xstate service
1 parent a194365 commit 18a6810

File tree

9 files changed

+144
-11
lines changed

9 files changed

+144
-11
lines changed

site/src/api/api.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,11 @@ export const getTemplateVersionResources = async (versionId: string): Promise<Ty
108108
return response.data
109109
}
110110

111-
export const getWorkspace = async (workspaceId: string): Promise<TypesGen.Workspace> => {
112-
const response = await axios.get<TypesGen.Workspace>(`/api/v2/workspaces/${workspaceId}`)
111+
export const getWorkspace = async (
112+
workspaceId: string,
113+
params?: TypesGen.WorkspaceOptions,
114+
): Promise<TypesGen.Workspace> => {
115+
const response = await axios.get<TypesGen.Workspace>(`/api/v2/workspaces/${workspaceId}`, { params })
113116
return response.data
114117
}
115118

@@ -141,8 +144,11 @@ export const getWorkspaces = async (filter?: TypesGen.WorkspaceFilter): Promise<
141144
export const getWorkspaceByOwnerAndName = async (
142145
username = "me",
143146
workspaceName: string,
147+
params?: TypesGen.WorkspaceByOwnerAndNameParams,
144148
): Promise<TypesGen.Workspace> => {
145-
const response = await axios.get<TypesGen.Workspace>(`/api/v2/users/${username}/workspace/${workspaceName}`)
149+
const response = await axios.get<TypesGen.Workspace>(`/api/v2/users/${username}/workspace/${workspaceName}`, {
150+
params,
151+
})
146152
return response.data
147153
}
148154

site/src/components/Workspace/Workspace.tsx

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { makeStyles } from "@material-ui/core/styles"
22
import { FC } from "react"
3+
import { useNavigate } from "react-router-dom"
34
import * as TypesGen from "../../api/typesGenerated"
45
import { BuildsTable } from "../BuildsTable/BuildsTable"
56
import { Margins } from "../Margins/Margins"
67
import { PageHeader, PageHeaderSubtitle, PageHeaderTitle } from "../PageHeader/PageHeader"
78
import { Resources } from "../Resources/Resources"
89
import { Stack } from "../Stack/Stack"
910
import { WorkspaceActions } from "../WorkspaceActions/WorkspaceActions"
11+
import { WorkspaceDeletedBanner } from "../WorkspaceDeletedBanner/WorkspaceDeletedBanner"
1012
import { WorkspaceSchedule } from "../WorkspaceSchedule/WorkspaceSchedule"
1113
import { WorkspaceScheduleBanner } from "../WorkspaceScheduleBanner/WorkspaceScheduleBanner"
1214
import { WorkspaceSection } from "../WorkspaceSection/WorkspaceSection"
@@ -44,6 +46,7 @@ export const Workspace: FC<WorkspaceProps> = ({
4446
builds,
4547
}) => {
4648
const styles = useStyles()
49+
const navigate = useNavigate()
4750

4851
return (
4952
<Margins>
@@ -72,9 +75,13 @@ export const Workspace: FC<WorkspaceProps> = ({
7275
workspace={workspace}
7376
/>
7477

78+
<WorkspaceDeletedBanner workspace={workspace} handleClick={() => navigate(`/workspaces/new`)} />
79+
7580
<WorkspaceStats workspace={workspace} />
7681

77-
<Resources resources={resources} getResourcesError={getResourcesError} workspace={workspace} />
82+
{!!resources && !!resources.length && (
83+
<Resources resources={resources} getResourcesError={getResourcesError} workspace={workspace} />
84+
)}
7885

7986
<WorkspaceSection title="Timeline" contentsProps={{ className: styles.timelineContents }}>
8087
<BuildsTable builds={builds} className={styles.timelineTable} />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { action } from "@storybook/addon-actions"
2+
import { Story } from "@storybook/react"
3+
import * as Mocks from "../../testHelpers/entities"
4+
import { WorkspaceDeletedBanner, WorkspaceDeletedBannerProps } from "./WorkspaceDeletedBanner"
5+
6+
export default {
7+
title: "components/WorkspaceDeletedBanner",
8+
component: WorkspaceDeletedBanner,
9+
}
10+
11+
const Template: Story<WorkspaceDeletedBannerProps> = (args) => <WorkspaceDeletedBanner {...args} />
12+
13+
export const Example = Template.bind({})
14+
Example.args = {
15+
handleClick: action("extend"),
16+
workspace: {
17+
...Mocks.MockWorkspace,
18+
19+
latest_build: {
20+
...Mocks.MockWorkspaceBuild,
21+
job: {
22+
...Mocks.MockProvisionerJob,
23+
status: "succeeded",
24+
},
25+
transition: "delete",
26+
},
27+
},
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import Button from "@material-ui/core/Button"
2+
import { makeStyles } from "@material-ui/core/styles"
3+
import Alert from "@material-ui/lab/Alert"
4+
import AlertTitle from "@material-ui/lab/AlertTitle"
5+
import { FC } from "react"
6+
import * as TypesGen from "../../api/typesGenerated"
7+
import { isWorkspaceDeleted } from "../../util/workspace"
8+
9+
const Language = {
10+
bannerTitle: "This workspace has been deleted and cannot be edited.",
11+
createWorkspaceCta: "Create new workspace",
12+
}
13+
14+
export interface WorkspaceDeletedBannerProps {
15+
workspace: TypesGen.Workspace
16+
handleClick: () => void
17+
}
18+
19+
export const WorkspaceDeletedBanner: FC<WorkspaceDeletedBannerProps> = ({ workspace, handleClick }) => {
20+
const styles = useStyles()
21+
22+
if (!isWorkspaceDeleted(workspace)) {
23+
return null
24+
}
25+
26+
return (
27+
<Alert
28+
className={styles.root}
29+
action={
30+
<Button color="inherit" onClick={handleClick} size="small">
31+
{Language.createWorkspaceCta}
32+
</Button>
33+
}
34+
severity="warning"
35+
>
36+
<AlertTitle>{Language.bannerTitle}</AlertTitle>
37+
</Alert>
38+
)
39+
}
40+
41+
export const useStyles = makeStyles(() => {
42+
return {
43+
root: {
44+
alignItems: "center",
45+
"& .MuiAlertTitle-root": {
46+
marginBottom: "0px",
47+
},
48+
},
49+
}
50+
})

site/src/components/WorkspaceScheduleBanner/WorkspaceScheduleBanner.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export const shouldDisplay = (workspace: TypesGen.Workspace): boolean => {
2626
if (!isWorkspaceOn(workspace)) {
2727
return false
2828
} else {
29-
// a mannual shutdown has a deadline of '"0001-01-01T00:00:00Z"'
29+
// a manual shutdown has a deadline of '"0001-01-01T00:00:00Z"'
3030
// SEE: #1834
3131
const deadline = dayjs(workspace.latest_build.deadline).utc()
3232
const hasDeadline = deadline.year() > 1

site/src/pages/WorkspacePage/WorkspacePage.tsx

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useMachine } from "@xstate/react"
22
import React, { useEffect } from "react"
33
import { Helmet } from "react-helmet"
4-
import { useNavigate, useParams } from "react-router-dom"
4+
import { useParams } from "react-router-dom"
55
import { DeleteWorkspaceDialog } from "../../components/DeleteWorkspaceDialog/DeleteWorkspaceDialog"
66
import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary"
77
import { FullScreenLoader } from "../../components/Loader/FullScreenLoader"
@@ -13,7 +13,6 @@ import { workspaceScheduleBannerMachine } from "../../xServices/workspaceSchedul
1313

1414
export const WorkspacePage: React.FC = () => {
1515
const { username: usernameQueryParam, workspace: workspaceQueryParam } = useParams()
16-
const navigate = useNavigate()
1716
const username = firstOrItem(usernameQueryParam, null)
1817
const workspaceName = firstOrItem(workspaceQueryParam, null)
1918

@@ -63,7 +62,6 @@ export const WorkspacePage: React.FC = () => {
6362
handleCancel={() => workspaceSend("CANCEL_DELETE")}
6463
handleConfirm={() => {
6564
workspaceSend("DELETE")
66-
navigate("/workspaces")
6765
}}
6866
/>
6967
</>

site/src/util/workspace.test.ts

+39-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import dayjs from "dayjs"
22
import * as TypesGen from "../api/typesGenerated"
33
import * as Mocks from "../testHelpers/entities"
4-
import { defaultWorkspaceExtension, isWorkspaceOn, workspaceQueryToFilter } from "./workspace"
4+
import { defaultWorkspaceExtension, isWorkspaceDeleted, isWorkspaceOn, workspaceQueryToFilter } from "./workspace"
55

66
describe("util > workspace", () => {
77
describe("isWorkspaceOn", () => {
@@ -42,6 +42,44 @@ describe("util > workspace", () => {
4242
})
4343
})
4444

45+
describe("isWorkspaceDeleted", () => {
46+
it.each<[TypesGen.WorkspaceTransition, TypesGen.ProvisionerJobStatus, boolean]>([
47+
["delete", "canceled", false],
48+
["delete", "canceling", false],
49+
["delete", "failed", false],
50+
["delete", "pending", false],
51+
["delete", "running", false],
52+
["delete", "succeeded", true],
53+
54+
["stop", "canceled", false],
55+
["stop", "canceling", false],
56+
["stop", "failed", false],
57+
["stop", "pending", false],
58+
["stop", "running", false],
59+
["stop", "succeeded", false],
60+
61+
["start", "canceled", false],
62+
["start", "canceling", false],
63+
["start", "failed", false],
64+
["start", "pending", false],
65+
["start", "running", false],
66+
["start", "succeeded", false],
67+
])(`transition=%p, status=%p, isWorkspaceDeleted=%p`, (transition, status, isDeleted) => {
68+
const workspace: TypesGen.Workspace = {
69+
...Mocks.MockWorkspace,
70+
latest_build: {
71+
...Mocks.MockWorkspaceBuild,
72+
job: {
73+
...Mocks.MockProvisionerJob,
74+
status,
75+
},
76+
transition,
77+
},
78+
}
79+
expect(isWorkspaceDeleted(workspace)).toBe(isDeleted)
80+
})
81+
})
82+
4583
describe("defaultWorkspaceExtension", () => {
4684
it.each<[string, TypesGen.PutExtendWorkspaceRequest]>([
4785
[

site/src/util/workspace.ts

+4
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,10 @@ export const isWorkspaceOn = (workspace: TypesGen.Workspace): boolean => {
249249
return transition === "start" && status === "succeeded"
250250
}
251251

252+
export const isWorkspaceDeleted = (workspace: TypesGen.Workspace): boolean => {
253+
return getWorkspaceStatus(workspace.latest_build) === succeededToStatus["delete"]
254+
}
255+
252256
export const defaultWorkspaceExtension = (__startDate?: dayjs.Dayjs): TypesGen.PutExtendWorkspaceRequest => {
253257
const now = __startDate ? dayjs(__startDate) : dayjs()
254258
const fourHoursFromNow = now.add(4, "hours").utc()

site/src/xServices/workspace/workspaceXService.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,7 @@ export const workspaceMachine = createMachine(
431431
},
432432
services: {
433433
getWorkspace: async (_, event) => {
434-
return await API.getWorkspaceByOwnerAndName(event.username, event.workspaceName)
434+
return await API.getWorkspaceByOwnerAndName(event.username, event.workspaceName, { include_deleted: true })
435435
},
436436
getTemplate: async (context) => {
437437
if (context.workspace) {
@@ -470,7 +470,9 @@ export const workspaceMachine = createMachine(
470470
},
471471
refreshWorkspace: async (context) => {
472472
if (context.workspace) {
473-
return await API.getWorkspaceByOwnerAndName(context.workspace.owner_name, context.workspace.name)
473+
return await API.getWorkspaceByOwnerAndName(context.workspace.owner_name, context.workspace.name, {
474+
include_deleted: true,
475+
})
474476
} else {
475477
throw Error("Cannot refresh workspace without id")
476478
}

0 commit comments

Comments
 (0)