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
This commit is contained in:
Kira Pilot
2022-06-09 11:43:49 -04:00
committed by GitHub
parent d0ac4d9e74
commit 518495a6c5
9 changed files with 144 additions and 11 deletions
+9 -3
View File
@@ -108,8 +108,11 @@ export const getTemplateVersionResources = async (versionId: string): Promise<Ty
return response.data
}
export const getWorkspace = async (workspaceId: string): Promise<TypesGen.Workspace> => {
const response = await axios.get<TypesGen.Workspace>(`/api/v2/workspaces/${workspaceId}`)
export const getWorkspace = async (
workspaceId: string,
params?: TypesGen.WorkspaceOptions,
): Promise<TypesGen.Workspace> => {
const response = await axios.get<TypesGen.Workspace>(`/api/v2/workspaces/${workspaceId}`, { params })
return response.data
}
@@ -141,8 +144,11 @@ export const getWorkspaces = async (filter?: TypesGen.WorkspaceFilter): Promise<
export const getWorkspaceByOwnerAndName = async (
username = "me",
workspaceName: string,
params?: TypesGen.WorkspaceByOwnerAndNameParams,
): Promise<TypesGen.Workspace> => {
const response = await axios.get<TypesGen.Workspace>(`/api/v2/users/${username}/workspace/${workspaceName}`)
const response = await axios.get<TypesGen.Workspace>(`/api/v2/users/${username}/workspace/${workspaceName}`, {
params,
})
return response.data
}
+8 -1
View File
@@ -1,5 +1,6 @@
import { makeStyles } from "@material-ui/core/styles"
import { FC } from "react"
import { useNavigate } from "react-router-dom"
import * as TypesGen from "../../api/typesGenerated"
import { BuildsTable } from "../BuildsTable/BuildsTable"
import { Margins } from "../Margins/Margins"
@@ -7,6 +8,7 @@ import { PageHeader, PageHeaderSubtitle, PageHeaderTitle } from "../PageHeader/P
import { Resources } from "../Resources/Resources"
import { Stack } from "../Stack/Stack"
import { WorkspaceActions } from "../WorkspaceActions/WorkspaceActions"
import { WorkspaceDeletedBanner } from "../WorkspaceDeletedBanner/WorkspaceDeletedBanner"
import { WorkspaceSchedule } from "../WorkspaceSchedule/WorkspaceSchedule"
import { WorkspaceScheduleBanner } from "../WorkspaceScheduleBanner/WorkspaceScheduleBanner"
import { WorkspaceSection } from "../WorkspaceSection/WorkspaceSection"
@@ -44,6 +46,7 @@ export const Workspace: FC<WorkspaceProps> = ({
builds,
}) => {
const styles = useStyles()
const navigate = useNavigate()
return (
<Margins>
@@ -72,9 +75,13 @@ export const Workspace: FC<WorkspaceProps> = ({
workspace={workspace}
/>
<WorkspaceDeletedBanner workspace={workspace} handleClick={() => navigate(`/workspaces/new`)} />
<WorkspaceStats workspace={workspace} />
<Resources resources={resources} getResourcesError={getResourcesError} workspace={workspace} />
{!!resources && !!resources.length && (
<Resources resources={resources} getResourcesError={getResourcesError} workspace={workspace} />
)}
<WorkspaceSection title="Timeline" contentsProps={{ className: styles.timelineContents }}>
<BuildsTable builds={builds} className={styles.timelineTable} />
@@ -0,0 +1,28 @@
import { action } from "@storybook/addon-actions"
import { Story } from "@storybook/react"
import * as Mocks from "../../testHelpers/entities"
import { WorkspaceDeletedBanner, WorkspaceDeletedBannerProps } from "./WorkspaceDeletedBanner"
export default {
title: "components/WorkspaceDeletedBanner",
component: WorkspaceDeletedBanner,
}
const Template: Story<WorkspaceDeletedBannerProps> = (args) => <WorkspaceDeletedBanner {...args} />
export const Example = Template.bind({})
Example.args = {
handleClick: action("extend"),
workspace: {
...Mocks.MockWorkspace,
latest_build: {
...Mocks.MockWorkspaceBuild,
job: {
...Mocks.MockProvisionerJob,
status: "succeeded",
},
transition: "delete",
},
},
}
@@ -0,0 +1,50 @@
import Button from "@material-ui/core/Button"
import { makeStyles } from "@material-ui/core/styles"
import Alert from "@material-ui/lab/Alert"
import AlertTitle from "@material-ui/lab/AlertTitle"
import { FC } from "react"
import * as TypesGen from "../../api/typesGenerated"
import { isWorkspaceDeleted } from "../../util/workspace"
const Language = {
bannerTitle: "This workspace has been deleted and cannot be edited.",
createWorkspaceCta: "Create new workspace",
}
export interface WorkspaceDeletedBannerProps {
workspace: TypesGen.Workspace
handleClick: () => void
}
export const WorkspaceDeletedBanner: FC<WorkspaceDeletedBannerProps> = ({ workspace, handleClick }) => {
const styles = useStyles()
if (!isWorkspaceDeleted(workspace)) {
return null
}
return (
<Alert
className={styles.root}
action={
<Button color="inherit" onClick={handleClick} size="small">
{Language.createWorkspaceCta}
</Button>
}
severity="warning"
>
<AlertTitle>{Language.bannerTitle}</AlertTitle>
</Alert>
)
}
export const useStyles = makeStyles(() => {
return {
root: {
alignItems: "center",
"& .MuiAlertTitle-root": {
marginBottom: "0px",
},
},
}
})
@@ -26,7 +26,7 @@ export const shouldDisplay = (workspace: TypesGen.Workspace): boolean => {
if (!isWorkspaceOn(workspace)) {
return false
} else {
// a mannual shutdown has a deadline of '"0001-01-01T00:00:00Z"'
// a manual shutdown has a deadline of '"0001-01-01T00:00:00Z"'
// SEE: #1834
const deadline = dayjs(workspace.latest_build.deadline).utc()
const hasDeadline = deadline.year() > 1
@@ -1,7 +1,7 @@
import { useMachine } from "@xstate/react"
import React, { useEffect } from "react"
import { Helmet } from "react-helmet"
import { useNavigate, useParams } from "react-router-dom"
import { useParams } from "react-router-dom"
import { DeleteWorkspaceDialog } from "../../components/DeleteWorkspaceDialog/DeleteWorkspaceDialog"
import { ErrorSummary } from "../../components/ErrorSummary/ErrorSummary"
import { FullScreenLoader } from "../../components/Loader/FullScreenLoader"
@@ -13,7 +13,6 @@ import { workspaceScheduleBannerMachine } from "../../xServices/workspaceSchedul
export const WorkspacePage: React.FC = () => {
const { username: usernameQueryParam, workspace: workspaceQueryParam } = useParams()
const navigate = useNavigate()
const username = firstOrItem(usernameQueryParam, null)
const workspaceName = firstOrItem(workspaceQueryParam, null)
@@ -63,7 +62,6 @@ export const WorkspacePage: React.FC = () => {
handleCancel={() => workspaceSend("CANCEL_DELETE")}
handleConfirm={() => {
workspaceSend("DELETE")
navigate("/workspaces")
}}
/>
</>
+39 -1
View File
@@ -1,7 +1,7 @@
import dayjs from "dayjs"
import * as TypesGen from "../api/typesGenerated"
import * as Mocks from "../testHelpers/entities"
import { defaultWorkspaceExtension, isWorkspaceOn, workspaceQueryToFilter } from "./workspace"
import { defaultWorkspaceExtension, isWorkspaceDeleted, isWorkspaceOn, workspaceQueryToFilter } from "./workspace"
describe("util > workspace", () => {
describe("isWorkspaceOn", () => {
@@ -42,6 +42,44 @@ describe("util > workspace", () => {
})
})
describe("isWorkspaceDeleted", () => {
it.each<[TypesGen.WorkspaceTransition, TypesGen.ProvisionerJobStatus, boolean]>([
["delete", "canceled", false],
["delete", "canceling", false],
["delete", "failed", false],
["delete", "pending", false],
["delete", "running", false],
["delete", "succeeded", true],
["stop", "canceled", false],
["stop", "canceling", false],
["stop", "failed", false],
["stop", "pending", false],
["stop", "running", false],
["stop", "succeeded", false],
["start", "canceled", false],
["start", "canceling", false],
["start", "failed", false],
["start", "pending", false],
["start", "running", false],
["start", "succeeded", false],
])(`transition=%p, status=%p, isWorkspaceDeleted=%p`, (transition, status, isDeleted) => {
const workspace: TypesGen.Workspace = {
...Mocks.MockWorkspace,
latest_build: {
...Mocks.MockWorkspaceBuild,
job: {
...Mocks.MockProvisionerJob,
status,
},
transition,
},
}
expect(isWorkspaceDeleted(workspace)).toBe(isDeleted)
})
})
describe("defaultWorkspaceExtension", () => {
it.each<[string, TypesGen.PutExtendWorkspaceRequest]>([
[
+4
View File
@@ -249,6 +249,10 @@ export const isWorkspaceOn = (workspace: TypesGen.Workspace): boolean => {
return transition === "start" && status === "succeeded"
}
export const isWorkspaceDeleted = (workspace: TypesGen.Workspace): boolean => {
return getWorkspaceStatus(workspace.latest_build) === succeededToStatus["delete"]
}
export const defaultWorkspaceExtension = (__startDate?: dayjs.Dayjs): TypesGen.PutExtendWorkspaceRequest => {
const now = __startDate ? dayjs(__startDate) : dayjs()
const fourHoursFromNow = now.add(4, "hours").utc()
@@ -431,7 +431,7 @@ export const workspaceMachine = createMachine(
},
services: {
getWorkspace: async (_, event) => {
return await API.getWorkspaceByOwnerAndName(event.username, event.workspaceName)
return await API.getWorkspaceByOwnerAndName(event.username, event.workspaceName, { include_deleted: true })
},
getTemplate: async (context) => {
if (context.workspace) {
@@ -470,7 +470,9 @@ export const workspaceMachine = createMachine(
},
refreshWorkspace: async (context) => {
if (context.workspace) {
return await API.getWorkspaceByOwnerAndName(context.workspace.owner_name, context.workspace.name)
return await API.getWorkspaceByOwnerAndName(context.workspace.owner_name, context.workspace.name, {
include_deleted: true,
})
} else {
throw Error("Cannot refresh workspace without id")
}