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

Lines changed: 9 additions & 3 deletions
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

Lines changed: 8 additions & 1 deletion
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} />
Lines changed: 28 additions & 0 deletions
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+
}
Lines changed: 50 additions & 0 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 1 addition & 3 deletions
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

Lines changed: 39 additions & 1 deletion
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

Lines changed: 4 additions & 0 deletions
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

Lines changed: 4 additions & 2 deletions
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)
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