123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253 |
- import { css } from '@emotion/css';
- import React, { useCallback, useEffect, useMemo, useState } from 'react';
- import { useObservable } from 'react-use';
- import { GrafanaTheme2, LoadingState, PanelData } from '@grafana/data';
- import {
- Alert,
- Button,
- Icon,
- LoadingPlaceholder,
- PanelChromeLoadingIndicator,
- useStyles2,
- VerticalGroup,
- withErrorBoundary,
- } from '@grafana/ui';
- import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
- import { AlertQuery } from '../../../types/unified-alerting-dto';
- import { AlertLabels } from './components/AlertLabels';
- import { DetailsField } from './components/DetailsField';
- import { RuleViewerLayout, RuleViewerLayoutContent } from './components/rule-viewer/RuleViewerLayout';
- import { RuleViewerVisualization } from './components/rule-viewer/RuleViewerVisualization';
- import { RuleDetailsActionButtons } from './components/rules/RuleDetailsActionButtons';
- import { RuleDetailsAnnotations } from './components/rules/RuleDetailsAnnotations';
- import { RuleDetailsDataSources } from './components/rules/RuleDetailsDataSources';
- import { RuleDetailsExpression } from './components/rules/RuleDetailsExpression';
- import { RuleDetailsFederatedSources } from './components/rules/RuleDetailsFederatedSources';
- import { RuleDetailsMatchingInstances } from './components/rules/RuleDetailsMatchingInstances';
- import { RuleHealth } from './components/rules/RuleHealth';
- import { RuleState } from './components/rules/RuleState';
- import { useAlertQueriesStatus } from './hooks/useAlertQueriesStatus';
- import { useCombinedRule } from './hooks/useCombinedRule';
- import { AlertingQueryRunner } from './state/AlertingQueryRunner';
- import { getRulesSourceByName } from './utils/datasource';
- import { alertRuleToQueries } from './utils/query';
- import * as ruleId from './utils/rule-id';
- import { isFederatedRuleGroup } from './utils/rules';
- 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 } = match.params;
- const identifier = ruleId.tryParse(id, true);
- const { loading, error, result: rule } = useCombinedRule(identifier, identifier?.ruleSourceName);
- const runner = useMemo(() => new AlertingQueryRunner(), []);
- const data = useObservable(runner.get());
- const queries2 = useMemo(() => alertRuleToQueries(rule), [rule]);
- const [queries, setQueries] = useState<AlertQuery[]>([]);
- const { allDataSourcesAvailable } = useAlertQueriesStatus(queries2);
- const onRunQueries = useCallback(() => {
- if (queries.length > 0 && allDataSourcesAvailable) {
- runner.run(queries);
- }
- }, [queries, runner, allDataSourcesAvailable]);
- useEffect(() => {
- setQueries(queries2);
- }, [queries2]);
- useEffect(() => {
- if (allDataSourcesAvailable) {
- onRunQueries();
- }
- }, [onRunQueries, allDataSourcesAvailable]);
- 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 (!identifier?.ruleSourceName) {
- return (
- <RuleViewerLayout title={pageTitle}>
- <Alert title={errorTitle}>
- <details className={styles.errorMessage}>{errorMessage}</details>
- </Alert>
- </RuleViewerLayout>
- );
- }
- const rulesSource = getRulesSourceByName(identifier.ruleSourceName);
- 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());
- const isFederatedRule = isFederatedRuleGroup(rule.group);
- return (
- <RuleViewerLayout wrapInContent={false} title={pageTitle}>
- {isFederatedRule && (
- <Alert severity="info" title="This rule is part of a federated rule group.">
- <VerticalGroup>
- Federated rule groups are currently an experimental feature.
- <Button fill="text" icon="book">
- <a href="https://grafana.com/docs/metrics-enterprise/latest/tenant-management/tenant-federation/#cross-tenant-alerting-and-recording-rule-federation">
- Read documentation
- </a>
- </Button>
- </VerticalGroup>
- </Alert>
- )}
- <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} />
- {isFederatedRule && <RuleDetailsFederatedSources group={rule.group} />}
- <DetailsField label="Namespace / Group">{`${rule.namespace.name} / ${rule.group.name}`}</DetailsField>
- </div>
- </div>
- <div>
- <RuleDetailsMatchingInstances rule={rule} />
- </div>
- </RuleViewerLayoutContent>
- {!isFederatedRule && 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>
- </>
- )}
- {!isFederatedRule && !allDataSourcesAvailable && (
- <Alert title="Query not available" severity="warning" className={styles.queryWarning}>
- Cannot display the query preview. Some of the data sources used in the queries are not available.
- </Alert>
- )}
- </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)};
- `,
- queryWarning: css`
- margin: ${theme.spacing(4, 0)};
- `,
- details: css`
- display: flex;
- flex-direction: row;
- `,
- leftSide: css`
- flex: 1;
- `,
- rightSide: css`
- padding-left: 90px;
- width: 300px;
- `,
- };
- };
- export default withErrorBoundary(RuleViewer, { style: 'page' });
|