diff --git a/site/src/api/api.ts b/site/src/api/api.ts index e778b9fc0a4fb..1abd7643fb29c 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -3,6 +3,7 @@ import dayjs from "dayjs" import * as Types from "./types" import { DeploymentConfig } from "./types" import * as TypesGen from "./typesGenerated" +import { delay } from "utils/delay" // Adds 304 for the default axios validateStatus function // https://github.com/axios/axios#handling-errors Check status here @@ -476,6 +477,35 @@ export const getWorkspaceByOwnerAndName = async ( return response.data } +export function waitForBuild(build: TypesGen.WorkspaceBuild) { + return new Promise((res, reject) => { + void (async () => { + let latestJobInfo: TypesGen.ProvisionerJob | undefined = undefined + + while ( + !["succeeded", "canceled"].some((status) => + latestJobInfo?.status.includes(status), + ) + ) { + const { job } = await getWorkspaceBuildByNumber( + build.workspace_owner_name, + build.workspace_name, + String(build.build_number), + ) + latestJobInfo = job + + if (latestJobInfo.status === "failed") { + return reject(latestJobInfo) + } + + await delay(1000) + } + + return res(latestJobInfo) + })() + }) +} + export const postWorkspaceBuild = async ( workspaceId: string, data: TypesGen.CreateWorkspaceBuildRequest, @@ -489,12 +519,12 @@ export const postWorkspaceBuild = async ( export const startWorkspace = ( workspaceId: string, - templateVersionID: string, + templateVersionId: string, logLevel?: TypesGen.CreateWorkspaceBuildRequest["log_level"], ) => postWorkspaceBuild(workspaceId, { transition: "start", - template_version_id: templateVersionID, + template_version_id: templateVersionId, log_level: logLevel, }) export const stopWorkspace = ( @@ -505,6 +535,7 @@ export const stopWorkspace = ( transition: "stop", log_level: logLevel, }) + export const deleteWorkspace = ( workspaceId: string, logLevel?: TypesGen.CreateWorkspaceBuildRequest["log_level"], @@ -523,6 +554,22 @@ export const cancelWorkspaceBuild = async ( return response.data } +export const restartWorkspace = async (workspace: TypesGen.Workspace) => { + const stopBuild = await stopWorkspace(workspace.id) + const awaitedStopBuild = await waitForBuild(stopBuild) + + // If the restart is canceled halfway through, make sure we bail + if (awaitedStopBuild?.status === "canceled") { + return + } + + const startBuild = await startWorkspace( + workspace.id, + workspace.latest_build.template_version_id, + ) + await waitForBuild(startBuild) +} + export const cancelTemplateVersionBuild = async ( templateVersionId: TypesGen.TemplateVersion["id"], ): Promise => { diff --git a/site/src/components/Workspace/Workspace.tsx b/site/src/components/Workspace/Workspace.tsx index b0377eeb29847..5be87ec6a95e1 100644 --- a/site/src/components/Workspace/Workspace.tsx +++ b/site/src/components/Workspace/Workspace.tsx @@ -41,12 +41,14 @@ export interface WorkspaceProps { } handleStart: () => void handleStop: () => void + handleRestart: () => void handleDelete: () => void handleUpdate: () => void handleCancel: () => void handleSettings: () => void handleChangeVersion: () => void isUpdating: boolean + isRestarting: boolean workspace: TypesGen.Workspace resources?: TypesGen.WorkspaceResource[] builds?: TypesGen.WorkspaceBuild[] @@ -72,6 +74,7 @@ export const Workspace: FC> = ({ scheduleProps, handleStart, handleStop, + handleRestart, handleDelete, handleUpdate, handleCancel, @@ -79,6 +82,7 @@ export const Workspace: FC> = ({ handleChangeVersion, workspace, isUpdating, + isRestarting, resources, builds, canUpdateWorkspace, @@ -132,6 +136,7 @@ export const Workspace: FC> = ({ isOutdated={workspace.outdated} handleStart={handleStart} handleStop={handleStop} + handleRestart={handleRestart} handleDelete={handleDelete} handleUpdate={handleUpdate} handleCancel={handleCancel} @@ -139,6 +144,7 @@ export const Workspace: FC> = ({ handleChangeVersion={handleChangeVersion} canChangeVersions={canChangeVersions} isUpdating={isUpdating} + isRestarting={isRestarting} /> } diff --git a/site/src/components/WorkspaceActions/Buttons.tsx b/site/src/components/WorkspaceActions/Buttons.tsx index b8c38469df68f..d6207952a4ac8 100644 --- a/site/src/components/WorkspaceActions/Buttons.tsx +++ b/site/src/components/WorkspaceActions/Buttons.tsx @@ -3,8 +3,9 @@ import BlockIcon from "@material-ui/icons/Block" import CloudQueueIcon from "@material-ui/icons/CloudQueue" import CropSquareIcon from "@material-ui/icons/CropSquare" import PlayCircleOutlineIcon from "@material-ui/icons/PlayCircleOutline" +import ReplayIcon from "@material-ui/icons/Replay" import { LoadingButton } from "components/LoadingButton/LoadingButton" -import { FC } from "react" +import { FC, PropsWithChildren } from "react" import { useTranslation } from "react-i18next" import { makeStyles } from "@material-ui/core/styles" @@ -12,7 +13,7 @@ interface WorkspaceAction { handleAction: () => void } -export const UpdateButton: FC> = ({ +export const UpdateButton: FC> = ({ handleAction, }) => { const { t } = useTranslation("workspacePage") @@ -30,7 +31,7 @@ export const UpdateButton: FC> = ({ ) } -export const StartButton: FC> = ({ +export const StartButton: FC> = ({ handleAction, }) => { const { t } = useTranslation("workspacePage") @@ -48,7 +49,7 @@ export const StartButton: FC> = ({ ) } -export const StopButton: FC> = ({ +export const StopButton: FC> = ({ handleAction, }) => { const { t } = useTranslation("workspacePage") @@ -66,7 +67,25 @@ export const StopButton: FC> = ({ ) } -export const CancelButton: FC> = ({ +export const RestartButton: FC> = ({ + handleAction, +}) => { + const { t } = useTranslation("workspacePage") + const styles = useStyles() + + return ( + + ) +} + +export const CancelButton: FC> = ({ handleAction, }) => { return ( @@ -80,7 +99,7 @@ interface DisabledProps { label: string } -export const DisabledButton: FC> = ({ +export const DisabledButton: FC> = ({ label, }) => { return ( @@ -94,7 +113,7 @@ interface LoadingProps { label: string } -export const ActionLoadingButton: FC> = ({ +export const ActionLoadingButton: FC> = ({ label, }) => { const styles = useStyles() diff --git a/site/src/components/WorkspaceActions/WorkspaceActions.stories.tsx b/site/src/components/WorkspaceActions/WorkspaceActions.stories.tsx index 090e81cb2bb03..b2b2526811d0d 100644 --- a/site/src/components/WorkspaceActions/WorkspaceActions.stories.tsx +++ b/site/src/components/WorkspaceActions/WorkspaceActions.stories.tsx @@ -15,6 +15,7 @@ const Template: Story = (args) => ( const defaultArgs = { handleStart: action("start"), handleStop: action("stop"), + handleRestart: action("restart"), handleDelete: action("delete"), handleUpdate: action("update"), handleCancel: action("cancel"), diff --git a/site/src/components/WorkspaceActions/WorkspaceActions.tsx b/site/src/components/WorkspaceActions/WorkspaceActions.tsx index d7508ed64405b..aacf1f10f085e 100644 --- a/site/src/components/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/components/WorkspaceActions/WorkspaceActions.tsx @@ -5,13 +5,14 @@ import { makeStyles } from "@material-ui/core/styles" import MoreVertOutlined from "@material-ui/icons/MoreVertOutlined" import { FC, ReactNode, useRef, useState } from "react" import { useTranslation } from "react-i18next" -import { WorkspaceStatus } from "../../api/typesGenerated" +import { WorkspaceStatus } from "api/typesGenerated" import { ActionLoadingButton, CancelButton, DisabledButton, StartButton, StopButton, + RestartButton, UpdateButton, } from "./Buttons" import { @@ -28,12 +29,14 @@ export interface WorkspaceActionsProps { isOutdated: boolean handleStart: () => void handleStop: () => void + handleRestart: () => void handleDelete: () => void handleUpdate: () => void handleCancel: () => void handleSettings: () => void handleChangeVersion: () => void isUpdating: boolean + isRestarting: boolean children?: ReactNode canChangeVersions: boolean } @@ -43,12 +46,14 @@ export const WorkspaceActions: FC = ({ isOutdated, handleStart, handleStop, + handleRestart, handleDelete, handleUpdate, handleCancel, handleSettings, handleChangeVersion, isUpdating, + isRestarting, canChangeVersions, }) => { const styles = useStyles() @@ -91,6 +96,13 @@ export const WorkspaceActions: FC = ({ key={ButtonTypesEnum.stopping} /> ), + [ButtonTypesEnum.restart]: , + [ButtonTypesEnum.restarting]: ( + + ), [ButtonTypesEnum.deleting]: ( = ({ (isUpdating ? buttonMapping[ButtonTypesEnum.updating] : buttonMapping[ButtonTypesEnum.update])} - {actionsByStatus.map((action) => buttonMapping[action])} + {isRestarting && buttonMapping[ButtonTypesEnum.restarting]} + {!isRestarting && + actionsByStatus.map((action) => ( + {buttonMapping[action]} + ))} {canCancel && }