Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 1b19a09

Browse files
authored
feat: New static error summary component (#3107)
1 parent fd4954b commit 1b19a09

File tree

4 files changed

+213
-21
lines changed

4 files changed

+213
-21
lines changed

site/src/api/errors.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,6 @@ export const getValidationErrorMessage = (error: Error | ApiError | unknown): st
8383
isApiError(error) && error.response.data.validations ? error.response.data.validations : []
8484
return validationErrors.map((error) => error.detail).join("\n")
8585
}
86+
87+
export const getErrorDetail = (error: Error | ApiError | unknown): string | undefined | null =>
88+
isApiError(error) ? error.response.data.detail : error instanceof Error ? error.stack : null

site/src/components/ErrorSummary/ErrorSummary.stories.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,39 @@ WithRetry.args = {
2323
}
2424

2525
export const WithUndefined = Template.bind({})
26+
27+
export const WithDefaultMessage = Template.bind({})
28+
WithDefaultMessage.args = {
29+
// Unknown error type
30+
error: {
31+
message: "Failed to fetch something!",
32+
},
33+
defaultMessage: "This is a default error message",
34+
}
35+
36+
export const WithDismissible = Template.bind({})
37+
WithDismissible.args = {
38+
error: {
39+
response: {
40+
data: {
41+
message: "Failed to fetch something!",
42+
},
43+
},
44+
isAxiosError: true,
45+
},
46+
dismissible: true,
47+
}
48+
49+
export const WithDetails = Template.bind({})
50+
WithDetails.args = {
51+
error: {
52+
response: {
53+
data: {
54+
message: "Failed to fetch something!",
55+
detail: "The resource you requested does not exist in the database.",
56+
},
57+
},
58+
isAxiosError: true,
59+
},
60+
dismissible: true,
61+
}
Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { render, screen } from "@testing-library/react"
1+
import { fireEvent, render, screen } from "@testing-library/react"
22
import { ErrorSummary } from "./ErrorSummary"
33

44
describe("ErrorSummary", () => {
@@ -8,7 +8,67 @@ describe("ErrorSummary", () => {
88
render(<ErrorSummary error={error} />)
99

1010
// Then
11-
const element = await screen.findByText("test error message", { exact: false })
11+
const element = await screen.findByText("test error message")
1212
expect(element).toBeDefined()
1313
})
14+
15+
it("shows details on More click", async () => {
16+
// When
17+
const error = {
18+
response: {
19+
data: {
20+
message: "Failed to fetch something!",
21+
detail: "The resource you requested does not exist in the database.",
22+
},
23+
},
24+
isAxiosError: true,
25+
}
26+
render(<ErrorSummary error={error} />)
27+
28+
// Then
29+
fireEvent.click(screen.getByText("More"))
30+
const element = await screen.findByText(
31+
"The resource you requested does not exist in the database.",
32+
{ exact: false },
33+
)
34+
expect(element.closest(".MuiCollapse-entered")).toBeDefined()
35+
})
36+
37+
it("hides details on Less click", async () => {
38+
// When
39+
const error = {
40+
response: {
41+
data: {
42+
message: "Failed to fetch something!",
43+
detail: "The resource you requested does not exist in the database.",
44+
},
45+
},
46+
isAxiosError: true,
47+
}
48+
render(<ErrorSummary error={error} />)
49+
50+
// Then
51+
fireEvent.click(screen.getByText("More"))
52+
fireEvent.click(screen.getByText("Less"))
53+
const element = await screen.findByText(
54+
"The resource you requested does not exist in the database.",
55+
{ exact: false },
56+
)
57+
expect(element.closest(".MuiCollapse-hidden")).toBeDefined()
58+
})
59+
60+
it("renders nothing on closing", async () => {
61+
// When
62+
const error = new Error("test error message")
63+
render(<ErrorSummary error={error} dismissible />)
64+
65+
// Then
66+
const element = await screen.findByText("test error message")
67+
expect(element).toBeDefined()
68+
69+
const closeIcon = screen.getAllByRole("button")[0]
70+
fireEvent.click(closeIcon)
71+
const nullElement = screen.queryByText("test error message")
72+
expect(nullElement).toBeNull()
73+
})
1474
})
Lines changed: 112 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,125 @@
11
import Button from "@material-ui/core/Button"
2+
import Collapse from "@material-ui/core/Collapse"
3+
import IconButton from "@material-ui/core/IconButton"
4+
import Link from "@material-ui/core/Link"
5+
import { darken, makeStyles, Theme } from "@material-ui/core/styles"
6+
import CloseIcon from "@material-ui/icons/Close"
27
import RefreshIcon from "@material-ui/icons/Refresh"
8+
import { ApiError, getErrorDetail, getErrorMessage } from "api/errors"
39
import { Stack } from "components/Stack/Stack"
4-
import { FC } from "react"
10+
import { FC, useState } from "react"
511

612
const Language = {
713
retryMessage: "Retry",
814
unknownErrorMessage: "An unknown error has occurred",
15+
moreDetails: "More",
16+
lessDetails: "Less",
917
}
1018

1119
export interface ErrorSummaryProps {
12-
error: Error | unknown
20+
error: ApiError | Error | unknown
1321
retry?: () => void
22+
dismissible?: boolean
23+
defaultMessage?: string
1424
}
1525

16-
export const ErrorSummary: FC<ErrorSummaryProps> = ({ error, retry }) => (
17-
<Stack>
18-
{!(error instanceof Error) ? (
19-
<div>{Language.unknownErrorMessage}</div>
20-
) : (
21-
<div>{error.toString()}</div>
22-
)}
23-
24-
{retry && (
25-
<div>
26-
<Button onClick={retry} startIcon={<RefreshIcon />} variant="outlined">
27-
{Language.retryMessage}
28-
</Button>
29-
</div>
30-
)}
31-
</Stack>
32-
)
26+
export const ErrorSummary: FC<ErrorSummaryProps> = ({
27+
error,
28+
retry,
29+
dismissible,
30+
defaultMessage,
31+
}) => {
32+
const message = getErrorMessage(error, defaultMessage || Language.unknownErrorMessage)
33+
const detail = getErrorDetail(error)
34+
const [showDetails, setShowDetails] = useState(false)
35+
const [isOpen, setOpen] = useState(true)
36+
37+
const styles = useStyles({ showDetails })
38+
39+
const toggleShowDetails = () => {
40+
setShowDetails(!showDetails)
41+
}
42+
43+
const closeError = () => {
44+
setOpen(false)
45+
}
46+
47+
if (!isOpen) {
48+
return null
49+
}
50+
51+
return (
52+
<Stack className={styles.root}>
53+
<Stack direction="row" alignItems="center" className={styles.messageBox}>
54+
<div>
55+
<span className={styles.errorMessage}>{message}</span>
56+
{!!detail && (
57+
<Link
58+
aria-expanded={showDetails}
59+
onClick={toggleShowDetails}
60+
className={styles.detailsLink}
61+
tabIndex={0}
62+
>
63+
{showDetails ? Language.lessDetails : Language.moreDetails}
64+
</Link>
65+
)}
66+
</div>
67+
{dismissible && (
68+
<IconButton onClick={closeError} className={styles.iconButton}>
69+
<CloseIcon className={styles.closeIcon} />
70+
</IconButton>
71+
)}
72+
</Stack>
73+
<Collapse in={showDetails}>
74+
<div className={styles.details}>{detail}</div>
75+
</Collapse>
76+
{retry && (
77+
<div className={styles.retry}>
78+
<Button size="small" onClick={retry} startIcon={<RefreshIcon />} variant="outlined">
79+
{Language.retryMessage}
80+
</Button>
81+
</div>
82+
)}
83+
</Stack>
84+
)
85+
}
86+
87+
interface StyleProps {
88+
showDetails?: boolean
89+
}
90+
91+
const useStyles = makeStyles<Theme, StyleProps>((theme) => ({
92+
root: {
93+
background: darken(theme.palette.error.main, 0.6),
94+
margin: `${theme.spacing(2)}px`,
95+
padding: `${theme.spacing(2)}px`,
96+
borderRadius: theme.shape.borderRadius,
97+
gap: 0,
98+
},
99+
messageBox: {
100+
justifyContent: "space-between",
101+
},
102+
errorMessage: {
103+
marginRight: `${theme.spacing(1)}px`,
104+
},
105+
detailsLink: {
106+
cursor: "pointer",
107+
},
108+
details: {
109+
marginTop: `${theme.spacing(2)}px`,
110+
padding: `${theme.spacing(2)}px`,
111+
background: darken(theme.palette.error.main, 0.7),
112+
borderRadius: theme.shape.borderRadius,
113+
},
114+
iconButton: {
115+
padding: 0,
116+
},
117+
closeIcon: {
118+
width: 25,
119+
height: 25,
120+
color: theme.palette.primary.contrastText,
121+
},
122+
retry: {
123+
marginTop: `${theme.spacing(2)}px`,
124+
},
125+
}))

0 commit comments

Comments
 (0)