D7net
Home
Console
Upload
information
Create File
Create Folder
About
Tools
:
/
usr
/
share
/
grafana
/
public
/
app
/
features
/
alerting
/
unified
/
Filename :
GrafanaRuleQueryViewer.tsx
back
Copy
import { css, cx } from '@emotion/css'; import { dump } from 'js-yaml'; import { keyBy, startCase } from 'lodash'; import React from 'react'; import { DataSourceInstanceSettings, GrafanaTheme2, PanelData, RelativeTimeRange } from '@grafana/data'; import { Stack } from '@grafana/experimental'; import { config } from '@grafana/runtime'; import { Badge, useStyles2 } from '@grafana/ui'; import { mapRelativeTimeRangeToOption } from '@grafana/ui/src/components/DateTimePickers/RelativeTimeRangePicker/utils'; import { AlertQuery } from '../../../types/unified-alerting-dto'; import { isExpressionQuery } from '../../expressions/guards'; import { downsamplingTypes, ExpressionQuery, ExpressionQueryType, reducerModes, ReducerMode, reducerTypes, thresholdFunctions, upsamplingTypes, } from '../../expressions/types'; import alertDef, { EvalFunction } from '../state/alertDef'; import { ExpressionResult } from './components/expressions/Expression'; import { getThresholdsForQueries, ThresholdDefinition } from './components/rule-editor/util'; import { RuleViewerVisualization } from './components/rule-viewer/RuleViewerVisualization'; interface GrafanaRuleViewerProps { queries: AlertQuery[]; condition: string; evalDataByQuery?: Record<string, PanelData>; evalTimeRanges?: Record<string, RelativeTimeRange>; onTimeRangeChange: (queryRef: string, timeRange: RelativeTimeRange) => void; } export function GrafanaRuleQueryViewer({ queries, condition, evalDataByQuery = {}, evalTimeRanges = {}, onTimeRangeChange, }: 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); return ( <Stack gap={2} direction="column"> <div className={styles.maxWidthContainer}> <Stack gap={2}> {dataQueries.map(({ model, relativeTimeRange, refId, datasourceUid }, index) => { const dataSource = dsByUid[datasourceUid]; return ( <QueryPreview key={index} refId={refId} isAlertCondition={condition === refId} model={model} relativeTimeRange={relativeTimeRange} evalTimeRange={evalTimeRanges[refId]} dataSource={dataSource} thresholds={thresholds[refId]} queryData={evalDataByQuery[refId]} onEvalTimeRangeChange={(timeRange) => onTimeRangeChange(refId, timeRange)} /> ); })} </Stack> </div> <div className={styles.maxWidthContainer}> <Stack gap={1}> {expressions.map(({ model, refId, datasourceUid }, index) => { const dataSource = dsByUid[datasourceUid]; return ( isExpressionQuery(model) && ( <ExpressionPreview key={index} refId={refId} isAlertCondition={condition === refId} model={model} dataSource={dataSource} evalData={evalDataByQuery[refId]} /> ) ); })} </Stack> </div> </Stack> ); } interface QueryPreviewProps extends Pick<AlertQuery, 'refId' | 'relativeTimeRange' | 'model'> { isAlertCondition: boolean; dataSource?: DataSourceInstanceSettings; queryData?: PanelData; thresholds?: ThresholdDefinition; evalTimeRange?: RelativeTimeRange; onEvalTimeRangeChange: (timeRange: RelativeTimeRange) => void; } export function QueryPreview({ refId, relativeTimeRange, thresholds, model, dataSource, queryData, evalTimeRange, onEvalTimeRangeChange, }: QueryPreviewProps) { const styles = useStyles2(getQueryPreviewStyles); // relativeTimeRange is what is defined for a query // evalTimeRange is temporary value which the user can change const headerItems = [dataSource?.name ?? '[[Data source not found]]']; if (relativeTimeRange) { headerItems.push(mapRelativeTimeRangeToOption(relativeTimeRange).display); } return ( <QueryBox refId={refId} headerItems={headerItems} className={styles.contentBox}> <pre className={styles.code}> <code>{dump(model)}</code> </pre> {dataSource && ( <RuleViewerVisualization refId={refId} dsSettings={dataSource} model={model} data={queryData} thresholds={thresholds} relativeTimeRange={evalTimeRange} onTimeRangeChange={onEvalTimeRangeChange} className={styles.visualization} /> )} </QueryBox> ); } const getQueryPreviewStyles = (theme: GrafanaTheme2) => ({ code: css` margin: ${theme.spacing(1)}; `, contentBox: css` flex: 1 0 100%; `, visualization: css` padding: ${theme.spacing(1)}; `, }); interface ExpressionPreviewProps extends Pick<AlertQuery, 'refId'> { isAlertCondition: boolean; model: ExpressionQuery; dataSource: DataSourceInstanceSettings; evalData?: PanelData; } function ExpressionPreview({ refId, model, evalData, isAlertCondition }: ExpressionPreviewProps) { function renderPreview() { switch (model.type) { case ExpressionQueryType.math: return <MathExpressionViewer model={model} />; case ExpressionQueryType.reduce: return <ReduceConditionViewer model={model} />; case ExpressionQueryType.resample: return <ResampleExpressionViewer model={model} />; case ExpressionQueryType.classic: return <ClassicConditionViewer model={model} />; case ExpressionQueryType.threshold: return <ThresholdExpressionViewer model={model} />; default: return <>Expression not supported: {model.type}</>; } } return ( <QueryBox refId={refId} headerItems={[startCase(model.type)]} isAlertCondition={isAlertCondition}> {renderPreview()} {evalData && <ExpressionResult series={evalData.series} isAlertCondition={isAlertCondition} />} </QueryBox> ); } interface QueryBoxProps extends React.PropsWithChildren<unknown> { refId: string; headerItems?: string[]; isAlertCondition?: boolean; className?: string; } function QueryBox({ refId, headerItems = [], children, isAlertCondition, className }: QueryBoxProps) { const styles = useStyles2(getQueryBoxStyles); return ( <div className={cx(styles.container, className)}> <header className={styles.header}> <span className={styles.refId}>{refId}</span> {headerItems.map((item, index) => ( <span key={index} className={styles.textBlock}> {item} </span> ))} {isAlertCondition && ( <div className={styles.conditionIndicator}> <Badge color="green" icon="check" text="Alert condition" /> </div> )} </header> {children} </div> ); } const getQueryBoxStyles = (theme: GrafanaTheme2) => ({ container: css` flex: 1 0 25%; border: 1px solid ${theme.colors.border.strong}; max-width: 100%; `, header: css` display: flex; align-items: center; gap: ${theme.spacing(1)}; padding: ${theme.spacing(1)}; background-color: ${theme.colors.background.secondary}; `, textBlock: css` border: 1px solid ${theme.colors.border.weak}; padding: ${theme.spacing(0.5, 1)}; background-color: ${theme.colors.background.primary}; `, refId: css` color: ${theme.colors.text.link}; padding: ${theme.spacing(0.5, 1)}; border: 1px solid ${theme.colors.border.weak}; `, conditionIndicator: css` margin-left: auto; `, }); 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 ( <div className={styles.container}> {model.conditions?.map(({ query, operator, reducer, evaluator }, index) => { const isRange = isRangeEvaluator(evaluator); return ( <React.Fragment key={index}> <div className={styles.blue}> {index === 0 ? 'WHEN' : !!operator?.type && evalOperators[operator?.type]?.text} </div> <div className={styles.bold}>{reducer?.type && reducerFunctions[reducer.type]?.text}</div> <div className={styles.blue}>OF</div> <div className={styles.bold}>{query.params[0]}</div> <div className={styles.blue}>{evalFunctions[evaluator.type].text}</div> <div className={styles.bold}> {isRange ? `(${evaluator.params[0]}; ${evaluator.params[1]})` : evaluator.params[0]} </div> </React.Fragment> ); })} </div> ); } const getClassicConditionViewerStyles = (theme: GrafanaTheme2) => ({ container: css` padding: ${theme.spacing(1)}; display: grid; grid-template-columns: max-content max-content max-content max-content max-content 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 ( <div className={styles.container}> <div className={styles.label}>Function</div> <div className={styles.value}>{reducerType?.label}</div> <div className={styles.label}>Input</div> <div className={styles.value}>{expression}</div> <div className={styles.label}>Mode</div> <div className={styles.value}>{modeName?.label}</div> </div> ); } const getReduceConditionViewerStyles = (theme: GrafanaTheme2) => ({ container: css` padding: ${theme.spacing(1)}; display: grid; gap: ${theme.spacing(1)}; grid-template-rows: 1fr 1fr; grid-template-columns: 1fr 1fr 1fr 1fr; > :nth-child(6) { grid-column: 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 ( <div className={styles.container}> <div className={styles.label}>Input</div> <div className={styles.value}>{expression}</div> <div className={styles.label}>Resample to</div> <div className={styles.value}>{window}</div> <div className={styles.label}>Downsample</div> <div className={styles.value}>{downsamplerType?.label}</div> <div className={styles.label}>Upsample</div> <div className={styles.value}>{upsamplerType?.label}</div> </div> ); } const getResampleExpressionViewerStyles = (theme: GrafanaTheme2) => ({ container: css` padding: ${theme.spacing(1)}; display: grid; gap: ${theme.spacing(1)}; grid-template-columns: 1fr 1fr 1fr 1fr; grid-template-rows: 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; return ( <div className={styles.container}> <div className={styles.label}>Input</div> <div className={styles.value}>{expression}</div> {evaluator && ( <> <div className={styles.blue}>{thresholdFunction?.label}</div> <div className={styles.bold}> {isRange ? `(${evaluator.params[0]}; ${evaluator.params[1]})` : evaluator.params[0]} </div> </> )} </div> ); } const getExpressionViewerStyles = (theme: GrafanaTheme2) => { const { blue, bold, ...common } = getCommonQueryStyles(theme); return { ...common, maxWidthContainer: css` max-width: 100%; `, container: css` padding: ${theme.spacing(1)}; display: flex; gap: ${theme.spacing(1)}; `, 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 ( <div className={styles.container}> <div className={styles.label}>Input</div> <div className={styles.value}>{expression}</div> </div> ); } const getCommonQueryStyles = (theme: GrafanaTheme2) => ({ blue: css` color: ${theme.colors.text.link}; `, bold: css` font-weight: ${theme.typography.fontWeightBold}; `, label: css` display: flex; align-items: center; padding: ${theme.spacing(0.5, 1)}; background-color: ${theme.colors.background.secondary}; font-size: ${theme.typography.bodySmall.fontSize}; line-height: ${theme.typography.bodySmall.lineHeight}; font-weight: ${theme.typography.fontWeightBold}; `, value: css` padding: ${theme.spacing(0.5, 1)}; border: 1px solid ${theme.colors.border.weak}; `, }); function isRangeEvaluator(evaluator: { params: number[]; type: EvalFunction }) { return evaluator.type === EvalFunction.IsWithinRange || evaluator.type === EvalFunction.IsOutsideRange; }