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