From a1cbeeb5816c7cc1bcde0bbdeb597b4f92bea975 Mon Sep 17 00:00:00 2001 From: Abhineet Jain Date: Fri, 22 Jul 2022 04:54:29 +0000 Subject: [PATCH 1/4] create new static error summary component --- site/src/api/errors.ts | 3 + .../ErrorSummary/ErrorSummary.stories.tsx | 36 ++++++ .../ErrorSummary/ErrorSummary.test.tsx | 64 +++++++++- .../components/ErrorSummary/ErrorSummary.tsx | 118 +++++++++++++++--- 4 files changed, 200 insertions(+), 21 deletions(-) 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 e47561a1641fc..8a84bd7ade9f4 100644 --- a/site/src/components/ErrorSummary/ErrorSummary.tsx +++ b/site/src/components/ErrorSummary/ErrorSummary.tsx @@ -1,32 +1,112 @@ 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 { makeStyles, Theme } from "@material-ui/core/styles" +import CloseIcon from "@material-ui/icons/Close" import RefreshIcon from "@material-ui/icons/Refresh" -import { FC } from "react" +import { ApiError, getErrorDetail, getErrorMessage } from "api/errors" +import { FC, useState } from "react" import { Stack } from "../Stack/Stack" 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: `${theme.palette.error.main}60`, + margin: `${theme.spacing(2)}px`, + padding: `${theme.spacing(2)}px`, + borderRadius: theme.shape.borderRadius, + gap: (props) => (props.showDetails ? `${theme.spacing(2)}px` : 0), + }, + message: { + justifyContent: "space-between", + }, + errorMessage: { + marginRight: `${theme.spacing(1)}px`, + }, + detailsLink: { + cursor: "pointer", + }, + iconButton: { + padding: 0, + }, + closeIcon: { + width: 25, + height: 25, + color: theme.palette.primary.contrastText, + }, +})) From 935d4bf470cb70f7666c906ff88ba2cee1b720ff Mon Sep 17 00:00:00 2001 From: Abhineet Jain Date: Fri, 22 Jul 2022 08:34:59 +0000 Subject: [PATCH 2/4] fix import path --- site/src/components/ErrorSummary/ErrorSummary.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/components/ErrorSummary/ErrorSummary.tsx b/site/src/components/ErrorSummary/ErrorSummary.tsx index 8a84bd7ade9f4..d6c3eee4fe0ea 100644 --- a/site/src/components/ErrorSummary/ErrorSummary.tsx +++ b/site/src/components/ErrorSummary/ErrorSummary.tsx @@ -5,7 +5,7 @@ import Link from "@material-ui/core/Link" import { 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 { ApiError, getErrorDetail, getErrorMessage } from "../../api/errors" import { FC, useState } from "react" import { Stack } from "../Stack/Stack" From 793c689bb1291909829afecfd4972cfb20d35004 Mon Sep 17 00:00:00 2001 From: Abhineet Jain Date: Fri, 22 Jul 2022 16:59:46 +0000 Subject: [PATCH 3/4] add aria-expanded --- site/src/components/ErrorSummary/ErrorSummary.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/site/src/components/ErrorSummary/ErrorSummary.tsx b/site/src/components/ErrorSummary/ErrorSummary.tsx index 11f8629e3cb86..edf99eff7c422 100644 --- a/site/src/components/ErrorSummary/ErrorSummary.tsx +++ b/site/src/components/ErrorSummary/ErrorSummary.tsx @@ -55,7 +55,11 @@ export const ErrorSummary: FC = ({
{message} {!!detail && ( - + {showDetails ? Language.lessDetails : Language.moreDetails} )} From 8334a6c8b3dc9669b0b32947a2b937edeae70339 Mon Sep 17 00:00:00 2001 From: Abhineet Jain Date: Fri, 22 Jul 2022 18:57:36 +0000 Subject: [PATCH 4/4] add detail background, redesign retry button --- .../components/ErrorSummary/ErrorSummary.tsx | 65 +++++++++++-------- 1 file changed, 37 insertions(+), 28 deletions(-) diff --git a/site/src/components/ErrorSummary/ErrorSummary.tsx b/site/src/components/ErrorSummary/ErrorSummary.tsx index edf99eff7c422..77bea7b056363 100644 --- a/site/src/components/ErrorSummary/ErrorSummary.tsx +++ b/site/src/components/ErrorSummary/ErrorSummary.tsx @@ -2,7 +2,7 @@ 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 { makeStyles, Theme } from "@material-ui/core/styles" +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" @@ -49,33 +49,33 @@ export const ErrorSummary: FC = ({ } return ( - - - -
- {message} - {!!detail && ( - - {showDetails ? Language.lessDetails : Language.moreDetails} - - )} -
- {dismissible && ( - - - + + +
+ {message} + {!!detail && ( + + {showDetails ? Language.lessDetails : Language.moreDetails} + )} - - {detail} +
+ {dismissible && ( + + + + )}
- + +
{detail}
+
{retry && ( -
-
@@ -90,13 +90,13 @@ interface StyleProps { const useStyles = makeStyles((theme) => ({ root: { - background: `${theme.palette.error.main}60`, + background: darken(theme.palette.error.main, 0.6), margin: `${theme.spacing(2)}px`, padding: `${theme.spacing(2)}px`, borderRadius: theme.shape.borderRadius, - gap: (props) => (props.showDetails ? `${theme.spacing(2)}px` : 0), + gap: 0, }, - message: { + messageBox: { justifyContent: "space-between", }, errorMessage: { @@ -105,6 +105,12 @@ const useStyles = makeStyles((theme) => ({ 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, }, @@ -113,4 +119,7 @@ const useStyles = makeStyles((theme) => ({ height: 25, color: theme.palette.primary.contrastText, }, + retry: { + marginTop: `${theme.spacing(2)}px`, + }, }))