mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
5f5f51b3bf
commit
0a73ac36ad
@ -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);
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
|
40
public/app/features/alerting/unified/utils/annotations.tsx
Normal file
40
public/app/features/alerting/unified/utils/annotations.tsx
Normal 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;
|
||||
}
|
@ -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>,
|
||||
|
Loading…
Reference in New Issue
Block a user