mirror of
https://github.com/grafana/grafana.git
synced 2024-11-22 00:47:38 -06:00
Alerting: wrap top level components in ErrorBoundary (#34040)
This commit is contained in:
parent
8ecfad6995
commit
098b4fc319
@ -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' });
|
||||
|
||||
```
|
@ -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;
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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`
|
||||
|
@ -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' });
|
||||
|
@ -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' });
|
||||
|
@ -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`
|
||||
|
@ -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' });
|
||||
|
Loading…
Reference in New Issue
Block a user