Skip to content

Commit 491e0e3

Browse files
authored
fix: display explicit 'retry' button(s) when a workspace fails (#10720)
* refactor: remove workspace error enums * fix: add in retry button for failed workspaces * fix: make handleBuildRetry auto-detect debug permissions * chore: consolidate retry messaging * chore: update renderWorkspacePage to accept parameters * chore: make workspace test helpers take explicit workspace parameter * refactor: update how parameters for tests are defined * fix: update old tests to be correctly parameterized
1 parent 65c726e commit 491e0e3

File tree

7 files changed

+371
-228
lines changed

7 files changed

+371
-228
lines changed

site/src/pages/WorkspacePage/Workspace.stories.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Meta, StoryObj } from "@storybook/react";
33
import { WatchAgentMetadataContext } from "components/Resources/AgentMetadata";
44
import { ProvisionerJobLog } from "api/typesGenerated";
55
import * as Mocks from "testHelpers/entities";
6-
import { Workspace, WorkspaceErrors } from "./Workspace";
6+
import { Workspace } from "./Workspace";
77
import { withReactContext } from "storybook-react-context";
88
import EventSource from "eventsourcemock";
99
import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext";
@@ -133,7 +133,7 @@ export const Failed: Story = {
133133
...Running.args,
134134
workspace: Mocks.MockFailedWorkspace,
135135
workspaceErrors: {
136-
[WorkspaceErrors.BUILD_ERROR]: Mocks.mockApiError({
136+
buildError: Mocks.mockApiError({
137137
message: "A workspace build is already active.",
138138
}),
139139
},
@@ -172,7 +172,6 @@ export const FailedWithRetry: Story = {
172172
},
173173
},
174174
},
175-
canRetryDebugMode: true,
176175
buildLogs: <WorkspaceBuildLogsSection logs={makeFailedBuildLogs()} />,
177176
},
178177
};
@@ -224,7 +223,7 @@ export const GetBuildsError: Story = {
224223
args: {
225224
...Running.args,
226225
workspaceErrors: {
227-
[WorkspaceErrors.GET_BUILDS_ERROR]: Mocks.mockApiError({
226+
getBuildsError: Mocks.mockApiError({
228227
message: "There is a problem fetching builds.",
229228
}),
230229
},
@@ -235,7 +234,7 @@ export const CancellationError: Story = {
235234
args: {
236235
...Failed.args,
237236
workspaceErrors: {
238-
[WorkspaceErrors.CANCELLATION_ERROR]: Mocks.mockApiError({
237+
cancellationError: Mocks.mockApiError({
239238
message: "Job could not be canceled.",
240239
}),
241240
},

site/src/pages/WorkspacePage/Workspace.tsx

Lines changed: 45 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,13 @@ import { BuildsTable } from "./BuildsTable";
2929
import { WorkspaceDeletedBanner } from "./WorkspaceDeletedBanner";
3030
import { WorkspaceStats } from "./WorkspaceStats";
3131

32-
export enum WorkspaceErrors {
33-
GET_BUILDS_ERROR = "getBuildsError",
34-
BUILD_ERROR = "buildError",
35-
CANCELLATION_ERROR = "cancellationError",
36-
}
32+
export type WorkspaceError =
33+
| "getBuildsError"
34+
| "buildError"
35+
| "cancellationError";
36+
37+
export type WorkspaceErrors = Partial<Record<WorkspaceError, unknown>>;
38+
3739
export interface WorkspaceProps {
3840
scheduleProps: {
3941
onDeadlinePlus: (hours: number) => void;
@@ -56,16 +58,17 @@ export interface WorkspaceProps {
5658
resources?: TypesGen.WorkspaceResource[];
5759
canUpdateWorkspace: boolean;
5860
updateMessage?: string;
59-
canRetryDebugMode: boolean;
6061
canChangeVersions: boolean;
6162
hideSSHButton?: boolean;
6263
hideVSCodeDesktopButton?: boolean;
63-
workspaceErrors: Partial<Record<WorkspaceErrors, unknown>>;
64+
workspaceErrors: WorkspaceErrors;
6465
buildInfo?: TypesGen.BuildInfoResponse;
6566
sshPrefix?: string;
6667
template?: TypesGen.Template;
6768
quotaBudget?: number;
69+
canRetryDebugMode: boolean;
6870
handleBuildRetry: () => void;
71+
handleBuildRetryDebug: () => void;
6972
buildLogs?: React.ReactNode;
7073
builds: TypesGen.WorkspaceBuild[] | undefined;
7174
onLoadMoreBuilds: () => void;
@@ -87,55 +90,39 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
8790
handleCancel,
8891
handleSettings,
8992
handleChangeVersion,
90-
handleDormantActivate: handleDormantActivate,
93+
handleDormantActivate,
9194
workspace,
9295
isUpdating,
9396
isRestarting,
9497
resources,
9598
builds,
9699
canUpdateWorkspace,
97100
updateMessage,
98-
canRetryDebugMode,
99101
canChangeVersions,
100102
workspaceErrors,
101103
hideSSHButton,
102104
hideVSCodeDesktopButton,
103105
buildInfo,
104106
sshPrefix,
105107
template,
108+
canRetryDebugMode,
106109
handleBuildRetry,
110+
handleBuildRetryDebug,
107111
buildLogs,
108112
onLoadMoreBuilds,
109113
isLoadingMoreBuilds,
110114
hasMoreBuilds,
111115
canAutostart,
112116
}) => {
113117
const navigate = useNavigate();
114-
const serverVersion = buildInfo?.version || "";
115118
const { saveLocal, getLocal } = useLocalStorage();
116119

117-
const buildError = Boolean(workspaceErrors[WorkspaceErrors.BUILD_ERROR]) && (
118-
<ErrorAlert
119-
error={workspaceErrors[WorkspaceErrors.BUILD_ERROR]}
120-
dismissible
121-
/>
122-
);
123-
124-
const cancellationError = Boolean(
125-
workspaceErrors[WorkspaceErrors.CANCELLATION_ERROR],
126-
) && (
127-
<ErrorAlert
128-
error={workspaceErrors[WorkspaceErrors.CANCELLATION_ERROR]}
129-
dismissible
130-
/>
131-
);
132-
133-
let transitionStats: TypesGen.TransitionStats | undefined = undefined;
134-
if (template !== undefined) {
135-
transitionStats = ActiveTransition(template, workspace);
136-
}
137-
138120
const [showAlertPendingInQueue, setShowAlertPendingInQueue] = useState(false);
121+
122+
// 2023-11-15 - MES - This effect will be called every single render because
123+
// "now" will always change and invalidate the dependency array. Need to
124+
// figure out if this effect really should run every render (possibly meaning
125+
// no dependency array at all), or how to get the array stabilized (ideal)
139126
const now = dayjs();
140127
useEffect(() => {
141128
if (
@@ -174,6 +161,9 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
174161
const autoStartFailing = workspace.autostart_schedule && !canAutostart;
175162
const requiresManualUpdate = updateRequired && autoStartFailing;
176163

164+
const transitionStats =
165+
template !== undefined ? ActiveTransition(template, workspace) : undefined;
166+
177167
return (
178168
<>
179169
<FullWidthPageHeader>
@@ -213,8 +203,11 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
213203
handleUpdate={handleUpdate}
214204
handleCancel={handleCancel}
215205
handleSettings={handleSettings}
206+
handleRetry={handleBuildRetry}
207+
handleRetryDebug={handleBuildRetryDebug}
216208
handleChangeVersion={handleChangeVersion}
217209
handleDormantActivate={handleDormantActivate}
210+
canRetryDebug={canRetryDebugMode}
218211
canChangeVersions={canChangeVersions}
219212
isUpdating={isUpdating}
220213
isRestarting={isRestarting}
@@ -244,8 +237,15 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
244237
{updateMessage && <AlertDetail>{updateMessage}</AlertDetail>}
245238
</Alert>
246239
))}
247-
{buildError}
248-
{cancellationError}
240+
241+
{Boolean(workspaceErrors.buildError) && (
242+
<ErrorAlert error={workspaceErrors.buildError} dismissible />
243+
)}
244+
245+
{Boolean(workspaceErrors.cancellationError) && (
246+
<ErrorAlert error={workspaceErrors.cancellationError} dismissible />
247+
)}
248+
249249
{workspace.latest_build.status === "running" &&
250250
!workspace.health.healthy && (
251251
<Alert
@@ -311,16 +311,15 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
311311
<Alert
312312
severity="error"
313313
actions={
314-
canRetryDebugMode && (
315-
<Button
316-
key={0}
317-
onClick={handleBuildRetry}
318-
variant="text"
319-
size="small"
320-
>
321-
Try in debug mode
322-
</Button>
323-
)
314+
<Button
315+
onClick={
316+
canRetryDebugMode ? handleBuildRetryDebug : handleBuildRetry
317+
}
318+
variant="text"
319+
size="small"
320+
>
321+
Retry{canRetryDebugMode && " in debug mode"}
322+
</Button>
324323
}
325324
>
326325
<AlertTitle>Workspace build failed</AlertTitle>
@@ -357,17 +356,15 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
357356
showBuiltinApps={canUpdateWorkspace}
358357
hideSSHButton={hideSSHButton}
359358
hideVSCodeDesktopButton={hideVSCodeDesktopButton}
360-
serverVersion={serverVersion}
359+
serverVersion={buildInfo?.version || ""}
361360
onUpdateAgent={handleUpdate} // On updating the workspace the agent version is also updated
362361
/>
363362
)}
364363
/>
365364
)}
366365

367-
{workspaceErrors[WorkspaceErrors.GET_BUILDS_ERROR] ? (
368-
<ErrorAlert
369-
error={workspaceErrors[WorkspaceErrors.GET_BUILDS_ERROR]}
370-
/>
366+
{workspaceErrors.getBuildsError ? (
367+
<ErrorAlert error={workspaceErrors.getBuildsError} />
371368
) : (
372369
<BuildsTable
373370
builds={builds}

site/src/pages/WorkspacePage/WorkspaceActions/Buttons.tsx

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,29 @@
1+
import { type FC } from "react";
2+
import type { Workspace, WorkspaceBuildParameter } from "api/typesGenerated";
3+
import { BuildParametersPopover } from "./BuildParametersPopover";
4+
5+
import Tooltip from "@mui/material/Tooltip";
16
import Button from "@mui/material/Button";
2-
import BlockIcon from "@mui/icons-material/Block";
7+
import LoadingButton from "@mui/lab/LoadingButton";
8+
import ButtonGroup from "@mui/material/ButtonGroup";
39
import CloudQueueIcon from "@mui/icons-material/CloudQueue";
410
import CropSquareIcon from "@mui/icons-material/CropSquare";
511
import PlayCircleOutlineIcon from "@mui/icons-material/PlayCircleOutline";
612
import ReplayIcon from "@mui/icons-material/Replay";
7-
import { FC } from "react";
8-
import BlockOutlined from "@mui/icons-material/BlockOutlined";
9-
import ButtonGroup from "@mui/material/ButtonGroup";
10-
import { Workspace, WorkspaceBuildParameter } from "api/typesGenerated";
11-
import { BuildParametersPopover } from "./BuildParametersPopover";
13+
import BlockIcon from "@mui/icons-material/Block";
14+
import OutlinedBlockIcon from "@mui/icons-material/BlockOutlined";
1215
import PowerSettingsNewIcon from "@mui/icons-material/PowerSettingsNew";
13-
import LoadingButton from "@mui/lab/LoadingButton";
14-
import Tooltip from "@mui/material/Tooltip";
16+
import RetryIcon from "@mui/icons-material/BuildOutlined";
17+
import RetryDebugIcon from "@mui/icons-material/BugReportOutlined";
1518

16-
interface WorkspaceAction {
19+
interface WorkspaceActionProps {
1720
loading?: boolean;
1821
handleAction: () => void;
1922
disabled?: boolean;
2023
tooltipText?: string;
2124
}
2225

23-
export const UpdateButton: FC<WorkspaceAction> = ({
26+
export const UpdateButton: FC<WorkspaceActionProps> = ({
2427
handleAction,
2528
loading,
2629
}) => {
@@ -37,7 +40,7 @@ export const UpdateButton: FC<WorkspaceAction> = ({
3740
);
3841
};
3942

40-
export const ActivateButton: FC<WorkspaceAction> = ({
43+
export const ActivateButton: FC<WorkspaceActionProps> = ({
4144
handleAction,
4245
loading,
4346
}) => {
@@ -54,7 +57,7 @@ export const ActivateButton: FC<WorkspaceAction> = ({
5457
};
5558

5659
export const StartButton: FC<
57-
Omit<WorkspaceAction, "handleAction"> & {
60+
Omit<WorkspaceActionProps, "handleAction"> & {
5861
workspace: Workspace;
5962
handleAction: (buildParameters?: WorkspaceBuildParameter[]) => void;
6063
}
@@ -63,7 +66,7 @@ export const StartButton: FC<
6366
<ButtonGroup
6467
variant="outlined"
6568
sx={{
66-
// Workaround to make the border transitions smmothly on button groups
69+
// Workaround to make the border transitions smoothly on button groups
6770
"& > button:hover + button": {
6871
borderLeft: "1px solid #FFF",
6972
},
@@ -94,7 +97,10 @@ export const StartButton: FC<
9497
);
9598
};
9699

97-
export const StopButton: FC<WorkspaceAction> = ({ handleAction, loading }) => {
100+
export const StopButton: FC<WorkspaceActionProps> = ({
101+
handleAction,
102+
loading,
103+
}) => {
98104
return (
99105
<LoadingButton
100106
loading={loading}
@@ -109,7 +115,7 @@ export const StopButton: FC<WorkspaceAction> = ({ handleAction, loading }) => {
109115
};
110116

111117
export const RestartButton: FC<
112-
Omit<WorkspaceAction, "handleAction"> & {
118+
Omit<WorkspaceActionProps, "handleAction"> & {
113119
workspace: Workspace;
114120
handleAction: (buildParameters?: WorkspaceBuildParameter[]) => void;
115121
}
@@ -118,7 +124,7 @@ export const RestartButton: FC<
118124
<ButtonGroup
119125
variant="outlined"
120126
sx={{
121-
// Workaround to make the border transitions smmothly on button groups
127+
// Workaround to make the border transitions smoothly on button groups
122128
"& > button:hover + button": {
123129
borderLeft: "1px solid #FFF",
124130
},
@@ -150,7 +156,7 @@ export const RestartButton: FC<
150156
);
151157
};
152158

153-
export const CancelButton: FC<WorkspaceAction> = ({ handleAction }) => {
159+
export const CancelButton: FC<WorkspaceActionProps> = ({ handleAction }) => {
154160
return (
155161
<Button startIcon={<BlockIcon />} onClick={handleAction}>
156162
Cancel
@@ -164,7 +170,7 @@ interface DisabledProps {
164170

165171
export const DisabledButton: FC<DisabledProps> = ({ label }) => {
166172
return (
167-
<Button startIcon={<BlockOutlined />} disabled>
173+
<Button startIcon={<OutlinedBlockIcon />} disabled>
168174
{label}
169175
</Button>
170176
);
@@ -181,3 +187,21 @@ export const ActionLoadingButton: FC<LoadingProps> = ({ label }) => {
181187
</LoadingButton>
182188
);
183189
};
190+
191+
type DebugButtonProps = Omit<WorkspaceActionProps, "loading"> & {
192+
debug?: boolean;
193+
};
194+
195+
export const RetryButton = ({
196+
handleAction,
197+
debug = false,
198+
}: DebugButtonProps) => {
199+
return (
200+
<Button
201+
startIcon={debug ? <RetryDebugIcon /> : <RetryIcon />}
202+
onClick={handleAction}
203+
>
204+
Retry{debug && " (Debug)"}
205+
</Button>
206+
);
207+
};

0 commit comments

Comments
 (0)