diff --git a/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.mdx b/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.mdx index 042e977a10b..ac5d481b320 100644 --- a/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.mdx +++ b/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.mdx @@ -54,3 +54,21 @@ import { ErrorWithStack } from '@grafana/ui'; +# withErrorBoundary + +a HOC to conveniently wrap component in an error boundary + +### Usage + +```jsx +import { withErrorBoundary } from '@grafana/ui'; + +interface MyCompProps { + ... +} + +const MyComp = withErrorBoundary((props: MyCompProps) => { + return <>... +}, { style: 'page' }); + +``` \ No newline at end of file diff --git a/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.tsx b/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.tsx index bcdfcd3186f..3ec96221a1b 100644 --- a/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.tsx +++ b/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.tsx @@ -1,4 +1,4 @@ -import React, { PureComponent, ReactNode } from 'react'; +import React, { PureComponent, ReactNode, ComponentType } from 'react'; import { captureException } from '@sentry/browser'; import { Alert } from '../Alert/Alert'; import { ErrorWithStack } from './ErrorWithStack'; @@ -46,14 +46,24 @@ export class ErrorBoundary extends PureComponent { } } -interface WithAlertBoxProps { +/** + * Props for the ErrorBoundaryAlert component + * + * @public + */ +export interface ErrorBoundaryAlertProps { + /** Title for the error boundary alert */ title?: string; + + /** Component to be wrapped with an error boundary */ children: ReactNode; + + /** 'page' will render full page error with stacktrace. 'alertbox' will render an . Default 'alertbox' */ style?: 'page' | 'alertbox'; } -export class ErrorBoundaryAlert extends PureComponent { - static defaultProps: Partial = { +export class ErrorBoundaryAlert extends PureComponent { + static defaultProps: Partial = { title: 'An unexpected error happened', style: 'alertbox', }; @@ -86,3 +96,25 @@ export class ErrorBoundaryAlert extends PureComponent { ); } } + +/** + * HOC for wrapping a component in an error boundary. + * + * @param Component - the react component to wrap in error boundary + * @param errorBoundaryProps - error boundary options + * + * @public + */ +export function withErrorBoundary

( + Component: ComponentType

, + errorBoundaryProps: Omit = {} +): ComponentType

{ + const comp = (props: P) => ( + + + + ); + comp.displayName = 'WithErrorBoundary'; + + return comp; +} diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 2a7cf210424..6ac0c481cbc 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -132,7 +132,12 @@ export { FeatureBadge, FeatureInfoBox } from './InfoBox/FeatureInfoBox'; export { JSONFormatter } from './JSONFormatter/JSONFormatter'; export { JsonExplorer } from './JSONFormatter/json_explorer/json_explorer'; -export { ErrorBoundary, ErrorBoundaryAlert } from './ErrorBoundary/ErrorBoundary'; +export { + ErrorBoundary, + ErrorBoundaryAlert, + ErrorBoundaryAlertProps, + withErrorBoundary, +} from './ErrorBoundary/ErrorBoundary'; export { ErrorWithStack } from './ErrorBoundary/ErrorWithStack'; export { AlphaNotice } from './AlphaNotice/AlphaNotice'; export { DataSourceHttpSettings } from './DataSourceSettings/DataSourceHttpSettings'; diff --git a/public/app/core/hooks/useAsyncAction.ts b/public/app/core/hooks/useAsyncAction.ts deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/public/app/features/alerting/unified/AmRoutes.tsx b/public/app/features/alerting/unified/AmRoutes.tsx index ce12516fb3c..1a5e2fa7198 100644 --- a/public/app/features/alerting/unified/AmRoutes.tsx +++ b/public/app/features/alerting/unified/AmRoutes.tsx @@ -1,7 +1,7 @@ import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; -import { Alert, LoadingPlaceholder, useStyles2 } from '@grafana/ui'; +import { Alert, LoadingPlaceholder, useStyles2, withErrorBoundary } from '@grafana/ui'; import { useDispatch } from 'react-redux'; import { Redirect } from 'react-router-dom'; import { Receiver } from 'app/plugins/datasource/alertmanager/types'; @@ -132,7 +132,7 @@ const AmRoutes: FC = () => { ); }; -export default AmRoutes; +export default withErrorBoundary(AmRoutes, { style: 'page' }); const getStyles = (theme: GrafanaTheme2) => ({ break: css` diff --git a/public/app/features/alerting/unified/Receivers.tsx b/public/app/features/alerting/unified/Receivers.tsx index 9fa0f949524..46bc85d0cec 100644 --- a/public/app/features/alerting/unified/Receivers.tsx +++ b/public/app/features/alerting/unified/Receivers.tsx @@ -1,4 +1,4 @@ -import { Alert, LoadingPlaceholder } from '@grafana/ui'; +import { Alert, LoadingPlaceholder, withErrorBoundary } from '@grafana/ui'; import React, { FC, useEffect } from 'react'; import { useDispatch } from 'react-redux'; import { Redirect, Route, RouteChildrenProps, Switch, useLocation } from 'react-router-dom'; @@ -104,4 +104,4 @@ const Receivers: FC = () => { ); }; -export default Receivers; +export default withErrorBoundary(Receivers, { style: 'page' }); diff --git a/public/app/features/alerting/unified/RuleEditor.tsx b/public/app/features/alerting/unified/RuleEditor.tsx index 42864bc3e97..65c5b3cf9a5 100644 --- a/public/app/features/alerting/unified/RuleEditor.tsx +++ b/public/app/features/alerting/unified/RuleEditor.tsx @@ -1,6 +1,6 @@ import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; -import { Alert, LinkButton, LoadingPlaceholder, useStyles2 } from '@grafana/ui'; +import { Alert, LinkButton, LoadingPlaceholder, useStyles2, withErrorBoundary } from '@grafana/ui'; import Page from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/services/context_srv'; import { useCleanup } from 'app/core/hooks/useCleanup'; @@ -82,4 +82,4 @@ const warningStyles = (theme: GrafanaTheme2) => ({ `, }); -export default RuleEditor; +export default withErrorBoundary(RuleEditor, { style: 'page' }); diff --git a/public/app/features/alerting/unified/RuleList.tsx b/public/app/features/alerting/unified/RuleList.tsx index db6f03f503c..14e6f25076d 100644 --- a/public/app/features/alerting/unified/RuleList.tsx +++ b/public/app/features/alerting/unified/RuleList.tsx @@ -1,7 +1,7 @@ import { DataSourceInstanceSettings, GrafanaTheme, urlUtil } from '@grafana/data'; -import { useStyles, ButtonGroup, ToolbarButton, Alert, LinkButton } from '@grafana/ui'; +import { useStyles, ButtonGroup, ToolbarButton, Alert, LinkButton, withErrorBoundary } from '@grafana/ui'; import { SerializedError } from '@reduxjs/toolkit'; -import React, { FC, useEffect, useMemo } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import { AlertingPageWrapper } from './components/AlertingPageWrapper'; import { NoRulesSplash } from './components/rules/NoRulesCTA'; @@ -25,126 +25,129 @@ const VIEWS = { state: RuleListStateView, }; -export const RuleList: FC = () => { - const dispatch = useDispatch(); - const styles = useStyles(getStyles); - const rulesDataSourceNames = useMemo(getAllRulesSourceNames, []); - const location = useLocation(); +export const RuleList = withErrorBoundary( + () => { + const dispatch = useDispatch(); + const styles = useStyles(getStyles); + const rulesDataSourceNames = useMemo(getAllRulesSourceNames, []); + const location = useLocation(); - const [queryParams] = useQueryParams(); + const [queryParams] = useQueryParams(); - const view = VIEWS[queryParams['view'] as keyof typeof VIEWS] - ? (queryParams['view'] as keyof typeof VIEWS) - : 'groups'; + const view = VIEWS[queryParams['view'] as keyof typeof VIEWS] + ? (queryParams['view'] as keyof typeof VIEWS) + : 'groups'; - const ViewComponent = VIEWS[view]; + const ViewComponent = VIEWS[view]; - // fetch rules, then poll every RULE_LIST_POLL_INTERVAL_MS - useEffect(() => { - dispatch(fetchAllPromAndRulerRulesAction()); - const interval = setInterval(() => dispatch(fetchAllPromAndRulerRulesAction()), RULE_LIST_POLL_INTERVAL_MS); - return () => { - clearInterval(interval); - }; - }, [dispatch]); + // fetch rules, then poll every RULE_LIST_POLL_INTERVAL_MS + useEffect(() => { + dispatch(fetchAllPromAndRulerRulesAction()); + const interval = setInterval(() => dispatch(fetchAllPromAndRulerRulesAction()), RULE_LIST_POLL_INTERVAL_MS); + return () => { + clearInterval(interval); + }; + }, [dispatch]); - const promRuleRequests = useUnifiedAlertingSelector((state) => state.promRules); - const rulerRuleRequests = useUnifiedAlertingSelector((state) => state.rulerRules); + const promRuleRequests = useUnifiedAlertingSelector((state) => state.promRules); + const rulerRuleRequests = useUnifiedAlertingSelector((state) => state.rulerRules); - const dispatched = rulesDataSourceNames.some( - (name) => promRuleRequests[name]?.dispatched || rulerRuleRequests[name]?.dispatched - ); - const loading = rulesDataSourceNames.some( - (name) => promRuleRequests[name]?.loading || rulerRuleRequests[name]?.loading - ); - const haveResults = rulesDataSourceNames.some( - (name) => - (promRuleRequests[name]?.result?.length && !promRuleRequests[name]?.error) || - (Object.keys(rulerRuleRequests[name]?.result || {}).length && !rulerRuleRequests[name]?.error) - ); + const dispatched = rulesDataSourceNames.some( + (name) => promRuleRequests[name]?.dispatched || rulerRuleRequests[name]?.dispatched + ); + const loading = rulesDataSourceNames.some( + (name) => promRuleRequests[name]?.loading || rulerRuleRequests[name]?.loading + ); + const haveResults = rulesDataSourceNames.some( + (name) => + (promRuleRequests[name]?.result?.length && !promRuleRequests[name]?.error) || + (Object.keys(rulerRuleRequests[name]?.result || {}).length && !rulerRuleRequests[name]?.error) + ); - const [promReqeustErrors, rulerRequestErrors] = useMemo( - () => - [promRuleRequests, rulerRuleRequests].map((requests) => - getRulesDataSources().reduce>( - (result, dataSource) => { - const error = requests[dataSource.name]?.error; - if (requests[dataSource.name] && error && !isRulerNotSupportedResponse(requests[dataSource.name])) { - return [...result, { dataSource, error }]; - } - return result; - }, - [] - ) - ), - [promRuleRequests, rulerRuleRequests] - ); + const [promReqeustErrors, rulerRequestErrors] = useMemo( + () => + [promRuleRequests, rulerRuleRequests].map((requests) => + getRulesDataSources().reduce>( + (result, dataSource) => { + const error = requests[dataSource.name]?.error; + if (requests[dataSource.name] && error && !isRulerNotSupportedResponse(requests[dataSource.name])) { + return [...result, { dataSource, error }]; + } + return result; + }, + [] + ) + ), + [promRuleRequests, rulerRuleRequests] + ); - const grafanaPromError = promRuleRequests[GRAFANA_RULES_SOURCE_NAME]?.error; - const grafanaRulerError = rulerRuleRequests[GRAFANA_RULES_SOURCE_NAME]?.error; + const grafanaPromError = promRuleRequests[GRAFANA_RULES_SOURCE_NAME]?.error; + const grafanaRulerError = rulerRuleRequests[GRAFANA_RULES_SOURCE_NAME]?.error; - const showNewAlertSplash = dispatched && !loading && !haveResults; + const showNewAlertSplash = dispatched && !loading && !haveResults; - const combinedNamespaces = useCombinedRuleNamespaces(); - const filteredNamespaces = useFilteredRules(combinedNamespaces); - return ( - - {(promReqeustErrors.length || rulerRequestErrors.length || grafanaPromError) && ( - - {grafanaPromError && ( -

Failed to load Grafana threshold rules state: {grafanaPromError.message || 'Unknown error.'}
- )} - {grafanaRulerError && ( -
Failed to load Grafana threshold rules config: {grafanaRulerError.message || 'Unknown error.'}
- )} - {promReqeustErrors.map(({ dataSource, error }) => ( -
- Failed to load rules state from {dataSource.name}:{' '} - {error.message || 'Unknown error.'} -
- ))} - {rulerRequestErrors.map(({ dataSource, error }) => ( -
- Failed to load rules config from {dataSource.name}:{' '} - {error.message || 'Unknown error.'} -
- ))} -
- )} - {!showNewAlertSplash && ( - <> - -
-
- - - - Groups - - - - - State - - - -
- {(contextSrv.hasEditPermissionInFolders || contextSrv.isEditor) && ( - - New alert rule - + const combinedNamespaces = useCombinedRuleNamespaces(); + const filteredNamespaces = useFilteredRules(combinedNamespaces); + return ( + + {(promReqeustErrors.length || rulerRequestErrors.length || grafanaPromError) && ( + + {grafanaPromError && ( +
Failed to load Grafana threshold rules state: {grafanaPromError.message || 'Unknown error.'}
)} -
- - )} - {showNewAlertSplash && } - {haveResults && } - - ); -}; + {grafanaRulerError && ( +
Failed to load Grafana threshold rules config: {grafanaRulerError.message || 'Unknown error.'}
+ )} + {promReqeustErrors.map(({ dataSource, error }) => ( +
+ Failed to load rules state from {dataSource.name}:{' '} + {error.message || 'Unknown error.'} +
+ ))} + {rulerRequestErrors.map(({ dataSource, error }) => ( +
+ Failed to load rules config from {dataSource.name}:{' '} + {error.message || 'Unknown error.'} +
+ ))} + + )} + {!showNewAlertSplash && ( + <> + +
+
+ + + + Groups + + + + + State + + + +
+ {(contextSrv.hasEditPermissionInFolders || contextSrv.isEditor) && ( + + New alert rule + + )} +
+ + )} + {showNewAlertSplash && } + {haveResults && } + + ); + }, + { style: 'page' } +); const getStyles = (theme: GrafanaTheme) => ({ break: css` diff --git a/public/app/features/alerting/unified/Silences.tsx b/public/app/features/alerting/unified/Silences.tsx index f0c0449e089..8e6f8490daa 100644 --- a/public/app/features/alerting/unified/Silences.tsx +++ b/public/app/features/alerting/unified/Silences.tsx @@ -1,5 +1,5 @@ import React, { FC, useEffect, useCallback } from 'react'; -import { Alert, LoadingPlaceholder } from '@grafana/ui'; +import { Alert, LoadingPlaceholder, withErrorBoundary } from '@grafana/ui'; import { useDispatch } from 'react-redux'; import { Redirect, Route, RouteChildrenProps, Switch, useLocation } from 'react-router-dom'; @@ -93,4 +93,4 @@ const Silences: FC = () => { ); }; -export default Silences; +export default withErrorBoundary(Silences, { style: 'page' });