Alerting: Add dashboard and panel links to rule and instance annotations (#63243)

* Add dashboard and panel links in rule and instance annotations

* Use clean annotations in RuleViewer
This commit is contained in:
Konrad Lalik 2023-02-22 13:17:13 +01:00 committed by GitHub
parent 5f5f51b3bf
commit 0a73ac36ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 81 additions and 15 deletions

View File

@ -37,6 +37,7 @@ import { RuleState } from './components/rules/RuleState';
import { useAlertQueriesStatus } from './hooks/useAlertQueriesStatus';
import { useCombinedRule } from './hooks/useCombinedRule';
import { AlertingQueryRunner } from './state/AlertingQueryRunner';
import { useCleanAnnotations } from './utils/annotations';
import { getRulesSourceByName } from './utils/datasource';
import { alertRuleToQueries } from './utils/query';
import * as ruleId from './utils/rule-id';
@ -59,6 +60,7 @@ export function RuleViewer({ match }: RuleViewerProps) {
const runner = useMemo(() => new AlertingQueryRunner(), []);
const data = useObservable(runner.get());
const queries = useMemo(() => alertRuleToQueries(rule), [rule]);
const annotations = useCleanAnnotations(rule?.annotations || {});
const [evaluationTimeRanges, setEvaluationTimeRanges] = useState<Record<string, RelativeTimeRange>>({});
@ -146,7 +148,6 @@ export function RuleViewer({ match }: RuleViewerProps) {
);
}
const annotations = Object.entries(rule.annotations).filter(([_, value]) => !!value.trim());
const isFederatedRule = isFederatedRuleGroup(rule.group);
const isProvisioned = isGrafanaRulerRule(rule.rulerRule) && Boolean(rule.rulerRule.grafana_alert.provenance);

View File

@ -1,7 +1,7 @@
import { css } from '@emotion/css';
import React, { FC } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { GrafanaTheme2, textUtil } from '@grafana/data';
import { Tooltip, useStyles2 } from '@grafana/ui';
import { Annotation, annotationLabels } from '../utils/constants';
@ -15,9 +15,10 @@ const wellableAnnotationKeys = ['message', 'description'];
interface Props {
annotationKey: string;
value: string;
valueLink?: string;
}
export const AnnotationDetailsField: FC<Props> = ({ annotationKey, value }) => {
export const AnnotationDetailsField: FC<Props> = ({ annotationKey, value, valueLink }) => {
const label = annotationLabels[annotationKey as Annotation] ? (
<Tooltip content={annotationKey} placement="top" theme="info">
<span>{annotationLabels[annotationKey as Annotation]}</span>
@ -28,26 +29,34 @@ export const AnnotationDetailsField: FC<Props> = ({ annotationKey, value }) => {
return (
<DetailsField label={label} horizontal={true}>
<AnnotationValue annotationKey={annotationKey} value={value} />
<AnnotationValue annotationKey={annotationKey} value={value} valueLink={valueLink} />
</DetailsField>
);
};
const AnnotationValue: FC<Props> = ({ annotationKey, value }) => {
const AnnotationValue: FC<Props> = ({ annotationKey, value, valueLink }) => {
const styles = useStyles2(getStyles);
const needsWell = wellableAnnotationKeys.includes(annotationKey);
const needsLink = value && value.startsWith('http');
const needsExternalLink = value && value.startsWith('http');
const tokenizeValue = <Tokenize input={value} delimiter={['{{', '}}']} />;
if (valueLink) {
return (
<a href={textUtil.sanitizeUrl(valueLink)} className={styles.link}>
{value}
</a>
);
}
if (needsWell) {
return <Well className={styles.well}>{tokenizeValue}</Well>;
}
if (needsLink) {
if (needsExternalLink) {
return (
<a href={value} target="__blank" className={styles.link}>
<a href={textUtil.sanitizeUrl(value)} target="__blank" className={styles.link}>
{value}
</a>
);

View File

@ -2,6 +2,7 @@ import React, { FC } from 'react';
import { Alert } from 'app/types/unified-alerting';
import { useAnnotationLinks, useCleanAnnotations } from '../../utils/annotations';
import { AnnotationDetailsField } from '../AnnotationDetailsField';
import { DetailsField } from '../DetailsField';
@ -10,7 +11,8 @@ interface Props {
}
export const AlertInstanceDetails: FC<Props> = ({ instance }) => {
const annotations = (Object.entries(instance.annotations || {}) || []).filter(([_, value]) => !!value.trim());
const annotations = useCleanAnnotations(instance.annotations);
const annotationLinks = useAnnotationLinks(annotations);
return (
<div>
@ -19,9 +21,11 @@ export const AlertInstanceDetails: FC<Props> = ({ instance }) => {
{instance.value}
</DetailsField>
)}
{annotations.map(([key, value]) => (
<AnnotationDetailsField key={key} annotationKey={key} value={value} />
))}
{annotations.map(([key, value]) => {
return (
<AnnotationDetailsField key={key} annotationKey={key} value={value} valueLink={annotationLinks.get(key)} />
);
})}
</div>
);
};

View File

@ -5,6 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
import { CombinedRule } from 'app/types/unified-alerting';
import { useCleanAnnotations } from '../../utils/annotations';
import { isRecordingRulerRule } from '../../utils/rules';
import { AlertLabels } from '../AlertLabels';
import { DetailsField } from '../DetailsField';
@ -30,7 +31,7 @@ export const RuleDetails: FC<Props> = ({ rule }) => {
namespace: { rulesSource },
} = rule;
const annotations = Object.entries(rule.annotations).filter(([_, value]) => !!value.trim());
const annotations = useCleanAnnotations(rule.annotations);
return (
<div>

View File

@ -3,6 +3,7 @@ import React from 'react';
import { useStyles2 } from '@grafana/ui';
import { useAnnotationLinks } from '../../utils/annotations';
import { AnnotationDetailsField } from '../AnnotationDetailsField';
type Props = {
@ -10,9 +11,11 @@ type Props = {
};
export function RuleDetailsAnnotations(props: Props): JSX.Element | null {
const { annotations } = props;
const styles = useStyles2(getStyles);
const { annotations } = props;
const annotationLinks = useAnnotationLinks(annotations);
if (annotations.length === 0) {
return null;
}
@ -20,7 +23,7 @@ export function RuleDetailsAnnotations(props: Props): JSX.Element | null {
return (
<div className={styles.annotations}>
{annotations.map(([key, value]) => (
<AnnotationDetailsField key={key} annotationKey={key} value={value} />
<AnnotationDetailsField key={key} annotationKey={key} value={value} valueLink={annotationLinks.get(key)} />
))}
</div>
);

View File

@ -0,0 +1,40 @@
import { useMemo } from 'react';
import { Annotations } from 'app/types/unified-alerting-dto';
import { Annotation } from './constants';
import { makeDashboardLink, makePanelLink } from './misc';
export function usePanelAndDashboardIds(annotations: Array<[string, string]>): {
dashboardUID?: string;
panelId?: string;
} {
return {
dashboardUID: annotations.find(([key]) => key === Annotation.dashboardUID)?.[1],
panelId: annotations.find(([key]) => key === Annotation.panelID)?.[1],
};
}
/**
* Removes annotations with empty or whitespace values
*/
export function useCleanAnnotations(annotations: Annotations): Array<[string, string]> {
return useMemo(() => {
return Object.entries(annotations || {}).filter(([_, value]) => !!value.trim());
}, [annotations]);
}
export function useAnnotationLinks(annotations: Array<[string, string]>): Map<string, string> {
const links = new Map<string, string>();
const { panelId, dashboardUID } = usePanelAndDashboardIds(annotations);
if (dashboardUID) {
links.set(Annotation.dashboardUID, makeDashboardLink(dashboardUID));
}
if (dashboardUID && panelId) {
links.set(Annotation.panelID, makePanelLink(dashboardUID, panelId));
}
return links;
}

View File

@ -110,6 +110,14 @@ export function makeFolderSettingsLink(folder: FolderDTO): string {
return createUrl(`/dashboards/f/${folder.uid}/${folder.title}/settings`);
}
export function makeDashboardLink(dashboardUID: string): string {
return createUrl(`/d/${encodeURIComponent(dashboardUID)}`);
}
export function makePanelLink(dashboardUID: string, panelId: string): string {
return createUrl(`/d/${encodeURIComponent(dashboardUID)}`, { viewPanel: panelId });
}
// keep retrying fn if it's error passes shouldRetry(error) and timeout has not elapsed yet
export function retryWhile<T, E = Error>(
fn: () => Promise<T>,