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