RuleViewer.tsx 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. import { css } from '@emotion/css';
  2. import React, { useCallback, useEffect, useMemo, useState } from 'react';
  3. import { useObservable } from 'react-use';
  4. import { GrafanaTheme2, LoadingState, PanelData } from '@grafana/data';
  5. import {
  6. Alert,
  7. Button,
  8. Icon,
  9. LoadingPlaceholder,
  10. PanelChromeLoadingIndicator,
  11. useStyles2,
  12. VerticalGroup,
  13. withErrorBoundary,
  14. } from '@grafana/ui';
  15. import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
  16. import { AlertQuery } from '../../../types/unified-alerting-dto';
  17. import { AlertLabels } from './components/AlertLabels';
  18. import { DetailsField } from './components/DetailsField';
  19. import { RuleViewerLayout, RuleViewerLayoutContent } from './components/rule-viewer/RuleViewerLayout';
  20. import { RuleViewerVisualization } from './components/rule-viewer/RuleViewerVisualization';
  21. import { RuleDetailsActionButtons } from './components/rules/RuleDetailsActionButtons';
  22. import { RuleDetailsAnnotations } from './components/rules/RuleDetailsAnnotations';
  23. import { RuleDetailsDataSources } from './components/rules/RuleDetailsDataSources';
  24. import { RuleDetailsExpression } from './components/rules/RuleDetailsExpression';
  25. import { RuleDetailsFederatedSources } from './components/rules/RuleDetailsFederatedSources';
  26. import { RuleDetailsMatchingInstances } from './components/rules/RuleDetailsMatchingInstances';
  27. import { RuleHealth } from './components/rules/RuleHealth';
  28. import { RuleState } from './components/rules/RuleState';
  29. import { useAlertQueriesStatus } from './hooks/useAlertQueriesStatus';
  30. import { useCombinedRule } from './hooks/useCombinedRule';
  31. import { AlertingQueryRunner } from './state/AlertingQueryRunner';
  32. import { getRulesSourceByName } from './utils/datasource';
  33. import { alertRuleToQueries } from './utils/query';
  34. import * as ruleId from './utils/rule-id';
  35. import { isFederatedRuleGroup } from './utils/rules';
  36. type RuleViewerProps = GrafanaRouteComponentProps<{ id?: string; sourceName?: string }>;
  37. const errorMessage = 'Could not find data source for rule';
  38. const errorTitle = 'Could not view rule';
  39. const pageTitle = 'Alerting / View rule';
  40. export function RuleViewer({ match }: RuleViewerProps) {
  41. const styles = useStyles2(getStyles);
  42. const { id } = match.params;
  43. const identifier = ruleId.tryParse(id, true);
  44. const { loading, error, result: rule } = useCombinedRule(identifier, identifier?.ruleSourceName);
  45. const runner = useMemo(() => new AlertingQueryRunner(), []);
  46. const data = useObservable(runner.get());
  47. const queries2 = useMemo(() => alertRuleToQueries(rule), [rule]);
  48. const [queries, setQueries] = useState<AlertQuery[]>([]);
  49. const { allDataSourcesAvailable } = useAlertQueriesStatus(queries2);
  50. const onRunQueries = useCallback(() => {
  51. if (queries.length > 0 && allDataSourcesAvailable) {
  52. runner.run(queries);
  53. }
  54. }, [queries, runner, allDataSourcesAvailable]);
  55. useEffect(() => {
  56. setQueries(queries2);
  57. }, [queries2]);
  58. useEffect(() => {
  59. if (allDataSourcesAvailable) {
  60. onRunQueries();
  61. }
  62. }, [onRunQueries, allDataSourcesAvailable]);
  63. useEffect(() => {
  64. return () => runner.destroy();
  65. }, [runner]);
  66. const onChangeQuery = useCallback((query: AlertQuery) => {
  67. setQueries((queries) =>
  68. queries.map((q) => {
  69. if (q.refId === query.refId) {
  70. return query;
  71. }
  72. return q;
  73. })
  74. );
  75. }, []);
  76. if (!identifier?.ruleSourceName) {
  77. return (
  78. <RuleViewerLayout title={pageTitle}>
  79. <Alert title={errorTitle}>
  80. <details className={styles.errorMessage}>{errorMessage}</details>
  81. </Alert>
  82. </RuleViewerLayout>
  83. );
  84. }
  85. const rulesSource = getRulesSourceByName(identifier.ruleSourceName);
  86. if (loading) {
  87. return (
  88. <RuleViewerLayout title={pageTitle}>
  89. <LoadingPlaceholder text="Loading rule..." />
  90. </RuleViewerLayout>
  91. );
  92. }
  93. if (error || !rulesSource) {
  94. return (
  95. <RuleViewerLayout title={pageTitle}>
  96. <Alert title={errorTitle}>
  97. <details className={styles.errorMessage}>
  98. {error?.message ?? errorMessage}
  99. <br />
  100. {!!error?.stack && error.stack}
  101. </details>
  102. </Alert>
  103. </RuleViewerLayout>
  104. );
  105. }
  106. if (!rule) {
  107. return (
  108. <RuleViewerLayout title={pageTitle}>
  109. <span>Rule could not be found.</span>
  110. </RuleViewerLayout>
  111. );
  112. }
  113. const annotations = Object.entries(rule.annotations).filter(([_, value]) => !!value.trim());
  114. const isFederatedRule = isFederatedRuleGroup(rule.group);
  115. return (
  116. <RuleViewerLayout wrapInContent={false} title={pageTitle}>
  117. {isFederatedRule && (
  118. <Alert severity="info" title="This rule is part of a federated rule group.">
  119. <VerticalGroup>
  120. Federated rule groups are currently an experimental feature.
  121. <Button fill="text" icon="book">
  122. <a href="https://grafana.com/docs/metrics-enterprise/latest/tenant-management/tenant-federation/#cross-tenant-alerting-and-recording-rule-federation">
  123. Read documentation
  124. </a>
  125. </Button>
  126. </VerticalGroup>
  127. </Alert>
  128. )}
  129. <RuleViewerLayoutContent>
  130. <div>
  131. <h4>
  132. <Icon name="bell" size="lg" /> {rule.name}
  133. </h4>
  134. <RuleState rule={rule} isCreating={false} isDeleting={false} />
  135. <RuleDetailsActionButtons rule={rule} rulesSource={rulesSource} />
  136. </div>
  137. <div className={styles.details}>
  138. <div className={styles.leftSide}>
  139. {rule.promRule && (
  140. <DetailsField label="Health" horizontal={true}>
  141. <RuleHealth rule={rule.promRule} />
  142. </DetailsField>
  143. )}
  144. {!!rule.labels && !!Object.keys(rule.labels).length && (
  145. <DetailsField label="Labels" horizontal={true}>
  146. <AlertLabels labels={rule.labels} />
  147. </DetailsField>
  148. )}
  149. <RuleDetailsExpression rulesSource={rulesSource} rule={rule} annotations={annotations} />
  150. <RuleDetailsAnnotations annotations={annotations} />
  151. </div>
  152. <div className={styles.rightSide}>
  153. <RuleDetailsDataSources rule={rule} rulesSource={rulesSource} />
  154. {isFederatedRule && <RuleDetailsFederatedSources group={rule.group} />}
  155. <DetailsField label="Namespace / Group">{`${rule.namespace.name} / ${rule.group.name}`}</DetailsField>
  156. </div>
  157. </div>
  158. <div>
  159. <RuleDetailsMatchingInstances rule={rule} />
  160. </div>
  161. </RuleViewerLayoutContent>
  162. {!isFederatedRule && data && Object.keys(data).length > 0 && (
  163. <>
  164. <div className={styles.queriesTitle}>
  165. Query results <PanelChromeLoadingIndicator loading={isLoading(data)} onCancel={() => runner.cancel()} />
  166. </div>
  167. <RuleViewerLayoutContent padding={0}>
  168. <div className={styles.queries}>
  169. {queries.map((query) => {
  170. return (
  171. <div key={query.refId} className={styles.query}>
  172. <RuleViewerVisualization
  173. query={query}
  174. data={data && data[query.refId]}
  175. onChangeQuery={onChangeQuery}
  176. />
  177. </div>
  178. );
  179. })}
  180. </div>
  181. </RuleViewerLayoutContent>
  182. </>
  183. )}
  184. {!isFederatedRule && !allDataSourcesAvailable && (
  185. <Alert title="Query not available" severity="warning" className={styles.queryWarning}>
  186. Cannot display the query preview. Some of the data sources used in the queries are not available.
  187. </Alert>
  188. )}
  189. </RuleViewerLayout>
  190. );
  191. }
  192. function isLoading(data: Record<string, PanelData>): boolean {
  193. return !!Object.values(data).find((d) => d.state === LoadingState.Loading);
  194. }
  195. const getStyles = (theme: GrafanaTheme2) => {
  196. return {
  197. errorMessage: css`
  198. white-space: pre-wrap;
  199. `,
  200. queries: css`
  201. height: 100%;
  202. width: 100%;
  203. `,
  204. queriesTitle: css`
  205. padding: ${theme.spacing(2, 0.5)};
  206. font-size: ${theme.typography.h5.fontSize};
  207. font-weight: ${theme.typography.fontWeightBold};
  208. font-family: ${theme.typography.h5.fontFamily};
  209. `,
  210. query: css`
  211. border-bottom: 1px solid ${theme.colors.border.medium};
  212. padding: ${theme.spacing(2)};
  213. `,
  214. queryWarning: css`
  215. margin: ${theme.spacing(4, 0)};
  216. `,
  217. details: css`
  218. display: flex;
  219. flex-direction: row;
  220. `,
  221. leftSide: css`
  222. flex: 1;
  223. `,
  224. rightSide: css`
  225. padding-left: 90px;
  226. width: 300px;
  227. `,
  228. };
  229. };
  230. export default withErrorBoundary(RuleViewer, { style: 'page' });