AlertTab.tsx 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. import React, { PureComponent } from 'react';
  2. import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux';
  3. import { EventBusSrv } from '@grafana/data';
  4. import { selectors } from '@grafana/e2e-selectors';
  5. import { AngularComponent, config, getAngularLoader, getDataSourceSrv } from '@grafana/runtime';
  6. import { Alert, Button, ConfirmModal, Container, CustomScrollbar, HorizontalGroup, IconName, Modal } from '@grafana/ui';
  7. import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
  8. import { getPanelStateForModel } from 'app/features/panel/state/selectors';
  9. import { AppNotificationSeverity, StoreState } from 'app/types';
  10. import { AlertState } from '../../plugins/datasource/alertmanager/types';
  11. import { PanelNotSupported } from '../dashboard/components/PanelEditor/PanelNotSupported';
  12. import { DashboardModel } from '../dashboard/state/DashboardModel';
  13. import { PanelModel } from '../dashboard/state/PanelModel';
  14. import StateHistory from './StateHistory';
  15. import { TestRuleResult } from './TestRuleResult';
  16. import { getAlertingValidationMessage } from './getAlertingValidationMessage';
  17. interface AngularPanelController {
  18. _enableAlert: () => void;
  19. alertState: AlertState | null;
  20. render: () => void;
  21. refresh: () => void;
  22. }
  23. interface OwnProps {
  24. dashboard: DashboardModel;
  25. panel: PanelModel;
  26. }
  27. interface ConnectedProps {
  28. angularPanelComponent?: AngularComponent | null;
  29. }
  30. interface DispatchProps {}
  31. export type Props = OwnProps & ConnectedProps & DispatchProps;
  32. interface State {
  33. validationMessage: string;
  34. showStateHistory: boolean;
  35. showDeleteConfirmation: boolean;
  36. showTestRule: boolean;
  37. }
  38. class UnConnectedAlertTab extends PureComponent<Props, State> {
  39. element?: HTMLDivElement | null;
  40. component?: AngularComponent;
  41. panelCtrl?: AngularPanelController;
  42. state: State = {
  43. validationMessage: '',
  44. showStateHistory: false,
  45. showDeleteConfirmation: false,
  46. showTestRule: false,
  47. };
  48. async componentDidMount() {
  49. if (config.angularSupportEnabled) {
  50. await import(/* webpackChunkName: "AlertTabCtrl" */ 'app/features/alerting/AlertTabCtrl');
  51. this.loadAlertTab();
  52. } else {
  53. // TODO probably need to migrate AlertTab to react
  54. alert('Angular support disabled, legacy alerting cannot function without angular support');
  55. }
  56. }
  57. onAngularPanelUpdated = () => {
  58. this.forceUpdate();
  59. };
  60. componentDidUpdate(prevProps: Props) {
  61. this.loadAlertTab();
  62. }
  63. componentWillUnmount() {
  64. if (this.component) {
  65. this.component.destroy();
  66. }
  67. }
  68. async loadAlertTab() {
  69. const { panel, angularPanelComponent } = this.props;
  70. if (!this.element || this.component) {
  71. return;
  72. }
  73. if (angularPanelComponent) {
  74. const scope = angularPanelComponent.getScope();
  75. // When full page reloading in edit mode the angular panel has on fully compiled & instantiated yet
  76. if (!scope.$$childHead) {
  77. setTimeout(() => {
  78. this.forceUpdate();
  79. });
  80. return;
  81. }
  82. this.panelCtrl = scope.$$childHead.ctrl;
  83. } else {
  84. this.panelCtrl = this.getReactAlertPanelCtrl();
  85. }
  86. const loader = getAngularLoader();
  87. const template = '<alert-tab />';
  88. const scopeProps = { ctrl: this.panelCtrl };
  89. this.component = loader.load(this.element, scopeProps, template);
  90. const validationMessage = await getAlertingValidationMessage(
  91. panel.transformations,
  92. panel.targets,
  93. getDataSourceSrv(),
  94. panel.datasource
  95. );
  96. if (validationMessage) {
  97. this.setState({ validationMessage });
  98. }
  99. }
  100. getReactAlertPanelCtrl() {
  101. return {
  102. panel: this.props.panel,
  103. events: new EventBusSrv(),
  104. render: () => {
  105. this.props.panel.render();
  106. },
  107. } as any;
  108. }
  109. onAddAlert = () => {
  110. this.panelCtrl?._enableAlert();
  111. this.component?.digest();
  112. this.forceUpdate();
  113. };
  114. onToggleModal = (prop: keyof Omit<State, 'validationMessage'>) => {
  115. const value = this.state[prop];
  116. this.setState({ ...this.state, [prop]: !value });
  117. };
  118. renderTestRule = () => {
  119. if (!this.state.showTestRule) {
  120. return null;
  121. }
  122. const { panel, dashboard } = this.props;
  123. const onDismiss = () => this.onToggleModal('showTestRule');
  124. return (
  125. <Modal isOpen={true} icon="bug" title="Testing rule" onDismiss={onDismiss} onClickBackdrop={onDismiss}>
  126. <TestRuleResult panel={panel} dashboard={dashboard} />
  127. </Modal>
  128. );
  129. };
  130. renderDeleteConfirmation = () => {
  131. if (!this.state.showDeleteConfirmation) {
  132. return null;
  133. }
  134. const { panel } = this.props;
  135. const onDismiss = () => this.onToggleModal('showDeleteConfirmation');
  136. return (
  137. <ConfirmModal
  138. isOpen={true}
  139. icon="trash-alt"
  140. title="Delete"
  141. body={
  142. <div>
  143. Are you sure you want to delete this alert rule?
  144. <br />
  145. <small>You need to save dashboard for the delete to take effect.</small>
  146. </div>
  147. }
  148. confirmText="Delete alert"
  149. onDismiss={onDismiss}
  150. onConfirm={() => {
  151. delete panel.alert;
  152. panel.thresholds = [];
  153. if (this.panelCtrl) {
  154. this.panelCtrl.alertState = null;
  155. this.panelCtrl.render();
  156. }
  157. this.component?.digest();
  158. onDismiss();
  159. }}
  160. />
  161. );
  162. };
  163. renderStateHistory = () => {
  164. if (!this.state.showStateHistory) {
  165. return null;
  166. }
  167. const { panel, dashboard } = this.props;
  168. const onDismiss = () => this.onToggleModal('showStateHistory');
  169. return (
  170. <Modal isOpen={true} icon="history" title="State history" onDismiss={onDismiss} onClickBackdrop={onDismiss}>
  171. <StateHistory dashboard={dashboard} panelId={panel.id} onRefresh={() => this.panelCtrl?.refresh()} />
  172. </Modal>
  173. );
  174. };
  175. render() {
  176. const { alert, transformations } = this.props.panel;
  177. const { validationMessage } = this.state;
  178. const hasTransformations = transformations && transformations.length > 0;
  179. if (!alert && validationMessage) {
  180. return <PanelNotSupported message={validationMessage} />;
  181. }
  182. const model = {
  183. title: 'Panel has no alert rule defined',
  184. buttonIcon: 'bell' as IconName,
  185. onClick: this.onAddAlert,
  186. buttonTitle: 'Create Alert',
  187. };
  188. return (
  189. <>
  190. <CustomScrollbar autoHeightMin="100%">
  191. <Container padding="md">
  192. <div aria-label={selectors.components.AlertTab.content}>
  193. {alert && hasTransformations && (
  194. <Alert
  195. severity={AppNotificationSeverity.Error}
  196. title="Transformations are not supported in alert queries"
  197. />
  198. )}
  199. <div ref={(element) => (this.element = element)} />
  200. {alert && (
  201. <HorizontalGroup>
  202. <Button onClick={() => this.onToggleModal('showStateHistory')} variant="secondary">
  203. State history
  204. </Button>
  205. <Button onClick={() => this.onToggleModal('showTestRule')} variant="secondary">
  206. Test rule
  207. </Button>
  208. <Button onClick={() => this.onToggleModal('showDeleteConfirmation')} variant="destructive">
  209. Delete
  210. </Button>
  211. </HorizontalGroup>
  212. )}
  213. {!alert && !validationMessage && <EmptyListCTA {...model} />}
  214. </div>
  215. </Container>
  216. </CustomScrollbar>
  217. {this.renderTestRule()}
  218. {this.renderDeleteConfirmation()}
  219. {this.renderStateHistory()}
  220. </>
  221. );
  222. }
  223. }
  224. const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (state, props) => {
  225. return {
  226. angularPanelComponent: getPanelStateForModel(state, props.panel)?.angularComponent,
  227. };
  228. };
  229. const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = {};
  230. export const AlertTab = connect(mapStateToProps, mapDispatchToProps)(UnConnectedAlertTab);