Skip to content

Commit 8d95285

Browse files
presleypKira-Pilot
andauthored
feat: offer to restart workspace when ttl is changed (#5391)
* Update xstate machine * Fix autoStopChanged * Add dialog * Restart workspace * Clearing location doesn't work and doesn't seem necessary * Fix test * Fix second test * Format * Lint * Use i18n * Switch to fire and forget restart * Improve error handling * Format * Format * Update site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx Co-authored-by: Kira Pilot <kira@coder.com> * Fix name of guard * Make done state final * Format Co-authored-by: Kira Pilot <kira@coder.com>
1 parent 2bbeff5 commit 8d95285

File tree

6 files changed

+332
-176
lines changed

6 files changed

+332
-176
lines changed

site/src/i18n/en/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import templateSettingsPage from "./templateSettingsPage.json"
1212
import templateVersionPage from "./templateVersionPage.json"
1313
import loginPage from "./loginPage.json"
1414
import workspaceChangeVersionPage from "./workspaceChangeVersionPage.json"
15+
import workspaceSchedulePage from "./workspaceSchedulePage.json"
1516
import serviceBannerSettings from "./serviceBannerSettings.json"
1617

1718
export const en = {
@@ -29,5 +30,6 @@ export const en = {
2930
templateVersionPage,
3031
loginPage,
3132
workspaceChangeVersionPage,
33+
workspaceSchedulePage,
3234
serviceBannerSettings,
3335
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"forbiddenError": "You don't have permissions to update the schedule for this workspace.",
3+
"dialogTitle": "Restart workspace?",
4+
"dialogDescription": "Would you like to restart your workspace now to apply your new auto-stop setting, or let it apply after your next workspace start?",
5+
"restart": "Restart workspace now",
6+
"applyLater": "Apply update later"
7+
}

site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
import {
2+
MockUser,
3+
MockWorkspace,
4+
renderWithAuth,
5+
} from "testHelpers/renderHelpers"
6+
import userEvent from "@testing-library/user-event"
7+
import { screen } from "@testing-library/react"
18
import {
29
formValuesToAutoStartRequest,
310
formValuesToTTLRequest,
@@ -8,7 +15,14 @@ import {
815
} from "pages/WorkspaceSchedulePage/schedule"
916
import { AutoStop, ttlMsToAutoStop } from "pages/WorkspaceSchedulePage/ttl"
1017
import * as TypesGen from "../../api/typesGenerated"
11-
import { WorkspaceScheduleFormValues } from "../../components/WorkspaceScheduleForm/WorkspaceScheduleForm"
18+
import {
19+
WorkspaceScheduleFormValues,
20+
Language as FormLanguage,
21+
} from "components/WorkspaceScheduleForm/WorkspaceScheduleForm"
22+
import { WorkspaceSchedulePage } from "./WorkspaceSchedulePage"
23+
import i18next from "i18next"
24+
25+
const { t } = i18next
1226

1327
const validValues: WorkspaceScheduleFormValues = {
1428
autoStartEnabled: true,
@@ -241,4 +255,44 @@ describe("WorkspaceSchedulePage", () => {
241255
expect(ttlMsToAutoStop(ttlMs)).toEqual(autoStop)
242256
})
243257
})
258+
259+
describe("autoStop change dialog", () => {
260+
it("shows if autoStop is changed", async () => {
261+
renderWithAuth(<WorkspaceSchedulePage />, {
262+
route: `/@${MockUser.username}/${MockWorkspace.name}/schedule`,
263+
path: "/@:username/:workspace/schedule",
264+
})
265+
const user = userEvent.setup()
266+
const autoStopToggle = await screen.findByLabelText(
267+
FormLanguage.stopSwitch,
268+
)
269+
await user.click(autoStopToggle)
270+
const submitButton = await screen.findByRole("button", {
271+
name: /submit/i,
272+
})
273+
await user.click(submitButton)
274+
const title = t("dialogTitle", { ns: "workspaceSchedulePage" })
275+
const dialog = await screen.findByText(title)
276+
expect(dialog).toBeInTheDocument()
277+
})
278+
279+
it("doesn't show if autoStop is not changed", async () => {
280+
renderWithAuth(<WorkspaceSchedulePage />, {
281+
route: `/@${MockUser.username}/${MockWorkspace.name}/schedule`,
282+
path: "/@:username/:workspace/schedule",
283+
})
284+
const user = userEvent.setup()
285+
const autoStartToggle = await screen.findByLabelText(
286+
FormLanguage.startSwitch,
287+
)
288+
await user.click(autoStartToggle)
289+
const submitButton = await screen.findByRole("button", {
290+
name: /submit/i,
291+
})
292+
await user.click(submitButton)
293+
const title = t("dialogTitle", { ns: "workspaceSchedulePage" })
294+
const dialog = screen.queryByText(title)
295+
expect(dialog).not.toBeInTheDocument()
296+
})
297+
})
244298
})

site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx

Lines changed: 63 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
import { makeStyles } from "@material-ui/core/styles"
12
import { useMachine } from "@xstate/react"
23
import { AlertBanner } from "components/AlertBanner/AlertBanner"
4+
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"
5+
import { Margins } from "components/Margins/Margins"
36
import { scheduleToAutoStart } from "pages/WorkspaceSchedulePage/schedule"
47
import { ttlMsToAutoStop } from "pages/WorkspaceSchedulePage/ttl"
5-
import React, { useEffect, useState } from "react"
8+
import React, { useEffect } from "react"
9+
import { useTranslation } from "react-i18next"
610
import { Navigate, useNavigate, useParams } from "react-router-dom"
711
import { scheduleChanged } from "util/schedule"
812
import * as TypesGen from "../../api/typesGenerated"
@@ -15,14 +19,20 @@ import {
1519
formValuesToTTLRequest,
1620
} from "./formToRequest"
1721

18-
const Language = {
19-
forbiddenError:
20-
"You don't have permissions to update the schedule for this workspace.",
21-
getWorkspaceError: "Failed to fetch workspace.",
22-
checkPermissionsError: "Failed to fetch permissions.",
23-
}
22+
const getAutoStart = (workspace?: TypesGen.Workspace) =>
23+
scheduleToAutoStart(workspace?.autostart_schedule)
24+
const getAutoStop = (workspace?: TypesGen.Workspace) =>
25+
ttlMsToAutoStop(workspace?.ttl_ms)
26+
27+
const useStyles = makeStyles((theme) => ({
28+
topMargin: {
29+
marginTop: `${theme.spacing(3)}px`,
30+
},
31+
}))
2432

2533
export const WorkspaceSchedulePage: React.FC = () => {
34+
const { t } = useTranslation("workspaceSchedulePage")
35+
const styles = useStyles()
2636
const { username: usernameQueryParam, workspace: workspaceQueryParam } =
2737
useParams()
2838
const navigate = useNavigate()
@@ -33,6 +43,7 @@ export const WorkspaceSchedulePage: React.FC = () => {
3343
checkPermissionsError,
3444
submitScheduleError,
3545
getWorkspaceError,
46+
getTemplateError,
3647
permissions,
3748
workspace,
3849
} = scheduleState.context
@@ -45,52 +56,39 @@ export const WorkspaceSchedulePage: React.FC = () => {
4556
scheduleSend({ type: "GET_WORKSPACE", username, workspaceName })
4657
}, [username, workspaceName, scheduleSend])
4758

48-
const getAutoStart = (workspace?: TypesGen.Workspace) =>
49-
scheduleToAutoStart(workspace?.autostart_schedule)
50-
const getAutoStop = (workspace?: TypesGen.Workspace) =>
51-
ttlMsToAutoStop(workspace?.ttl_ms)
52-
53-
const [autoStart, setAutoStart] = useState(getAutoStart(workspace))
54-
const [autoStop, setAutoStop] = useState(getAutoStop(workspace))
55-
56-
useEffect(() => {
57-
setAutoStart(getAutoStart(workspace))
58-
setAutoStop(getAutoStop(workspace))
59-
}, [workspace])
60-
6159
if (!username || !workspaceName) {
6260
return <Navigate to="/workspaces" />
6361
}
6462

65-
if (
66-
scheduleState.matches("idle") ||
67-
scheduleState.matches("gettingWorkspace") ||
68-
scheduleState.matches("gettingPermissions") ||
69-
!workspace
70-
) {
63+
if (scheduleState.hasTag("loading")) {
7164
return <FullScreenLoader />
7265
}
7366

7467
if (scheduleState.matches("error")) {
7568
return (
76-
<AlertBanner
77-
severity="error"
78-
error={getWorkspaceError || checkPermissionsError}
79-
text={
80-
getWorkspaceError
81-
? Language.getWorkspaceError
82-
: Language.checkPermissionsError
83-
}
84-
retry={() =>
85-
scheduleSend({ type: "GET_WORKSPACE", username, workspaceName })
86-
}
87-
/>
69+
<Margins>
70+
<div className={styles.topMargin}>
71+
<AlertBanner
72+
severity="error"
73+
error={
74+
getWorkspaceError || checkPermissionsError || getTemplateError
75+
}
76+
retry={() =>
77+
scheduleSend({ type: "GET_WORKSPACE", username, workspaceName })
78+
}
79+
/>
80+
</div>
81+
</Margins>
8882
)
8983
}
9084

9185
if (!permissions?.updateWorkspace) {
9286
return (
93-
<AlertBanner severity="error" error={Error(Language.forbiddenError)} />
87+
<Margins>
88+
<div className={styles.topMargin}>
89+
<AlertBanner severity="error" error={Error(t("forbiddenError"))} />
90+
</div>
91+
</Margins>
9492
)
9593
}
9694

@@ -101,7 +99,10 @@ export const WorkspaceSchedulePage: React.FC = () => {
10199
return (
102100
<WorkspaceScheduleForm
103101
submitScheduleError={submitScheduleError}
104-
initialValues={{ ...autoStart, ...autoStop }}
102+
initialValues={{
103+
...getAutoStart(workspace),
104+
...getAutoStop(workspace),
105+
}}
105106
isLoading={scheduleState.tags.has("loading")}
106107
onCancel={() => {
107108
navigate(`/@${username}/${workspaceName}`)
@@ -111,15 +112,34 @@ export const WorkspaceSchedulePage: React.FC = () => {
111112
type: "SUBMIT_SCHEDULE",
112113
autoStart: formValuesToAutoStartRequest(values),
113114
ttl: formValuesToTTLRequest(values),
114-
autoStartChanged: scheduleChanged(autoStart, values),
115-
autoStopChanged: scheduleChanged(autoStop, values),
115+
autoStartChanged: scheduleChanged(getAutoStart(workspace), values),
116+
autoStopChanged: scheduleChanged(getAutoStop(workspace), values),
116117
})
117118
}}
118119
/>
119120
)
120121
}
121122

122-
if (scheduleState.matches("submitSuccess")) {
123+
if (scheduleState.matches("showingRestartDialog")) {
124+
return (
125+
<ConfirmDialog
126+
open
127+
title={t("dialogTitle")}
128+
description={t("dialogDescription")}
129+
confirmText={t("restart")}
130+
cancelText={t("applyLater")}
131+
hideCancel={false}
132+
onConfirm={() => {
133+
scheduleSend("RESTART_WORKSPACE")
134+
}}
135+
onClose={() => {
136+
scheduleSend("APPLY_LATER")
137+
}}
138+
/>
139+
)
140+
}
141+
142+
if (scheduleState.matches("done")) {
123143
return <Navigate to={`/@${username}/${workspaceName}`} />
124144
}
125145

site/src/testHelpers/handlers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ export const handlers = [
131131
const permissions = [
132132
...Object.keys(permissionsToCheck),
133133
"canUpdateTemplate",
134+
"updateWorkspace",
134135
]
135136
const response = permissions.reduce((obj, permission) => {
136137
return {

0 commit comments

Comments
 (0)