The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/features/alerting/unified/RuleViewer.tsx

216 lines
7.1 KiB

import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useObservable } from 'react-use';
import { css } from '@emotion/css';
import { GrafanaTheme2, LoadingState, PanelData } from '@grafana/data';
import {
withErrorBoundary,
useStyles2,
Alert,
LoadingPlaceholder,
PanelChromeLoadingIndicator,
Icon,
} from '@grafana/ui';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { AlertingQueryRunner } from './state/AlertingQueryRunner';
import { useCombinedRule } from './hooks/useCombinedRule';
import { alertRuleToQueries } from './utils/query';
import { RuleState } from './components/rules/RuleState';
import { getRulesSourceByName } from './utils/datasource';
import { DetailsField } from './components/DetailsField';
import { RuleHealth } from './components/rules/RuleHealth';
import { RuleViewerVisualization } from './components/rule-viewer/RuleViewerVisualization';
import { RuleDetailsActionButtons } from './components/rules/RuleDetailsActionButtons';
import { RuleDetailsMatchingInstances } from './components/rules/RuleDetailsMatchingInstances';
import { RuleDetailsDataSources } from './components/rules/RuleDetailsDataSources';
import { RuleViewerLayout, RuleViewerLayoutContent } from './components/rule-viewer/RuleViewerLayout';
import { AlertLabels } from './components/AlertLabels';
import { RuleDetailsExpression } from './components/rules/RuleDetailsExpression';
import { RuleDetailsAnnotations } from './components/rules/RuleDetailsAnnotations';
import * as ruleId from './utils/rule-id';
import { AlertQuery } from '../../../types/unified-alerting-dto';
type RuleViewerProps = GrafanaRouteComponentProps<{ id?: string; sourceName?: string }>;
const errorMessage = 'Could not find data source for rule';
const errorTitle = 'Could not view rule';
const pageTitle = 'Alerting / View rule';
export function RuleViewer({ match }: RuleViewerProps) {
const styles = useStyles2(getStyles);
const { id, sourceName } = match.params;
const identifier = ruleId.tryParse(id, true);
const { loading, error, result: rule } = useCombinedRule(identifier, sourceName);
const runner = useMemo(() => new AlertingQueryRunner(), []);
const data = useObservable(runner.get());
const queries2 = useMemo(() => alertRuleToQueries(rule), [rule]);
const [queries, setQueries] = useState<AlertQuery[]>([]);
const onRunQueries = useCallback(() => {
if (queries.length > 0) {
runner.run(queries);
}
}, [queries, runner]);
useEffect(() => {
setQueries(queries2);
}, [queries2]);
useEffect(() => {
onRunQueries();
}, [onRunQueries]);
useEffect(() => {
return () => runner.destroy();
}, [runner]);
const onChangeQuery = useCallback((query: AlertQuery) => {
setQueries((queries) =>
queries.map((q) => {
if (q.refId === query.refId) {
return query;
}
return q;
})
);
}, []);
if (!sourceName) {
return (
<RuleViewerLayout title={pageTitle}>
<Alert title={errorTitle}>
<details className={styles.errorMessage}>{errorMessage}</details>
</Alert>
</RuleViewerLayout>
);
}
const rulesSource = getRulesSourceByName(sourceName);
if (loading) {
return (
<RuleViewerLayout title={pageTitle}>
<LoadingPlaceholder text="Loading rule..." />
</RuleViewerLayout>
);
}
if (error || !rulesSource) {
return (
<RuleViewerLayout title={pageTitle}>
<Alert title={errorTitle}>
<details className={styles.errorMessage}>
{error?.message ?? errorMessage}
<br />
{!!error?.stack && error.stack}
</details>
</Alert>
</RuleViewerLayout>
);
}
if (!rule) {
return (
<RuleViewerLayout title={pageTitle}>
<span>Rule could not be found.</span>
</RuleViewerLayout>
);
}
const annotations = Object.entries(rule.annotations).filter(([_, value]) => !!value.trim());
return (
<RuleViewerLayout wrapInContent={false} title={pageTitle}>
<RuleViewerLayoutContent>
<div>
<h4>
<Icon name="bell" size="lg" /> {rule.name}
</h4>
<RuleState rule={rule} isCreating={false} isDeleting={false} />
<RuleDetailsActionButtons rule={rule} rulesSource={rulesSource} />
</div>
<div className={styles.details}>
<div className={styles.leftSide}>
{rule.promRule && (
<DetailsField label="Health" horizontal={true}>
<RuleHealth rule={rule.promRule} />
</DetailsField>
)}
{!!rule.labels && !!Object.keys(rule.labels).length && (
<DetailsField label="Labels" horizontal={true}>
<AlertLabels labels={rule.labels} />
</DetailsField>
)}
<RuleDetailsExpression rulesSource={rulesSource} rule={rule} annotations={annotations} />
<RuleDetailsAnnotations annotations={annotations} />
</div>
<div className={styles.rightSide}>
<RuleDetailsDataSources rule={rule} rulesSource={rulesSource} />
<DetailsField label="Namespace / Group">{`${rule.namespace.name} / ${rule.group.name}`}</DetailsField>
</div>
</div>
<div>
<RuleDetailsMatchingInstances promRule={rule.promRule} />
</div>
</RuleViewerLayoutContent>
{data && Object.keys(data).length > 0 && (
<>
<div className={styles.queriesTitle}>
Query results <PanelChromeLoadingIndicator loading={isLoading(data)} onCancel={() => runner.cancel()} />
</div>
<RuleViewerLayoutContent padding={0}>
<div className={styles.queries}>
{queries.map((query) => {
return (
<div key={query.refId} className={styles.query}>
<RuleViewerVisualization
query={query}
data={data && data[query.refId]}
onChangeQuery={onChangeQuery}
/>
</div>
);
})}
</div>
</RuleViewerLayoutContent>
</>
)}
</RuleViewerLayout>
);
}
function isLoading(data: Record<string, PanelData>): boolean {
return !!Object.values(data).find((d) => d.state === LoadingState.Loading);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
errorMessage: css`
white-space: pre-wrap;
`,
queries: css`
height: 100%;
width: 100%;
`,
queriesTitle: css`
padding: ${theme.spacing(2, 0.5)};
font-size: ${theme.typography.h5.fontSize};
font-weight: ${theme.typography.fontWeightBold};
font-family: ${theme.typography.h5.fontFamily};
`,
query: css`
border-bottom: 1px solid ${theme.colors.border.medium};
padding: ${theme.spacing(2)};
`,
details: css`
display: flex;
flex-direction: row;
`,
leftSide: css`
flex: 1;
`,
rightSide: css`
padding-left: 90px;
width: 300px;
`,
};
};
export default withErrorBoundary(RuleViewer, { style: 'page' });