From c28f1d82f82bac44cf7cf6a147f1b0ae079eefb3 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Wed, 15 Nov 2023 21:36:31 +0000 Subject: [PATCH 01/15] refactor: remove workspace error enums --- .../pages/WorkspacePage/Workspace.stories.tsx | 8 +-- site/src/pages/WorkspacePage/Workspace.tsx | 60 +++++++++---------- .../WorkspacePage/WorkspaceReadyPage.tsx | 8 +-- 3 files changed, 35 insertions(+), 41 deletions(-) diff --git a/site/src/pages/WorkspacePage/Workspace.stories.tsx b/site/src/pages/WorkspacePage/Workspace.stories.tsx index 10887f5b5ebdc..0cd16234d4b71 100644 --- a/site/src/pages/WorkspacePage/Workspace.stories.tsx +++ b/site/src/pages/WorkspacePage/Workspace.stories.tsx @@ -3,7 +3,7 @@ import { Meta, StoryObj } from "@storybook/react"; import { WatchAgentMetadataContext } from "components/Resources/AgentMetadata"; import { ProvisionerJobLog } from "api/typesGenerated"; import * as Mocks from "testHelpers/entities"; -import { Workspace, WorkspaceErrors } from "./Workspace"; +import { Workspace } from "./Workspace"; import { withReactContext } from "storybook-react-context"; import EventSource from "eventsourcemock"; import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext"; @@ -133,7 +133,7 @@ export const Failed: Story = { ...Running.args, workspace: Mocks.MockFailedWorkspace, workspaceErrors: { - [WorkspaceErrors.BUILD_ERROR]: Mocks.mockApiError({ + buildError: Mocks.mockApiError({ message: "A workspace build is already active.", }), }, @@ -224,7 +224,7 @@ export const GetBuildsError: Story = { args: { ...Running.args, workspaceErrors: { - [WorkspaceErrors.GET_BUILDS_ERROR]: Mocks.mockApiError({ + getBuildsError: Mocks.mockApiError({ message: "There is a problem fetching builds.", }), }, @@ -235,7 +235,7 @@ export const CancellationError: Story = { args: { ...Failed.args, workspaceErrors: { - [WorkspaceErrors.CANCELLATION_ERROR]: Mocks.mockApiError({ + cancellationError: Mocks.mockApiError({ message: "Job could not be canceled.", }), }, diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index 060ee10c5b8a2..cd070eec307f5 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -29,11 +29,13 @@ import { BuildsTable } from "./BuildsTable"; import { WorkspaceDeletedBanner } from "./WorkspaceDeletedBanner"; import { WorkspaceStats } from "./WorkspaceStats"; -export enum WorkspaceErrors { - GET_BUILDS_ERROR = "getBuildsError", - BUILD_ERROR = "buildError", - CANCELLATION_ERROR = "cancellationError", -} +export type WorkspaceError = + | "getBuildsError" + | "buildError" + | "cancellationError"; + +export type WorkspaceErrors = Partial>; + export interface WorkspaceProps { scheduleProps: { onDeadlinePlus: (hours: number) => void; @@ -60,7 +62,7 @@ export interface WorkspaceProps { canChangeVersions: boolean; hideSSHButton?: boolean; hideVSCodeDesktopButton?: boolean; - workspaceErrors: Partial>; + workspaceErrors: WorkspaceErrors; buildInfo?: TypesGen.BuildInfoResponse; sshPrefix?: string; template?: TypesGen.Template; @@ -114,28 +116,12 @@ export const Workspace: FC> = ({ const serverVersion = buildInfo?.version || ""; const { saveLocal, getLocal } = useLocalStorage(); - const buildError = Boolean(workspaceErrors[WorkspaceErrors.BUILD_ERROR]) && ( - - ); - - const cancellationError = Boolean( - workspaceErrors[WorkspaceErrors.CANCELLATION_ERROR], - ) && ( - - ); - - let transitionStats: TypesGen.TransitionStats | undefined = undefined; - if (template !== undefined) { - transitionStats = ActiveTransition(template, workspace); - } - const [showAlertPendingInQueue, setShowAlertPendingInQueue] = useState(false); + + // 2023-11-15 - MES - This effect will be called every single render because + // "now" will always change and invalidate the dependency array. Need to + // figure out if this effect really should run every render (possibly meaning + // no dependency array at all), or how to get the array stabilized (ideal) const now = dayjs(); useEffect(() => { if ( @@ -174,6 +160,9 @@ export const Workspace: FC> = ({ const autoStartFailing = workspace.autostart_schedule && !canAutostart; const requiresManualUpdate = updateRequired && autoStartFailing; + const transitionStats = + template !== undefined ? ActiveTransition(template, workspace) : undefined; + return ( <> @@ -244,8 +233,15 @@ export const Workspace: FC> = ({ {updateMessage && {updateMessage}} ))} - {buildError} - {cancellationError} + + {Boolean(workspaceErrors.buildError) && ( + + )} + + {Boolean(workspaceErrors.cancellationError) && ( + + )} + {workspace.latest_build.status === "running" && !workspace.health.healthy && ( > = ({ /> )} - {workspaceErrors[WorkspaceErrors.GET_BUILDS_ERROR] ? ( - + {workspaceErrors.getBuildsError ? ( + ) : ( Date: Wed, 15 Nov 2023 21:37:36 +0000 Subject: [PATCH 02/15] refactor: improve co-location for serverVersion --- site/src/pages/WorkspacePage/Workspace.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index cd070eec307f5..1ebfcb41e6bb8 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -113,7 +113,6 @@ export const Workspace: FC> = ({ canAutostart, }) => { const navigate = useNavigate(); - const serverVersion = buildInfo?.version || ""; const { saveLocal, getLocal } = useLocalStorage(); const [showAlertPendingInQueue, setShowAlertPendingInQueue] = useState(false); @@ -346,7 +345,7 @@ export const Workspace: FC> = ({ showBuiltinApps={canUpdateWorkspace} hideSSHButton={hideSSHButton} hideVSCodeDesktopButton={hideVSCodeDesktopButton} - serverVersion={serverVersion} + serverVersion={buildInfo?.version || ""} onUpdateAgent={handleUpdate} // On updating the workspace the agent version is also updated /> )} From 718972ffca1a73973d051419bcb5430768b0c892 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Wed, 15 Nov 2023 21:44:57 +0000 Subject: [PATCH 03/15] fix: add in retry button for failed workspaces --- site/src/pages/WorkspacePage/Workspace.tsx | 2 +- .../WorkspaceActions/Buttons.tsx | 45 ++++++--- .../WorkspaceActions/WorkspaceActions.tsx | 91 ++++++++----------- .../WorkspaceActions/constants.ts | 86 ++++++++++-------- 4 files changed, 118 insertions(+), 106 deletions(-) diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index 1ebfcb41e6bb8..de4ff1a4c7b16 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -201,6 +201,7 @@ export const Workspace: FC> = ({ handleUpdate={handleUpdate} handleCancel={handleCancel} handleSettings={handleSettings} + handleRetry={handleBuildRetry} handleChangeVersion={handleChangeVersion} handleDormantActivate={handleDormantActivate} canChangeVersions={canChangeVersions} @@ -308,7 +309,6 @@ export const Workspace: FC> = ({ actions={ canRetryDebugMode && ( ); @@ -162,3 +167,13 @@ export const ActionLoadingButton: FC = ({ label }) => { ); }; + +export function RetryButton({ + handleAction, +}: Omit) { + return ( + + ); +} diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx index 4efc94df53623..5e924352d3912 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx @@ -1,4 +1,4 @@ -import { FC, Fragment, ReactNode } from "react"; +import { type FC, type ReactNode, Fragment } from "react"; import { Workspace, WorkspaceBuildParameter } from "api/typesGenerated"; import { useWorkspaceDuplication } from "pages/CreateWorkspacePage/useWorkspaceDuplication"; import { @@ -10,12 +10,9 @@ import { RestartButton, UpdateButton, ActivateButton, + RetryButton, } from "./Buttons"; -import { - ButtonMapping, - ButtonTypesEnum, - actionsByWorkspaceStatus, -} from "./constants"; +import { type ButtonType, actionsByWorkspaceStatus } from "./constants"; import Divider from "@mui/material/Divider"; import DuplicateIcon from "@mui/icons-material/FileCopyOutlined"; @@ -41,6 +38,7 @@ export interface WorkspaceActionsProps { handleCancel: () => void; handleSettings: () => void; handleChangeVersion: () => void; + handleRetry: () => void; handleDormantActivate: () => void; isUpdating: boolean; isRestarting: boolean; @@ -57,81 +55,72 @@ export const WorkspaceActions: FC = ({ handleUpdate, handleCancel, handleSettings, + handleRetry, handleChangeVersion, handleDormantActivate: handleDormantActivate, isUpdating, isRestarting, canChangeVersions, }) => { - const { - canCancel, - canAcceptJobs, - actions: actionsByStatus, - } = actionsByWorkspaceStatus( - workspace, - workspace.latest_build.status, - canChangeVersions, - ); - const canBeUpdated = workspace.outdated && canAcceptJobs; const { duplicateWorkspace, isDuplicationReady } = useWorkspaceDuplication(workspace); - // A mapping of button type to the corresponding React component - const buttonMapping: ButtonMapping = { - [ButtonTypesEnum.update]: , - [ButtonTypesEnum.updating]: ( - - ), - [ButtonTypesEnum.start]: ( - - ), - [ButtonTypesEnum.starting]: ( + // A mapping of button type to their corresponding React components + const buttonMapping: Record = { + update: , + updating: , + start: , + starting: ( ), - [ButtonTypesEnum.stop]: , - [ButtonTypesEnum.stopping]: ( - - ), - [ButtonTypesEnum.restart]: ( + stop: , + stopping: , + restart: ( ), - [ButtonTypesEnum.restarting]: ( + restarting: ( ), - [ButtonTypesEnum.deleting]: , - [ButtonTypesEnum.canceling]: , - [ButtonTypesEnum.deleted]: , - [ButtonTypesEnum.pending]: , - [ButtonTypesEnum.activate]: ( - - ), - [ButtonTypesEnum.activating]: ( - - ), + deleting: , + canceling: , + deleted: , + pending: , + activate: , + activating: , + retry: , }; + const { actions, canCancel, canAcceptJobs } = actionsByWorkspaceStatus( + workspace, + workspace.latest_build.status, + canChangeVersions, + ); + const canBeUpdated = workspace.outdated && canAcceptJobs; + return (
+ {/* + * Parentheses important – if canBeUpdated is false, nothing should + * appear in the UI + */} {canBeUpdated && - (isUpdating - ? buttonMapping[ButtonTypesEnum.updating] - : buttonMapping[ButtonTypesEnum.update])} + (isUpdating ? buttonMapping.updating : buttonMapping.update)} - {isRestarting && buttonMapping[ButtonTypesEnum.restarting]} - - {!isRestarting && - actionsByStatus.map((action) => ( - {buttonMapping[action]} - ))} + {isRestarting + ? buttonMapping.restarting + : actions.map((action) => ( + {buttonMapping[action]} + ))} {canCancel && } + { if (workspace.dormant_at) { return { - actions: [ButtonTypesEnum.activate], + actions: ["activate"], canCancel: false, canAcceptJobs: false, }; @@ -49,7 +57,7 @@ export const actionsByWorkspaceStatus = ( ) { if (status === "running") { return { - actions: [ButtonTypesEnum.stop], + actions: ["stop"], canCancel: false, canAcceptJobs: true, }; @@ -67,56 +75,56 @@ export const actionsByWorkspaceStatus = ( const statusToActions: Record = { starting: { - actions: [ButtonTypesEnum.starting], + actions: ["starting"], canCancel: true, canAcceptJobs: false, }, running: { - actions: [ButtonTypesEnum.stop, ButtonTypesEnum.restart], + actions: ["stop", "restart"], canCancel: false, canAcceptJobs: true, }, stopping: { - actions: [ButtonTypesEnum.stopping], + actions: ["stopping"], canCancel: true, canAcceptJobs: false, }, stopped: { - actions: [ButtonTypesEnum.start], + actions: ["start"], canCancel: false, canAcceptJobs: true, }, canceled: { - actions: [ButtonTypesEnum.start, ButtonTypesEnum.stop], + actions: ["start", "stop"], canCancel: false, canAcceptJobs: true, }, + // in the case of an error failed: { - actions: [ButtonTypesEnum.start, ButtonTypesEnum.stop], + actions: ["retry"], canCancel: false, canAcceptJobs: true, }, - /** - * disabled states - */ + + // Disabled states canceling: { - actions: [ButtonTypesEnum.canceling], + actions: ["canceling"], canCancel: false, canAcceptJobs: false, }, deleting: { - actions: [ButtonTypesEnum.deleting], + actions: ["deleting"], canCancel: true, canAcceptJobs: false, }, deleted: { - actions: [ButtonTypesEnum.deleted], + actions: ["deleted"], canCancel: false, canAcceptJobs: false, }, pending: { - actions: [ButtonTypesEnum.pending], + actions: ["pending"], canCancel: false, canAcceptJobs: false, }, From 6005ceca30ea9853c72fa51a4a144d72a29be0f9 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Wed, 15 Nov 2023 21:49:30 +0000 Subject: [PATCH 04/15] fix: make handleBuildRetry auto-detect debug permissions --- site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index 3252e6d65140e..e77a38b758079 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -243,15 +243,16 @@ export const WorkspaceReadyPage = ({ handleCancel={cancelBuildMutation.mutate} handleSettings={() => navigate("settings")} handleBuildRetry={() => { + const logLevel = canRetryDebugMode ? "debug" : undefined; switch (workspace.latest_build.transition) { case "start": - startWorkspaceMutation.mutate({ logLevel: "debug" }); + startWorkspaceMutation.mutate({ logLevel }); break; case "stop": - stopWorkspaceMutation.mutate({ logLevel: "debug" }); + stopWorkspaceMutation.mutate({ logLevel }); break; case "delete": - deleteWorkspaceMutation.mutate({ logLevel: "debug" }); + deleteWorkspaceMutation.mutate({ logLevel }); break; } }} @@ -297,6 +298,7 @@ export const WorkspaceReadyPage = ({ } canAutostart={canAutostart} /> + + + + + { From 0f91d44eb4619ca3995063d05294eeefa800b093 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Wed, 15 Nov 2023 22:12:19 +0000 Subject: [PATCH 05/15] chore: consolidate retry messaging --- site/src/pages/WorkspacePage/Workspace.stories.tsx | 1 - site/src/pages/WorkspacePage/Workspace.tsx | 14 +++----------- .../src/pages/WorkspacePage/WorkspaceReadyPage.tsx | 1 - 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/site/src/pages/WorkspacePage/Workspace.stories.tsx b/site/src/pages/WorkspacePage/Workspace.stories.tsx index 0cd16234d4b71..7c456aeac43c3 100644 --- a/site/src/pages/WorkspacePage/Workspace.stories.tsx +++ b/site/src/pages/WorkspacePage/Workspace.stories.tsx @@ -172,7 +172,6 @@ export const FailedWithRetry: Story = { }, }, }, - canRetryDebugMode: true, buildLogs: , }, }; diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index de4ff1a4c7b16..6500bb140b145 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -58,7 +58,6 @@ export interface WorkspaceProps { resources?: TypesGen.WorkspaceResource[]; canUpdateWorkspace: boolean; updateMessage?: string; - canRetryDebugMode: boolean; canChangeVersions: boolean; hideSSHButton?: boolean; hideVSCodeDesktopButton?: boolean; @@ -97,7 +96,6 @@ export const Workspace: FC> = ({ builds, canUpdateWorkspace, updateMessage, - canRetryDebugMode, canChangeVersions, workspaceErrors, hideSSHButton, @@ -307,15 +305,9 @@ export const Workspace: FC> = ({ - Try in debug mode - - ) + } > Workspace build failed diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index e77a38b758079..e8147a563008f 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -274,7 +274,6 @@ export const WorkspaceReadyPage = ({ hasMoreBuilds={hasMoreBuilds} canUpdateWorkspace={canUpdateWorkspace} updateMessage={latestVersion?.message} - canRetryDebugMode={canRetryDebugMode} canChangeVersions={canChangeVersions} hideSSHButton={featureVisibility["browser_only"]} hideVSCodeDesktopButton={featureVisibility["browser_only"]} From d635ecdf02eb235c6cc586eba8e341a3d626d5c2 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Wed, 15 Nov 2023 22:20:46 +0000 Subject: [PATCH 06/15] docs: clean up formatting for WorkspacePage.test comments --- site/src/pages/WorkspacePage/WorkspacePage.test.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index 008a3d2c54ce5..0513dbbbdc080 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -42,10 +42,12 @@ const renderWorkspacePage = async () => { }; /** - * Requests and responses related to workspace status are unrelated, so we can't test in the usual way. - * Instead, test that button clicks produce the correct requests and that responses produce the correct UI. - * We don't need to test the UI exhaustively because Storybook does that; just enough to prove that the - * workspaceStatus was calculated correctly. + * Requests and responses related to workspace status are unrelated, so we can't + * test in the usual way. Instead, test that button clicks produce the correct + * requests and that responses produce the correct UI. + * + * We don't need to test the UI exhaustively because Storybook does that; just + * enough to prove that the workspaceStatus was calculated correctly. */ const testButton = async (label: string, actionMock: jest.SpyInstance) => { const user = userEvent.setup(); From 404e4c77544ce1bec913f7ffcabbcf89d9c65e7b Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Wed, 15 Nov 2023 22:48:28 +0000 Subject: [PATCH 07/15] chore: update renderWorkspacePage to accept parameters --- site/src/pages/WorkspacePage/WorkspacePage.test.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index 0513dbbbdc080..be1b6ae0dd404 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -22,7 +22,7 @@ import { server } from "testHelpers/server"; import { WorkspacePage } from "./WorkspacePage"; // It renders the workspace page and waits for it be loaded -const renderWorkspacePage = async () => { +const renderWorkspacePage = async (mockWorkspace = MockWorkspace) => { jest.spyOn(api, "getTemplate").mockResolvedValueOnce(MockTemplate); jest.spyOn(api, "getTemplateVersionRichParameters").mockResolvedValueOnce([]); jest @@ -34,11 +34,13 @@ const renderWorkspacePage = async () => { options.onDone && options.onDone(); return new WebSocket(""); }); + renderWithAuth(, { - route: `/@${MockWorkspace.owner_name}/${MockWorkspace.name}`, + route: `/@${mockWorkspace.owner_name}/${mockWorkspace.name}`, path: "/:username/:workspace", }); - await screen.findByText(MockWorkspace.name); + + await screen.findByText(mockWorkspace.name); }; /** From edecd14e50c6966137daf205bdadbdde9518ab69 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 16 Nov 2023 15:07:45 +0000 Subject: [PATCH 08/15] fix: restore granularity to retry operations (debug vs normal) --- site/src/pages/WorkspacePage/Workspace.tsx | 18 +++++++-- .../WorkspaceActions/Buttons.tsx | 16 +++++--- .../WorkspaceActions/WorkspaceActions.tsx | 20 +++++----- .../WorkspaceActions/constants.ts | 31 +++++++++++---- .../WorkspacePage/WorkspaceReadyPage.tsx | 38 ++++++++++--------- 5 files changed, 82 insertions(+), 41 deletions(-) diff --git a/site/src/pages/WorkspacePage/Workspace.tsx b/site/src/pages/WorkspacePage/Workspace.tsx index 6500bb140b145..7ab3bd75aac47 100644 --- a/site/src/pages/WorkspacePage/Workspace.tsx +++ b/site/src/pages/WorkspacePage/Workspace.tsx @@ -66,7 +66,9 @@ export interface WorkspaceProps { sshPrefix?: string; template?: TypesGen.Template; quotaBudget?: number; + canRetryDebugMode: boolean; handleBuildRetry: () => void; + handleBuildRetryDebug: () => void; buildLogs?: React.ReactNode; builds: TypesGen.WorkspaceBuild[] | undefined; onLoadMoreBuilds: () => void; @@ -88,7 +90,7 @@ export const Workspace: FC> = ({ handleCancel, handleSettings, handleChangeVersion, - handleDormantActivate: handleDormantActivate, + handleDormantActivate, workspace, isUpdating, isRestarting, @@ -103,7 +105,9 @@ export const Workspace: FC> = ({ buildInfo, sshPrefix, template, + canRetryDebugMode, handleBuildRetry, + handleBuildRetryDebug, buildLogs, onLoadMoreBuilds, isLoadingMoreBuilds, @@ -200,8 +204,10 @@ export const Workspace: FC> = ({ handleCancel={handleCancel} handleSettings={handleSettings} handleRetry={handleBuildRetry} + handleRetryDebug={handleBuildRetryDebug} handleChangeVersion={handleChangeVersion} handleDormantActivate={handleDormantActivate} + canRetryDebug={canRetryDebugMode} canChangeVersions={canChangeVersions} isUpdating={isUpdating} isRestarting={isRestarting} @@ -305,8 +311,14 @@ export const Workspace: FC> = ({ - Retry + } > diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/Buttons.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/Buttons.tsx index 42adaef5ec5e6..aa1f77a70c545 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/Buttons.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/Buttons.tsx @@ -13,6 +13,7 @@ import BlockIcon from "@mui/icons-material/Block"; import OutlinedBlockIcon from "@mui/icons-material/BlockOutlined"; import PowerSettingsNewIcon from "@mui/icons-material/PowerSettingsNew"; import RetryIcon from "@mui/icons-material/BuildOutlined"; +import RetryDebugIcon from "@mui/icons-material/BugReportOutlined"; interface WorkspaceActionProps { loading?: boolean; @@ -168,12 +169,17 @@ export const ActionLoadingButton: FC = ({ label }) => { ); }; -export function RetryButton({ - handleAction, -}: Omit) { +type DebugButtonProps = Omit & { + debug?: boolean; +}; + +export function RetryButton({ handleAction, debug = false }: DebugButtonProps) { return ( - ); } diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx index 5e924352d3912..4202b11e575b4 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx @@ -39,11 +39,13 @@ export interface WorkspaceActionsProps { handleSettings: () => void; handleChangeVersion: () => void; handleRetry: () => void; + handleRetryDebug: () => void; handleDormantActivate: () => void; isUpdating: boolean; isRestarting: boolean; children?: ReactNode; canChangeVersions: boolean; + canRetryDebug: boolean; } export const WorkspaceActions: FC = ({ @@ -56,11 +58,13 @@ export const WorkspaceActions: FC = ({ handleCancel, handleSettings, handleRetry, + handleRetryDebug, handleChangeVersion, - handleDormantActivate: handleDormantActivate, + handleDormantActivate, isUpdating, isRestarting, canChangeVersions, + canRetryDebug, }) => { const { duplicateWorkspace, isDuplicationReady } = useWorkspaceDuplication(workspace); @@ -92,13 +96,14 @@ export const WorkspaceActions: FC = ({ activate: , activating: , retry: , + retryDebug: , }; const { actions, canCancel, canAcceptJobs } = actionsByWorkspaceStatus( workspace, - workspace.latest_build.status, - canChangeVersions, + { canChangeVersions, canRetryDebug }, ); + const canBeUpdated = workspace.outdated && canAcceptJobs; return ( @@ -106,12 +111,9 @@ export const WorkspaceActions: FC = ({ css={{ display: "flex", alignItems: "center", gap: 12 }} data-testid="workspace-actions" > - {/* - * Parentheses important – if canBeUpdated is false, nothing should - * appear in the UI - */} - {canBeUpdated && - (isUpdating ? buttonMapping.updating : buttonMapping.update)} + {canBeUpdated && ( + <>{isUpdating ? buttonMapping.updating : buttonMapping.update} + )} {isRestarting ? buttonMapping.restarting diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/constants.ts b/site/src/pages/WorkspacePage/WorkspaceActions/constants.ts index 886db9869099d..fabd2336a71a2 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/constants.ts +++ b/site/src/pages/WorkspacePage/WorkspaceActions/constants.ts @@ -21,6 +21,7 @@ const buttonTypes = [ // into one of the starting, stopping, or deleting states (based on the // WorkspaceTransition type) "retry", + "retryDebug", // These are buttons that should be used with disabled UI elements "canceling", @@ -33,16 +34,20 @@ const buttonTypes = [ */ export type ButtonType = (typeof buttonTypes)[number]; -interface WorkspaceAbilities { +type WorkspaceAbilities = { actions: readonly ButtonType[]; canCancel: boolean; canAcceptJobs: boolean; -} +}; + +type UserInfo = Readonly<{ + canChangeVersions: boolean; + canRetryDebug: boolean; +}>; export const actionsByWorkspaceStatus = ( workspace: Workspace, - status: WorkspaceStatus, - canChangeVersions: boolean, + userInfo: UserInfo, ): WorkspaceAbilities => { if (workspace.dormant_at) { return { @@ -51,10 +56,13 @@ export const actionsByWorkspaceStatus = ( canAcceptJobs: false, }; } - if ( + + const status = workspace.latest_build.status; + const mustUpdate = workspace.outdated && - workspaceUpdatePolicy(workspace, canChangeVersions) === "always" - ) { + workspaceUpdatePolicy(workspace, userInfo.canChangeVersions) === "always"; + + if (mustUpdate) { if (status === "running") { return { actions: ["stop"], @@ -62,6 +70,7 @@ export const actionsByWorkspaceStatus = ( canAcceptJobs: true, }; } + if (status === "stopped") { return { actions: [], @@ -70,6 +79,14 @@ export const actionsByWorkspaceStatus = ( }; } } + + if (status === "failed" && userInfo.canRetryDebug) { + return { + ...statusToActions.failed, + actions: ["retry", "retryDebug"], + }; + } + return statusToActions[status]; }; diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index e8147a563008f..77b3f9d0d5f04 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -79,9 +79,6 @@ export const WorkspaceReadyPage = ({ ...deploymentConfig(), enabled: permissions?.viewDeploymentValues, }); - const canRetryDebugMode = Boolean( - deploymentValues?.config.enable_terraform_debug_mode, - ); // Build logs const buildLogs = useWorkspaceBuildLogs(workspace.latest_build.id); @@ -196,6 +193,22 @@ export const WorkspaceReadyPage = ({ // Cancel build const cancelBuildMutation = useMutation(cancelBuild(workspace, queryClient)); + const handleBuildRetry = (debug = false) => { + const logLevel = debug ? "debug" : undefined; + + switch (workspace.latest_build.transition) { + case "start": + startWorkspaceMutation.mutate({ logLevel }); + break; + case "stop": + stopWorkspaceMutation.mutate({ logLevel }); + break; + case "delete": + deleteWorkspaceMutation.mutate({ logLevel }); + break; + } + }; + return ( <> @@ -242,20 +255,11 @@ export const WorkspaceReadyPage = ({ }} handleCancel={cancelBuildMutation.mutate} handleSettings={() => navigate("settings")} - handleBuildRetry={() => { - const logLevel = canRetryDebugMode ? "debug" : undefined; - switch (workspace.latest_build.transition) { - case "start": - startWorkspaceMutation.mutate({ logLevel }); - break; - case "stop": - stopWorkspaceMutation.mutate({ logLevel }); - break; - case "delete": - deleteWorkspaceMutation.mutate({ logLevel }); - break; - } - }} + handleBuildRetry={() => handleBuildRetry(false)} + handleBuildRetryDebug={() => handleBuildRetry(true)} + canRetryDebugMode={ + deploymentValues?.config.enable_terraform_debug_mode ?? false + } handleChangeVersion={() => { setChangeVersionDialogOpen(true); }} From b1f66606ecb893624b1219544a5f4080c35d3144 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 16 Nov 2023 19:31:11 +0000 Subject: [PATCH 09/15] chore: make workspace helpers take explicit workspace parameter --- .../WorkspacePage/WorkspacePage.test.tsx | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index be1b6ae0dd404..8adfe478dbd54 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -1,3 +1,4 @@ +import { type Workspace } from "api/typesGenerated"; import { screen, waitFor, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import EventSourceMock from "eventsourcemock"; @@ -5,6 +6,7 @@ import { rest } from "msw"; import { MockTemplate, MockWorkspace, + MockFailedWorkspace, MockWorkspaceBuild, MockStoppedWorkspace, MockStartingWorkspace, @@ -22,7 +24,7 @@ import { server } from "testHelpers/server"; import { WorkspacePage } from "./WorkspacePage"; // It renders the workspace page and waits for it be loaded -const renderWorkspacePage = async (mockWorkspace = MockWorkspace) => { +const renderWorkspacePage = async (workspace: Workspace) => { jest.spyOn(api, "getTemplate").mockResolvedValueOnce(MockTemplate); jest.spyOn(api, "getTemplateVersionRichParameters").mockResolvedValueOnce([]); jest @@ -36,11 +38,11 @@ const renderWorkspacePage = async (mockWorkspace = MockWorkspace) => { }); renderWithAuth(, { - route: `/@${mockWorkspace.owner_name}/${mockWorkspace.name}`, + route: `/@${workspace.owner_name}/${workspace.name}`, path: "/:username/:workspace", }); - await screen.findByText(mockWorkspace.name); + await screen.findByText(workspace.name); }; /** @@ -51,11 +53,17 @@ const renderWorkspacePage = async (mockWorkspace = MockWorkspace) => { * We don't need to test the UI exhaustively because Storybook does that; just * enough to prove that the workspaceStatus was calculated correctly. */ -const testButton = async (label: string, actionMock: jest.SpyInstance) => { +const testButton = async ( + workspace: Workspace, + label: string, + actionMock: jest.SpyInstance, +) => { const user = userEvent.setup(); - await renderWorkspacePage(); + await renderWorkspacePage(workspace); + const workspaceActions = screen.getByTestId("workspace-actions"); const button = within(workspaceActions).getByRole("button", { name: label }); + await user.click(button); expect(actionMock).toBeCalled(); }; @@ -82,7 +90,7 @@ describe("WorkspacePage", () => { const deleteWorkspaceMock = jest .spyOn(api, "deleteWorkspace") .mockResolvedValueOnce(MockWorkspaceBuild); - await renderWorkspacePage(); + await renderWorkspacePage(MockWorkspace); // open the workspace action popover so we have access to all available ctas const trigger = screen.getByTestId("workspace-options-button"); @@ -125,7 +133,7 @@ describe("WorkspacePage", () => { const deleteWorkspaceMock = jest .spyOn(api, "deleteWorkspace") .mockResolvedValueOnce(MockWorkspaceBuildDelete); - await renderWorkspacePage(); + await renderWorkspacePage(MockWorkspace); // open the workspace action popover so we have access to all available ctas const trigger = screen.getByTestId("workspace-options-button"); @@ -173,7 +181,7 @@ describe("WorkspacePage", () => { const startWorkspaceMock = jest .spyOn(api, "startWorkspace") .mockImplementation(() => Promise.resolve(MockWorkspaceBuild)); - await testButton("Start", startWorkspaceMock); + await testButton(MockWorkspace, "Start", startWorkspaceMock); }); it("requests a stop job when the user presses Stop", async () => { @@ -181,7 +189,7 @@ describe("WorkspacePage", () => { .spyOn(api, "stopWorkspace") .mockResolvedValueOnce(MockWorkspaceBuild); - await testButton("Stop", stopWorkspaceMock); + await testButton(MockWorkspace, "Stop", stopWorkspaceMock); }); it("requests a stop when the user presses Restart", async () => { @@ -190,7 +198,7 @@ describe("WorkspacePage", () => { .mockResolvedValueOnce(MockWorkspaceBuild); // Render - await renderWorkspacePage(); + await renderWorkspacePage(MockWorkspace); // Actions const user = userEvent.setup(); @@ -217,7 +225,7 @@ describe("WorkspacePage", () => { .spyOn(api, "cancelWorkspaceBuild") .mockImplementation(() => Promise.resolve({ message: "job canceled" })); - await renderWorkspacePage(); + await renderWorkspacePage(MockWorkspace); const workspaceActions = screen.getByTestId("workspace-actions"); const cancelButton = within(workspaceActions).getByRole("button", { @@ -240,7 +248,7 @@ describe("WorkspacePage", () => { .mockResolvedValueOnce(MockWorkspaceBuild); // Render - await renderWorkspacePage(); + await renderWorkspacePage(MockWorkspace); // Actions const user = userEvent.setup(); @@ -269,7 +277,7 @@ describe("WorkspacePage", () => { ); // Render - await renderWorkspacePage(); + await renderWorkspacePage(MockWorkspace); // Actions const user = userEvent.setup(); @@ -316,7 +324,7 @@ describe("WorkspacePage", () => { }); it("shows the timeline build", async () => { - await renderWorkspacePage(); + await renderWorkspacePage(MockWorkspace); const table = await screen.findByTestId("builds-table"); // Wait for the results to be loaded @@ -343,7 +351,7 @@ describe("WorkspacePage", () => { }); const restartWorkspaceSpy = jest.spyOn(api, "restartWorkspace"); const user = userEvent.setup(); - await renderWorkspacePage(); + await renderWorkspacePage(MockWorkspace); await user.click(screen.getByTestId("build-parameters-button")); const buildParametersForm = await screen.findByTestId( "build-parameters-form", From c3edba7f4339472d12610325575c4f91f82691c6 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 16 Nov 2023 20:08:31 +0000 Subject: [PATCH 10/15] refactor: update how parameters for tests are defined --- site/src/pages/WorkspacePage/WorkspacePage.test.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index 8adfe478dbd54..2c89d66d26486 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -6,7 +6,6 @@ import { rest } from "msw"; import { MockTemplate, MockWorkspace, - MockFailedWorkspace, MockWorkspaceBuild, MockStoppedWorkspace, MockStartingWorkspace, @@ -25,6 +24,7 @@ import { WorkspacePage } from "./WorkspacePage"; // It renders the workspace page and waits for it be loaded const renderWorkspacePage = async (workspace: Workspace) => { + jest.spyOn(api, "getWorkspaceByOwnerAndName").mockResolvedValue(workspace); jest.spyOn(api, "getTemplate").mockResolvedValueOnce(MockTemplate); jest.spyOn(api, "getTemplateVersionRichParameters").mockResolvedValueOnce([]); jest @@ -55,15 +55,14 @@ const renderWorkspacePage = async (workspace: Workspace) => { */ const testButton = async ( workspace: Workspace, - label: string, + name: string | RegExp, actionMock: jest.SpyInstance, ) => { - const user = userEvent.setup(); await renderWorkspacePage(workspace); - const workspaceActions = screen.getByTestId("workspace-actions"); - const button = within(workspaceActions).getByRole("button", { name: label }); + const button = within(workspaceActions).getByRole("button", { name }); + const user = userEvent.setup(); await user.click(button); expect(actionMock).toBeCalled(); }; From 2c55fd80f63030677550f41b6bd56c0b28aaafb5 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 16 Nov 2023 20:49:59 +0000 Subject: [PATCH 11/15] wip: add tests for workspace retries (breaks 2 previous tests --- .../WorkspacePage/WorkspacePage.test.tsx | 90 ++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index 2c89d66d26486..8d14a42db0841 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -6,6 +6,7 @@ import { rest } from "msw"; import { MockTemplate, MockWorkspace, + MockFailedWorkspace, MockWorkspaceBuild, MockStoppedWorkspace, MockStartingWorkspace, @@ -22,7 +23,7 @@ import { renderWithAuth } from "testHelpers/renderHelpers"; import { server } from "testHelpers/server"; import { WorkspacePage } from "./WorkspacePage"; -// It renders the workspace page and waits for it be loaded +// Renders the workspace page and waits for it be loaded const renderWorkspacePage = async (workspace: Workspace) => { jest.spyOn(api, "getWorkspaceByOwnerAndName").mockResolvedValue(workspace); jest.spyOn(api, "getTemplate").mockResolvedValueOnce(MockTemplate); @@ -369,4 +370,91 @@ describe("WorkspacePage", () => { }); }); }); + + // Tried to get these wired up via describe.each to reduce repetition, but the + // syntax just got too convoluted because of the variance in what arguments + // each function gets called with + describe("Retrying failed workspaces", () => { + const retryButtonRe = /^Retry$/i; + const retryDebugButtonRe = /^Retry \(Debug\)$/i; + + describe("Retries a failed 'Start' transition", () => { + const mockStart = jest.spyOn(api, "startWorkspace"); + const failedStart: Workspace = { + ...MockFailedWorkspace, + latest_build: { + ...MockFailedWorkspace.latest_build, + transition: "start", + }, + }; + + test("Retry with no debug", async () => { + await testButton(failedStart, retryButtonRe, mockStart); + + expect(mockStart).toBeCalledWith( + failedStart.id, + failedStart.latest_build.template_version_id, + undefined, + undefined, + ); + }); + + test("Retry with debug logs", async () => { + await testButton(failedStart, retryDebugButtonRe, mockStart); + + expect(mockStart).toBeCalledWith( + failedStart.id, + failedStart.latest_build.template_version_id, + "debug", + undefined, + ); + }); + }); + + describe("Retries a failed 'Stop' transition", () => { + const mockStop = jest.spyOn(api, "stopWorkspace"); + const failedStop: Workspace = { + ...MockFailedWorkspace, + latest_build: { + ...MockFailedWorkspace.latest_build, + transition: "stop", + }, + }; + + test("Retry with no debug", async () => { + await testButton(failedStop, retryButtonRe, mockStop); + expect(mockStop).toBeCalledWith(failedStop.id, undefined); + }); + + test("Retry with debug logs", async () => { + await testButton(failedStop, retryDebugButtonRe, mockStop); + expect(mockStop).toBeCalledWith(failedStop.id, "debug"); + }); + }); + + describe("Retries a failed 'Delete' transition", () => { + const mockDelete = jest.spyOn(api, "deleteWorkspace"); + const failedDelete: Workspace = { + ...MockFailedWorkspace, + latest_build: { + ...MockFailedWorkspace.latest_build, + transition: "delete", + }, + }; + + test("Retry with no debug", async () => { + await testButton(failedDelete, retryButtonRe, mockDelete); + expect(mockDelete).toBeCalledWith(failedDelete.id, { + logLevel: undefined, + }); + }); + + test("Retry with debug logs", async () => { + await testButton(failedDelete, retryDebugButtonRe, mockDelete); + expect(mockDelete).toBeCalledWith(failedDelete.id, { + logLevel: "debug", + }); + }); + }); + }); }); From 8d7fe73adf30fb25cdc40d63dc72b83bef9b9c36 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 17 Nov 2023 02:07:05 +0000 Subject: [PATCH 12/15] fix: update old tests to be correctly parameterized --- .../pages/WorkspacePage/WorkspacePage.test.tsx | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index 8d14a42db0841..a664e1761e784 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -178,10 +178,12 @@ describe("WorkspacePage", () => { }, ), ); + const startWorkspaceMock = jest .spyOn(api, "startWorkspace") .mockImplementation(() => Promise.resolve(MockWorkspaceBuild)); - await testButton(MockWorkspace, "Start", startWorkspaceMock); + + await testButton(MockStoppedWorkspace, "Start", startWorkspaceMock); }); it("requests a stop job when the user presses Stop", async () => { @@ -221,20 +223,12 @@ describe("WorkspacePage", () => { }, ), ); + const cancelWorkspaceMock = jest .spyOn(api, "cancelWorkspaceBuild") .mockImplementation(() => Promise.resolve({ message: "job canceled" })); - await renderWorkspacePage(MockWorkspace); - - const workspaceActions = screen.getByTestId("workspace-actions"); - const cancelButton = within(workspaceActions).getByRole("button", { - name: "Cancel", - }); - - await userEvent.click(cancelButton); - - expect(cancelWorkspaceMock).toBeCalled(); + await testButton(MockStartingWorkspace, "Cancel", cancelWorkspaceMock); }); it("requests an update when the user presses Update", async () => { From 91ce668efe590294e4cac02640ffe916efc36584 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Fri, 17 Nov 2023 02:37:20 +0000 Subject: [PATCH 13/15] fix: make sure file formatting goes through --- site/src/pages/WorkspacePage/WorkspacePage.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx index a664e1761e784..cdaa83a92b569 100644 --- a/site/src/pages/WorkspacePage/WorkspacePage.test.tsx +++ b/site/src/pages/WorkspacePage/WorkspacePage.test.tsx @@ -183,7 +183,7 @@ describe("WorkspacePage", () => { .spyOn(api, "startWorkspace") .mockImplementation(() => Promise.resolve(MockWorkspaceBuild)); - await testButton(MockStoppedWorkspace, "Start", startWorkspaceMock); + await testButton(MockStoppedWorkspace, "Start", startWorkspaceMock); }); it("requests a stop job when the user presses Stop", async () => { From abc3ae6ab3325bc5437048e412de1c86ada33bb9 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Wed, 22 Nov 2023 20:48:24 +0000 Subject: [PATCH 14/15] fix: update variable name for clarity --- .../WorkspaceActions/WorkspaceActions.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx index bc464a7644dfc..181c3b4ba0803 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceActions/WorkspaceActions.tsx @@ -77,11 +77,11 @@ export const WorkspaceActions: FC = ({ canRetryDebug, ); - const disabled = + const mustUpdate = workspaceUpdatePolicy(workspace, canChangeVersions) === "always" && workspace.outdated; - const tooltipText = getTooltipText(workspace, disabled); + const tooltipText = getTooltipText(workspace, mustUpdate); const canBeUpdated = workspace.outdated && canAcceptJobs; // A mapping of button type to the corresponding React component @@ -92,7 +92,7 @@ export const WorkspaceActions: FC = ({ ), @@ -101,7 +101,7 @@ export const WorkspaceActions: FC = ({ loading workspace={workspace} handleAction={handleStart} - disabled={disabled} + disabled={mustUpdate} tooltipText={tooltipText} /> ), @@ -111,7 +111,7 @@ export const WorkspaceActions: FC = ({ ), @@ -120,7 +120,7 @@ export const WorkspaceActions: FC = ({ loading workspace={workspace} handleAction={handleRestart} - disabled={disabled} + disabled={mustUpdate} tooltipText={tooltipText} /> ), From fa6493230b6012b5de5eacd6466211a69b1283ff Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Wed, 22 Nov 2023 20:50:16 +0000 Subject: [PATCH 15/15] fix: add export for runtime buttonTypes array --- site/src/pages/WorkspacePage/WorkspaceActions/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/pages/WorkspacePage/WorkspaceActions/constants.ts b/site/src/pages/WorkspacePage/WorkspaceActions/constants.ts index ec7b744797ff3..5d043cc300e5d 100644 --- a/site/src/pages/WorkspacePage/WorkspaceActions/constants.ts +++ b/site/src/pages/WorkspacePage/WorkspaceActions/constants.ts @@ -3,7 +3,7 @@ import { type Workspace, type WorkspaceStatus } from "api/typesGenerated"; /** * An iterable of all button types supported by the workspace actions UI */ -const buttonTypes = [ +export const buttonTypes = [ "start", "starting", "stop",