diff --git a/site/src/api/api.ts b/site/src/api/api.ts index c86748b2b64cd..39de3d7aff046 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -108,8 +108,11 @@ export const getTemplateVersionResources = async (versionId: string): Promise => { - const response = await axios.get(`/api/v2/workspaces/${workspaceId}`) +export const getWorkspace = async ( + workspaceId: string, + params?: TypesGen.WorkspaceOptions, +): Promise => { + const response = await axios.get(`/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 => { - const response = await axios.get(`/api/v2/users/${username}/workspace/${workspaceName}`) + const response = await axios.get(`/api/v2/users/${username}/workspace/${workspaceName}`, { + params, + }) return response.data } diff --git a/site/src/components/Workspace/Workspace.tsx b/site/src/components/Workspace/Workspace.tsx index fa2b07c72253e..e607017341687 100644 --- a/site/src/components/Workspace/Workspace.tsx +++ b/site/src/components/Workspace/Workspace.tsx @@ -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 = ({ builds, }) => { const styles = useStyles() + const navigate = useNavigate() return ( @@ -72,9 +75,13 @@ export const Workspace: FC = ({ workspace={workspace} /> + navigate(`/workspaces/new`)} /> + - + {!!resources && !!resources.length && ( + + )} diff --git a/site/src/components/WorkspaceDeletedBanner/WorkspaceDeletedBanner.stories.tsx b/site/src/components/WorkspaceDeletedBanner/WorkspaceDeletedBanner.stories.tsx new file mode 100644 index 0000000000000..a53d234579332 --- /dev/null +++ b/site/src/components/WorkspaceDeletedBanner/WorkspaceDeletedBanner.stories.tsx @@ -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 = (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", + }, + }, +} diff --git a/site/src/components/WorkspaceDeletedBanner/WorkspaceDeletedBanner.tsx b/site/src/components/WorkspaceDeletedBanner/WorkspaceDeletedBanner.tsx new file mode 100644 index 0000000000000..310a18fac95f1 --- /dev/null +++ b/site/src/components/WorkspaceDeletedBanner/WorkspaceDeletedBanner.tsx @@ -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 = ({ workspace, handleClick }) => { + const styles = useStyles() + + if (!isWorkspaceDeleted(workspace)) { + return null + } + + return ( + + {Language.createWorkspaceCta} + + } + severity="warning" + > + {Language.bannerTitle} + + ) +} + +export const useStyles = makeStyles(() => { + return { + root: { + alignItems: "center", + "& .MuiAlertTitle-root": { + marginBottom: "0px", + }, + }, + } +}) diff --git a/site/src/components/WorkspaceScheduleBanner/WorkspaceScheduleBanner.tsx b/site/src/components/WorkspaceScheduleBanner/WorkspaceScheduleBanner.tsx index 65c64fc75f37e..100bc502023a6 100644 --- a/site/src/components/WorkspaceScheduleBanner/WorkspaceScheduleBanner.tsx +++ b/site/src/components/WorkspaceScheduleBanner/WorkspaceScheduleBanner.tsx @@ -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 diff --git a/site/src/pages/WorkspacePage/WorkspacePage.tsx b/site/src/pages/WorkspacePage/WorkspacePage.tsx index f33b1c31cf8ab..7584c8d193a4e 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 { 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") }} /> diff --git a/site/src/util/workspace.test.ts b/site/src/util/workspace.test.ts index 7c139b72d0ea1..a7789dfbf8285 100644 --- a/site/src/util/workspace.test.ts +++ b/site/src/util/workspace.test.ts @@ -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]>([ [ diff --git a/site/src/util/workspace.ts b/site/src/util/workspace.ts index fd0684115bb88..94758aebd00d7 100644 --- a/site/src/util/workspace.ts +++ b/site/src/util/workspace.ts @@ -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() diff --git a/site/src/xServices/workspace/workspaceXService.ts b/site/src/xServices/workspace/workspaceXService.ts index d552e961a6b65..c24c5b968c8de 100644 --- a/site/src/xServices/workspace/workspaceXService.ts +++ b/site/src/xServices/workspace/workspaceXService.ts @@ -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") }