3.1 KB121 lines
Blame
1/**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8import type {ReactNode} from 'react';
9
10import React, {Component, useState} from 'react';
11import {Icon} from './Icon';
12import type {Placement} from './Tooltip';
13import {Tooltip} from './Tooltip';
14
15import './error-notice.css';
16
17export function ErrorNotice({
18 title,
19 description,
20 details,
21 error,
22 buttons,
23 startExpanded,
24}: {
25 title: React.ReactNode;
26 description?: React.ReactNode;
27 /** Hidden details shown when expanded */
28 details?: React.ReactNode;
29 error?: Error;
30 buttons?: Array<React.ReactNode>;
31 startExpanded?: boolean;
32}) {
33 const [isExpanded, setIsExpanded] = useState(startExpanded === true);
34 return (
35 <div className="error-notice" onClick={() => setIsExpanded(e => !e)}>
36 <div className="error-notice-left">
37 <div className="error-notice-arrow">
38 <Icon icon={isExpanded ? 'triangle-down' : 'triangle-right'} />
39 </div>
40 <div className="error-notice-content">
41 <span className="error-notice-title">{title}</span>
42 <span className="error-notice-byline">{description ?? error?.message}</span>
43 {isExpanded ? (
44 <div
45 className="error-notice-details"
46 onClick={e => {
47 // don't close the notice when clicking/text selecting the details
48 e.stopPropagation();
49 }}>
50 {details}
51 {error != null && (
52 <span className="error-notice-stack-trace">{error.stack ?? error.message}</span>
53 )}
54 </div>
55 ) : null}
56 </div>
57 </div>
58 {buttons ? <div className="error-notice-buttons">{buttons}</div> : null}
59 </div>
60 );
61}
62
63type Props = {
64 children: React.ReactNode;
65};
66
67type State = {error: Error | null};
68
69export class ErrorBoundary extends Component<Props, State> {
70 constructor(props: Props) {
71 super(props);
72 this.state = {error: null};
73 }
74
75 static getDerivedStateFromError(error: Error) {
76 return {error};
77 }
78
79 render() {
80 if (this.state.error != null) {
81 return <ErrorNotice title="Something went wrong" error={this.state.error} />;
82 }
83
84 return this.props.children;
85 }
86}
87
88/**
89 * One-line error message, which shows the full ErrorNotice in a tooltip on hover.
90 */
91export function InlineErrorBadge({
92 children,
93 error,
94 title,
95 placement,
96}: {
97 children: ReactNode;
98 error: Error;
99 title?: ReactNode;
100 placement?: Placement;
101}) {
102 return (
103 <div className="inline-error-badge">
104 <Tooltip component={TooltipErrorDetails(error, title)} placement={placement}>
105 <Icon icon="error" slot="start" />
106 <span>{children}</span>
107 </Tooltip>
108 </div>
109 );
110}
111
112function TooltipErrorDetails(error: Error, title?: ReactNode) {
113 return function Component() {
114 return (
115 <div className="inline-error-tooltip">
116 <ErrorNotice title={title ?? error.message} error={error} startExpanded />
117 </div>
118 );
119 };
120}
121