mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
+9
-3
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -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]>([
|
||||
[
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user