Skip to content

Commit 31d0c6f

Browse files
authored
feat: add better error display for workspace builds (#18518)
Classic parameters templates <img width="548" alt="Screenshot 2025-06-23 at 23 27 46" src="https://github.com/user-attachments/assets/e8e774bf-e201-4a80-a90c-3d6cc3658c20" /> Dynamic parameters templates <img width="541" alt="Screenshot 2025-06-23 at 23 52 05" src="https://github.com/user-attachments/assets/6a40f144-c0b2-4e16-8137-d31a52b71460" />
1 parent bca5c35 commit 31d0c6f

File tree

5 files changed

+184
-32
lines changed

5 files changed

+184
-32
lines changed

site/src/api/errors.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export interface ApiErrorResponse {
1919
validations?: FieldError[];
2020
}
2121

22-
type ApiError = AxiosError<ApiErrorResponse> & {
22+
export type ApiError = AxiosError<ApiErrorResponse> & {
2323
response: AxiosResponse<ApiErrorResponse>;
2424
};
2525

site/src/components/Dialog/Dialog.tsx

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* @see {@link https://ui.shadcn.com/docs/components/dialog}
44
*/
55
import * as DialogPrimitive from "@radix-ui/react-dialog";
6+
import { type VariantProps, cva } from "class-variance-authority";
67
import {
78
type ComponentPropsWithoutRef,
89
type ElementRef,
@@ -36,25 +37,41 @@ const DialogOverlay = forwardRef<
3637
/>
3738
));
3839

40+
const dialogVariants = cva(
41+
`fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg gap-6
42+
border border-solid bg-surface-primary p-8 shadow-lg duration-200 sm:rounded-lg
43+
translate-x-[-50%] translate-y-[-50%]
44+
data-[state=open]:animate-in data-[state=closed]:animate-out
45+
data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0
46+
data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95
47+
data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%]
48+
data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]`,
49+
{
50+
variants: {
51+
variant: {
52+
default: "border-border-primary",
53+
destructive: "border-border-destructive",
54+
},
55+
},
56+
defaultVariants: {
57+
variant: "default",
58+
},
59+
},
60+
);
61+
62+
interface DialogContentProps
63+
extends ComponentPropsWithoutRef<typeof DialogPrimitive.Content>,
64+
VariantProps<typeof dialogVariants> {}
65+
3966
export const DialogContent = forwardRef<
4067
ElementRef<typeof DialogPrimitive.Content>,
41-
ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
42-
>(({ className, children, ...props }, ref) => (
68+
DialogContentProps
69+
>(({ className, variant, children, ...props }, ref) => (
4370
<DialogPortal>
4471
<DialogOverlay />
4572
<DialogPrimitive.Content
4673
ref={ref}
47-
className={cn(
48-
`fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg gap-6
49-
border border-solid border-border bg-surface-primary p-8 shadow-lg duration-200 sm:rounded-lg
50-
translate-x-[-50%] translate-y-[-50%]
51-
data-[state=open]:animate-in data-[state=closed]:animate-out
52-
data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0
53-
data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95
54-
data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%]
55-
data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]`,
56-
className,
57-
)}
74+
className={cn(dialogVariants({ variant }), className)}
5875
{...props}
5976
>
6077
{children}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { getErrorDetail, getErrorMessage, isApiError } from "api/errors";
2+
import { Button } from "components/Button/Button";
3+
import {
4+
Dialog,
5+
DialogContent,
6+
DialogDescription,
7+
DialogFooter,
8+
DialogHeader,
9+
DialogTitle,
10+
} from "components/Dialog/Dialog";
11+
import type { FC } from "react";
12+
import { useNavigate } from "react-router-dom";
13+
14+
interface WorkspaceErrorDialogProps {
15+
open: boolean;
16+
error?: unknown;
17+
onClose: () => void;
18+
showDetail: boolean;
19+
workspaceOwner: string;
20+
workspaceName: string;
21+
templateVersionId: string;
22+
}
23+
24+
export const WorkspaceErrorDialog: FC<WorkspaceErrorDialogProps> = ({
25+
open,
26+
error,
27+
onClose,
28+
showDetail,
29+
workspaceOwner,
30+
workspaceName,
31+
templateVersionId,
32+
}) => {
33+
const navigate = useNavigate();
34+
35+
if (!error) {
36+
return null;
37+
}
38+
39+
const handleGoToParameters = () => {
40+
onClose();
41+
navigate(
42+
`/@${workspaceOwner}/${workspaceName}/settings/parameters?templateVersionId=${templateVersionId}`,
43+
);
44+
};
45+
46+
const errorDetail = getErrorDetail(error);
47+
const validations = isApiError(error)
48+
? error.response.data.validations
49+
: undefined;
50+
51+
return (
52+
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onClose()}>
53+
<DialogContent variant="destructive">
54+
<DialogHeader>
55+
<DialogTitle>Error building workspace</DialogTitle>
56+
<DialogDescription className="flex flex-row gap-4">
57+
<strong className="text-content-primary">Message</strong>{" "}
58+
<span>{getErrorMessage(error, "Failed to build workspace.")}</span>
59+
</DialogDescription>
60+
{errorDetail && showDetail && (
61+
<DialogDescription className="flex flex-row gap-9">
62+
<strong className="text-content-primary">Detail</strong>{" "}
63+
<span>{errorDetail}</span>
64+
</DialogDescription>
65+
)}
66+
{validations && (
67+
<DialogDescription className="flex flex-row gap-4">
68+
<strong className="text-content-primary">Validations</strong>{" "}
69+
<span>
70+
{validations.map((validation) => validation.detail).join(", ")}
71+
</span>
72+
</DialogDescription>
73+
)}
74+
</DialogHeader>
75+
<DialogFooter>
76+
<Button onClick={handleGoToParameters}>
77+
Review workspace settings
78+
</Button>
79+
</DialogFooter>
80+
</DialogContent>
81+
</Dialog>
82+
);
83+
};

site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx

Lines changed: 70 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { API } from "api/api";
2-
import { getErrorMessage } from "api/errors";
2+
import { type ApiError, getErrorMessage } from "api/errors";
3+
import { isApiError } from "api/errors";
34
import { templateVersion } from "api/queries/templates";
45
import { workspaceBuildTimings } from "api/queries/workspaceBuilds";
56
import {
@@ -15,9 +16,10 @@ import {
1516
ConfirmDialog,
1617
type ConfirmDialogProps,
1718
} from "components/Dialogs/ConfirmDialog/ConfirmDialog";
18-
import { EphemeralParametersDialog } from "components/EphemeralParametersDialog/EphemeralParametersDialog";
1919
import { displayError } from "components/GlobalSnackbar/utils";
2020
import { useWorkspaceBuildLogs } from "hooks/useWorkspaceBuildLogs";
21+
import { EphemeralParametersDialog } from "modules/workspaces/EphemeralParametersDialog/EphemeralParametersDialog";
22+
import { WorkspaceErrorDialog } from "modules/workspaces/ErrorDialog/WorkspaceErrorDialog";
2123
import {
2224
WorkspaceUpdateDialogs,
2325
useWorkspaceUpdate,
@@ -55,15 +57,35 @@ export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
5557
buildParameters?: TypesGen.WorkspaceBuildParameter[];
5658
}>({ open: false });
5759

60+
const [workspaceErrorDialog, setWorkspaceErrorDialog] = useState<{
61+
open: boolean;
62+
error?: ApiError;
63+
}>({ open: false });
64+
65+
const handleError = (error: unknown) => {
66+
if (isApiError(error) && error.code === "ERR_BAD_REQUEST") {
67+
setWorkspaceErrorDialog({
68+
open: true,
69+
error: error,
70+
});
71+
} else {
72+
displayError(getErrorMessage(error, "Failed to build workspace."));
73+
}
74+
};
75+
5876
const [ephemeralParametersDialog, setEphemeralParametersDialog] = useState<{
5977
open: boolean;
6078
action: "start" | "restart";
6179
buildParameters?: TypesGen.WorkspaceBuildParameter[];
6280
ephemeralParameters: TypesGen.TemplateVersionParameter[];
6381
}>({ open: false, action: "start", ephemeralParameters: [] });
82+
6483
const { mutate: mutateRestartWorkspace, isPending: isRestarting } =
6584
useMutation({
6685
mutationFn: API.restartWorkspace,
86+
onError: (error: unknown) => {
87+
handleError(error);
88+
},
6789
});
6890

6991
// Favicon
@@ -92,32 +114,52 @@ export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
92114
});
93115

94116
// Delete workspace
95-
const deleteWorkspaceMutation = useMutation(
96-
deleteWorkspace(workspace, queryClient),
97-
);
117+
const deleteWorkspaceMutation = useMutation({
118+
...deleteWorkspace(workspace, queryClient),
119+
onError: (error: unknown) => {
120+
handleError(error);
121+
},
122+
});
98123

99124
// Activate workspace
100-
const activateWorkspaceMutation = useMutation(
101-
activate(workspace, queryClient),
102-
);
125+
const activateWorkspaceMutation = useMutation({
126+
...activate(workspace, queryClient),
127+
onError: (error: unknown) => {
128+
handleError(error);
129+
},
130+
});
103131

104132
// Stop workspace
105-
const stopWorkspaceMutation = useMutation(
106-
stopWorkspace(workspace, queryClient),
107-
);
133+
const stopWorkspaceMutation = useMutation({
134+
...stopWorkspace(workspace, queryClient),
135+
onError: (error: unknown) => {
136+
handleError(error);
137+
},
138+
});
108139

109140
// Start workspace
110-
const startWorkspaceMutation = useMutation(
111-
startWorkspace(workspace, queryClient),
112-
);
141+
const startWorkspaceMutation = useMutation({
142+
...startWorkspace(workspace, queryClient),
143+
onError: (error: unknown) => {
144+
handleError(error);
145+
},
146+
});
113147

114148
// Toggle workspace favorite
115-
const toggleFavoriteMutation = useMutation(
116-
toggleFavorite(workspace, queryClient),
117-
);
149+
const toggleFavoriteMutation = useMutation({
150+
...toggleFavorite(workspace, queryClient),
151+
onError: (error: unknown) => {
152+
handleError(error);
153+
},
154+
});
118155

119156
// Cancel build
120-
const cancelBuildMutation = useMutation(cancelBuild(workspace, queryClient));
157+
const cancelBuildMutation = useMutation({
158+
...cancelBuild(workspace, queryClient),
159+
onError: (error: unknown) => {
160+
handleError(error);
161+
},
162+
});
121163

122164
// Workspace Timings.
123165
const timingsQuery = useQuery({
@@ -341,6 +383,16 @@ export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
341383
/>
342384

343385
<WorkspaceUpdateDialogs {...workspaceUpdate.dialogs} />
386+
387+
<WorkspaceErrorDialog
388+
open={workspaceErrorDialog.open}
389+
error={workspaceErrorDialog.error}
390+
onClose={() => setWorkspaceErrorDialog({ open: false })}
391+
showDetail={workspace.template_use_classic_parameter_flow}
392+
workspaceOwner={workspace.owner_name}
393+
workspaceName={workspace.name}
394+
templateVersionId={workspace.latest_build.template_version_id}
395+
/>
344396
</>
345397
);
346398
};

0 commit comments

Comments
 (0)