import { css, cx } from '@emotion/css'; import { keyBy, startCase, uniqueId } from 'lodash'; import * as React from 'react'; import { DataSourceInstanceSettings, DataSourceRef, GrafanaTheme2, PanelData, urlUtil } from '@grafana/data'; import { secondsToHms } from '@grafana/data/src/datetime/rangeutil'; import { config } from '@grafana/runtime'; import { Preview } from '@grafana/sql/src/components/visual-query-builder/Preview'; import { Alert, Badge, ErrorBoundaryAlert, LinkButton, Stack, Text, useStyles2 } from '@grafana/ui'; import { CombinedRule } from 'app/types/unified-alerting'; import { AlertDataQuery, AlertQuery } from '../../../types/unified-alerting-dto'; import { isExpressionQuery } from '../../expressions/guards'; import { ExpressionQuery, ExpressionQueryType, ReducerMode, downsamplingTypes, reducerModes, reducerTypes, thresholdFunctions, upsamplingTypes, } from '../../expressions/types'; import alertDef, { EvalFunction } from '../state/alertDef'; import { Spacer } from './components/Spacer'; import { WithReturnButton } from './components/WithReturnButton'; import { ExpressionResult } from './components/expressions/Expression'; import { ThresholdDefinition, getThresholdsForQueries } from './components/rule-editor/util'; import { RuleViewerVisualization } from './components/rule-viewer/RuleViewerVisualization'; import { DatasourceModelPreview } from './components/rule-viewer/tabs/Query/DataSourceModelPreview'; import { AlertRuleAction, useAlertRuleAbility } from './hooks/useAbilities'; interface GrafanaRuleViewerProps { rule: CombinedRule; queries: AlertQuery[]; condition: string; evalDataByQuery?: Record; } export function GrafanaRuleQueryViewer({ rule, queries, condition, evalDataByQuery = {} }: GrafanaRuleViewerProps) { const dsByUid = keyBy(Object.values(config.datasources), (ds) => ds.uid); const dataQueries = queries.filter((q) => !isExpressionQuery(q.model)); const expressions = queries.filter((q) => isExpressionQuery(q.model)); const styles = useStyles2(getExpressionViewerStyles); const thresholds = getThresholdsForQueries(queries, condition); return (
{dataQueries.map(({ model, relativeTimeRange, refId, datasourceUid }, index) => { const dataSource = dsByUid[datasourceUid]; return ( ); })}
{expressions.map(({ model, refId, datasourceUid }, index) => { return ( isExpressionQuery(model) && ( ) ); })}
); } interface QueryPreviewProps extends Pick { rule: CombinedRule; dataSource?: DataSourceInstanceSettings; queryData?: PanelData; thresholds?: ThresholdDefinition; } export function QueryPreview({ refId, rule, thresholds, model, dataSource, queryData, relativeTimeRange, }: QueryPreviewProps) { const styles = useStyles2(getQueryPreviewStyles); const isExpression = isExpressionQuery(model); const [exploreSupported, exploreAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Explore); const canExplore = exploreSupported && exploreAllowed; const headerItems: React.ReactNode[] = []; if (dataSource) { const dataSourceName = dataSource.name ?? '[[Data source not found]]'; const dataSourceImgUrl = dataSource.meta.info.logos.small; headerItems.push(); } if (relativeTimeRange) { headerItems.push( {secondsToHms(relativeTimeRange.from)} to now ); } let exploreLink: string | undefined = undefined; if (!isExpression && canExplore) { exploreLink = dataSource && createExploreLink(dataSource, model); } return ( <>
{model && dataSource && }
{dataSource && } ); } function createExploreLink(settings: DataSourceRef, model: AlertDataQuery): string { const { uid, type } = settings; const { refId, ...rest } = model; /* In my testing I've found some alerts that don't have a data source embedded inside the model. At this moment in time it is unclear to me why some alert definitions not have a data source embedded in the model. I don't think that should happen here, the fact that the datasource ref is sometimes missing here is a symptom of another cause. (Gilles) */ return urlUtil.renderUrl(`${config.appSubUrl}/explore`, { left: JSON.stringify({ datasource: settings.uid, queries: [{ refId: 'A', ...rest, datasource: { type, uid } }], range: { from: 'now-1h', to: 'now' }, }), }); } interface DataSourceBadgeProps { name: string; imgUrl: string; } function DataSourceBadge({ name, imgUrl }: DataSourceBadgeProps) { const styles = useStyles2(getQueryPreviewStyles); return (
{name} {name}
); } const getQueryPreviewStyles = (theme: GrafanaTheme2) => ({ queryPreviewWrapper: css({ margin: theme.spacing(1), }), contentBox: css({ flex: '1 0 100%', }), dataSource: css({ border: `1px solid ${theme.colors.border.weak}`, borderRadius: theme.shape.radius.default, padding: theme.spacing(0.5, 1), display: 'flex', alignItems: 'center', gap: theme.spacing(1), }), }); interface ExpressionPreviewProps extends Pick { isAlertCondition: boolean; model: ExpressionQuery; evalData?: PanelData; } function ExpressionPreview({ refId, model, evalData, isAlertCondition }: ExpressionPreviewProps) { const styles = useStyles2(getQueryBoxStyles); function renderPreview() { switch (model.type) { case ExpressionQueryType.math: return ; case ExpressionQueryType.reduce: return ; case ExpressionQueryType.resample: return ; case ExpressionQueryType.classic: return ; case ExpressionQueryType.threshold: return ; case ExpressionQueryType.sql: return ; default: return <>Expression not supported: {model.type}; } } return ( {startCase(model.type)} , ]} isAlertCondition={isAlertCondition} >
{evalData?.errors?.map((error) => ( {error.message} ))} {renderPreview()}
{evalData && }
); } interface QueryBoxProps extends React.PropsWithChildren { refId: string; headerItems?: React.ReactNode; isAlertCondition?: boolean; exploreLink?: string; } function QueryBox({ refId, headerItems = [], children, isAlertCondition, exploreLink }: QueryBoxProps) { const styles = useStyles2(getQueryBoxStyles); return (
{refId} {headerItems} {isAlertCondition && } {exploreLink && ( View in Explore } /> )}
{children}
); } const getQueryBoxStyles = (theme: GrafanaTheme2) => ({ container: css({ flex: '1 0 25%', border: `1px solid ${theme.colors.border.weak}`, maxWidth: '100%', borderRadius: theme.shape.radius.default, display: 'flex', flexDirection: 'column', }), header: css({ display: 'flex', alignItems: 'center', gap: theme.spacing(1), padding: theme.spacing(1), backgroundColor: theme.colors.background.secondary, }), textBlock: css({ border: `1px solid ${theme.colors.border.weak}`, padding: theme.spacing(0.5, 1), backgroundColor: theme.colors.background.primary, borderRadius: theme.shape.radius.default, }), refId: css({ color: theme.colors.text.link, padding: theme.spacing(0.5, 1), border: `1px solid ${theme.colors.border.weak}`, borderRadius: theme.shape.radius.default, }), previewWrapper: css({ padding: theme.spacing(1), }), }); function ClassicConditionViewer({ model }: { model: ExpressionQuery }) { const styles = useStyles2(getClassicConditionViewerStyles); const reducerFunctions = keyBy(alertDef.reducerTypes, (rt) => rt.value); const evalOperators = keyBy(alertDef.evalOperators, (eo) => eo.value); const evalFunctions = keyBy(alertDef.evalFunctions, (ef) => ef.value); return (
{model.conditions?.map(({ query, operator, reducer, evaluator }, index) => { const isRange = isRangeEvaluator(evaluator); return (
{index === 0 ? 'WHEN' : !!operator?.type && evalOperators[operator?.type]?.text}
{reducer?.type && reducerFunctions[reducer.type]?.text}
OF
{query.params[0]}
{evalFunctions[evaluator.type].text}
{isRange ? `(${evaluator.params[0]}; ${evaluator.params[1]})` : evaluator.params[0]}
); })}
); } const getClassicConditionViewerStyles = (theme: GrafanaTheme2) => ({ container: css({ display: 'grid', gridTemplateColumns: 'repeat(6, max-content)', gap: theme.spacing(0, 1), }), ...getCommonQueryStyles(theme), }); function ReduceConditionViewer({ model }: { model: ExpressionQuery }) { const styles = useStyles2(getReduceConditionViewerStyles); const { reducer, expression, settings } = model; const reducerType = reducerTypes.find((rt) => rt.value === reducer); const reducerMode = settings?.mode ?? ReducerMode.Strict; const modeName = reducerModes.find((rm) => rm.value === reducerMode); return (
Function
{reducerType?.label}
Input
{expression}
Mode
{modeName?.label}
); } const getReduceConditionViewerStyles = (theme: GrafanaTheme2) => ({ container: css({ display: 'grid', gap: theme.spacing(0.5), gridTemplateRows: '1fr 1fr', gridTemplateColumns: 'repeat(4, 1fr)', '> :nth-child(6)': { gridColumn: 'span 3', }, }), ...getCommonQueryStyles(theme), }); function ResampleExpressionViewer({ model }: { model: ExpressionQuery }) { const styles = useStyles2(getResampleExpressionViewerStyles); const { expression, window, downsampler, upsampler } = model; const downsamplerType = downsamplingTypes.find((dt) => dt.value === downsampler); const upsamplerType = upsamplingTypes.find((ut) => ut.value === upsampler); return (
Input
{expression}
Resample to
{window}
Downsample
{downsamplerType?.label}
Upsample
{upsamplerType?.label}
); } const getResampleExpressionViewerStyles = (theme: GrafanaTheme2) => ({ container: css({ display: 'grid', gap: theme.spacing(0.5), gridTemplateColumns: 'repeat(4, 1fr)', gridTemplateRows: '1fr 1fr', }), ...getCommonQueryStyles(theme), }); function ThresholdExpressionViewer({ model }: { model: ExpressionQuery }) { const styles = useStyles2(getExpressionViewerStyles); const { expression, conditions } = model; const evaluator = conditions && conditions[0]?.evaluator; const thresholdFunction = thresholdFunctions.find((tf) => tf.value === evaluator?.type); const isRange = evaluator ? isRangeEvaluator(evaluator) : false; const unloadEvaluator = conditions && conditions[0]?.unloadEvaluator; const unloadThresholdFunction = thresholdFunctions.find((tf) => tf.value === unloadEvaluator?.type); const unloadIsRange = unloadEvaluator ? isRangeEvaluator(unloadEvaluator) : false; return ( <>
Input
{expression}
{evaluator && ( <>
{thresholdFunction?.label}
{isRange ? `(${evaluator.params[0]}; ${evaluator.params[1]})` : evaluator.params[0]}
)}
{unloadEvaluator && ( <>
Stop alerting when
{expression}
<>
{unloadThresholdFunction?.label}
{unloadIsRange ? `(${unloadEvaluator.params[0]}; ${unloadEvaluator.params[1]})` : unloadEvaluator.params[0]}
)}
); } const getExpressionViewerStyles = (theme: GrafanaTheme2) => { const { blue, bold, ...common } = getCommonQueryStyles(theme); return { ...common, maxWidthContainer: css({ maxWidth: '100%', }), container: css({ display: 'flex', gap: theme.spacing(0.5), }), blue: css(blue, { margin: 'auto 0' }), bold: css(bold, { margin: 'auto 0' }), }; }; function MathExpressionViewer({ model }: { model: ExpressionQuery }) { const styles = useStyles2(getExpressionViewerStyles); const { expression } = model; return (
Input
{expression}
); } const getCommonQueryStyles = (theme: GrafanaTheme2) => ({ blue: css({ color: theme.colors.text.link, }), bold: css({ fontWeight: theme.typography.fontWeightBold, }), label: css({ display: 'flex', alignItems: 'center', padding: theme.spacing(0.5, 1), backgroundColor: theme.colors.background.secondary, fontSize: theme.typography.bodySmall.fontSize, lineHeight: theme.typography.bodySmall.lineHeight, fontWeight: theme.typography.fontWeightBold, borderRadius: theme.shape.radius.default, }), value: css({ padding: theme.spacing(0.5, 1), border: `1px solid ${theme.colors.border.weak}`, borderRadius: theme.shape.radius.default, }), }); function isRangeEvaluator(evaluator: { params: number[]; type: EvalFunction }) { return ( evaluator.type === EvalFunction.IsWithinRange || evaluator.type === EvalFunction.IsOutsideRange || evaluator.type === EvalFunction.IsOutsideRangeIncluded || evaluator.type === EvalFunction.IsWithinRangeIncluded ); }