diff --git a/site/src/api/queries/templates.ts b/site/src/api/queries/templates.ts index 45d5347302dee..85d33b7921baa 100644 --- a/site/src/api/queries/templates.ts +++ b/site/src/api/queries/templates.ts @@ -49,3 +49,17 @@ export const templateExamples = (orgId: string) => { queryFn: () => API.getTemplateExamples(orgId), }; }; + +export const templateVersion = (versionId: string) => { + return { + queryKey: ["templateVersion", versionId], + queryFn: () => API.getTemplateVersion(versionId), + }; +}; + +export const templateVersions = (templateId: string) => { + return { + queryKey: ["templateVersions", templateId], + queryFn: () => API.getTemplateVersions(templateId), + }; +}; diff --git a/site/src/components/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.tsx b/site/src/components/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.tsx index 87f0698a046bf..f23ed0f04a615 100644 --- a/site/src/components/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.tsx +++ b/site/src/components/WorkspaceOutdatedTooltip/WorkspaceOutdatedTooltip.tsx @@ -11,7 +11,7 @@ import InfoIcon from "@mui/icons-material/InfoOutlined"; import { makeStyles } from "@mui/styles"; import { colors } from "theme/colors"; import { useQuery } from "@tanstack/react-query"; -import { getTemplate, getTemplateVersion } from "api/api"; +import { templateVersion } from "api/queries/templates"; import Box from "@mui/material/Box"; import Skeleton from "@mui/material/Skeleton"; import Link from "@mui/material/Link"; @@ -25,7 +25,7 @@ export const Language = { interface TooltipProps { onUpdateVersion: () => void; - templateId: string; + latestVersionId: string; templateName: string; ariaLabel?: string; } @@ -33,20 +33,11 @@ interface TooltipProps { export const WorkspaceOutdatedTooltip: FC = ({ onUpdateVersion, ariaLabel, - templateId, + latestVersionId, templateName, }) => { const styles = useStyles(); - const { data: activeVersion } = useQuery({ - queryFn: async () => { - const template = await getTemplate(templateId); - const activeVersion = await getTemplateVersion( - template.active_version_id, - ); - return activeVersion; - }, - queryKey: ["templates", templateId, "activeVersion"], - }); + const { data: activeVersion } = useQuery(templateVersion(latestVersionId)); return ( > = ({ resources, builds, canUpdateWorkspace, + updateMessage, canRetryDebugMode, canChangeVersions, workspaceErrors, @@ -219,6 +221,12 @@ export const Workspace: FC> = ({ className={styles.firstColumnSpacer} spacing={4} > + {workspace.outdated && ( + + An update is available for your workspace + {updateMessage && {updateMessage}} + + )} {buildError} {cancellationError} {workspace.latest_build.status === "running" && diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/Buttons.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/Buttons.tsx index 9091330a14610..fad2e65a91ee1 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/Buttons.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/Buttons.tsx @@ -24,13 +24,13 @@ export const UpdateButton: FC = ({ return ( Updating…} loadingPosition="start" data-testid="workspace-update-button" startIcon={} onClick={handleAction} > - Update + Update… ); }; @@ -42,7 +42,7 @@ export const ActivateButton: FC = ({ return ( Activating…} loadingPosition="start" startIcon={} onClick={handleAction} @@ -70,7 +70,7 @@ export const StartButton: FC< > Starting…} loadingPosition="start" startIcon={} onClick={() => handleAction()} @@ -90,7 +90,7 @@ export const StopButton: FC = ({ handleAction, loading }) => { return ( Stopping…} loadingPosition="start" startIcon={} onClick={handleAction} @@ -119,13 +119,13 @@ export const RestartButton: FC< > Restarting…} loadingPosition="start" startIcon={} onClick={() => handleAction()} data-testid="workspace-restart-button" > - Restart + Restart… = ({ {canChangeVersions && ( - Change version + Change version… )} - + - Delete + Delete… diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index c420b8330ea47..4269e6e10c481 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -24,13 +24,13 @@ import { MockEntitlementsWithScheduling, MockDeploymentConfig, } from "testHelpers/entities"; -import * as api from "../../api/api"; -import { Workspace } from "../../api/typesGenerated"; +import * as api from "api/api"; +import { Workspace } from "api/typesGenerated"; import { renderWithAuth, waitForLoaderToBeRemoved, -} from "../../testHelpers/renderHelpers"; -import { server } from "../../testHelpers/server"; +} from "testHelpers/renderHelpers"; +import { server } from "testHelpers/server"; import { WorkspacePage } from "./WorkspacePage"; // It renders the workspace page and waits for it be loaded @@ -113,7 +113,7 @@ describe("WorkspacePage", () => { await user.click(trigger); // Click on delete - const button = await screen.findByText("Delete"); + const button = await screen.findByTestId("delete-button"); await user.click(button); // Get dialog and confirm @@ -172,28 +172,6 @@ describe("WorkspacePage", () => { }); }); - 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(); - - // 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 () => { server.use( rest.get( @@ -409,44 +387,4 @@ describe("WorkspacePage", () => { }); }); }); - - it("restart the workspace with one time parameters without the confirmation dialog", async () => { - window.localStorage.setItem( - `${MockUser.id}_ignoredWarnings`, - JSON.stringify({ - restart: new Date().toISOString(), - }), - ); - jest.spyOn(api, "getWorkspaceParameters").mockResolvedValue({ - templateVersionRichParameters: [ - { - ...MockTemplateVersionParameter1, - ephemeral: true, - name: "rebuild", - description: "Rebuild", - required: false, - }, - ], - buildParameters: [{ name: "rebuild", value: "false" }], - }); - const restartWorkspaceSpy = jest.spyOn(api, "restartWorkspace"); - const user = userEvent.setup(); - await renderWorkspacePage(); - await user.click(screen.getByTestId("build-parameters-button")); - const buildParametersForm = await screen.findByTestId( - "build-parameters-form", - ); - const rebuildField = within(buildParametersForm).getByLabelText("Rebuild", { - exact: false, - }); - await user.clear(rebuildField); - await user.type(rebuildField, "true"); - await user.click(screen.getByTestId("build-parameters-submit")); - await waitFor(() => { - expect(restartWorkspaceSpy).toBeCalledWith({ - workspace: MockWorkspace, - buildParameters: [{ name: "rebuild", value: "true" }], - }); - }); - }); }); diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index c1564fa15a07f..b37f30c8d6891 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -23,17 +23,17 @@ import { import { UpdateBuildParametersDialog } from "./UpdateBuildParametersDialog"; import { ChangeVersionDialog } from "./ChangeVersionDialog"; import { useMutation, useQuery } from "@tanstack/react-query"; -import { getTemplateVersions, restartWorkspace } from "api/api"; +import { restartWorkspace } from "api/api"; 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"; import { workspaceBuildMachine } from "xServices/workspaceBuild/workspaceBuildXService"; import * as TypesGen from "api/typesGenerated"; import { WorkspaceBuildLogsSection } from "./WorkspaceBuildLogsSection"; +import { templateVersion, templateVersions } from "api/queries/templates"; +import { Alert } from "components/Alert/Alert"; +import { Stack } from "components/Stack/Stack"; interface WorkspaceReadyPageProps { workspaceState: StateFrom; @@ -54,7 +54,7 @@ export const WorkspaceReadyPage = ({ const { workspace, template, - templateVersion, + templateVersion: currentVersion, deploymentValues, builds, getBuildsError, @@ -76,18 +76,21 @@ export const WorkspaceReadyPage = ({ const favicon = getFaviconByStatus(workspace.latest_build); const navigate = useNavigate(); const [changeVersionDialogOpen, setChangeVersionDialogOpen] = useState(false); - const { data: templateVersions } = useQuery({ - queryKey: ["template", "versions", workspace.template_id], - queryFn: () => getTemplateVersions(workspace.template_id), - enabled: changeVersionDialogOpen, - }); const [isConfirmingUpdate, setIsConfirmingUpdate] = useState(false); const [confirmingRestart, setConfirmingRestart] = useState<{ open: boolean; buildParameters?: TypesGen.WorkspaceBuildParameter[]; }>({ open: false }); - const user = useMe(); - const { isWarningIgnored, ignoreWarning } = useIgnoreWarnings(user.id); + + const { data: allVersions } = useQuery({ + ...templateVersions(workspace.template_id), + enabled: changeVersionDialogOpen, + }); + const { data: latestVersion } = useQuery({ + ...templateVersion(workspace.template_active_version_id), + enabled: workspace.outdated, + }); + const buildLogs = useBuildLogs(workspace); const shouldDisplayBuildLogs = hasJobError(workspace) || @@ -105,6 +108,7 @@ export const WorkspaceReadyPage = ({ useEffect(() => { bannerSend({ type: "REFRESH_WORKSPACE", workspace }); }, [bannerSend, workspace]); + return ( <> @@ -150,18 +154,10 @@ export const WorkspaceReadyPage = ({ handleStop={() => workspaceSend({ type: "STOP" })} handleDelete={() => workspaceSend({ type: "ASK_DELETE" })} handleRestart={(buildParameters) => { - if (isWarningIgnored("restart")) { - mutateRestartWorkspace({ workspace, buildParameters }); - } else { - setConfirmingRestart({ open: true, buildParameters }); - } + setConfirmingRestart({ open: true, buildParameters }); }} handleUpdate={() => { - if (isWarningIgnored("update")) { - workspaceSend({ type: "UPDATE" }); - } else { - setIsConfirmingUpdate(true); - } + setIsConfirmingUpdate(true); }} handleCancel={() => workspaceSend({ type: "CANCEL" })} handleSettings={() => navigate("settings")} @@ -173,6 +169,7 @@ export const WorkspaceReadyPage = ({ resources={workspace.latest_build.resources} builds={builds} canUpdateWorkspace={canUpdateWorkspace} + updateMessage={latestVersion?.message} canRetryDebugMode={canRetryDebugMode} canChangeVersions={canUpdateTemplate} hideSSHButton={featureVisibility["browser_only"]} @@ -186,7 +183,7 @@ export const WorkspaceReadyPage = ({ sshPrefix={sshPrefix} template={template} quotaBudget={quota?.budget} - templateWarnings={templateVersion?.warnings} + templateWarnings={currentVersion?.warnings} buildLogs={ shouldDisplayBuildLogs && ( @@ -218,9 +215,9 @@ export const WorkspaceReadyPage = ({ }} /> workspace.latest_build.template_version_id === v.id, )} open={changeVersionDialogOpen} @@ -237,25 +234,29 @@ export const WorkspaceReadyPage = ({ /> { - if (shouldIgnore) { - ignoreWarning("update"); - } + onConfirm={() => { workspaceSend({ type: "UPDATE" }); setIsConfirmingUpdate(false); }} onClose={() => setIsConfirmingUpdate(false)} - title="Confirm update" + title="Update and restart?" 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." + description={ + +

+ Restarting your workspace will stop all running processes and{" "} + delete non-persistent data. +

+ {latestVersion && ( + {latestVersion.message} + )} +
+ } /> { - if (shouldIgnore) { - ignoreWarning("restart"); - } + onConfirm={() => { mutateRestartWorkspace({ workspace, buildParameters: confirmingRestart.buildParameters, @@ -263,84 +264,26 @@ export const WorkspaceReadyPage = ({ setConfirmingRestart({ open: false }); }} onClose={() => setConfirmingRestart({ open: false })} - title="Confirm restart" + title="Restart your workspace?" 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." + description={ + <> + Restarting 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" - /> - - } - /> - ); + "open" | "onClose" | "title" | "confirmText" | "description" | "onConfirm" + > +> = (props) => { + return ; }; const useBuildLogs = (workspace: TypesGen.Workspace) => { diff --git a/site/src/pages/WorkspacePage/WorkspaceStats.tsx b/site/src/pages/WorkspacePage/WorkspaceStats.tsx index f5cfd4613db86..61345a0afd740 100644 --- a/site/src/pages/WorkspacePage/WorkspaceStats.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceStats.tsx @@ -103,7 +103,7 @@ export const WorkspaceStats: FC = ({ {workspace.outdated && ( diff --git a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx index 295431be5869c..b59e0f15c2db3 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx @@ -177,7 +177,9 @@ export const WorkspacesTable: FC = ({ {workspace.outdated && ( { onUpdateWorkspace(workspace); }}