Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2f33e0f
add kql filter to all aggs
miguelmartin-elastic Jan 13, 2026
2736185
Merge branch 'main' into 231158-Add-kql-filtering-custom-rule
miguelmartin-elastic Jan 14, 2026
40e0f07
await for popover to disappear
miguelmartin-elastic Jan 15, 2026
e45c202
fix(test): retry close popover
cesco-f Jan 15, 2026
7229945
Merge branch 'main' into 231158-Add-kql-filtering-custom-rule
miguelmartin-elastic Jan 16, 2026
3731278
Merge branch 'main' into 231158-Add-kql-filtering-custom-rule
miguelmartin-elastic Jan 19, 2026
6c3df8b
Merge branch 'main' into 231158-Add-kql-filtering-custom-rule
miguelmartin-elastic Jan 20, 2026
c10a2c3
Merge branch 'main' into 231158-Add-kql-filtering-custom-rule
miguelmartin-elastic Jan 20, 2026
c51e695
update custom threshold rule backend validation schema
miguelmartin-elastic Jan 21, 2026
338607f
update snapshot
miguelmartin-elastic Jan 21, 2026
e8d28e1
Merge branch 'main' into 231158-Add-kql-filtering-custom-rule
miguelmartin-elastic Jan 23, 2026
bfd7b85
Merge branch 'main' into 231158-Add-kql-filtering-custom-rule
miguelmartin-elastic Jan 23, 2026
730981d
Merge branch 'main' into 231158-Add-kql-filtering-custom-rule
miguelmartin-elastic Jan 26, 2026
94d6b57
Merge branch 'main' into 231158-Add-kql-filtering-custom-rule
miguelmartin-elastic Jan 27, 2026
40b56d6
Changes from yarn openapi:bundle
kibanamachine Jan 27, 2026
6c0cf3d
Merge branch 'main' into 231158-Add-kql-filtering-custom-rule
miguelmartin-elastic Jan 28, 2026
a9adc86
Merge branch 'main' into 231158-Add-kql-filtering-custom-rule
miguelmartin-elastic Jan 30, 2026
e278c7f
Merge branch 'main' into 231158-Add-kql-filtering-custom-rule
miguelmartin-elastic Feb 2, 2026
ac8f48d
add filter info in char title and tooltip
miguelmartin-elastic Feb 2, 2026
8c36fa0
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Feb 2, 2026
d3e673a
commmit suggestion: use CustomThresholdExpressionMetric
miguelmartin-elastic Feb 4, 2026
5d25c3b
Changes from node scripts/eslint_all_files --no-cache --fix
kibanamachine Feb 4, 2026
e4a1ef1
Merge branch 'main' into 231158-Add-kql-filtering-custom-rule
miguelmartin-elastic Feb 5, 2026
94b4028
Merge branch 'main' into 231158-Add-kql-filtering-custom-rule
miguelmartin-elastic Feb 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { AggType } from '../../../../../../common/custom_threshold_rule/types';
import { Aggregators } from '../../../../../../common/custom_threshold_rule/types';
import type { MetricExpression } from '../../../types';
import { COMPARATORS } from '@kbn/alerting-comparators';
import { generateChartTitleAndTooltip } from './generate_chart_title_and_tooltip';

describe('generateChartTitleAndTooltip', () => {
describe('alerts based on custom threshold rule', () => {
const buildCriterion = (criterion: Partial<MetricExpression> = {}): MetricExpression => ({
metrics: [
{ name: 'A', field: 'response_time', aggType: Aggregators.COUNT as unknown as AggType },
],
threshold: [100],
comparator: COMPARATORS.GREATER_THAN,
equation: 'A > 100',
...criterion,
});

it('should generate correct title and tooltip when using COUNT aggregation without equation and without filter', () => {
const criterion = buildCriterion({
metrics: [{ name: 'A', aggType: Aggregators.COUNT as unknown as AggType }],
equation: undefined,
});

const { title, tooltip } = generateChartTitleAndTooltip(criterion);

expect(title).toBe('Equation result for count (all documents)');
expect(tooltip).toBe('Equation result for count (all documents)');
});

it('should generate correct title and tooltip when using COUNT aggregation with equation and without filter', () => {
const criterion = buildCriterion({
metrics: [{ name: 'A', aggType: Aggregators.COUNT as unknown as AggType }],
});

const { title, tooltip } = generateChartTitleAndTooltip(criterion);

expect(title).toBe('Equation result for count (all documents) > 100');
expect(tooltip).toBe('Equation result for count (all documents) > 100');
});

it('should generate correct title and tooltip when using COUNT aggregation with equation and filter', () => {
const criterion = buildCriterion({
metrics: [
{
name: 'A',
aggType: Aggregators.COUNT as unknown as AggType,
filter: 'status_code:500',
},
],
});

const { title, tooltip } = generateChartTitleAndTooltip(criterion);

expect(title).toBe('Equation result for count (status_code:500) > 100');
expect(tooltip).toBe('Equation result for count (status_code:500) > 100');
});

it('should generate correct title and tooltip when using SUM aggregation with equation and wihout filter', () => {
const criterion = buildCriterion({
metrics: [{ name: 'A', aggType: Aggregators.SUM as unknown as AggType, field: 'bytes' }],
});

const { title, tooltip } = generateChartTitleAndTooltip(criterion);

expect(title).toBe('Equation result for sum (bytes) > 100');
expect(tooltip).toBe('Equation result for sum (bytes) > 100');
});

it('should generate correct title and tooltip when using SUM aggregation with equation and filter', () => {
const criterion = buildCriterion({
metrics: [
{
name: 'A',
aggType: Aggregators.SUM as unknown as AggType,
field: 'bytes',
filter: 'status_code:200',
},
],
});

const { title, tooltip } = generateChartTitleAndTooltip(criterion);

expect(title).toBe('Equation result for sum (bytes, status_code:200) > 100');
expect(tooltip).toBe('Equation result for sum (bytes, status_code:200) > 100');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import { i18n } from '@kbn/i18n';
import type { CustomThresholdExpressionMetric } from '../../../../../../common/custom_threshold_rule/types';
import type { MetricExpression } from '../../../types';

const CHART_TITLE_LIMIT = 120;
Expand All @@ -14,17 +15,23 @@ const equationResultText = i18n.translate('xpack.observability.customThreshold.a
defaultMessage: 'Equation result for ',
});

const resolveMetricDisplay = (metric: CustomThresholdExpressionMetric) => {
if (metric.field && metric.filter) {
return `${metric.aggType} (${metric.field}, ${metric.filter})`;
}

return `${metric.aggType} (${
metric.field ? metric.field : metric.filter ? metric.filter : 'all documents'
})`;
};
export const generateChartTitleAndTooltip = (
criterion: MetricExpression,
chartTitleLimit = CHART_TITLE_LIMIT
) => {
const metricNameResolver: Record<string, string> = {};

criterion.metrics.forEach(
(metric) =>
(metricNameResolver[metric.name] = `${metric.aggType} (${
metric.field ? metric.field : metric.filter ? metric.filter : 'all documents'
})`)
(metric) => (metricNameResolver[metric.name] = resolveMetricDisplay(metric))
);

let equation = criterion.equation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,10 @@ export function MetricRowWithAgg({
name,
field: (selectedOptions.length && selectedOptions[0].label) || undefined,
aggType,
filter,
});
},
[name, aggType, onChange]
[name, aggType, filter, onChange]
);

const handleAggChange = useCallback(
Expand All @@ -102,9 +103,10 @@ export function MetricRowWithAgg({
name,
field: customAggType === Aggregators.COUNT ? undefined : field,
aggType: customAggType as Aggregators,
filter,
});
},
[name, field, onChange]
[name, field, filter, onChange]
);

const handleFilterChange = useCallback(
Expand All @@ -113,14 +115,31 @@ export function MetricRowWithAgg({
name,
filter: filterString,
aggType,
field,
});
},
[name, aggType, onChange]
[name, aggType, field, onChange]
);

const isAggInvalid = get(errors, ['metrics', name, 'aggType']) != null;
const isFieldInvalid = get(errors, ['metrics', name, 'field']) != null || !field;

const expressionValue = useMemo(() => {
if (aggType === Aggregators.COUNT) {
return filter || DEFAULT_COUNT_FILTER_TITLE;
}
if (field && filter) {
return `${field} (${filter})`;
}
if (field) {
return field;
}
if (filter) {
return filter;
}
return '';
}, [aggType, field, filter]);

return (
<EuiFlexGroup gutterSize="xs" alignItems="flexEnd">
<EuiFlexItem grow>
Expand All @@ -147,7 +166,7 @@ export function MetricRowWithAgg({
<EuiExpression
data-test-subj={`aggregationName${name}`}
description={aggregationTypes[aggType].text}
value={aggType === Aggregators.COUNT ? filter || DEFAULT_COUNT_FILTER_TITLE : field}
value={expressionValue}
isActive={aggTypePopoverOpen}
display="columns"
onClick={() => {
Expand Down Expand Up @@ -201,24 +220,8 @@ export function MetricRowWithAgg({
/>
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem style={{ minWidth: 300 }}>
{aggType === Aggregators.COUNT ? (
<EuiFormRow
label={i18n.translate(
'xpack.observability.customThreshold.rule.alertFlyout.customEquationEditor.filterLabel',
{ defaultMessage: 'KQL Filter {name}', values: { name } }
)}
>
<RuleFlyoutKueryBar
placeholder={' '}
derivedIndexPattern={dataView}
onChange={handleFilterChange}
onSubmit={handleFilterChange}
value={filter}
kql={kql}
/>
</EuiFormRow>
) : (
{aggType !== Aggregators.COUNT && (
<EuiFlexItem style={{ minWidth: 300 }}>
<EuiFormRow
label={i18n.translate(
'xpack.observability.customThreshold.rule.alertFlyout.customEquationEditor.fieldLabel',
Expand All @@ -235,7 +238,24 @@ export function MetricRowWithAgg({
data-test-subj="aggregationField"
/>
</EuiFormRow>
)}
</EuiFlexItem>
)}
<EuiFlexItem style={{ minWidth: 300 }}>
<EuiFormRow
label={i18n.translate(
'xpack.observability.customThreshold.rule.alertFlyout.customEquationEditor.filterLabel',
{ defaultMessage: 'KQL Filter {name}', values: { name } }
)}
>
<RuleFlyoutKueryBar
placeholder={' '}
derivedIndexPattern={dataView}
onChange={handleFilterChange}
onSubmit={handleFilterChange}
value={filter}
kql={kql}
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => {
const toasts = getService('toasts');
const pageObjects = getPageObjects(['header']);

const closePopover = async () => {
await retry.waitFor('popover to close', async () => {
const isOpen = await testSubjects.exists('o11yClosablePopoverTitleButton', { timeout: 100 });
if (isOpen) {
await testSubjects.click('o11yClosablePopoverTitleButton');
}
return !isOpen;
});
};

describe('Custom threshold rule', function () {
this.tags('includeFirefox');

Expand Down Expand Up @@ -109,23 +119,25 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => {
await find.clickByCssSelector(`option[value="avg"]`);
const input1 = await find.byCssSelector('[data-test-subj="aggregationField"] input');
await input1.type('metricset.rtt');
await testSubjects.click('o11yClosablePopoverTitleButton');

await closePopover();

await retry.waitFor('first aggregation to happen', async () => {
const aggregationNameA = await testSubjects.find('aggregationNameA');
return (await aggregationNameA.getVisibleText()) === 'AVERAGE\nmetricset.rtt';
});
await new Promise((r) => setTimeout(r, 1000));

// set second aggregation
await testSubjects.click('thresholdRuleCustomEquationEditorAddAggregationFieldButton');
await testSubjects.click('aggregationNameB');
await testSubjects.setValue('o11ySearchField', 'service.name : "opbeans-node"');
await testSubjects.click('o11yClosablePopoverTitleButton');
await retry.waitFor('first aggregation to happen', async () => {

await closePopover();

await retry.waitFor('second aggregation to happen', async () => {
const aggregationNameB = await testSubjects.find('aggregationNameB');
return (await aggregationNameB.getVisibleText()) === 'COUNT\nservice.name : "opbeans-node"';
});
await new Promise((r) => setTimeout(r, 1000));
});

it('can set custom equation', async () => {
Expand All @@ -136,12 +148,13 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => {
);
await customEquationField.click();
await customEquationField.type('A - B');
await testSubjects.click('o11yClosablePopoverTitleButton');

await closePopover();

await retry.waitFor('custom equation update to happen', async () => {
const customEquation = await testSubjects.find('customEquation');
return (await customEquation.getVisibleText()) === 'EQUATION\nA - B';
});
await new Promise((r) => setTimeout(r, 1000));
});

it('can set threshold', async () => {
Expand Down
Loading