Add filter field to index threshold rule type (#142255)

* Add filter field to index threshold rule type
This commit is contained in:
Ersin Erdal 2022-10-14 00:17:30 +02:00 committed by GitHub
parent f2bbc534f5
commit 660a24e94f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 303 additions and 78 deletions

View file

@ -121,6 +121,7 @@ describe('IndexThresholdAlertTypeExpression', () => {
expect(wrapper.find('[data-test-subj="forLastExpression"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="visualizationPlaceholder"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="thresholdVisualization"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="filterKuery"]').exists()).toBeTruthy();
});
test(`should render IndexThresholdAlertTypeExpression with expected components when aggType does require field`, async () => {
@ -133,6 +134,7 @@ describe('IndexThresholdAlertTypeExpression', () => {
expect(wrapper.find('[data-test-subj="forLastExpression"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="visualizationPlaceholder"]').exists()).toBeTruthy();
expect(wrapper.find('[data-test-subj="thresholdVisualization"]').exists()).toBeFalsy();
expect(wrapper.find('[data-test-subj="filterKuery"]').exists()).toBeTruthy();
});
test(`should render IndexThresholdAlertTypeExpression with visualization when there are no expression errors`, async () => {
@ -175,6 +177,7 @@ describe('IndexThresholdAlertTypeExpression', () => {
expect(
wrapper.find('EuiEmptyPrompt[data-test-subj="visualizationPlaceholder"]').text()
).toEqual(`Complete the expression to generate a preview.`);
expect(wrapper.find('input[data-test-subj="filterKuery"]').text()).toEqual('');
});
test(`should use alert params when params are defined`, async () => {
@ -186,6 +189,7 @@ describe('IndexThresholdAlertTypeExpression', () => {
const groupBy = 'top';
const termSize = '27';
const termField = 'host.name';
const wrapper = await setup(
getAlertParams({
aggType,

View file

@ -5,10 +5,18 @@
* 2.0.
*/
import React, { useState, Fragment, useEffect } from 'react';
import React, { useState, Fragment, useEffect, useCallback, ChangeEvent } from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiSpacer, EuiCallOut, EuiEmptyPrompt, EuiText, EuiTitle } from '@elastic/eui';
import {
EuiSpacer,
EuiCallOut,
EuiEmptyPrompt,
EuiText,
EuiTitle,
EuiFieldSearch,
EuiFormRow,
} from '@elastic/eui';
import { HttpSetup } from '@kbn/core/public';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import {
@ -78,6 +86,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<
threshold,
timeWindowSize,
timeWindowUnit,
filterKuery,
} = ruleParams;
const indexArray = indexParamToArray(index);
@ -133,6 +142,13 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<
setEsFields(currentEsFields);
};
const handleFilterChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
setRuleParams('filterKuery', e.target.value || undefined);
},
[setRuleParams]
);
useEffect(() => {
setDefaultExpressionValues();
// eslint-disable-next-line react-hooks/exhaustive-deps
@ -261,6 +277,33 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent<
}
/>
<EuiSpacer />
<EuiTitle size="xs">
<h5>
<FormattedMessage
id="xpack.stackAlerts.threshold.ui.filterTitle"
defaultMessage="Filter (Optional)"
/>
</h5>
</EuiTitle>
<EuiSpacer size="s" />
<EuiFormRow
helpText={i18n.translate('xpack.stackAlerts.threshold.ui.filterKQLHelpText', {
defaultMessage: 'Use a KQL expression to limit the scope of your alert trigger.',
})}
fullWidth
display="rowCompressed"
isInvalid={errors.filterKuery.length > 0}
error={errors.filterKuery}
>
<EuiFieldSearch
data-test-subj="filterKuery"
onChange={handleFilterChange}
value={filterKuery}
fullWidth
isInvalid={errors.filterKuery.length > 0}
/>
</EuiFormRow>
<EuiSpacer />
<div className="actAlertVisualization__chart">
{cannotShowVisualization ? (
<Fragment>

View file

@ -36,6 +36,7 @@ export async function getThresholdAlertVisualizationData({
termSize: model.termSize,
timeWindowSize: model.timeWindowSize,
timeWindowUnit: model.timeWindowUnit,
filterKuery: model.filterKuery,
dateStart: new Date(visualizeOptions.rangeFrom).toISOString(),
dateEnd: new Date(visualizeOptions.rangeTo).toISOString(),
interval: visualizeOptions.interval,

View file

@ -39,4 +39,5 @@ export interface IndexThresholdAlertParams extends RuleTypeParams {
threshold: number[];
timeWindowSize: number;
timeWindowUnit: string;
filterKuery?: string;
}

View file

@ -94,4 +94,21 @@ describe('expression params validation', () => {
expect(validateExpression(initialParams).errors.threshold1.length).toBeGreaterThan(0);
expect(validateExpression(initialParams).errors.threshold1[0]).toBe('Threshold1 is required.');
});
test('if filterKuery is invalid should return proper error message', () => {
const initialParams: IndexThresholdAlertParams = {
index: ['test'],
aggType: 'count',
groupBy: 'top',
threshold: [1],
timeWindowSize: 1,
timeWindowUnit: 's',
thresholdComparator: 'between',
filterKuery: 'group:',
};
expect(validateExpression(initialParams).errors.filterKuery.length).toBeGreaterThan(0);
expect(validateExpression(initialParams).errors.filterKuery[0]).toBe(
'Filter query is invalid.'
);
});
});

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { toElasticsearchQuery, fromKueryExpression } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import {
ValidationResult,
@ -26,6 +27,7 @@ export const validateExpression = (alertParams: IndexThresholdAlertParams): Vali
threshold,
timeWindowSize,
thresholdComparator,
filterKuery,
} = alertParams;
const validationResult = { errors: {} };
const errors = {
@ -37,8 +39,22 @@ export const validateExpression = (alertParams: IndexThresholdAlertParams): Vali
threshold1: new Array<string>(),
index: new Array<string>(),
timeField: new Array<string>(),
filterKuery: new Array<string>(),
};
validationResult.errors = errors;
if (!!filterKuery) {
try {
toElasticsearchQuery(fromKueryExpression(filterKuery as string));
} catch (e) {
errors.filterKuery.push(
i18n.translate('xpack.stackAlerts.threshold.ui.validation.error.invalidKql', {
defaultMessage: 'Filter query is invalid.',
})
);
}
}
if (!index || index.length === 0) {
errors.index.push(
i18n.translate('xpack.stackAlerts.threshold.ui.validation.error.requiredIndexText', {

View file

@ -104,6 +104,10 @@ describe('ruleType', () => {
"description": "termField",
"name": "termField",
},
Object {
"description": "filterKuery",
"name": "filterKuery",
},
Object {
"description": "termSize",
"name": "termSize",

View file

@ -169,6 +169,7 @@ export function getRuleType(
timeWindowSize: params.timeWindowSize,
timeWindowUnit: params.timeWindowUnit,
interval: undefined,
filterKuery: params.filterKuery,
};
// console.log(`index_threshold: query: ${JSON.stringify(queryParams, null, 4)}`);
const result = await (