mirror of
https://github.com/grafana/grafana.git
synced 2024-11-22 08:56:43 -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}/>
|
<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 { captureException } from '@sentry/browser';
|
||||||
import { Alert } from '../Alert/Alert';
|
import { Alert } from '../Alert/Alert';
|
||||||
import { ErrorWithStack } from './ErrorWithStack';
|
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;
|
title?: string;
|
||||||
|
|
||||||
|
/** Component to be wrapped with an error boundary */
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
|
||||||
|
/** 'page' will render full page error with stacktrace. 'alertbox' will render an <Alert />. Default 'alertbox' */
|
||||||
style?: 'page' | 'alertbox';
|
style?: 'page' | 'alertbox';
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ErrorBoundaryAlert extends PureComponent<WithAlertBoxProps> {
|
export class ErrorBoundaryAlert extends PureComponent<ErrorBoundaryAlertProps> {
|
||||||
static defaultProps: Partial<WithAlertBoxProps> = {
|
static defaultProps: Partial<ErrorBoundaryAlertProps> = {
|
||||||
title: 'An unexpected error happened',
|
title: 'An unexpected error happened',
|
||||||
style: 'alertbox',
|
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 { JSONFormatter } from './JSONFormatter/JSONFormatter';
|
||||||
export { JsonExplorer } from './JSONFormatter/json_explorer/json_explorer';
|
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 { ErrorWithStack } from './ErrorBoundary/ErrorWithStack';
|
||||||
export { AlphaNotice } from './AlphaNotice/AlphaNotice';
|
export { AlphaNotice } from './AlphaNotice/AlphaNotice';
|
||||||
export { DataSourceHttpSettings } from './DataSourceSettings/DataSourceHttpSettings';
|
export { DataSourceHttpSettings } from './DataSourceSettings/DataSourceHttpSettings';
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
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 { useDispatch } from 'react-redux';
|
||||||
import { Redirect } from 'react-router-dom';
|
import { Redirect } from 'react-router-dom';
|
||||||
import { Receiver } from 'app/plugins/datasource/alertmanager/types';
|
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) => ({
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
break: css`
|
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 React, { FC, useEffect } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { Redirect, Route, RouteChildrenProps, Switch, useLocation } from 'react-router-dom';
|
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 { css } from '@emotion/css';
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
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 Page from 'app/core/components/Page/Page';
|
||||||
import { contextSrv } from 'app/core/services/context_srv';
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
import { useCleanup } from 'app/core/hooks/useCleanup';
|
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 { 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 { SerializedError } from '@reduxjs/toolkit';
|
||||||
import React, { FC, useEffect, useMemo } from 'react';
|
import React, { useEffect, useMemo } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
|
import { AlertingPageWrapper } from './components/AlertingPageWrapper';
|
||||||
import { NoRulesSplash } from './components/rules/NoRulesCTA';
|
import { NoRulesSplash } from './components/rules/NoRulesCTA';
|
||||||
@ -25,126 +25,129 @@ const VIEWS = {
|
|||||||
state: RuleListStateView,
|
state: RuleListStateView,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RuleList: FC = () => {
|
export const RuleList = withErrorBoundary(
|
||||||
const dispatch = useDispatch();
|
() => {
|
||||||
const styles = useStyles(getStyles);
|
const dispatch = useDispatch();
|
||||||
const rulesDataSourceNames = useMemo(getAllRulesSourceNames, []);
|
const styles = useStyles(getStyles);
|
||||||
const location = useLocation();
|
const rulesDataSourceNames = useMemo(getAllRulesSourceNames, []);
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
const [queryParams] = useQueryParams();
|
const [queryParams] = useQueryParams();
|
||||||
|
|
||||||
const view = VIEWS[queryParams['view'] as keyof typeof VIEWS]
|
const view = VIEWS[queryParams['view'] as keyof typeof VIEWS]
|
||||||
? (queryParams['view'] as keyof typeof VIEWS)
|
? (queryParams['view'] as keyof typeof VIEWS)
|
||||||
: 'groups';
|
: 'groups';
|
||||||
|
|
||||||
const ViewComponent = VIEWS[view];
|
const ViewComponent = VIEWS[view];
|
||||||
|
|
||||||
// fetch rules, then poll every RULE_LIST_POLL_INTERVAL_MS
|
// fetch rules, then poll every RULE_LIST_POLL_INTERVAL_MS
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(fetchAllPromAndRulerRulesAction());
|
dispatch(fetchAllPromAndRulerRulesAction());
|
||||||
const interval = setInterval(() => dispatch(fetchAllPromAndRulerRulesAction()), RULE_LIST_POLL_INTERVAL_MS);
|
const interval = setInterval(() => dispatch(fetchAllPromAndRulerRulesAction()), RULE_LIST_POLL_INTERVAL_MS);
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
};
|
};
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
const promRuleRequests = useUnifiedAlertingSelector((state) => state.promRules);
|
const promRuleRequests = useUnifiedAlertingSelector((state) => state.promRules);
|
||||||
const rulerRuleRequests = useUnifiedAlertingSelector((state) => state.rulerRules);
|
const rulerRuleRequests = useUnifiedAlertingSelector((state) => state.rulerRules);
|
||||||
|
|
||||||
const dispatched = rulesDataSourceNames.some(
|
const dispatched = rulesDataSourceNames.some(
|
||||||
(name) => promRuleRequests[name]?.dispatched || rulerRuleRequests[name]?.dispatched
|
(name) => promRuleRequests[name]?.dispatched || rulerRuleRequests[name]?.dispatched
|
||||||
);
|
);
|
||||||
const loading = rulesDataSourceNames.some(
|
const loading = rulesDataSourceNames.some(
|
||||||
(name) => promRuleRequests[name]?.loading || rulerRuleRequests[name]?.loading
|
(name) => promRuleRequests[name]?.loading || rulerRuleRequests[name]?.loading
|
||||||
);
|
);
|
||||||
const haveResults = rulesDataSourceNames.some(
|
const haveResults = rulesDataSourceNames.some(
|
||||||
(name) =>
|
(name) =>
|
||||||
(promRuleRequests[name]?.result?.length && !promRuleRequests[name]?.error) ||
|
(promRuleRequests[name]?.result?.length && !promRuleRequests[name]?.error) ||
|
||||||
(Object.keys(rulerRuleRequests[name]?.result || {}).length && !rulerRuleRequests[name]?.error)
|
(Object.keys(rulerRuleRequests[name]?.result || {}).length && !rulerRuleRequests[name]?.error)
|
||||||
);
|
);
|
||||||
|
|
||||||
const [promReqeustErrors, rulerRequestErrors] = useMemo(
|
const [promReqeustErrors, rulerRequestErrors] = useMemo(
|
||||||
() =>
|
() =>
|
||||||
[promRuleRequests, rulerRuleRequests].map((requests) =>
|
[promRuleRequests, rulerRuleRequests].map((requests) =>
|
||||||
getRulesDataSources().reduce<Array<{ error: SerializedError; dataSource: DataSourceInstanceSettings }>>(
|
getRulesDataSources().reduce<Array<{ error: SerializedError; dataSource: DataSourceInstanceSettings }>>(
|
||||||
(result, dataSource) => {
|
(result, dataSource) => {
|
||||||
const error = requests[dataSource.name]?.error;
|
const error = requests[dataSource.name]?.error;
|
||||||
if (requests[dataSource.name] && error && !isRulerNotSupportedResponse(requests[dataSource.name])) {
|
if (requests[dataSource.name] && error && !isRulerNotSupportedResponse(requests[dataSource.name])) {
|
||||||
return [...result, { dataSource, error }];
|
return [...result, { dataSource, error }];
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
[promRuleRequests, rulerRuleRequests]
|
[promRuleRequests, rulerRuleRequests]
|
||||||
);
|
);
|
||||||
|
|
||||||
const grafanaPromError = promRuleRequests[GRAFANA_RULES_SOURCE_NAME]?.error;
|
const grafanaPromError = promRuleRequests[GRAFANA_RULES_SOURCE_NAME]?.error;
|
||||||
const grafanaRulerError = rulerRuleRequests[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 combinedNamespaces = useCombinedRuleNamespaces();
|
||||||
const filteredNamespaces = useFilteredRules(combinedNamespaces);
|
const filteredNamespaces = useFilteredRules(combinedNamespaces);
|
||||||
return (
|
return (
|
||||||
<AlertingPageWrapper pageId="alert-list" isLoading={loading && !haveResults}>
|
<AlertingPageWrapper pageId="alert-list" isLoading={loading && !haveResults}>
|
||||||
{(promReqeustErrors.length || rulerRequestErrors.length || grafanaPromError) && (
|
{(promReqeustErrors.length || rulerRequestErrors.length || grafanaPromError) && (
|
||||||
<Alert data-testid="cloud-rulessource-errors" title="Errors loading rules" severity="error">
|
<Alert data-testid="cloud-rulessource-errors" title="Errors loading rules" severity="error">
|
||||||
{grafanaPromError && (
|
{grafanaPromError && (
|
||||||
<div>Failed to load Grafana threshold rules state: {grafanaPromError.message || 'Unknown error.'}</div>
|
<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>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
{grafanaRulerError && (
|
||||||
</>
|
<div>Failed to load Grafana threshold rules config: {grafanaRulerError.message || 'Unknown error.'}</div>
|
||||||
)}
|
)}
|
||||||
{showNewAlertSplash && <NoRulesSplash />}
|
{promReqeustErrors.map(({ dataSource, error }) => (
|
||||||
{haveResults && <ViewComponent namespaces={filteredNamespaces} />}
|
<div key={dataSource.name}>
|
||||||
</AlertingPageWrapper>
|
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) => ({
|
const getStyles = (theme: GrafanaTheme) => ({
|
||||||
break: css`
|
break: css`
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { FC, useEffect, useCallback } from 'react';
|
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 { useDispatch } from 'react-redux';
|
||||||
import { Redirect, Route, RouteChildrenProps, Switch, useLocation } from 'react-router-dom';
|
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