diff --git a/site/src/components/WorkspaceActions/Buttons.tsx b/site/src/components/WorkspaceActions/Buttons.tsx index 1936729f35713..b9d573ed35bac 100644 --- a/site/src/components/WorkspaceActions/Buttons.tsx +++ b/site/src/components/WorkspaceActions/Buttons.tsx @@ -59,7 +59,12 @@ export const RestartButton: FC> = ({ const { t } = useTranslation("workspacePage") return ( - ) diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index aad09d1c17a90..315bd7ec898ad 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -20,6 +20,7 @@ import { MockDeletedWorkspace, MockBuilds, MockTemplateVersion3, + MockUser, } from "testHelpers/entities" import * as api from "../../api/api" import { Workspace } from "../../api/typesGenerated" @@ -161,10 +162,41 @@ describe("WorkspacePage", () => { .spyOn(api, "stopWorkspace") .mockResolvedValueOnce(MockWorkspaceBuild) - await testButton("Restart", stopWorkspaceMock) + // Render + await renderWorkspacePage() + + // Actions + const user = userEvent.setup() + await user.click(screen.getByTestId("workspace-restart-button")) + const confirmButton = await screen.findByTestId("confirm-button") + await user.click(confirmButton) + + // Assertions + await waitFor(() => { + expect(stopWorkspaceMock).toBeCalled() + }) + }) + + it("requests a stop without confirmation when the user presses Restart", async () => { + const stopWorkspaceMock = jest + .spyOn(api, "stopWorkspace") + .mockResolvedValueOnce(MockWorkspaceBuild) + window.localStorage.setItem( + `${MockUser.id}_ignoredWarnings`, + JSON.stringify({ restart: new Date().toISOString() }), + ) + + // Render + await renderWorkspacePage() - const button = await screen.findByText("Restarting") - expect(button).toBeInTheDocument() + // Actions + const user = userEvent.setup() + await user.click(screen.getByTestId("workspace-restart-button")) + + // Assertions + await waitFor(() => { + expect(stopWorkspaceMock).toBeCalled() + }) }) it("requests cancellation when the user presses Cancel", async () => { diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index de80d7314e925..763793afbed64 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -3,7 +3,7 @@ import { ProvisionerJobLog } from "api/typesGenerated" import { useDashboard } from "components/Dashboard/DashboardProvider" import dayjs from "dayjs" import { useFeatureVisibility } from "hooks/useFeatureVisibility" -import { useEffect, useState } from "react" +import { FC, useEffect, useState } from "react" import { Helmet } from "react-helmet-async" import { useTranslation } from "react-i18next" import { useNavigate } from "react-router-dom" @@ -31,7 +31,13 @@ import { ChangeVersionDialog } from "./ChangeVersionDialog" import { useQuery } from "@tanstack/react-query" import { getTemplateVersions } from "api/api" import { useRestartWorkspace } from "./hooks" -import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog" +import { + ConfirmDialog, + ConfirmDialogProps, +} from "components/Dialogs/ConfirmDialog/ConfirmDialog" +import { useMe } from "hooks/useMe" +import Checkbox from "@mui/material/Checkbox" +import FormControlLabel from "@mui/material/FormControlLabel" interface WorkspaceReadyPageProps { workspaceState: StateFrom @@ -79,6 +85,9 @@ export const WorkspaceReadyPage = ({ enabled: changeVersionDialogOpen, }) const [isConfirmingUpdate, setIsConfirmingUpdate] = useState(false) + const [isConfirmingRestart, setIsConfirmingRestart] = useState(false) + const user = useMe() + const { isWarningIgnored, ignoreWarning } = useIgnoreWarnings(user.id) const { mutate: restartWorkspace, @@ -133,9 +142,21 @@ export const WorkspaceReadyPage = ({ workspace={workspace} handleStart={() => workspaceSend({ type: "START" })} handleStop={() => workspaceSend({ type: "STOP" })} - handleRestart={() => restartWorkspace(workspace)} handleDelete={() => workspaceSend({ type: "ASK_DELETE" })} - handleUpdate={() => setIsConfirmingUpdate(true)} + handleRestart={() => { + if (isWarningIgnored("restart")) { + restartWorkspace(workspace) + } else { + setIsConfirmingRestart(true) + } + }} + handleUpdate={() => { + if (isWarningIgnored("update")) { + workspaceSend({ type: "UPDATE" }) + } else { + setIsConfirmingUpdate(true) + } + }} handleCancel={() => workspaceSend({ type: "CANCEL" })} handleSettings={() => navigate("settings")} handleBuildRetry={() => workspaceSend({ type: "RETRY_BUILD" })} @@ -202,11 +223,12 @@ export const WorkspaceReadyPage = ({ }) }} /> - { + onConfirm={(shouldIgnore) => { + if (shouldIgnore) { + ignoreWarning("update") + } workspaceSend({ type: "UPDATE" }) setIsConfirmingUpdate(false) }} @@ -215,6 +237,93 @@ export const WorkspaceReadyPage = ({ confirmText="Update" description="Are you sure you want to update your workspace? Updating your workspace will stop all running processes and delete non-persistent data." /> + + { + if (shouldIgnore) { + ignoreWarning("restart") + } + restartWorkspace(workspace) + setIsConfirmingRestart(false) + }} + onClose={() => setIsConfirmingRestart(false)} + title="Confirm restart" + confirmText="Restart" + description="Are you sure you want to restart your workspace? Updating your workspace will stop all running processes and delete non-persistent data." + /> ) } + +type IgnoredWarnings = Record + +const useIgnoreWarnings = (prefix: string) => { + const ignoredWarningsJSON = localStorage.getItem(`${prefix}_ignoredWarnings`) + let ignoredWarnings: IgnoredWarnings | undefined + if (ignoredWarningsJSON) { + ignoredWarnings = JSON.parse(ignoredWarningsJSON) + } + + const isWarningIgnored = (warningId: string) => { + return Boolean(ignoredWarnings?.[warningId]) + } + + const ignoreWarning = (warningId: string) => { + if (!ignoredWarnings) { + ignoredWarnings = {} + } + ignoredWarnings[warningId] = new Date().toISOString() + localStorage.setItem( + `${prefix}_ignoredWarnings`, + JSON.stringify(ignoredWarnings), + ) + } + + return { + isWarningIgnored, + ignoreWarning, + } +} + +const WarningDialog: FC< + Pick< + ConfirmDialogProps, + "open" | "onClose" | "title" | "confirmText" | "description" + > & { onConfirm: (shouldIgnore: boolean) => void } +> = ({ open, onConfirm, onClose, title, confirmText, description }) => { + const [shouldIgnore, setShouldIgnore] = useState(false) + + return ( + { + onConfirm(shouldIgnore) + }} + onClose={onClose} + title={title} + confirmText={confirmText} + description={ + <> +
{description}
+ { + setShouldIgnore(e.target.checked) + }} + /> + } + label="Don't show me this message again" + /> + + } + /> + ) +} diff --git a/site/yarn.lock b/site/yarn.lock index 364daba1d89d0..89e8f666c53e8 100644 --- a/site/yarn.lock +++ b/site/yarn.lock @@ -62,7 +62,7 @@ json5 "^2.2.2" semver "^6.3.0" -"@babel/generator@^7.12.11", "@babel/generator@^7.21.0", "@babel/generator@^7.21.4", "@babel/generator@^7.7.2", "@babel/generator@~7.21.1": +"@babel/generator@^7.12.11", "@babel/generator@^7.21.4", "@babel/generator@^7.7.2", "@babel/generator@~7.21.1": version "7.21.4" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.21.4.tgz#64a94b7448989f421f919d5239ef553b37bb26bc" integrity sha512-NieM3pVIYW2SwGzKoqfPrQsf4xGs9M9AIG3ThppsSRmO+m7eQhmI6amajKMUeIO37wFfsvnvcxQFx6x6iqxDnA== @@ -173,7 +173,7 @@ dependencies: "@babel/types" "^7.21.4" -"@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.20.11", "@babel/helper-module-transforms@^7.21.0", "@babel/helper-module-transforms@^7.21.2": +"@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.20.11", "@babel/helper-module-transforms@^7.21.2": version "7.21.2" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.21.2.tgz#160caafa4978ac8c00ac66636cb0fa37b024e2d2" integrity sha512-79yj2AR4U/Oqq/WOV7Lx6hUjau1Zfo4cI+JLAVYeMV5XIlbOhmjEk5ulbTc9fMpmlojzZHkUUxAiK+UKn+hNQQ== @@ -252,7 +252,7 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== -"@babel/helper-validator-option@^7.18.6", "@babel/helper-validator-option@^7.21.0": +"@babel/helper-validator-option@^7.21.0": version "7.21.0" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz#8224c7e13ace4bafdc4004da2cf064ef42673180" integrity sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ== @@ -285,7 +285,7 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.13.16", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.21.0", "@babel/parser@^7.21.4", "@babel/parser@~7.21.2": +"@babel/parser@^7.1.0", "@babel/parser@^7.13.16", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.21.4", "@babel/parser@~7.21.2": version "7.21.4" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.4.tgz#94003fdfc520bbe2875d4ae557b43ddb6d880f17" integrity sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw== @@ -1457,7 +1457,7 @@ "@types/node" "*" jest-mock "^29.5.0" -"@jest/expect-utils@^29.4.3", "@jest/expect-utils@^29.5.0": +"@jest/expect-utils@^29.5.0": version "29.5.0" resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.5.0.tgz#f74fad6b6e20f924582dc8ecbf2cb800fe43a036" integrity sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg== @@ -1599,7 +1599,7 @@ "@types/yargs" "^16.0.0" chalk "^4.0.0" -"@jest/types@^29.4.3", "@jest/types@^29.5.0": +"@jest/types@^29.5.0": version "29.5.0" resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.5.0.tgz#f59ef9b031ced83047c67032700d8c807d6e1593" integrity sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog== @@ -3549,22 +3549,6 @@ "@typescript-eslint/types" "5.50.0" "@typescript-eslint/visitor-keys" "5.50.0" -"@typescript-eslint/scope-manager@5.53.0": - version "5.53.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.53.0.tgz#42b54f280e33c82939275a42649701024f3fafef" - integrity sha512-Opy3dqNsp/9kBBeCPhkCNR7fmdSQqA+47r21hr9a14Bx0xnkElEQmhoHga+VoaoQ6uDHjDKmQPIYcUcKJifS7w== - dependencies: - "@typescript-eslint/types" "5.53.0" - "@typescript-eslint/visitor-keys" "5.53.0" - -"@typescript-eslint/scope-manager@5.57.0": - version "5.57.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.57.0.tgz#79ccd3fa7bde0758059172d44239e871e087ea36" - integrity sha512-NANBNOQvllPlizl9LatX8+MHi7bx7WGIWYjPHDmQe5Si/0YEYfxSljJpoTyTWFTgRy3X8gLYSE4xQ2U+aCozSw== - dependencies: - "@typescript-eslint/types" "5.57.0" - "@typescript-eslint/visitor-keys" "5.57.0" - "@typescript-eslint/scope-manager@5.58.0": version "5.58.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.58.0.tgz#5e023a48352afc6a87be6ce3c8e763bc9e2f0bc8" @@ -3593,16 +3577,6 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.50.0.tgz#c461d3671a6bec6c2f41f38ed60bd87aa8a30093" integrity sha512-atruOuJpir4OtyNdKahiHZobPKFvZnBnfDiyEaBf6d9vy9visE7gDjlmhl+y29uxZ2ZDgvXijcungGFjGGex7w== -"@typescript-eslint/types@5.53.0": - version "5.53.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.53.0.tgz#f79eca62b97e518ee124086a21a24f3be267026f" - integrity sha512-5kcDL9ZUIP756K6+QOAfPkigJmCPHcLN7Zjdz76lQWWDdzfOhZDTj1irs6gPBKiXx5/6O3L0+AvupAut3z7D2A== - -"@typescript-eslint/types@5.57.0": - version "5.57.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.57.0.tgz#727bfa2b64c73a4376264379cf1f447998eaa132" - integrity sha512-mxsod+aZRSyLT+jiqHw1KK6xrANm19/+VFALVFP5qa/aiJnlP38qpyaTd0fEKhWvQk6YeNZ5LGwI1pDpBRBhtQ== - "@typescript-eslint/types@5.58.0": version "5.58.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.58.0.tgz#54c490b8522c18986004df7674c644ffe2ed77d8" @@ -3634,32 +3608,6 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/typescript-estree@5.53.0": - version "5.53.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.53.0.tgz#bc651dc28cf18ab248ecd18a4c886c744aebd690" - integrity sha512-eKmipH7QyScpHSkhbptBBYh9v8FxtngLquq292YTEQ1pxVs39yFBlLC1xeIZcPPz1RWGqb7YgERJRGkjw8ZV7w== - dependencies: - "@typescript-eslint/types" "5.53.0" - "@typescript-eslint/visitor-keys" "5.53.0" - debug "^4.3.4" - globby "^11.1.0" - is-glob "^4.0.3" - semver "^7.3.7" - tsutils "^3.21.0" - -"@typescript-eslint/typescript-estree@5.57.0": - version "5.57.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.57.0.tgz#ebcd0ee3e1d6230e888d88cddf654252d41e2e40" - integrity sha512-LTzQ23TV82KpO8HPnWuxM2V7ieXW8O142I7hQTxWIHDcCEIjtkat6H96PFkYBQqGFLW/G/eVVOB9Z8rcvdY/Vw== - dependencies: - "@typescript-eslint/types" "5.57.0" - "@typescript-eslint/visitor-keys" "5.57.0" - debug "^4.3.4" - globby "^11.1.0" - is-glob "^4.0.3" - semver "^7.3.7" - tsutils "^3.21.0" - "@typescript-eslint/typescript-estree@5.58.0": version "5.58.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.58.0.tgz#4966e6ff57eaf6e0fce2586497edc097e2ab3e61" @@ -3717,22 +3665,6 @@ "@typescript-eslint/types" "5.50.0" eslint-visitor-keys "^3.3.0" -"@typescript-eslint/visitor-keys@5.53.0": - version "5.53.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.53.0.tgz#8a5126623937cdd909c30d8fa72f79fa56cc1a9f" - integrity sha512-JqNLnX3leaHFZEN0gCh81sIvgrp/2GOACZNgO4+Tkf64u51kTpAyWFOY8XHx8XuXr3N2C9zgPPHtcpMg6z1g0w== - dependencies: - "@typescript-eslint/types" "5.53.0" - eslint-visitor-keys "^3.3.0" - -"@typescript-eslint/visitor-keys@5.57.0": - version "5.57.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.57.0.tgz#e2b2f4174aff1d15eef887ce3d019ecc2d7a8ac1" - integrity sha512-ery2g3k0hv5BLiKpPuwYt9KBkAp2ugT6VvyShXdLOkax895EC55sP0Tx5L0fZaQueiK3fBLvHVvEl3jFS5ia+g== - dependencies: - "@typescript-eslint/types" "5.57.0" - eslint-visitor-keys "^3.3.0" - "@typescript-eslint/visitor-keys@5.58.0": version "5.58.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.58.0.tgz#eb9de3a61d2331829e6761ce7fd13061781168b4" @@ -7517,7 +7449,7 @@ jest-diff@^28.0.2: jest-get-type "^28.0.2" pretty-format "^28.1.3" -jest-diff@^29.4.3, jest-diff@^29.5.0: +jest-diff@^29.5.0: version "29.5.0" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.5.0.tgz#e0d83a58eb5451dcc1fa61b1c3ee4e8f5a290d63" integrity sha512-LtxijLLZBduXnHSniy0WMdaHjmQnt3g5sa16W4p0HqukYTTsyTW3GD1q41TyGl5YFXj/5B2U6dlh5FM1LIMgxw== @@ -7629,7 +7561,7 @@ jest-location-mock@1.0.9: "@jedmao/location" "^3.0.0" jest-diff "^27.0.1" -jest-matcher-utils@^29.4.3, jest-matcher-utils@^29.5.0: +jest-matcher-utils@^29.5.0: version "29.5.0" resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.5.0.tgz#d957af7f8c0692c5453666705621ad4abc2c59c5" integrity sha512-lecRtgm/rjIK0CQ7LPQwzCs2VwW6WAahA55YBuI+xqmhm7LAaxokSB8C97yJeYyT+HvQkH741StzpU41wohhWw== @@ -7639,7 +7571,7 @@ jest-matcher-utils@^29.4.3, jest-matcher-utils@^29.5.0: jest-get-type "^29.4.3" pretty-format "^29.5.0" -jest-message-util@^29.4.3, jest-message-util@^29.5.0: +jest-message-util@^29.5.0: version "29.5.0" resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.5.0.tgz#1f776cac3aca332ab8dd2e3b41625435085c900e" integrity sha512-Kijeg9Dag6CKtIDA7O21zNTACqD5MD/8HfIV8pdD94vFyFuer52SigdC3IQMhab3vACxXMiFk+yMHNdbqtyTGA== @@ -7790,7 +7722,7 @@ jest-snapshot@^29.5.0: pretty-format "^29.5.0" semver "^7.3.5" -jest-util@^29.4.3, jest-util@^29.5.0: +jest-util@^29.5.0: version "29.5.0" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.5.0.tgz#24a4d3d92fc39ce90425311b23c27a6e0ef16b8f" integrity sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ== @@ -9702,7 +9634,7 @@ pretty-format@^28.1.3: ansi-styles "^5.0.0" react-is "^18.0.0" -pretty-format@^29.0.0, pretty-format@^29.4.3, pretty-format@^29.5.0: +pretty-format@^29.0.0, pretty-format@^29.5.0: version "29.5.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.5.0.tgz#283134e74f70e2e3e7229336de0e4fce94ccde5a" integrity sha512-V2mGkI31qdttvTFX7Mt4efOqHXqJWMu4/r66Xh3Z3BwZaPfPJgp6/gbwoujRpPUtfEF6AUUWx3Jim3GCw5g/Qw==