diff --git a/site/src/api/errors.ts b/site/src/api/errors.ts index bc981bbafb256..189ea12f43c4b 100644 --- a/site/src/api/errors.ts +++ b/site/src/api/errors.ts @@ -83,3 +83,6 @@ export const getValidationErrorMessage = (error: Error | ApiError | unknown): st isApiError(error) && error.response.data.validations ? error.response.data.validations : [] return validationErrors.map((error) => error.detail).join("\n") } + +export const getErrorDetail = (error: Error | ApiError | unknown): string | undefined | null => + isApiError(error) ? error.response.data.detail : error instanceof Error ? error.stack : null diff --git a/site/src/components/ErrorSummary/ErrorSummary.stories.tsx b/site/src/components/ErrorSummary/ErrorSummary.stories.tsx index 205b08da908a2..02da50d462219 100644 --- a/site/src/components/ErrorSummary/ErrorSummary.stories.tsx +++ b/site/src/components/ErrorSummary/ErrorSummary.stories.tsx @@ -23,3 +23,39 @@ WithRetry.args = { } export const WithUndefined = Template.bind({}) + +export const WithDefaultMessage = Template.bind({}) +WithDefaultMessage.args = { + // Unknown error type + error: { + message: "Failed to fetch something!", + }, + defaultMessage: "This is a default error message", +} + +export const WithDismissible = Template.bind({}) +WithDismissible.args = { + error: { + response: { + data: { + message: "Failed to fetch something!", + }, + }, + isAxiosError: true, + }, + dismissible: true, +} + +export const WithDetails = Template.bind({}) +WithDetails.args = { + error: { + response: { + data: { + message: "Failed to fetch something!", + detail: "The resource you requested does not exist in the database.", + }, + }, + isAxiosError: true, + }, + dismissible: true, +} diff --git a/site/src/components/ErrorSummary/ErrorSummary.test.tsx b/site/src/components/ErrorSummary/ErrorSummary.test.tsx index 4b153f8ebfde5..a6d222e0e3860 100644 --- a/site/src/components/ErrorSummary/ErrorSummary.test.tsx +++ b/site/src/components/ErrorSummary/ErrorSummary.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from "@testing-library/react" +import { fireEvent, render, screen } from "@testing-library/react" import { ErrorSummary } from "./ErrorSummary" describe("ErrorSummary", () => { @@ -8,7 +8,67 @@ describe("ErrorSummary", () => { render() // Then - const element = await screen.findByText("test error message", { exact: false }) + const element = await screen.findByText("test error message") expect(element).toBeDefined() }) + + it("shows details on More click", async () => { + // When + const error = { + response: { + data: { + message: "Failed to fetch something!", + detail: "The resource you requested does not exist in the database.", + }, + }, + isAxiosError: true, + } + render() + + // Then + fireEvent.click(screen.getByText("More")) + const element = await screen.findByText( + "The resource you requested does not exist in the database.", + { exact: false }, + ) + expect(element.closest(".MuiCollapse-entered")).toBeDefined() + }) + + it("hides details on Less click", async () => { + // When + const error = { + response: { + data: { + message: "Failed to fetch something!", + detail: "The resource you requested does not exist in the database.", + }, + }, + isAxiosError: true, + } + render() + + // Then + fireEvent.click(screen.getByText("More")) + fireEvent.click(screen.getByText("Less")) + const element = await screen.findByText( + "The resource you requested does not exist in the database.", + { exact: false }, + ) + expect(element.closest(".MuiCollapse-hidden")).toBeDefined() + }) + + it("renders nothing on closing", async () => { + // When + const error = new Error("test error message") + render() + + // Then + const element = await screen.findByText("test error message") + expect(element).toBeDefined() + + const closeIcon = screen.getAllByRole("button")[0] + fireEvent.click(closeIcon) + const nullElement = screen.queryByText("test error message") + expect(nullElement).toBeNull() + }) }) diff --git a/site/src/components/ErrorSummary/ErrorSummary.tsx b/site/src/components/ErrorSummary/ErrorSummary.tsx index ec690139660b4..77bea7b056363 100644 --- a/site/src/components/ErrorSummary/ErrorSummary.tsx +++ b/site/src/components/ErrorSummary/ErrorSummary.tsx @@ -1,32 +1,125 @@ import Button from "@material-ui/core/Button" +import Collapse from "@material-ui/core/Collapse" +import IconButton from "@material-ui/core/IconButton" +import Link from "@material-ui/core/Link" +import { darken, makeStyles, Theme } from "@material-ui/core/styles" +import CloseIcon from "@material-ui/icons/Close" import RefreshIcon from "@material-ui/icons/Refresh" +import { ApiError, getErrorDetail, getErrorMessage } from "api/errors" import { Stack } from "components/Stack/Stack" -import { FC } from "react" +import { FC, useState } from "react" const Language = { retryMessage: "Retry", unknownErrorMessage: "An unknown error has occurred", + moreDetails: "More", + lessDetails: "Less", } export interface ErrorSummaryProps { - error: Error | unknown + error: ApiError | Error | unknown retry?: () => void + dismissible?: boolean + defaultMessage?: string } -export const ErrorSummary: FC = ({ error, retry }) => ( - - {!(error instanceof Error) ? ( -
{Language.unknownErrorMessage}
- ) : ( -
{error.toString()}
- )} - - {retry && ( -
- -
- )} -
-) +export const ErrorSummary: FC = ({ + error, + retry, + dismissible, + defaultMessage, +}) => { + const message = getErrorMessage(error, defaultMessage || Language.unknownErrorMessage) + const detail = getErrorDetail(error) + const [showDetails, setShowDetails] = useState(false) + const [isOpen, setOpen] = useState(true) + + const styles = useStyles({ showDetails }) + + const toggleShowDetails = () => { + setShowDetails(!showDetails) + } + + const closeError = () => { + setOpen(false) + } + + if (!isOpen) { + return null + } + + return ( + + +
+ {message} + {!!detail && ( + + {showDetails ? Language.lessDetails : Language.moreDetails} + + )} +
+ {dismissible && ( + + + + )} +
+ +
{detail}
+
+ {retry && ( +
+ +
+ )} +
+ ) +} + +interface StyleProps { + showDetails?: boolean +} + +const useStyles = makeStyles((theme) => ({ + root: { + background: darken(theme.palette.error.main, 0.6), + margin: `${theme.spacing(2)}px`, + padding: `${theme.spacing(2)}px`, + borderRadius: theme.shape.borderRadius, + gap: 0, + }, + messageBox: { + justifyContent: "space-between", + }, + errorMessage: { + marginRight: `${theme.spacing(1)}px`, + }, + detailsLink: { + cursor: "pointer", + }, + details: { + marginTop: `${theme.spacing(2)}px`, + padding: `${theme.spacing(2)}px`, + background: darken(theme.palette.error.main, 0.7), + borderRadius: theme.shape.borderRadius, + }, + iconButton: { + padding: 0, + }, + closeIcon: { + width: 25, + height: 25, + color: theme.palette.primary.contrastText, + }, + retry: { + marginTop: `${theme.spacing(2)}px`, + }, +})) 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