feat: New static error summary component (#3107)

This commit is contained in:
Abhineet Jain
2022-07-22 19:10:40 +00:00
committed by GitHub
parent fd4954b4e5
commit 1b19a09a37
4 changed files with 212 additions and 20 deletions
+3
View File
@@ -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
@@ -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,
}
@@ -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(<ErrorSummary error={error} />)
// 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(<ErrorSummary error={error} />)
// 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(<ErrorSummary error={error} />)
// 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(<ErrorSummary error={error} dismissible />)
// 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()
})
})
+111 -18
View File
@@ -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<ErrorSummaryProps> = ({ error, retry }) => (
<Stack>
{!(error instanceof Error) ? (
<div>{Language.unknownErrorMessage}</div>
) : (
<div>{error.toString()}</div>
)}
export const ErrorSummary: FC<ErrorSummaryProps> = ({
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)
{retry && (
<div>
<Button onClick={retry} startIcon={<RefreshIcon />} variant="outlined">
{Language.retryMessage}
</Button>
</div>
)}
</Stack>
)
const styles = useStyles({ showDetails })
const toggleShowDetails = () => {
setShowDetails(!showDetails)
}
const closeError = () => {
setOpen(false)
}
if (!isOpen) {
return null
}
return (
<Stack className={styles.root}>
<Stack direction="row" alignItems="center" className={styles.messageBox}>
<div>
<span className={styles.errorMessage}>{message}</span>
{!!detail && (
<Link
aria-expanded={showDetails}
onClick={toggleShowDetails}
className={styles.detailsLink}
tabIndex={0}
>
{showDetails ? Language.lessDetails : Language.moreDetails}
</Link>
)}
</div>
{dismissible && (
<IconButton onClick={closeError} className={styles.iconButton}>
<CloseIcon className={styles.closeIcon} />
</IconButton>
)}
</Stack>
<Collapse in={showDetails}>
<div className={styles.details}>{detail}</div>
</Collapse>
{retry && (
<div className={styles.retry}>
<Button size="small" onClick={retry} startIcon={<RefreshIcon />} variant="outlined">
{Language.retryMessage}
</Button>
</div>
)}
</Stack>
)
}
interface StyleProps {
showDetails?: boolean
}
const useStyles = makeStyles<Theme, StyleProps>((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`,
},
}))