From 7ad291ba4882a3cc1443ac47f7789cfc8e53b5df Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Mon, 23 Jun 2025 22:58:55 +0000 Subject: [PATCH 1/2] feat: add better error display for workspace builds --- site/src/api/errors.ts | 2 +- site/src/components/Dialog/Dialog.tsx | 43 ++++++--- .../EphemeralParametersDialog.tsx | 0 .../ErrorDialog/WorkspaceErrorDialog.tsx | 83 +++++++++++++++++ .../WorkspacePage/WorkspaceReadyPage.tsx | 88 +++++++++++++++---- 5 files changed, 184 insertions(+), 32 deletions(-) rename site/src/{components => modules/workspaces}/EphemeralParametersDialog/EphemeralParametersDialog.tsx (100%) create mode 100644 site/src/modules/workspaces/ErrorDialog/WorkspaceErrorDialog.tsx diff --git a/site/src/api/errors.ts b/site/src/api/errors.ts index c9a328803bf15..9705a08ff057c 100644 --- a/site/src/api/errors.ts +++ b/site/src/api/errors.ts @@ -19,7 +19,7 @@ export interface ApiErrorResponse { validations?: FieldError[]; } -type ApiError = AxiosError & { +export type ApiError = AxiosError & { response: AxiosResponse; }; diff --git a/site/src/components/Dialog/Dialog.tsx b/site/src/components/Dialog/Dialog.tsx index 2ec8ab40781c7..6d217155e1467 100644 --- a/site/src/components/Dialog/Dialog.tsx +++ b/site/src/components/Dialog/Dialog.tsx @@ -3,6 +3,7 @@ * @see {@link https://ui.shadcn.com/docs/components/dialog} */ import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { type VariantProps, cva } from "class-variance-authority"; import { type ComponentPropsWithoutRef, type ElementRef, @@ -36,25 +37,41 @@ const DialogOverlay = forwardRef< /> )); +const dialogVariants = cva( + `fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg gap-6 + border border-solid bg-surface-primary p-8 shadow-lg duration-200 sm:rounded-lg + translate-x-[-50%] translate-y-[-50%] + data-[state=open]:animate-in data-[state=closed]:animate-out + data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 + data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 + data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] + data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]`, + { + variants: { + variant: { + default: "border-border-primary", + destructive: "border-border-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +export interface DialogContentProps + extends ComponentPropsWithoutRef, + VariantProps {} + export const DialogContent = forwardRef< ElementRef, - ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( + DialogContentProps +>(({ className, variant, children, ...props }, ref) => ( {children} diff --git a/site/src/components/EphemeralParametersDialog/EphemeralParametersDialog.tsx b/site/src/modules/workspaces/EphemeralParametersDialog/EphemeralParametersDialog.tsx similarity index 100% rename from site/src/components/EphemeralParametersDialog/EphemeralParametersDialog.tsx rename to site/src/modules/workspaces/EphemeralParametersDialog/EphemeralParametersDialog.tsx diff --git a/site/src/modules/workspaces/ErrorDialog/WorkspaceErrorDialog.tsx b/site/src/modules/workspaces/ErrorDialog/WorkspaceErrorDialog.tsx new file mode 100644 index 0000000000000..cc2512692c540 --- /dev/null +++ b/site/src/modules/workspaces/ErrorDialog/WorkspaceErrorDialog.tsx @@ -0,0 +1,83 @@ +import { getErrorDetail, getErrorMessage, isApiError } from "api/errors"; +import { Button } from "components/Button/Button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "components/Dialog/Dialog"; +import type { FC } from "react"; +import { useNavigate } from "react-router-dom"; + +interface WorkspaceErrorDialogProps { + open: boolean; + error?: unknown; + onClose: () => void; + showDetail: boolean; + workspaceOwner: string; + workspaceName: string; + templateVersionId: string; +} + +export const WorkspaceErrorDialog: FC = ({ + open, + error, + onClose, + showDetail, + workspaceOwner, + workspaceName, + templateVersionId, +}) => { + const navigate = useNavigate(); + + if (!error) { + return null; + } + + const handleGoToParameters = () => { + onClose(); + navigate( + `/@${workspaceOwner}/${workspaceName}/settings/parameters?templateVersionId=${templateVersionId}`, + ); + }; + + const errorDetail = getErrorDetail(error); + const validations = isApiError(error) + ? error.response.data.validations + : undefined; + + return ( + !isOpen && onClose()}> + + + Error building workspace + + Message{" "} + {getErrorMessage(error, "Failed to build workspace.")} + + {errorDetail && showDetail && ( + + Detail{" "} + {errorDetail} + + )} + {validations && ( + + Validations{" "} + + {validations.map((validation) => validation.detail).join(", ")} + + + )} + + + + + + + ); +}; diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index f1d48f0fccc7b..1e727faf46cd4 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -1,5 +1,6 @@ import { API } from "api/api"; -import { getErrorMessage } from "api/errors"; +import { type ApiError, getErrorMessage } from "api/errors"; +import { isApiError } from "api/errors"; import { templateVersion } from "api/queries/templates"; import { workspaceBuildTimings } from "api/queries/workspaceBuilds"; import { @@ -15,9 +16,10 @@ import { ConfirmDialog, type ConfirmDialogProps, } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; -import { EphemeralParametersDialog } from "components/EphemeralParametersDialog/EphemeralParametersDialog"; import { displayError } from "components/GlobalSnackbar/utils"; import { useWorkspaceBuildLogs } from "hooks/useWorkspaceBuildLogs"; +import { EphemeralParametersDialog } from "modules/workspaces/EphemeralParametersDialog/EphemeralParametersDialog"; +import { WorkspaceErrorDialog } from "modules/workspaces/ErrorDialog/WorkspaceErrorDialog"; import { WorkspaceUpdateDialogs, useWorkspaceUpdate, @@ -55,15 +57,35 @@ export const WorkspaceReadyPage: FC = ({ buildParameters?: TypesGen.WorkspaceBuildParameter[]; }>({ open: false }); + const [workspaceErrorDialog, setWorkspaceErrorDialog] = useState<{ + open: boolean; + error?: ApiError; + }>({ open: false }); + + const handleError = (error: unknown) => { + if (isApiError(error) && error.code === "ERR_BAD_REQUEST") { + setWorkspaceErrorDialog({ + open: true, + error: error, + }); + } else { + displayError(getErrorMessage(error, "Failed to build workspace.")); + } + }; + const [ephemeralParametersDialog, setEphemeralParametersDialog] = useState<{ open: boolean; action: "start" | "restart"; buildParameters?: TypesGen.WorkspaceBuildParameter[]; ephemeralParameters: TypesGen.TemplateVersionParameter[]; }>({ open: false, action: "start", ephemeralParameters: [] }); + const { mutate: mutateRestartWorkspace, isPending: isRestarting } = useMutation({ mutationFn: API.restartWorkspace, + onError: (error: unknown) => { + handleError(error); + }, }); // Favicon @@ -92,32 +114,52 @@ export const WorkspaceReadyPage: FC = ({ }); // Delete workspace - const deleteWorkspaceMutation = useMutation( - deleteWorkspace(workspace, queryClient), - ); + const deleteWorkspaceMutation = useMutation({ + ...deleteWorkspace(workspace, queryClient), + onError: (error: unknown) => { + handleError(error); + }, + }); // Activate workspace - const activateWorkspaceMutation = useMutation( - activate(workspace, queryClient), - ); + const activateWorkspaceMutation = useMutation({ + ...activate(workspace, queryClient), + onError: (error: unknown) => { + handleError(error); + }, + }); // Stop workspace - const stopWorkspaceMutation = useMutation( - stopWorkspace(workspace, queryClient), - ); + const stopWorkspaceMutation = useMutation({ + ...stopWorkspace(workspace, queryClient), + onError: (error: unknown) => { + handleError(error); + }, + }); // Start workspace - const startWorkspaceMutation = useMutation( - startWorkspace(workspace, queryClient), - ); + const startWorkspaceMutation = useMutation({ + ...startWorkspace(workspace, queryClient), + onError: (error: unknown) => { + handleError(error); + }, + }); // Toggle workspace favorite - const toggleFavoriteMutation = useMutation( - toggleFavorite(workspace, queryClient), - ); + const toggleFavoriteMutation = useMutation({ + ...toggleFavorite(workspace, queryClient), + onError: (error: unknown) => { + handleError(error); + }, + }); // Cancel build - const cancelBuildMutation = useMutation(cancelBuild(workspace, queryClient)); + const cancelBuildMutation = useMutation({ + ...cancelBuild(workspace, queryClient), + onError: (error: unknown) => { + handleError(error); + }, + }); // Workspace Timings. const timingsQuery = useQuery({ @@ -341,6 +383,16 @@ export const WorkspaceReadyPage: FC = ({ /> + + setWorkspaceErrorDialog({ open: false })} + showDetail={workspace.template_use_classic_parameter_flow} + workspaceOwner={workspace.owner_name} + workspaceName={workspace.name} + templateVersionId={workspace.latest_build.template_version_id} + /> ); }; From 3a3d712ceda0faa59401145ff6339610efdaac1b Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Mon, 23 Jun 2025 23:24:44 +0000 Subject: [PATCH 2/2] fix: remove export --- site/src/components/Dialog/Dialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/components/Dialog/Dialog.tsx b/site/src/components/Dialog/Dialog.tsx index 6d217155e1467..2ec5fa4dae212 100644 --- a/site/src/components/Dialog/Dialog.tsx +++ b/site/src/components/Dialog/Dialog.tsx @@ -59,7 +59,7 @@ const dialogVariants = cva( }, ); -export interface DialogContentProps +interface DialogContentProps extends ComponentPropsWithoutRef, VariantProps {} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy