mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Alerting] Configurable number of hits for ES query alert (#90089)
* Adding size parameter to ES query alert * Can't use const inside validation * Updating docs * Fixing functional test * License Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
1f5d52ea2e
commit
5f8de693b9
16 changed files with 382 additions and 8 deletions
|
@ -130,12 +130,13 @@ image::images/alert-types-es-query-select.png[Choosing an ES query alert type]
|
|||
[float]
|
||||
==== Defining the conditions
|
||||
|
||||
The ES query alert has 4 clauses that define the condition to detect.
|
||||
The ES query alert has 5 clauses that define the condition to detect.
|
||||
|
||||
[role="screenshot"]
|
||||
image::images/alert-types-es-query-conditions.png[Four clauses define the condition to detect]
|
||||
|
||||
Index:: This clause requires an *index or index pattern* and a *time field* that will be used for the *time window*.
|
||||
Size:: This clause specifies the number of documents to pass to the configured actions when the the threshold condition is met.
|
||||
ES query:: This clause specifies the ES DSL query to execute. The number of documents that match this query will be evaulated against the threshold
|
||||
condition. Aggregations are not supported at this time.
|
||||
Threshold:: This clause defines a threshold value and a comparison operator (`is above`, `is above or equals`, `is below`, `is below or equals`, or `is between`). The number of documents that match the specified query is compared to this threshold.
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 105 KiB |
|
@ -126,6 +126,7 @@ describe('EsQueryAlertTypeExpression', () => {
|
|||
index: ['test-index'],
|
||||
timeField: '@timestamp',
|
||||
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
|
||||
size: 100,
|
||||
thresholdComparator: '>',
|
||||
threshold: [0],
|
||||
timeWindowSize: 15,
|
||||
|
@ -137,6 +138,7 @@ describe('EsQueryAlertTypeExpression', () => {
|
|||
const errors = {
|
||||
index: [],
|
||||
esQuery: [],
|
||||
size: [],
|
||||
timeField: [],
|
||||
timeWindowSize: [],
|
||||
};
|
||||
|
@ -169,6 +171,7 @@ describe('EsQueryAlertTypeExpression', () => {
|
|||
test('should render EsQueryAlertTypeExpression with expected components', async () => {
|
||||
const wrapper = await setup(getAlertParams());
|
||||
expect(wrapper.find('[data-test-subj="indexSelectPopover"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="sizeValueExpression"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="queryJsonEditor"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="testQuerySuccess"]').exists()).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="testQueryError"]').exists()).toBeFalsy();
|
||||
|
|
|
@ -30,6 +30,7 @@ import {
|
|||
COMPARATORS,
|
||||
ThresholdExpression,
|
||||
ForLastExpression,
|
||||
ValueExpression,
|
||||
AlertTypeParamsExpressionProps,
|
||||
} from '../../../../triggers_actions_ui/public';
|
||||
import { validateExpression } from './validation';
|
||||
|
@ -45,6 +46,7 @@ const DEFAULT_VALUES = {
|
|||
"match_all" : {}
|
||||
}
|
||||
}`,
|
||||
SIZE: 100,
|
||||
TIME_WINDOW_SIZE: 5,
|
||||
TIME_WINDOW_UNIT: 'm',
|
||||
THRESHOLD: [1000],
|
||||
|
@ -53,6 +55,7 @@ const DEFAULT_VALUES = {
|
|||
const expressionFieldsWithValidation = [
|
||||
'index',
|
||||
'esQuery',
|
||||
'size',
|
||||
'timeField',
|
||||
'threshold0',
|
||||
'threshold1',
|
||||
|
@ -74,6 +77,7 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent<
|
|||
index,
|
||||
timeField,
|
||||
esQuery,
|
||||
size,
|
||||
thresholdComparator,
|
||||
threshold,
|
||||
timeWindowSize,
|
||||
|
@ -83,6 +87,7 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent<
|
|||
const getDefaultParams = () => ({
|
||||
...alertParams,
|
||||
esQuery: esQuery ?? DEFAULT_VALUES.QUERY,
|
||||
size: size ?? DEFAULT_VALUES.SIZE,
|
||||
timeWindowSize: timeWindowSize ?? DEFAULT_VALUES.TIME_WINDOW_SIZE,
|
||||
timeWindowUnit: timeWindowUnit ?? DEFAULT_VALUES.TIME_WINDOW_UNIT,
|
||||
threshold: threshold ?? DEFAULT_VALUES.THRESHOLD,
|
||||
|
@ -214,7 +219,7 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent<
|
|||
<h5>
|
||||
<FormattedMessage
|
||||
id="xpack.stackAlerts.esQuery.ui.selectIndex"
|
||||
defaultMessage="Select an index"
|
||||
defaultMessage="Select an index and size"
|
||||
/>
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
|
@ -234,6 +239,7 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent<
|
|||
...alertParams,
|
||||
index: indices,
|
||||
esQuery: DEFAULT_VALUES.QUERY,
|
||||
size: DEFAULT_VALUES.SIZE,
|
||||
thresholdComparator: DEFAULT_VALUES.THRESHOLD_COMPARATOR,
|
||||
timeWindowSize: DEFAULT_VALUES.TIME_WINDOW_SIZE,
|
||||
timeWindowUnit: DEFAULT_VALUES.TIME_WINDOW_UNIT,
|
||||
|
@ -246,6 +252,19 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent<
|
|||
}}
|
||||
onTimeFieldChange={(updatedTimeField: string) => setParam('timeField', updatedTimeField)}
|
||||
/>
|
||||
<ValueExpression
|
||||
description={i18n.translate('xpack.stackAlerts.esQuery.ui.sizeExpression', {
|
||||
defaultMessage: 'Size',
|
||||
})}
|
||||
data-test-subj="sizeValueExpression"
|
||||
value={size}
|
||||
errors={errors.size}
|
||||
display="fullWidth"
|
||||
popupPosition={'upLeft'}
|
||||
onChangeSelectedValue={(updatedValue) => {
|
||||
setParam('size', updatedValue);
|
||||
}}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
<EuiTitle size="xs">
|
||||
<h5>
|
||||
|
|
|
@ -17,6 +17,7 @@ export interface EsQueryAlertParams extends AlertTypeParams {
|
|||
index: string[];
|
||||
timeField?: string;
|
||||
esQuery: string;
|
||||
size: number;
|
||||
thresholdComparator?: string;
|
||||
threshold: number[];
|
||||
timeWindowSize: number;
|
||||
|
|
|
@ -13,6 +13,7 @@ describe('expression params validation', () => {
|
|||
const initialParams: EsQueryAlertParams = {
|
||||
index: [],
|
||||
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
|
||||
size: 100,
|
||||
timeWindowSize: 1,
|
||||
timeWindowUnit: 's',
|
||||
threshold: [0],
|
||||
|
@ -25,6 +26,7 @@ describe('expression params validation', () => {
|
|||
const initialParams: EsQueryAlertParams = {
|
||||
index: ['test'],
|
||||
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
|
||||
size: 100,
|
||||
timeWindowSize: 1,
|
||||
timeWindowUnit: 's',
|
||||
threshold: [0],
|
||||
|
@ -37,6 +39,7 @@ describe('expression params validation', () => {
|
|||
const initialParams: EsQueryAlertParams = {
|
||||
index: ['test'],
|
||||
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n`,
|
||||
size: 100,
|
||||
timeWindowSize: 1,
|
||||
timeWindowUnit: 's',
|
||||
threshold: [0],
|
||||
|
@ -49,6 +52,7 @@ describe('expression params validation', () => {
|
|||
const initialParams: EsQueryAlertParams = {
|
||||
index: ['test'],
|
||||
esQuery: `{\n \"aggs\":{\n \"match_all\" : {}\n }\n}`,
|
||||
size: 100,
|
||||
timeWindowSize: 1,
|
||||
timeWindowUnit: 's',
|
||||
threshold: [0],
|
||||
|
@ -61,6 +65,7 @@ describe('expression params validation', () => {
|
|||
const initialParams: EsQueryAlertParams = {
|
||||
index: ['test'],
|
||||
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
|
||||
size: 100,
|
||||
threshold: [],
|
||||
timeWindowSize: 1,
|
||||
timeWindowUnit: 's',
|
||||
|
@ -74,6 +79,7 @@ describe('expression params validation', () => {
|
|||
const initialParams: EsQueryAlertParams = {
|
||||
index: ['test'],
|
||||
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
|
||||
size: 100,
|
||||
threshold: [1],
|
||||
timeWindowSize: 1,
|
||||
timeWindowUnit: 's',
|
||||
|
@ -87,6 +93,7 @@ describe('expression params validation', () => {
|
|||
const initialParams: EsQueryAlertParams = {
|
||||
index: ['test'],
|
||||
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
|
||||
size: 100,
|
||||
threshold: [10, 1],
|
||||
timeWindowSize: 1,
|
||||
timeWindowUnit: 's',
|
||||
|
@ -97,4 +104,34 @@ describe('expression params validation', () => {
|
|||
'Threshold 1 must be > Threshold 0.'
|
||||
);
|
||||
});
|
||||
|
||||
test('if size property is < 0 should return proper error message', () => {
|
||||
const initialParams: EsQueryAlertParams = {
|
||||
index: ['test'],
|
||||
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n`,
|
||||
size: -1,
|
||||
timeWindowSize: 1,
|
||||
timeWindowUnit: 's',
|
||||
threshold: [0],
|
||||
};
|
||||
expect(validateExpression(initialParams).errors.size.length).toBeGreaterThan(0);
|
||||
expect(validateExpression(initialParams).errors.size[0]).toBe(
|
||||
'Size must be between 0 and 10,000.'
|
||||
);
|
||||
});
|
||||
|
||||
test('if size property is > 10000 should return proper error message', () => {
|
||||
const initialParams: EsQueryAlertParams = {
|
||||
index: ['test'],
|
||||
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n`,
|
||||
size: 25000,
|
||||
timeWindowSize: 1,
|
||||
timeWindowUnit: 's',
|
||||
threshold: [0],
|
||||
};
|
||||
expect(validateExpression(initialParams).errors.size.length).toBeGreaterThan(0);
|
||||
expect(validateExpression(initialParams).errors.size[0]).toBe(
|
||||
'Size must be between 0 and 10,000.'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,12 +10,21 @@ import { EsQueryAlertParams } from './types';
|
|||
import { ValidationResult, builtInComparators } from '../../../../triggers_actions_ui/public';
|
||||
|
||||
export const validateExpression = (alertParams: EsQueryAlertParams): ValidationResult => {
|
||||
const { index, timeField, esQuery, threshold, timeWindowSize, thresholdComparator } = alertParams;
|
||||
const {
|
||||
index,
|
||||
timeField,
|
||||
esQuery,
|
||||
size,
|
||||
threshold,
|
||||
timeWindowSize,
|
||||
thresholdComparator,
|
||||
} = alertParams;
|
||||
const validationResult = { errors: {} };
|
||||
const errors = {
|
||||
index: new Array<string>(),
|
||||
timeField: new Array<string>(),
|
||||
esQuery: new Array<string>(),
|
||||
size: new Array<string>(),
|
||||
threshold0: new Array<string>(),
|
||||
threshold1: new Array<string>(),
|
||||
thresholdComparator: new Array<string>(),
|
||||
|
@ -94,5 +103,20 @@ export const validateExpression = (alertParams: EsQueryAlertParams): ValidationR
|
|||
})
|
||||
);
|
||||
}
|
||||
if (!size) {
|
||||
errors.size.push(
|
||||
i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredSizeText', {
|
||||
defaultMessage: 'Size is required.',
|
||||
})
|
||||
);
|
||||
}
|
||||
if ((size && size < 0) || size > 10000) {
|
||||
errors.size.push(
|
||||
i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.invalidSizeRangeText', {
|
||||
defaultMessage: 'Size must be between 0 and {max, number}.',
|
||||
values: { max: 10000 },
|
||||
})
|
||||
);
|
||||
}
|
||||
return validationResult;
|
||||
};
|
||||
|
|
|
@ -14,6 +14,7 @@ describe('ActionContext', () => {
|
|||
index: ['[index]'],
|
||||
timeField: '[timeField]',
|
||||
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
|
||||
size: 100,
|
||||
timeWindowSize: 5,
|
||||
timeWindowUnit: 'm',
|
||||
thresholdComparator: '>',
|
||||
|
@ -41,6 +42,7 @@ describe('ActionContext', () => {
|
|||
index: ['[index]'],
|
||||
timeField: '[timeField]',
|
||||
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
|
||||
size: 100,
|
||||
timeWindowSize: 5,
|
||||
timeWindowUnit: 'm',
|
||||
thresholdComparator: 'between',
|
||||
|
|
|
@ -57,6 +57,10 @@ describe('alertType', () => {
|
|||
"description": "The string representation of the ES query.",
|
||||
"name": "esQuery",
|
||||
},
|
||||
Object {
|
||||
"description": "The number of hits to retrieve for each query.",
|
||||
"name": "size",
|
||||
},
|
||||
Object {
|
||||
"description": "An array of values to use as the threshold; 'between' and 'notBetween' require two values, the others require one.",
|
||||
"name": "threshold",
|
||||
|
@ -75,6 +79,7 @@ describe('alertType', () => {
|
|||
index: ['index-name'],
|
||||
timeField: 'time-field',
|
||||
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
|
||||
size: 100,
|
||||
timeWindowSize: 5,
|
||||
timeWindowUnit: 'm',
|
||||
thresholdComparator: '<',
|
||||
|
@ -92,6 +97,7 @@ describe('alertType', () => {
|
|||
index: ['index-name'],
|
||||
timeField: 'time-field',
|
||||
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
|
||||
size: 100,
|
||||
timeWindowSize: 5,
|
||||
timeWindowUnit: 'm',
|
||||
thresholdComparator: 'between',
|
||||
|
|
|
@ -23,8 +23,6 @@ import { ESSearchHit } from '../../../../../typings/elasticsearch';
|
|||
|
||||
export const ES_QUERY_ID = '.es-query';
|
||||
|
||||
const DEFAULT_MAX_HITS_PER_EXECUTION = 1000;
|
||||
|
||||
const ActionGroupId = 'query matched';
|
||||
const ConditionMetAlertInstanceId = 'query matched';
|
||||
|
||||
|
@ -88,6 +86,13 @@ export function getAlertType(
|
|||
}
|
||||
);
|
||||
|
||||
const actionVariableContextSizeLabel = i18n.translate(
|
||||
'xpack.stackAlerts.esQuery.actionVariableContextSizeLabel',
|
||||
{
|
||||
defaultMessage: 'The number of hits to retrieve for each query.',
|
||||
}
|
||||
);
|
||||
|
||||
const actionVariableContextThresholdLabel = i18n.translate(
|
||||
'xpack.stackAlerts.esQuery.actionVariableContextThresholdLabel',
|
||||
{
|
||||
|
@ -130,6 +135,7 @@ export function getAlertType(
|
|||
params: [
|
||||
{ name: 'index', description: actionVariableContextIndexLabel },
|
||||
{ name: 'esQuery', description: actionVariableContextQueryLabel },
|
||||
{ name: 'size', description: actionVariableContextSizeLabel },
|
||||
{ name: 'threshold', description: actionVariableContextThresholdLabel },
|
||||
{ name: 'thresholdComparator', description: actionVariableContextThresholdComparatorLabel },
|
||||
],
|
||||
|
@ -160,7 +166,7 @@ export function getAlertType(
|
|||
}
|
||||
|
||||
// During each alert execution, we run the configured query, get a hit count
|
||||
// (hits.total) and retrieve up to DEFAULT_MAX_HITS_PER_EXECUTION hits. We
|
||||
// (hits.total) and retrieve up to params.size hits. We
|
||||
// evaluate the threshold condition using the value of hits.total. If the threshold
|
||||
// condition is met, the hits are counted toward the query match and we update
|
||||
// the alert state with the timestamp of the latest hit. In the next execution
|
||||
|
@ -200,7 +206,7 @@ export function getAlertType(
|
|||
from: dateStart,
|
||||
to: dateEnd,
|
||||
filter,
|
||||
size: DEFAULT_MAX_HITS_PER_EXECUTION,
|
||||
size: params.size,
|
||||
sortOrder: 'desc',
|
||||
searchAfterSortId: undefined,
|
||||
timeField: params.timeField,
|
||||
|
|
|
@ -7,12 +7,17 @@
|
|||
|
||||
import { TypeOf } from '@kbn/config-schema';
|
||||
import type { Writable } from '@kbn/utility-types';
|
||||
import { EsQueryAlertParamsSchema, EsQueryAlertParams } from './alert_type_params';
|
||||
import {
|
||||
EsQueryAlertParamsSchema,
|
||||
EsQueryAlertParams,
|
||||
ES_QUERY_MAX_HITS_PER_EXECUTION,
|
||||
} from './alert_type_params';
|
||||
|
||||
const DefaultParams: Writable<Partial<EsQueryAlertParams>> = {
|
||||
index: ['index-name'],
|
||||
timeField: 'time-field',
|
||||
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
|
||||
size: 100,
|
||||
timeWindowSize: 5,
|
||||
timeWindowUnit: 'm',
|
||||
thresholdComparator: '>',
|
||||
|
@ -99,6 +104,28 @@ describe('alertType Params validate()', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('fails for invalid size', async () => {
|
||||
delete params.size;
|
||||
expect(onValidate()).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[size]: expected value of type [number] but got [undefined]"`
|
||||
);
|
||||
|
||||
params.size = 'foo';
|
||||
expect(onValidate()).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[size]: expected value of type [number] but got [string]"`
|
||||
);
|
||||
|
||||
params.size = -1;
|
||||
expect(onValidate()).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[size]: Value must be equal to or greater than [0]."`
|
||||
);
|
||||
|
||||
params.size = ES_QUERY_MAX_HITS_PER_EXECUTION + 1;
|
||||
expect(onValidate()).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[size]: Value must be equal to or lower than [10000]."`
|
||||
);
|
||||
});
|
||||
|
||||
it('fails for invalid timeWindowSize', async () => {
|
||||
delete params.timeWindowSize;
|
||||
expect(onValidate()).toThrowErrorMatchingInlineSnapshot(
|
||||
|
|
|
@ -11,6 +11,8 @@ import { ComparatorFnNames } from '../lib';
|
|||
import { validateTimeWindowUnits } from '../../../../triggers_actions_ui/server';
|
||||
import { AlertTypeState } from '../../../../alerts/server';
|
||||
|
||||
export const ES_QUERY_MAX_HITS_PER_EXECUTION = 10000;
|
||||
|
||||
// alert type parameters
|
||||
export type EsQueryAlertParams = TypeOf<typeof EsQueryAlertParamsSchema>;
|
||||
export interface EsQueryAlertState extends AlertTypeState {
|
||||
|
@ -21,6 +23,7 @@ export const EsQueryAlertParamsSchemaProperties = {
|
|||
index: schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1 }),
|
||||
timeField: schema.string({ minLength: 1 }),
|
||||
esQuery: schema.string({ minLength: 1 }),
|
||||
size: schema.number({ min: 0, max: ES_QUERY_MAX_HITS_PER_EXECUTION }),
|
||||
timeWindowSize: schema.number({ min: 1 }),
|
||||
timeWindowUnit: schema.string({ validate: validateTimeWindowUnits }),
|
||||
threshold: schema.arrayOf(schema.number(), { minSize: 1, maxSize: 2 }),
|
||||
|
|
|
@ -10,3 +10,4 @@ export { OfExpression } from './of';
|
|||
export { GroupByExpression } from './group_by_over';
|
||||
export { ThresholdExpression } from './threshold';
|
||||
export { ForLastExpression } from './for_the_last';
|
||||
export { ValueExpression } from './value';
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* 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 * as React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { ValueExpression } from './value';
|
||||
import { mountWithIntl, nextTick } from '@kbn/test/jest';
|
||||
|
||||
describe('value expression', () => {
|
||||
it('renders description and value', () => {
|
||||
const wrapper = shallow(
|
||||
<ValueExpression
|
||||
description="test"
|
||||
value={1000}
|
||||
errors={[]}
|
||||
onChangeSelectedValue={jest.fn()}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="valueFieldTitle"]')).toMatchInlineSnapshot(`
|
||||
<ClosablePopoverTitle
|
||||
data-test-subj="valueFieldTitle"
|
||||
onClose={[Function]}
|
||||
>
|
||||
test
|
||||
</ClosablePopoverTitle>
|
||||
`);
|
||||
expect(wrapper.find('[data-test-subj="valueFieldNumberForm"]')).toMatchInlineSnapshot(`
|
||||
<EuiFormRow
|
||||
data-test-subj="valueFieldNumberForm"
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
error={Array []}
|
||||
fullWidth={false}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
isInvalid={false}
|
||||
labelType="label"
|
||||
>
|
||||
<EuiFieldNumber
|
||||
data-test-subj="valueFieldNumber"
|
||||
isInvalid={false}
|
||||
min={0}
|
||||
onChange={[Function]}
|
||||
value={1000}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
`);
|
||||
});
|
||||
|
||||
it('renders errors', () => {
|
||||
const wrapper = shallow(
|
||||
<ValueExpression
|
||||
description="test"
|
||||
value={1000}
|
||||
errors={['value is not valid']}
|
||||
onChangeSelectedValue={jest.fn()}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find('[data-test-subj="valueFieldNumberForm"]')).toMatchInlineSnapshot(`
|
||||
<EuiFormRow
|
||||
data-test-subj="valueFieldNumberForm"
|
||||
describedByIds={Array []}
|
||||
display="row"
|
||||
error={
|
||||
Array [
|
||||
"value is not valid",
|
||||
]
|
||||
}
|
||||
fullWidth={false}
|
||||
hasChildLabel={true}
|
||||
hasEmptyLabelSpace={false}
|
||||
isInvalid={true}
|
||||
labelType="label"
|
||||
>
|
||||
<EuiFieldNumber
|
||||
data-test-subj="valueFieldNumber"
|
||||
isInvalid={true}
|
||||
min={0}
|
||||
onChange={[Function]}
|
||||
value={1000}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
`);
|
||||
});
|
||||
|
||||
it('renders closed popover initially and opens on click', async () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ValueExpression
|
||||
description="test"
|
||||
value={1000}
|
||||
errors={[]}
|
||||
onChangeSelectedValue={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(wrapper.find('[data-test-subj="valueExpression"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="valueFieldTitle"]').exists()).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="valueFieldNumber"]').exists()).toBeFalsy();
|
||||
|
||||
wrapper.find('[data-test-subj="valueExpression"]').first().simulate('click');
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
expect(wrapper.find('[data-test-subj="valueFieldTitle"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="valueFieldNumber"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('emits onChangeSelectedValue action when value is updated', async () => {
|
||||
const onChangeSelectedValue = jest.fn();
|
||||
const wrapper = mountWithIntl(
|
||||
<ValueExpression
|
||||
description="test"
|
||||
value={1000}
|
||||
errors={[]}
|
||||
onChangeSelectedValue={onChangeSelectedValue}
|
||||
/>
|
||||
);
|
||||
|
||||
wrapper.find('[data-test-subj="valueExpression"]').first().simulate('click');
|
||||
await act(async () => {
|
||||
await nextTick();
|
||||
wrapper.update();
|
||||
});
|
||||
wrapper
|
||||
.find('input[data-test-subj="valueFieldNumber"]')
|
||||
.simulate('change', { target: { value: 3000 } });
|
||||
expect(onChangeSelectedValue).toHaveBeenCalledWith(3000);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* 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 React, { useState } from 'react';
|
||||
import {
|
||||
EuiExpression,
|
||||
EuiPopover,
|
||||
EuiFieldNumber,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
} from '@elastic/eui';
|
||||
import { ClosablePopoverTitle } from './components';
|
||||
import { IErrorObject } from '../../types';
|
||||
|
||||
interface ValueExpressionProps {
|
||||
description: string;
|
||||
value: number;
|
||||
onChangeSelectedValue: (updatedValue: number) => void;
|
||||
popupPosition?:
|
||||
| 'upCenter'
|
||||
| 'upLeft'
|
||||
| 'upRight'
|
||||
| 'downCenter'
|
||||
| 'downLeft'
|
||||
| 'downRight'
|
||||
| 'leftCenter'
|
||||
| 'leftUp'
|
||||
| 'leftDown'
|
||||
| 'rightCenter'
|
||||
| 'rightUp'
|
||||
| 'rightDown';
|
||||
display?: 'fullWidth' | 'inline';
|
||||
errors: string | string[] | IErrorObject;
|
||||
}
|
||||
|
||||
export const ValueExpression = ({
|
||||
description,
|
||||
value,
|
||||
onChangeSelectedValue,
|
||||
display = 'inline',
|
||||
popupPosition,
|
||||
errors,
|
||||
}: ValueExpressionProps) => {
|
||||
const [valuePopoverOpen, setValuePopoverOpen] = useState(false);
|
||||
return (
|
||||
<EuiPopover
|
||||
button={
|
||||
<EuiExpression
|
||||
data-test-subj="valueExpression"
|
||||
description={description}
|
||||
value={value}
|
||||
isActive={valuePopoverOpen}
|
||||
display={display === 'inline' ? 'inline' : 'columns'}
|
||||
onClick={() => {
|
||||
setValuePopoverOpen(true);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
isOpen={valuePopoverOpen}
|
||||
closePopover={() => {
|
||||
setValuePopoverOpen(false);
|
||||
}}
|
||||
ownFocus
|
||||
display={display === 'fullWidth' ? 'block' : 'inlineBlock'}
|
||||
anchorPosition={popupPosition ?? 'downLeft'}
|
||||
repositionOnScroll
|
||||
>
|
||||
<div>
|
||||
<ClosablePopoverTitle
|
||||
data-test-subj="valueFieldTitle"
|
||||
onClose={() => setValuePopoverOpen(false)}
|
||||
>
|
||||
<>{description}</>
|
||||
</ClosablePopoverTitle>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFormRow
|
||||
data-test-subj="valueFieldNumberForm"
|
||||
isInvalid={errors.length > 0 && value !== undefined}
|
||||
error={errors}
|
||||
>
|
||||
<EuiFieldNumber
|
||||
data-test-subj="valueFieldNumber"
|
||||
min={0}
|
||||
value={value}
|
||||
isInvalid={errors.length > 0 && value !== undefined}
|
||||
onChange={(e: any) => {
|
||||
onChangeSelectedValue(e.target.value as number);
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
|
@ -68,6 +68,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
|
|||
await createAlert({
|
||||
name: 'never fire',
|
||||
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
|
||||
size: 100,
|
||||
thresholdComparator: '<',
|
||||
threshold: [0],
|
||||
});
|
||||
|
@ -75,6 +76,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
|
|||
await createAlert({
|
||||
name: 'always fire',
|
||||
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
|
||||
size: 100,
|
||||
thresholdComparator: '>',
|
||||
threshold: [-1],
|
||||
});
|
||||
|
@ -123,6 +125,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
|
|||
await createAlert({
|
||||
name: 'never fire',
|
||||
esQuery: JSON.stringify(rangeQuery(ES_GROUPS_TO_WRITE * ALERT_INTERVALS_TO_WRITE + 1)),
|
||||
size: 100,
|
||||
thresholdComparator: '>=',
|
||||
threshold: [0],
|
||||
});
|
||||
|
@ -132,6 +135,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
|
|||
esQuery: JSON.stringify(
|
||||
rangeQuery(Math.floor((ES_GROUPS_TO_WRITE * ALERT_INTERVALS_TO_WRITE) / 2))
|
||||
),
|
||||
size: 100,
|
||||
thresholdComparator: '>=',
|
||||
threshold: [0],
|
||||
});
|
||||
|
@ -173,6 +177,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
|
|||
name: string;
|
||||
timeField?: string;
|
||||
esQuery: string;
|
||||
size: number;
|
||||
thresholdComparator: string;
|
||||
threshold: number[];
|
||||
timeWindowSize?: number;
|
||||
|
@ -215,6 +220,7 @@ export default function alertTests({ getService }: FtrProviderContext) {
|
|||
index: [ES_TEST_INDEX_NAME],
|
||||
timeField: params.timeField || 'date',
|
||||
esQuery: params.esQuery,
|
||||
size: params.size,
|
||||
timeWindowSize: params.timeWindowSize || ALERT_INTERVAL_SECONDS * 5,
|
||||
timeWindowUnit: 's',
|
||||
thresholdComparator: params.thresholdComparator,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue