Skip to content

Commit a2ff674

Browse files
fix(UI): workspace restart button stops build before starting a new one (#7301)
* feat(UI): add workspace restart button (#7137) * Refactor primary buttons * refactor(site): Always show the main actions * Remove tests that are testes on Storybook * Fix tests * Fix keys * added restart btn --------- Co-authored-by: BrunoQuaresma <bruno_nonato_quaresma@hotmail.com> * added restart hook * added error handling * going back to chaining in success callback * add restarting btn * added test * PR feedback --------- Co-authored-by: BrunoQuaresma <bruno_nonato_quaresma@hotmail.com>
1 parent 3078cd3 commit a2ff674

File tree

10 files changed

+133
-13
lines changed

10 files changed

+133
-13
lines changed

site/src/api/api.ts

+49-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import dayjs from "dayjs"
33
import * as Types from "./types"
44
import { DeploymentConfig } from "./types"
55
import * as TypesGen from "./typesGenerated"
6+
import { delay } from "utils/delay"
67

78
// Adds 304 for the default axios validateStatus function
89
// https://github.com/axios/axios#handling-errors Check status here
@@ -476,6 +477,35 @@ export const getWorkspaceByOwnerAndName = async (
476477
return response.data
477478
}
478479

480+
export function waitForBuild(build: TypesGen.WorkspaceBuild) {
481+
return new Promise<TypesGen.ProvisionerJob | undefined>((res, reject) => {
482+
void (async () => {
483+
let latestJobInfo: TypesGen.ProvisionerJob | undefined = undefined
484+
485+
while (
486+
!["succeeded", "canceled"].some((status) =>
487+
latestJobInfo?.status.includes(status),
488+
)
489+
) {
490+
const { job } = await getWorkspaceBuildByNumber(
491+
build.workspace_owner_name,
492+
build.workspace_name,
493+
String(build.build_number),
494+
)
495+
latestJobInfo = job
496+
497+
if (latestJobInfo.status === "failed") {
498+
return reject(latestJobInfo)
499+
}
500+
501+
await delay(1000)
502+
}
503+
504+
return res(latestJobInfo)
505+
})()
506+
})
507+
}
508+
479509
export const postWorkspaceBuild = async (
480510
workspaceId: string,
481511
data: TypesGen.CreateWorkspaceBuildRequest,
@@ -489,12 +519,12 @@ export const postWorkspaceBuild = async (
489519

490520
export const startWorkspace = (
491521
workspaceId: string,
492-
templateVersionID: string,
522+
templateVersionId: string,
493523
logLevel?: TypesGen.CreateWorkspaceBuildRequest["log_level"],
494524
) =>
495525
postWorkspaceBuild(workspaceId, {
496526
transition: "start",
497-
template_version_id: templateVersionID,
527+
template_version_id: templateVersionId,
498528
log_level: logLevel,
499529
})
500530
export const stopWorkspace = (
@@ -505,6 +535,7 @@ export const stopWorkspace = (
505535
transition: "stop",
506536
log_level: logLevel,
507537
})
538+
508539
export const deleteWorkspace = (
509540
workspaceId: string,
510541
logLevel?: TypesGen.CreateWorkspaceBuildRequest["log_level"],
@@ -523,6 +554,22 @@ export const cancelWorkspaceBuild = async (
523554
return response.data
524555
}
525556

557+
export const restartWorkspace = async (workspace: TypesGen.Workspace) => {
558+
const stopBuild = await stopWorkspace(workspace.id)
559+
const awaitedStopBuild = await waitForBuild(stopBuild)
560+
561+
// If the restart is canceled halfway through, make sure we bail
562+
if (awaitedStopBuild?.status === "canceled") {
563+
return
564+
}
565+
566+
const startBuild = await startWorkspace(
567+
workspace.id,
568+
workspace.latest_build.template_version_id,
569+
)
570+
await waitForBuild(startBuild)
571+
}
572+
526573
export const cancelTemplateVersionBuild = async (
527574
templateVersionId: TypesGen.TemplateVersion["id"],
528575
): Promise<Types.Message> => {

site/src/components/Workspace/Workspace.tsx

+6
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,14 @@ export interface WorkspaceProps {
4141
}
4242
handleStart: () => void
4343
handleStop: () => void
44+
handleRestart: () => void
4445
handleDelete: () => void
4546
handleUpdate: () => void
4647
handleCancel: () => void
4748
handleSettings: () => void
4849
handleChangeVersion: () => void
4950
isUpdating: boolean
51+
isRestarting: boolean
5052
workspace: TypesGen.Workspace
5153
resources?: TypesGen.WorkspaceResource[]
5254
builds?: TypesGen.WorkspaceBuild[]
@@ -72,13 +74,15 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
7274
scheduleProps,
7375
handleStart,
7476
handleStop,
77+
handleRestart,
7578
handleDelete,
7679
handleUpdate,
7780
handleCancel,
7881
handleSettings,
7982
handleChangeVersion,
8083
workspace,
8184
isUpdating,
85+
isRestarting,
8286
resources,
8387
builds,
8488
canUpdateWorkspace,
@@ -132,13 +136,15 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
132136
isOutdated={workspace.outdated}
133137
handleStart={handleStart}
134138
handleStop={handleStop}
139+
handleRestart={handleRestart}
135140
handleDelete={handleDelete}
136141
handleUpdate={handleUpdate}
137142
handleCancel={handleCancel}
138143
handleSettings={handleSettings}
139144
handleChangeVersion={handleChangeVersion}
140145
canChangeVersions={canChangeVersions}
141146
isUpdating={isUpdating}
147+
isRestarting={isRestarting}
142148
/>
143149
</Stack>
144150
}

site/src/components/WorkspaceActions/Buttons.tsx

+26-7
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,17 @@ import BlockIcon from "@material-ui/icons/Block"
33
import CloudQueueIcon from "@material-ui/icons/CloudQueue"
44
import CropSquareIcon from "@material-ui/icons/CropSquare"
55
import PlayCircleOutlineIcon from "@material-ui/icons/PlayCircleOutline"
6+
import ReplayIcon from "@material-ui/icons/Replay"
67
import { LoadingButton } from "components/LoadingButton/LoadingButton"
7-
import { FC } from "react"
8+
import { FC, PropsWithChildren } from "react"
89
import { useTranslation } from "react-i18next"
910
import { makeStyles } from "@material-ui/core/styles"
1011

1112
interface WorkspaceAction {
1213
handleAction: () => void
1314
}
1415

15-
export const UpdateButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({
16+
export const UpdateButton: FC<PropsWithChildren<WorkspaceAction>> = ({
1617
handleAction,
1718
}) => {
1819
const { t } = useTranslation("workspacePage")
@@ -30,7 +31,7 @@ export const UpdateButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({
3031
)
3132
}
3233

33-
export const StartButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({
34+
export const StartButton: FC<PropsWithChildren<WorkspaceAction>> = ({
3435
handleAction,
3536
}) => {
3637
const { t } = useTranslation("workspacePage")
@@ -48,7 +49,7 @@ export const StartButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({
4849
)
4950
}
5051

51-
export const StopButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({
52+
export const StopButton: FC<PropsWithChildren<WorkspaceAction>> = ({
5253
handleAction,
5354
}) => {
5455
const { t } = useTranslation("workspacePage")
@@ -66,7 +67,25 @@ export const StopButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({
6667
)
6768
}
6869

69-
export const CancelButton: FC<React.PropsWithChildren<WorkspaceAction>> = ({
70+
export const RestartButton: FC<PropsWithChildren<WorkspaceAction>> = ({
71+
handleAction,
72+
}) => {
73+
const { t } = useTranslation("workspacePage")
74+
const styles = useStyles()
75+
76+
return (
77+
<Button
78+
variant="outlined"
79+
startIcon={<ReplayIcon />}
80+
onClick={handleAction}
81+
className={styles.fixedWidth}
82+
>
83+
{t("actionButton.restart")}
84+
</Button>
85+
)
86+
}
87+
88+
export const CancelButton: FC<PropsWithChildren<WorkspaceAction>> = ({
7089
handleAction,
7190
}) => {
7291
return (
@@ -80,7 +99,7 @@ interface DisabledProps {
8099
label: string
81100
}
82101

83-
export const DisabledButton: FC<React.PropsWithChildren<DisabledProps>> = ({
102+
export const DisabledButton: FC<PropsWithChildren<DisabledProps>> = ({
84103
label,
85104
}) => {
86105
return (
@@ -94,7 +113,7 @@ interface LoadingProps {
94113
label: string
95114
}
96115

97-
export const ActionLoadingButton: FC<React.PropsWithChildren<LoadingProps>> = ({
116+
export const ActionLoadingButton: FC<PropsWithChildren<LoadingProps>> = ({
98117
label,
99118
}) => {
100119
const styles = useStyles()

site/src/components/WorkspaceActions/WorkspaceActions.stories.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const Template: Story<WorkspaceActionsProps> = (args) => (
1515
const defaultArgs = {
1616
handleStart: action("start"),
1717
handleStop: action("stop"),
18+
handleRestart: action("restart"),
1819
handleDelete: action("delete"),
1920
handleUpdate: action("update"),
2021
handleCancel: action("cancel"),

site/src/components/WorkspaceActions/WorkspaceActions.tsx

+18-2
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@ import { makeStyles } from "@material-ui/core/styles"
55
import MoreVertOutlined from "@material-ui/icons/MoreVertOutlined"
66
import { FC, ReactNode, useRef, useState } from "react"
77
import { useTranslation } from "react-i18next"
8-
import { WorkspaceStatus } from "../../api/typesGenerated"
8+
import { WorkspaceStatus } from "api/typesGenerated"
99
import {
1010
ActionLoadingButton,
1111
CancelButton,
1212
DisabledButton,
1313
StartButton,
1414
StopButton,
15+
RestartButton,
1516
UpdateButton,
1617
} from "./Buttons"
1718
import {
@@ -28,12 +29,14 @@ export interface WorkspaceActionsProps {
2829
isOutdated: boolean
2930
handleStart: () => void
3031
handleStop: () => void
32+
handleRestart: () => void
3133
handleDelete: () => void
3234
handleUpdate: () => void
3335
handleCancel: () => void
3436
handleSettings: () => void
3537
handleChangeVersion: () => void
3638
isUpdating: boolean
39+
isRestarting: boolean
3740
children?: ReactNode
3841
canChangeVersions: boolean
3942
}
@@ -43,12 +46,14 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
4346
isOutdated,
4447
handleStart,
4548
handleStop,
49+
handleRestart,
4650
handleDelete,
4751
handleUpdate,
4852
handleCancel,
4953
handleSettings,
5054
handleChangeVersion,
5155
isUpdating,
56+
isRestarting,
5257
canChangeVersions,
5358
}) => {
5459
const styles = useStyles()
@@ -91,6 +96,13 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
9196
key={ButtonTypesEnum.stopping}
9297
/>
9398
),
99+
[ButtonTypesEnum.restart]: <RestartButton handleAction={handleRestart} />,
100+
[ButtonTypesEnum.restarting]: (
101+
<ActionLoadingButton
102+
label="Restarting"
103+
key={ButtonTypesEnum.restarting}
104+
/>
105+
),
94106
[ButtonTypesEnum.deleting]: (
95107
<ActionLoadingButton
96108
label={t("actionButton.deleting")}
@@ -129,7 +141,11 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
129141
(isUpdating
130142
? buttonMapping[ButtonTypesEnum.updating]
131143
: buttonMapping[ButtonTypesEnum.update])}
132-
{actionsByStatus.map((action) => buttonMapping[action])}
144+
{isRestarting && buttonMapping[ButtonTypesEnum.restarting]}
145+
{!isRestarting &&
146+
actionsByStatus.map((action) => (
147+
<span key={action}>{buttonMapping[action]}</span>
148+
))}
133149
{canCancel && <CancelButton handleAction={handleCancel} />}
134150
<div>
135151
<Button

site/src/components/WorkspaceActions/constants.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ export enum ButtonTypesEnum {
77
starting = "starting",
88
stop = "stop",
99
stopping = "stopping",
10+
restart = "restart",
11+
restarting = "restarting",
1012
deleting = "deleting",
1113
update = "update",
1214
updating = "updating",
@@ -39,7 +41,7 @@ const statusToActions: Record<WorkspaceStatus, WorkspaceAbilities> = {
3941
canAcceptJobs: false,
4042
},
4143
running: {
42-
actions: [ButtonTypesEnum.stop],
44+
actions: [ButtonTypesEnum.stop, ButtonTypesEnum.restart],
4345
canCancel: false,
4446
canAcceptJobs: true,
4547
},

site/src/i18n/en/workspacePage.json

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
"actionButton": {
2222
"start": "Start",
2323
"stop": "Stop",
24+
"restart": "Restart",
2425
"delete": "Delete",
2526
"cancel": "Cancel",
2627
"update": "Update",

site/src/pages/WorkspacePage/WorkspacePage.test.tsx

+11
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,17 @@ describe("WorkspacePage", () => {
151151
)
152152
})
153153

154+
it("requests a stop when the user presses Restart", async () => {
155+
const stopWorkspaceMock = jest
156+
.spyOn(api, "stopWorkspace")
157+
.mockResolvedValueOnce(MockWorkspaceBuild)
158+
159+
await testButton("Restart", stopWorkspaceMock)
160+
161+
const button = await screen.findByText("Restarting")
162+
expect(button).toBeInTheDocument()
163+
})
164+
154165
it("requests cancellation when the user presses Cancel", async () => {
155166
server.use(
156167
rest.get(

site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx

+10-1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { UpdateBuildParametersDialog } from "./UpdateBuildParametersDialog"
3030
import { ChangeVersionDialog } from "./ChangeVersionDialog"
3131
import { useQuery } from "@tanstack/react-query"
3232
import { getTemplateVersions } from "api/api"
33+
import { useRestartWorkspace } from "./hooks"
3334

3435
interface WorkspaceReadyPageProps {
3536
workspaceState: StateFrom<typeof workspaceMachine>
@@ -77,6 +78,12 @@ export const WorkspaceReadyPage = ({
7778
enabled: changeVersionDialogOpen,
7879
})
7980

81+
const {
82+
mutate: restartWorkspace,
83+
error: restartBuildError,
84+
isLoading: isRestarting,
85+
} = useRestartWorkspace()
86+
8087
// keep banner machine in sync with workspace
8188
useEffect(() => {
8289
bannerSend({ type: "REFRESH_WORKSPACE", workspace })
@@ -120,9 +127,11 @@ export const WorkspaceReadyPage = ({
120127
),
121128
}}
122129
isUpdating={workspaceState.matches("ready.build.requestingUpdate")}
130+
isRestarting={isRestarting}
123131
workspace={workspace}
124132
handleStart={() => workspaceSend({ type: "START" })}
125133
handleStop={() => workspaceSend({ type: "STOP" })}
134+
handleRestart={() => restartWorkspace(workspace)}
126135
handleDelete={() => workspaceSend({ type: "ASK_DELETE" })}
127136
handleUpdate={() => workspaceSend({ type: "UPDATE" })}
128137
handleCancel={() => workspaceSend({ type: "CANCEL" })}
@@ -140,7 +149,7 @@ export const WorkspaceReadyPage = ({
140149
hideVSCodeDesktopButton={featureVisibility["browser_only"]}
141150
workspaceErrors={{
142151
[WorkspaceErrors.GET_BUILDS_ERROR]: getBuildsError,
143-
[WorkspaceErrors.BUILD_ERROR]: buildError,
152+
[WorkspaceErrors.BUILD_ERROR]: buildError || restartBuildError,
144153
[WorkspaceErrors.CANCELLATION_ERROR]: cancellationError,
145154
}}
146155
buildInfo={buildInfo}

site/src/pages/WorkspacePage/hooks.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { restartWorkspace } from "api/api"
2+
import { useMutation } from "@tanstack/react-query"
3+
4+
export const useRestartWorkspace = () => {
5+
return useMutation({
6+
mutationFn: restartWorkspace,
7+
})
8+
}

0 commit comments

Comments
 (0)