AlertList.tsx 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. import { css, cx } from '@emotion/css';
  2. import { sortBy } from 'lodash';
  3. import React, { useState } from 'react';
  4. import { useAsync } from 'react-use';
  5. import { dateMath, dateTime, GrafanaTheme, PanelProps } from '@grafana/data';
  6. import { getBackendSrv, getTemplateSrv } from '@grafana/runtime';
  7. import { Card, CustomScrollbar, Icon, stylesFactory, useStyles } from '@grafana/ui';
  8. import alertDef from 'app/features/alerting/state/alertDef';
  9. import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
  10. import { AlertRuleDTO, AnnotationItemDTO } from 'app/types';
  11. import { AlertListOptions, ShowOption, SortOrder } from './types';
  12. export function AlertList(props: PanelProps<AlertListOptions>) {
  13. const [noAlertsMessage, setNoAlertsMessage] = useState('');
  14. const currentAlertState = useAsync(async () => {
  15. if (props.options.showOptions !== ShowOption.Current) {
  16. return;
  17. }
  18. const params: any = {
  19. state: getStateFilter(props.options.stateFilter),
  20. };
  21. const panel = getDashboardSrv().getCurrent()?.getPanelById(props.id)!;
  22. if (props.options.alertName) {
  23. params.query = getTemplateSrv().replace(props.options.alertName, panel.scopedVars);
  24. }
  25. if (props.options.folderId >= 0) {
  26. params.folderId = props.options.folderId;
  27. }
  28. if (props.options.dashboardTitle) {
  29. params.dashboardQuery = props.options.dashboardTitle;
  30. }
  31. if (props.options.dashboardAlerts) {
  32. params.dashboardId = getDashboardSrv().getCurrent()?.id;
  33. }
  34. if (props.options.tags) {
  35. params.dashboardTag = props.options.tags;
  36. }
  37. const alerts: AlertRuleDTO[] = await getBackendSrv().get(
  38. '/api/alerts',
  39. params,
  40. `alert-list-get-current-alert-state-${props.id}`
  41. );
  42. let currentAlerts = sortAlerts(
  43. props.options.sortOrder,
  44. alerts.map((al) => ({
  45. ...al,
  46. stateModel: alertDef.getStateDisplayModel(al.state),
  47. newStateDateAgo: dateTime(al.newStateDate).locale('en').fromNow(true),
  48. }))
  49. );
  50. if (currentAlerts.length > props.options.maxItems) {
  51. currentAlerts = currentAlerts.slice(0, props.options.maxItems);
  52. }
  53. setNoAlertsMessage(currentAlerts.length === 0 ? 'No alerts' : '');
  54. return currentAlerts;
  55. }, [
  56. props.options.showOptions,
  57. props.options.stateFilter.alerting,
  58. props.options.stateFilter.execution_error,
  59. props.options.stateFilter.no_data,
  60. props.options.stateFilter.ok,
  61. props.options.stateFilter.paused,
  62. props.options.stateFilter.pending,
  63. props.options.maxItems,
  64. props.options.tags,
  65. props.options.dashboardAlerts,
  66. props.options.dashboardTitle,
  67. props.options.folderId,
  68. props.options.alertName,
  69. props.options.sortOrder,
  70. props.timeRange,
  71. ]);
  72. const recentStateChanges = useAsync(async () => {
  73. if (props.options.showOptions !== ShowOption.RecentChanges) {
  74. return;
  75. }
  76. const params: any = {
  77. limit: props.options.maxItems,
  78. type: 'alert',
  79. newState: getStateFilter(props.options.stateFilter),
  80. };
  81. const currentDashboard = getDashboardSrv().getCurrent();
  82. if (props.options.dashboardAlerts) {
  83. params.dashboardId = currentDashboard?.id;
  84. }
  85. params.from = dateMath.parse(currentDashboard?.time.from)!.unix() * 1000;
  86. params.to = dateMath.parse(currentDashboard?.time.to)!.unix() * 1000;
  87. const data: AnnotationItemDTO[] = await getBackendSrv().get(
  88. '/api/annotations',
  89. params,
  90. `alert-list-get-state-changes-${props.id}`
  91. );
  92. const alertHistory = sortAlerts(
  93. props.options.sortOrder,
  94. data.map((al) => {
  95. return {
  96. ...al,
  97. time: currentDashboard?.formatDate(al.time, 'MMM D, YYYY HH:mm:ss'),
  98. stateModel: alertDef.getStateDisplayModel(al.newState),
  99. info: alertDef.getAlertAnnotationInfo(al),
  100. };
  101. })
  102. );
  103. setNoAlertsMessage(alertHistory.length === 0 ? 'No alerts in current time range' : '');
  104. return alertHistory;
  105. }, [
  106. props.options.showOptions,
  107. props.options.maxItems,
  108. props.options.stateFilter.alerting,
  109. props.options.stateFilter.execution_error,
  110. props.options.stateFilter.no_data,
  111. props.options.stateFilter.ok,
  112. props.options.stateFilter.paused,
  113. props.options.stateFilter.pending,
  114. props.options.dashboardAlerts,
  115. props.options.sortOrder,
  116. ]);
  117. const styles = useStyles(getStyles);
  118. return (
  119. <CustomScrollbar autoHeightMin="100%" autoHeightMax="100%">
  120. <div className={styles.container}>
  121. {noAlertsMessage && <div className={styles.noAlertsMessage}>{noAlertsMessage}</div>}
  122. <section>
  123. <ol className={styles.alertRuleList}>
  124. {props.options.showOptions === ShowOption.Current
  125. ? !currentAlertState.loading &&
  126. currentAlertState.value &&
  127. currentAlertState.value!.map((alert) => (
  128. <li className={styles.alertRuleItem} key={`alert-${alert.id}`}>
  129. <Card href={`${alert.url}?viewPanel=${alert.panelId}`} className={styles.cardContainer}>
  130. <Card.Heading>{alert.name}</Card.Heading>
  131. <Card.Figure className={cx(styles.alertRuleItemIcon, alert.stateModel.stateClass)}>
  132. <Icon name={alert.stateModel.iconClass} size="xl" className={styles.alertIcon} />
  133. </Card.Figure>
  134. <Card.Meta>
  135. <div className={styles.alertRuleItemText}>
  136. <span className={alert.stateModel.stateClass}>{alert.stateModel.text}</span>
  137. <span className={styles.alertRuleItemTime}> for {alert.newStateDateAgo}</span>
  138. </div>
  139. </Card.Meta>
  140. </Card>
  141. </li>
  142. ))
  143. : !recentStateChanges.loading &&
  144. recentStateChanges.value &&
  145. recentStateChanges.value.map((alert) => (
  146. <li className={styles.alertRuleItem} key={`alert-${alert.id}`}>
  147. <Card className={styles.cardContainer}>
  148. <Card.Heading>{alert.alertName}</Card.Heading>
  149. <Card.Figure className={cx(styles.alertRuleItemIcon, alert.stateModel.stateClass)}>
  150. <Icon name={alert.stateModel.iconClass} size="xl" />
  151. </Card.Figure>
  152. <Card.Meta>
  153. <span className={cx(styles.alertRuleItemText, alert.stateModel.stateClass)}>
  154. {alert.stateModel.text}
  155. </span>
  156. <span>{alert.time}</span>
  157. {alert.info && <span className={styles.alertRuleItemInfo}>{alert.info}</span>}
  158. </Card.Meta>
  159. </Card>
  160. </li>
  161. ))}
  162. </ol>
  163. </section>
  164. </div>
  165. </CustomScrollbar>
  166. );
  167. }
  168. function sortAlerts(sortOrder: SortOrder, alerts: any[]) {
  169. if (sortOrder === SortOrder.Importance) {
  170. // @ts-ignore
  171. return sortBy(alerts, (a) => alertDef.alertStateSortScore[a.state || a.newState]);
  172. } else if (sortOrder === SortOrder.TimeAsc) {
  173. return sortBy(alerts, (a) => new Date(a.newStateDate || a.time));
  174. } else if (sortOrder === SortOrder.TimeDesc) {
  175. return sortBy(alerts, (a) => new Date(a.newStateDate || a.time)).reverse();
  176. }
  177. const result = sortBy(alerts, (a) => (a.name || a.alertName).toLowerCase());
  178. if (sortOrder === SortOrder.AlphaDesc) {
  179. result.reverse();
  180. }
  181. return result;
  182. }
  183. function getStateFilter(stateFilter: Record<string, boolean>) {
  184. return Object.entries(stateFilter)
  185. .filter(([_, val]) => val)
  186. .map(([key, _]) => key);
  187. }
  188. const getStyles = stylesFactory((theme: GrafanaTheme) => ({
  189. cardContainer: css`
  190. padding: ${theme.spacing.xs} 0 ${theme.spacing.xxs} 0;
  191. line-height: ${theme.typography.lineHeight.md};
  192. margin-bottom: 0px;
  193. `,
  194. container: css`
  195. overflow-y: auto;
  196. height: 100%;
  197. `,
  198. alertRuleList: css`
  199. display: flex;
  200. flex-wrap: wrap;
  201. justify-content: space-between;
  202. list-style-type: none;
  203. `,
  204. alertRuleItem: css`
  205. display: flex;
  206. align-items: center;
  207. width: 100%;
  208. height: 100%;
  209. background: ${theme.colors.bg2};
  210. padding: ${theme.spacing.xs} ${theme.spacing.sm};
  211. border-radius: ${theme.border.radius.md};
  212. margin-bottom: ${theme.spacing.xs};
  213. `,
  214. alertRuleItemIcon: css`
  215. display: flex;
  216. justify-content: center;
  217. align-items: center;
  218. width: ${theme.spacing.xl};
  219. padding: 0 ${theme.spacing.xs} 0 ${theme.spacing.xxs};
  220. margin-right: 0px;
  221. `,
  222. alertRuleItemText: css`
  223. font-weight: ${theme.typography.weight.bold};
  224. font-size: ${theme.typography.size.sm};
  225. margin: 0;
  226. `,
  227. alertRuleItemTime: css`
  228. color: ${theme.colors.textWeak};
  229. font-weight: normal;
  230. white-space: nowrap;
  231. `,
  232. alertRuleItemInfo: css`
  233. font-weight: normal;
  234. flex-grow: 2;
  235. display: flex;
  236. align-items: flex-end;
  237. `,
  238. noAlertsMessage: css`
  239. display: flex;
  240. align-items: center;
  241. justify-content: center;
  242. width: 100%;
  243. height: 100%;
  244. `,
  245. alertIcon: css`
  246. margin-right: ${theme.spacing.xs};
  247. `,
  248. }));