update logic for expression parsing from panel to alerting

This commit is contained in:
laurenashleigh
2026-01-12 11:46:45 +00:00
parent a62aa4f6a2
commit 2b252b50f5
8 changed files with 150 additions and 23 deletions
@@ -70,7 +70,7 @@ exports[`PanelAlertTabContent Will render alerts belonging to panel and a button
"refId": "B",
"type": "reduce",
},
"queryType": "",
"queryType": "expression",
"refId": "B",
},
{
@@ -105,7 +105,7 @@ exports[`PanelAlertTabContent Will render alerts belonging to panel and a button
"refId": "C",
"type": "threshold",
},
"queryType": "",
"queryType": "expression",
"refId": "C",
},
],
@@ -24,6 +24,20 @@ jest.mock('react-use', () => ({
useAsync: () => ({ loading: false, value: {} }),
}));
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
config: {
...jest.requireActual('@grafana/runtime').config,
featureToggles: {
createAlertRuleFromPanel: true,
},
},
}));
jest.mock('../../components/AlertRuleDrawerForm', () => ({
AlertRuleDrawerForm: () => null,
}));
describe('Analytics', () => {
it('Sends log info when creating an alert rule from a panel', async () => {
const panel = new PanelModel({
@@ -10,6 +10,7 @@ import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
import { PanelModel } from 'app/features/dashboard/state/PanelModel';
import { useSelector } from 'app/types/store';
import { LogMessages, logInfo } from '../../Analytics';
import { AlertRuleDrawerForm } from '../../components/AlertRuleDrawerForm';
import { createPanelAlertRuleNavigation } from '../../utils/navigation';
import { panelToRuleFormValues } from '../../utils/rule-form';
@@ -72,7 +73,10 @@ export const NewRuleFromPanelButton = ({ dashboard, panel, className }: Props) =
icon="bell"
className={className}
data-testid="create-alert-rule-button-drawer"
onClick={() => setIsOpen(true)}
onClick={() => {
logInfo(LogMessages.alertRuleFromPanel);
setIsOpen(true);
}}
>
<Trans i18nKey="alerting.new-rule-from-panel-button.new-alert-rule">New alert rule</Trans>
</Button>
@@ -81,7 +81,9 @@ exports[`RuleEditor grafana managed rules can create new grafana managed alert 1
"type": "and",
},
"query": {
"params": [],
"params": [
"B",
],
},
"reducer": {
"params": [],
@@ -99,7 +101,7 @@ exports[`RuleEditor grafana managed rules can create new grafana managed alert 1
"refId": "B",
"type": "reduce",
},
"queryType": "",
"queryType": "expression",
"refId": "B",
},
{
@@ -117,7 +119,9 @@ exports[`RuleEditor grafana managed rules can create new grafana managed alert 1
"type": "and",
},
"query": {
"params": [],
"params": [
"C",
],
},
"reducer": {
"params": [],
@@ -134,7 +138,7 @@ exports[`RuleEditor grafana managed rules can create new grafana managed alert 1
"refId": "C",
"type": "threshold",
},
"queryType": "",
"queryType": "expression",
"refId": "C",
},
],
@@ -27,17 +27,28 @@ export function setQueryEditorSettings(values: RuleFormValues): RuleFormValues {
// data queries only
const dataQueries = values.queries.filter((query) => !isExpressionQuery(query.model));
// expression queries only
const expressionQueries = values.queries.filter((query) => isExpressionQueryInAlert(query));
// expression queries only - but filter out invalid ones that don't have a type field
const expressionQueries = values.queries.filter((query): query is AlertQuery<ExpressionQuery> => {
if (!isExpressionQueryInAlert(query)) {
return false;
}
// Check if the expression has a valid type field
// React Hook Form might strip the type field, so we need to check it exists
return 'type' in query.model && query.model.type !== undefined;
});
// If we have data queries but no expressions (e.g., coming from dashboard panel),
// default to simplified mode so the form can create appropriate expressions
// If we have data queries but no VALID expressions (e.g., coming from dashboard panel with malformed expressions),
// remove the invalid expressions and set condition to empty so simplified mode can regenerate them
const hasDataQueries = dataQueries.length > 0;
const hasExpressions = expressionQueries.length > 0;
const hasValidExpressions = expressionQueries.length > 0;
const totalExpressions = values.queries.filter((query) => isExpressionQueryInAlert(query)).length;
const hasInvalidExpressions = totalExpressions > expressionQueries.length;
if (hasDataQueries && !hasExpressions) {
if (hasDataQueries && (!hasValidExpressions || hasInvalidExpressions)) {
return {
...values,
queries: dataQueries, // Only keep data queries, remove invalid expressions
condition: '', // Clear condition so simplified editor can set it
editorSettings: {
simplifiedQueryEditor: true,
simplifiedNotificationEditor: true,
@@ -7,6 +7,7 @@ import {
ScopedVars,
TimeRange,
getDefaultRelativeTimeRange,
getNextRefId,
rangeUtil,
} from '@grafana/data';
import { PromQuery } from '@grafana/prometheus';
@@ -558,14 +559,85 @@ export const getDefaultRecordingRulesQueries = (
];
};
export const getDefaultExpressions = (...refIds: [string, string]) => {
const getDefaultExpressions = (...refIds: [string, string] | [string, string, string]): AlertQuery[] => {
const refOne = refIds[0];
const refTwo = refIds[1];
// If a third parameter is provided, use it as the source query refId, otherwise default to 'A'
const sourceRefId = refIds.length === 3 ? refIds[2] : 'A';
const reduceQuery = getDefaultReduceExpression({ inputRefId: 'A', reduceRefId: refOne });
const thresholdQuery = getDefaultThresholdExpression({ inputRefId: refOne, thresholdRefId: refTwo });
const reduceExpression: ExpressionQuery = {
refId: refIds[0],
type: ExpressionQueryType.reduce,
datasource: {
uid: ExpressionDatasourceUID,
type: ExpressionDatasourceRef.type,
},
conditions: [
{
type: 'query',
evaluator: {
params: [],
type: EvalFunction.IsAbove,
},
operator: {
type: 'and',
},
query: {
params: [refOne],
},
reducer: {
params: [],
type: 'last',
},
},
],
reducer: 'last',
expression: sourceRefId,
};
return [reduceQuery, thresholdQuery] as const;
const thresholdExpression: ExpressionQuery = {
refId: refTwo,
type: ExpressionQueryType.threshold,
datasource: {
uid: ExpressionDatasourceUID,
type: ExpressionDatasourceRef.type,
},
conditions: [
{
type: 'query',
evaluator: {
params: [0],
type: EvalFunction.IsAbove,
},
operator: {
type: 'and',
},
query: {
params: [refTwo],
},
reducer: {
params: [],
type: 'last',
},
},
],
expression: refOne,
};
return [
{
refId: refOne,
datasourceUid: ExpressionDatasourceUID,
queryType: 'expression',
model: reduceExpression,
},
{
refId: refTwo,
datasourceUid: ExpressionDatasourceUID,
queryType: 'expression',
model: thresholdExpression,
},
];
};
const getDefaultExpressionsForRecording = (refOne: string): Array<AlertQuery<ExpressionQuery>> => {
@@ -782,6 +854,17 @@ export const panelToRuleFormValues = async (
return undefined;
}
// Add default expression queries if they don't exist
if (!queries.find((query) => query.datasourceUid === ExpressionDatasourceUID)) {
// Get the last data query's refId to use as the source for the reduce expression
const lastDataQueryRefId = queries[queries.length - 1].refId;
const reduceRefId = getNextRefId(queries);
const queriesWithReduce = [...queries, { refId: reduceRefId, datasourceUid: '', queryType: '', model: {} }];
const thresholdRefId = getNextRefId(queriesWithReduce);
const expressions = getDefaultExpressions(reduceRefId, thresholdRefId, lastDataQueryRefId);
queries.push(...expressions);
}
const { folderTitle, folderUid } = dashboard.meta;
const folder =
folderUid && folderTitle
@@ -797,8 +880,7 @@ export const panelToRuleFormValues = async (
folder,
queries,
name: panel.title,
// Condition left empty - expressions will be created in the alert rule form under advanced options
condition: '',
condition: queries[queries.length - 1].refId,
annotations: [
{
key: Annotation.dashboardUID,
@@ -850,6 +932,17 @@ export const scenesPanelToRuleFormValues = async (vizPanel: VizPanel): Promise<P
return undefined;
}
// Add default expression queries if they don't exist
if (!grafanaQueries.find((query) => query.datasourceUid === ExpressionDatasourceUID)) {
// Get the last data query's refId to use as the source for the reduce expression
const lastDataQueryRefId = grafanaQueries[grafanaQueries.length - 1].refId;
const reduceRefId = getNextRefId(grafanaQueries);
const queriesWithReduce = [...grafanaQueries, { refId: reduceRefId, datasourceUid: '', queryType: '', model: {} }];
const thresholdRefId = getNextRefId(queriesWithReduce);
const expressions = getDefaultExpressions(reduceRefId, thresholdRefId, lastDataQueryRefId);
grafanaQueries.push(...expressions);
}
const { folderTitle, folderUid } = dashboard.state.meta;
const folder =
@@ -866,8 +959,7 @@ export const scenesPanelToRuleFormValues = async (vizPanel: VizPanel): Promise<P
folder,
queries: grafanaQueries,
name: vizPanel.state.title,
// Condition left empty - expressions will be created in the alert rule form under advanced options
condition: '',
condition: grafanaQueries[grafanaQueries.length - 1].refId,
annotations: [
{
key: Annotation.dashboardUID,
@@ -6,6 +6,7 @@ import { Trans, t } from '@grafana/i18n';
import { config } from '@grafana/runtime';
import { VizPanel } from '@grafana/scenes';
import { Alert, Button } from '@grafana/ui';
import { LogMessages, logInfo } from 'app/features/alerting/unified/Analytics';
import { AlertRuleDrawerForm } from 'app/features/alerting/unified/components/AlertRuleDrawerForm';
import { createPanelAlertRuleNavigation } from 'app/features/alerting/unified/utils/navigation';
import { scenesPanelToRuleFormValues } from 'app/features/alerting/unified/utils/rule-form';
@@ -59,6 +60,7 @@ export const ScenesNewRuleFromPanelButton = ({ panel, className }: ScenesNewRule
className={className}
data-testid="create-alert-rule-button-drawer"
onClick={() => {
logInfo(LogMessages.alertRuleFromPanel);
setIsOpen(true);
}}
>
@@ -65,7 +65,7 @@ exports[`PanelAlertTabContent Will render alerts belonging to panel and a button
"refId": "B",
"type": "reduce",
},
"queryType": "",
"queryType": "expression",
"refId": "B",
},
{
@@ -100,7 +100,7 @@ exports[`PanelAlertTabContent Will render alerts belonging to panel and a button
"refId": "C",
"type": "threshold",
},
"queryType": "",
"queryType": "expression",
"refId": "C",
},
],