Alerting: wrap top level components in ErrorBoundary (#34040)

This commit is contained in:
Domas 2021-05-18 13:56:31 +03:00 committed by GitHub
parent 8ecfad6995
commit 098b4fc319
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 182 additions and 124 deletions

View File

@ -54,3 +54,21 @@ import { ErrorWithStack } from '@grafana/ui';
<ArgsTable of={ErrorWithStack}/>
# 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' });
```

View File

@ -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<Props, State> {
}
}
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 <Alert />. Default 'alertbox' */
style?: 'page' | 'alertbox';
}
export class ErrorBoundaryAlert extends PureComponent<WithAlertBoxProps> {
static defaultProps: Partial<WithAlertBoxProps> = {
export class ErrorBoundaryAlert extends PureComponent<ErrorBoundaryAlertProps> {
static defaultProps: Partial<ErrorBoundaryAlertProps> = {
title: 'An unexpected error happened',
style: 'alertbox',
};
@ -86,3 +96,25 @@ export class ErrorBoundaryAlert extends PureComponent<WithAlertBoxProps> {
);
}
}
/**
* 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<P = {}>(
Component: ComponentType<P>,
errorBoundaryProps: Omit<ErrorBoundaryAlertProps, 'children'> = {}
): ComponentType<P> {
const comp = (props: P) => (
<ErrorBoundaryAlert {...errorBoundaryProps}>
<Component {...props} />
</ErrorBoundaryAlert>
);
comp.displayName = 'WithErrorBoundary';
return comp;
}

View File

@ -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';

View File

@ -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`

View File

@ -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' });

View File

@ -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' });

View File

@ -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<Array<{ error: SerializedError; dataSource: DataSourceInstanceSettings }>>(
(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<Array<{ error: SerializedError; dataSource: DataSourceInstanceSettings }>>(
(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 (
<AlertingPageWrapper pageId="alert-list" isLoading={loading && !haveResults}>
{(promReqeustErrors.length || rulerRequestErrors.length || grafanaPromError) && (
<Alert data-testid="cloud-rulessource-errors" title="Errors loading rules" severity="error">
{grafanaPromError && (
<div>Failed to load Grafana threshold rules state: {grafanaPromError.message || 'Unknown error.'}</div>
)}
{grafanaRulerError && (
<div>Failed to load Grafana threshold rules config: {grafanaRulerError.message || 'Unknown error.'}</div>
)}
{promReqeustErrors.map(({ dataSource, error }) => (
<div key={dataSource.name}>
Failed to load rules state from <a href={`datasources/edit/${dataSource.uid}`}>{dataSource.name}</a>:{' '}
{error.message || 'Unknown error.'}
</div>
))}
{rulerRequestErrors.map(({ dataSource, error }) => (
<div key={dataSource.name}>
Failed to load rules config from <a href={'datasources/edit/${dataSource.uid}'}>{dataSource.name}</a>:{' '}
{error.message || 'Unknown error.'}
</div>
))}
</Alert>
)}
{!showNewAlertSplash && (
<>
<RulesFilter />
<div className={styles.break} />
<div className={styles.buttonsContainer}>
<ButtonGroup>
<a href={urlUtil.renderUrl('alerting/list', { ...queryParams, view: 'group' })}>
<ToolbarButton variant={view === 'groups' ? 'active' : 'default'} icon="folder">
Groups
</ToolbarButton>
</a>
<a href={urlUtil.renderUrl('alerting/list', { ...queryParams, view: 'state' })}>
<ToolbarButton variant={view === 'state' ? 'active' : 'default'} icon="heart-rate">
State
</ToolbarButton>
</a>
</ButtonGroup>
<div />
{(contextSrv.hasEditPermissionInFolders || contextSrv.isEditor) && (
<LinkButton
href={urlUtil.renderUrl('alerting/new', { returnTo: location.pathname + location.search })}
icon="plus"
>
New alert rule
</LinkButton>
const combinedNamespaces = useCombinedRuleNamespaces();
const filteredNamespaces = useFilteredRules(combinedNamespaces);
return (
<AlertingPageWrapper pageId="alert-list" isLoading={loading && !haveResults}>
{(promReqeustErrors.length || rulerRequestErrors.length || grafanaPromError) && (
<Alert data-testid="cloud-rulessource-errors" title="Errors loading rules" severity="error">
{grafanaPromError && (
<div>Failed to load Grafana threshold rules state: {grafanaPromError.message || 'Unknown error.'}</div>
)}
</div>
</>
)}
{showNewAlertSplash && <NoRulesSplash />}
{haveResults && <ViewComponent namespaces={filteredNamespaces} />}
</AlertingPageWrapper>
);
};
{grafanaRulerError && (
<div>Failed to load Grafana threshold rules config: {grafanaRulerError.message || 'Unknown error.'}</div>
)}
{promReqeustErrors.map(({ dataSource, error }) => (
<div key={dataSource.name}>
Failed to load rules state from <a href={`datasources/edit/${dataSource.uid}`}>{dataSource.name}</a>:{' '}
{error.message || 'Unknown error.'}
</div>
))}
{rulerRequestErrors.map(({ dataSource, error }) => (
<div key={dataSource.name}>
Failed to load rules config from <a href={'datasources/edit/${dataSource.uid}'}>{dataSource.name}</a>:{' '}
{error.message || 'Unknown error.'}
</div>
))}
</Alert>
)}
{!showNewAlertSplash && (
<>
<RulesFilter />
<div className={styles.break} />
<div className={styles.buttonsContainer}>
<ButtonGroup>
<a href={urlUtil.renderUrl('alerting/list', { ...queryParams, view: 'group' })}>
<ToolbarButton variant={view === 'groups' ? 'active' : 'default'} icon="folder">
Groups
</ToolbarButton>
</a>
<a href={urlUtil.renderUrl('alerting/list', { ...queryParams, view: 'state' })}>
<ToolbarButton variant={view === 'state' ? 'active' : 'default'} icon="heart-rate">
State
</ToolbarButton>
</a>
</ButtonGroup>
<div />
{(contextSrv.hasEditPermissionInFolders || contextSrv.isEditor) && (
<LinkButton
href={urlUtil.renderUrl('alerting/new', { returnTo: location.pathname + location.search })}
icon="plus"
>
New alert rule
</LinkButton>
)}
</div>
</>
)}
{showNewAlertSplash && <NoRulesSplash />}
{haveResults && <ViewComponent namespaces={filteredNamespaces} />}
</AlertingPageWrapper>
);
},
{ style: 'page' }
);
const getStyles = (theme: GrafanaTheme) => ({
break: css`

View File

@ -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' });