mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Response Ops] Rule Specific Flapping - Create/Edit Rule Flyout Frontend Changes (#189341)
## Summary Issue: https://github.com/elastic/kibana/issues/189135 Frontend changes for the rule specific flapping feature in the existing rule flyout. To test: Simply go to `x-pack/plugins/triggers_actions_ui/public/common/constants/index.ts` and set line 89 `IS_RULE_SPECIFIC_FLAPPING_ENABLED = false` to `true`. This acts as a feature flag. Then you may go to the create/edit rule flyout to see this new input. This PR does not contain changes to allow for saving/editing of this field. That will come with the backend PR. ### Flapping Enabled Without Override <img width="648" alt="Screenshot 2024-07-29 at 12 11 02 AM" src="https://github.com/user-attachments/assets/82b552c5-faf3-459f-a22d-69ea95292d89"> ### Flapping Enabled With Override <img width="652" alt="Screenshot 2024-07-29 at 12 11 07 AM" src="https://github.com/user-attachments/assets/2d305570-8cd6-4488-af5b-8c78cb3c2b3a"> ### Flapping Disabled With or Without Override <img width="652" alt="image" src="https://github.com/user-attachments/assets/5bb76e6a-85e5-4992-a37f-a737d083ec54"> ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
01aae2331d
commit
a2873c0c87
31 changed files with 1159 additions and 321 deletions
|
@ -17,4 +17,5 @@ export * from './r_rule_types';
|
|||
export * from './rule_notify_when_type';
|
||||
export * from './rule_type_types';
|
||||
export * from './rule_types';
|
||||
export * from './rule_flapping';
|
||||
export * from './search_strategy_types';
|
||||
|
|
12
packages/kbn-alerting-types/rule_flapping.ts
Normal file
12
packages/kbn-alerting-types/rule_flapping.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export const MIN_LOOK_BACK_WINDOW = 2;
|
||||
export const MAX_LOOK_BACK_WINDOW = 20;
|
||||
export const MIN_STATUS_CHANGE_THRESHOLD = 2;
|
||||
export const MAX_STATUS_CHANGE_THRESHOLD = 20;
|
|
@ -239,6 +239,10 @@ export interface Rule<Params extends RuleTypeParams = never> {
|
|||
running?: boolean | null;
|
||||
viewInAppRelativeUrl?: string;
|
||||
alertDelay?: AlertDelay | null;
|
||||
flapping?: {
|
||||
lookBackWindow: number;
|
||||
statusChangeThreshold: number;
|
||||
};
|
||||
}
|
||||
|
||||
export type SanitizedRule<Params extends RuleTypeParams = never> = Omit<
|
||||
|
|
|
@ -62,6 +62,10 @@ describe('createRule', () => {
|
|||
alert_delay: {
|
||||
active: 10,
|
||||
},
|
||||
flapping: {
|
||||
look_back_window: 10,
|
||||
status_change_threshold: 10,
|
||||
},
|
||||
};
|
||||
|
||||
const ruleToCreate: CreateRuleBody<RuleTypeParams> = {
|
||||
|
@ -109,10 +113,18 @@ describe('createRule', () => {
|
|||
alertDelay: {
|
||||
active: 10,
|
||||
},
|
||||
flapping: {
|
||||
lookBackWindow: 10,
|
||||
statusChangeThreshold: 10,
|
||||
},
|
||||
};
|
||||
http.post.mockResolvedValueOnce(resolvedValue);
|
||||
|
||||
const result = await createRule({ http, rule: ruleToCreate as CreateRuleBody });
|
||||
expect(http.post).toHaveBeenCalledWith('/api/alerting/rule', {
|
||||
body: '{"params":{"aggType":"count","termSize":5,"thresholdComparator":">","timeWindowSize":5,"timeWindowUnit":"m","groupBy":"all","threshold":[1000],"index":[".kibana"],"timeField":"alert.executionStatus.lastExecutionDate"},"consumer":"alerts","schedule":{"interval":"1m"},"tags":[],"name":"test","enabled":true,"throttle":null,"notifyWhen":"onActionGroupChange","rule_type_id":".index-threshold","actions":[{"group":"threshold met","id":"83d4d860-9316-11eb-a145-93ab369a4461","params":{"level":"info","message":"Rule \'{{rule.name}}\' is active for group \'{{context.group}}\':\\n\\n- Value: {{context.value}}\\n- Conditions Met: {{context.conditions}} over {{rule.params.timeWindowSize}}{{rule.params.timeWindowUnit}}\\n- Timestamp: {{context.date}}"},"frequency":{"notify_when":"onActionGroupChange","throttle":null,"summary":false}},{"id":".test-system-action","params":{}}],"alert_delay":{"active":10},"flapping":{"look_back_window":10,"status_change_threshold":10}}',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
actions: [
|
||||
{
|
||||
|
@ -169,6 +181,10 @@ describe('createRule', () => {
|
|||
alertDelay: {
|
||||
active: 10,
|
||||
},
|
||||
flapping: {
|
||||
lookBackWindow: 10,
|
||||
statusChangeThreshold: 10,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -55,6 +55,10 @@ const ruleToCreate: CreateRuleBody<RuleTypeParams> = {
|
|||
alertDelay: {
|
||||
active: 10,
|
||||
},
|
||||
flapping: {
|
||||
lookBackWindow: 10,
|
||||
statusChangeThreshold: 10,
|
||||
},
|
||||
};
|
||||
|
||||
describe('transformCreateRuleBody', () => {
|
||||
|
@ -96,8 +100,11 @@ describe('transformCreateRuleBody', () => {
|
|||
},
|
||||
{ id: '.test-system-action', params: {} },
|
||||
],
|
||||
|
||||
alert_delay: { active: 10 },
|
||||
flapping: {
|
||||
look_back_window: 10,
|
||||
status_change_threshold: 10,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,11 +8,26 @@
|
|||
|
||||
import { RewriteResponseCase } from '@kbn/actions-types';
|
||||
import { CreateRuleBody } from './types';
|
||||
import { Rule } from '../../types';
|
||||
|
||||
const transformCreateRuleFlapping = (flapping: Rule['flapping']) => {
|
||||
if (!flapping) {
|
||||
return flapping;
|
||||
}
|
||||
|
||||
return {
|
||||
flapping: {
|
||||
look_back_window: flapping.lookBackWindow,
|
||||
status_change_threshold: flapping.statusChangeThreshold,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const transformCreateRuleBody: RewriteResponseCase<CreateRuleBody> = ({
|
||||
ruleTypeId,
|
||||
actions = [],
|
||||
alertDelay,
|
||||
flapping,
|
||||
...res
|
||||
}): any => ({
|
||||
...res,
|
||||
|
@ -44,4 +59,5 @@ export const transformCreateRuleBody: RewriteResponseCase<CreateRuleBody> = ({
|
|||
};
|
||||
}),
|
||||
...(alertDelay ? { alert_delay: alertDelay } : {}),
|
||||
...(flapping !== undefined ? transformCreateRuleFlapping(flapping) : {}),
|
||||
});
|
||||
|
|
|
@ -20,4 +20,5 @@ export interface CreateRuleBody<Params extends RuleTypeParams = RuleTypeParams>
|
|||
throttle?: Rule<Params>['throttle'];
|
||||
notifyWhen?: Rule<Params>['notifyWhen'];
|
||||
alertDelay?: Rule<Params>['alertDelay'];
|
||||
flapping?: Rule<Params>['flapping'];
|
||||
}
|
||||
|
|
|
@ -52,6 +52,10 @@ const ruleToUpdate: UpdateRuleBody<RuleTypeParams> = {
|
|||
alertDelay: {
|
||||
active: 10,
|
||||
},
|
||||
flapping: {
|
||||
lookBackWindow: 10,
|
||||
statusChangeThreshold: 10,
|
||||
},
|
||||
};
|
||||
|
||||
describe('transformUpdateRuleBody', () => {
|
||||
|
@ -98,6 +102,10 @@ describe('transformUpdateRuleBody', () => {
|
|||
},
|
||||
tags: [],
|
||||
throttle: null,
|
||||
flapping: {
|
||||
look_back_window: 10,
|
||||
status_change_threshold: 10,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,10 +8,25 @@
|
|||
|
||||
import { RewriteResponseCase } from '@kbn/actions-types';
|
||||
import { UpdateRuleBody } from './types';
|
||||
import { Rule } from '../../types';
|
||||
|
||||
const transformUpdateRuleFlapping = (flapping: Rule['flapping']) => {
|
||||
if (!flapping) {
|
||||
return flapping;
|
||||
}
|
||||
|
||||
return {
|
||||
flapping: {
|
||||
look_back_window: flapping.lookBackWindow,
|
||||
status_change_threshold: flapping.statusChangeThreshold,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const transformUpdateRuleBody: RewriteResponseCase<UpdateRuleBody> = ({
|
||||
actions = [],
|
||||
alertDelay,
|
||||
flapping,
|
||||
...res
|
||||
}): any => ({
|
||||
...res,
|
||||
|
@ -41,4 +56,5 @@ export const transformUpdateRuleBody: RewriteResponseCase<UpdateRuleBody> = ({
|
|||
};
|
||||
}),
|
||||
...(alertDelay ? { alert_delay: alertDelay } : {}),
|
||||
...(flapping !== undefined ? transformUpdateRuleFlapping(flapping) : {}),
|
||||
});
|
||||
|
|
|
@ -17,4 +17,5 @@ export interface UpdateRuleBody<Params extends RuleTypeParams = RuleTypeParams>
|
|||
throttle?: Rule<Params>['throttle'];
|
||||
notifyWhen?: Rule<Params>['notifyWhen'];
|
||||
alertDelay?: Rule<Params>['alertDelay'];
|
||||
flapping?: Rule<Params>['flapping'];
|
||||
}
|
||||
|
|
|
@ -7,75 +7,38 @@
|
|||
*/
|
||||
|
||||
import { httpServiceMock } from '@kbn/core/public/mocks';
|
||||
import { Rule } from '../../types';
|
||||
import { updateRule, UpdateRuleBody } from '.';
|
||||
|
||||
const http = httpServiceMock.createStartContract();
|
||||
|
||||
describe('updateRule', () => {
|
||||
test('should call rule update API', async () => {
|
||||
const ruleToUpdate = {
|
||||
const updatedRule = {
|
||||
params: {
|
||||
aggType: 'count',
|
||||
termSize: 5,
|
||||
thresholdComparator: '>',
|
||||
timeWindowSize: 5,
|
||||
timeWindowUnit: 'm',
|
||||
groupBy: 'all',
|
||||
threshold: [1000],
|
||||
index: ['.kibana'],
|
||||
timeField: 'alert.executionStatus.lastExecutionDate',
|
||||
},
|
||||
consumer: 'alerts',
|
||||
schedule: { interval: '1m' },
|
||||
tags: [],
|
||||
name: 'test',
|
||||
tags: ['foo'],
|
||||
schedule: {
|
||||
interval: '1m',
|
||||
},
|
||||
params: {},
|
||||
createdAt: new Date('1970-01-01T00:00:00.000Z'),
|
||||
updatedAt: new Date('1970-01-01T00:00:00.000Z'),
|
||||
apiKey: null,
|
||||
apiKeyOwner: null,
|
||||
revision: 0,
|
||||
alertDelay: {
|
||||
active: 10,
|
||||
},
|
||||
rule_type_id: '.index-threshold',
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: '2',
|
||||
actionTypeId: 'test',
|
||||
params: {},
|
||||
useAlertDataForTemplate: false,
|
||||
frequency: {
|
||||
notifyWhen: 'onActionGroupChange',
|
||||
throttle: null,
|
||||
summary: false,
|
||||
group: 'threshold met',
|
||||
id: '1',
|
||||
params: {
|
||||
level: 'info',
|
||||
message: 'alert ',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '.test-system-action',
|
||||
params: {},
|
||||
actionTypeId: '.system-action',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const resolvedValue: Rule = {
|
||||
...ruleToUpdate,
|
||||
id: '12/3',
|
||||
enabled: true,
|
||||
ruleTypeId: 'test',
|
||||
createdBy: null,
|
||||
updatedBy: null,
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
executionStatus: {
|
||||
status: 'unknown',
|
||||
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
|
||||
},
|
||||
revision: 1,
|
||||
};
|
||||
|
||||
http.put.mockResolvedValueOnce({
|
||||
...resolvedValue,
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: '2',
|
||||
connector_type_id: 'test',
|
||||
params: {},
|
||||
use_alert_data_for_template: false,
|
||||
connector_type_id: '.server-log',
|
||||
frequency: {
|
||||
notify_when: 'onActionGroupChange',
|
||||
throttle: null,
|
||||
|
@ -88,12 +51,31 @@ describe('updateRule', () => {
|
|||
connector_type_id: '.system-action',
|
||||
},
|
||||
],
|
||||
});
|
||||
scheduled_task_id: '1',
|
||||
execution_status: { status: 'pending', last_execution_date: '2021-04-01T21:33:13.250Z' },
|
||||
create_at: '2021-04-01T21:33:13.247Z',
|
||||
updated_at: '2021-04-01T21:33:13.247Z',
|
||||
create_by: 'user',
|
||||
updated_by: 'user',
|
||||
alert_delay: {
|
||||
active: 10,
|
||||
},
|
||||
flapping: {
|
||||
look_back_window: 10,
|
||||
status_change_threshold: 10,
|
||||
},
|
||||
};
|
||||
|
||||
const result = await updateRule({ http, id: '12/3', rule: ruleToUpdate as UpdateRuleBody });
|
||||
|
||||
expect(result).toEqual({
|
||||
...resolvedValue,
|
||||
const updateRuleBody = {
|
||||
name: 'test-update',
|
||||
tags: ['foo', 'bar'],
|
||||
schedule: {
|
||||
interval: '5m',
|
||||
},
|
||||
params: {},
|
||||
alertDelay: {
|
||||
active: 50,
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
|
@ -107,16 +89,88 @@ describe('updateRule', () => {
|
|||
summary: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
flapping: {
|
||||
lookBackWindow: 10,
|
||||
statusChangeThreshold: 10,
|
||||
},
|
||||
};
|
||||
|
||||
http.put.mockResolvedValueOnce({
|
||||
...updatedRule,
|
||||
name: 'test-update',
|
||||
tags: ['foo', 'bar'],
|
||||
schedule: {
|
||||
interval: '5m',
|
||||
},
|
||||
params: {},
|
||||
alert_delay: {
|
||||
active: 50,
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
id: '.test-system-action',
|
||||
group: 'default',
|
||||
id: '2',
|
||||
action_type_dd: 'test',
|
||||
params: {},
|
||||
actionTypeId: '.system-action',
|
||||
use_alert_data_for_template: false,
|
||||
frequency: {
|
||||
notify_when: 'onActionGroupChange',
|
||||
throttle: null,
|
||||
summary: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
flapping: {
|
||||
look_back_window: 10,
|
||||
status_change_threshold: 10,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await updateRule({ http, id: '12/3', rule: updateRuleBody as UpdateRuleBody });
|
||||
|
||||
expect(result).toEqual({
|
||||
actions: [
|
||||
{
|
||||
frequency: {
|
||||
notifyWhen: 'onActionGroupChange',
|
||||
summary: false,
|
||||
throttle: null,
|
||||
},
|
||||
group: 'default',
|
||||
id: '2',
|
||||
params: {},
|
||||
useAlertDataForTemplate: false,
|
||||
},
|
||||
],
|
||||
alertDelay: {
|
||||
active: 50,
|
||||
},
|
||||
consumer: 'alerts',
|
||||
create_at: '2021-04-01T21:33:13.247Z',
|
||||
create_by: 'user',
|
||||
executionStatus: {
|
||||
lastExecutionDate: '2021-04-01T21:33:13.250Z',
|
||||
status: 'pending',
|
||||
},
|
||||
flapping: {
|
||||
lookBackWindow: 10,
|
||||
statusChangeThreshold: 10,
|
||||
},
|
||||
name: 'test-update',
|
||||
params: {},
|
||||
ruleTypeId: '.index-threshold',
|
||||
schedule: {
|
||||
interval: '5m',
|
||||
},
|
||||
scheduledTaskId: '1',
|
||||
tags: ['foo', 'bar'],
|
||||
updatedAt: '2021-04-01T21:33:13.247Z',
|
||||
updatedBy: 'user',
|
||||
});
|
||||
|
||||
expect(http.put).toHaveBeenCalledWith('/api/alerting/rule/12%2F3', {
|
||||
body: '{"name":"test","tags":["foo"],"schedule":{"interval":"1m"},"params":{},"actions":[{"group":"default","id":"2","params":{},"frequency":{"notify_when":"onActionGroupChange","throttle":null,"summary":false},"use_alert_data_for_template":false},{"id":".test-system-action","params":{}}],"alert_delay":{"active":10}}',
|
||||
body: '{"name":"test-update","tags":["foo","bar"],"schedule":{"interval":"5m"},"params":{},"actions":[{"group":"default","id":"2","params":{},"frequency":{"notify_when":"onActionGroupChange","throttle":null,"summary":false},"use_alert_data_for_template":false}],"alert_delay":{"active":50},"flapping":{"look_back_window":10,"status_change_threshold":10}}',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -21,6 +21,7 @@ export const UPDATE_FIELDS: Array<keyof UpdateRuleBody> = [
|
|||
'schedule',
|
||||
'params',
|
||||
'alertDelay',
|
||||
'flapping',
|
||||
];
|
||||
|
||||
export const UPDATE_FIELDS_WITH_ACTIONS: Array<keyof UpdateRuleBody> = [
|
||||
|
@ -30,6 +31,7 @@ export const UPDATE_FIELDS_WITH_ACTIONS: Array<keyof UpdateRuleBody> = [
|
|||
'params',
|
||||
'alertDelay',
|
||||
'actions',
|
||||
'flapping',
|
||||
];
|
||||
|
||||
export async function updateRule({
|
||||
|
|
|
@ -33,6 +33,19 @@ const transformLastRun: RewriteRequestCase<RuleLastRun> = ({
|
|||
...rest,
|
||||
});
|
||||
|
||||
const transformFlapping = (flapping: AsApiContract<Rule['flapping']>) => {
|
||||
if (!flapping) {
|
||||
return flapping;
|
||||
}
|
||||
|
||||
return {
|
||||
flapping: {
|
||||
lookBackWindow: flapping.look_back_window,
|
||||
statusChangeThreshold: flapping.status_change_threshold,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const transformRule: RewriteRequestCase<Rule> = ({
|
||||
rule_type_id: ruleTypeId,
|
||||
created_by: createdBy,
|
||||
|
@ -53,6 +66,7 @@ export const transformRule: RewriteRequestCase<Rule> = ({
|
|||
last_run: lastRun,
|
||||
next_run: nextRun,
|
||||
alert_delay: alertDelay,
|
||||
flapping,
|
||||
...rest
|
||||
}: any) => ({
|
||||
ruleTypeId,
|
||||
|
@ -76,6 +90,7 @@ export const transformRule: RewriteRequestCase<Rule> = ({
|
|||
...(nextRun ? { nextRun } : {}),
|
||||
...(apiKeyCreatedByUser !== undefined ? { apiKeyCreatedByUser } : {}),
|
||||
...(alertDelay ? { alertDelay } : {}),
|
||||
...(flapping !== undefined ? transformFlapping(flapping) : {}),
|
||||
...rest,
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import {
|
||||
MIN_LOOK_BACK_WINDOW,
|
||||
MAX_LOOK_BACK_WINDOW,
|
||||
MIN_STATUS_CHANGE_THRESHOLD,
|
||||
MAX_STATUS_CHANGE_THRESHOLD,
|
||||
} from '@kbn/alerting-types';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { RuleSettingsRangeInput } from './rule_settings_range_input';
|
||||
|
||||
const lookBackWindowLabel = i18n.translate(
|
||||
'alertsUIShared.ruleSettingsFlappingInputsProps.lookBackWindowLabel',
|
||||
{
|
||||
defaultMessage: 'Rule run look back window',
|
||||
}
|
||||
);
|
||||
|
||||
const lookBackWindowHelp = i18n.translate(
|
||||
'alertsUIShared.ruleSettingsFlappingInputsProps.lookBackWindowHelp',
|
||||
{
|
||||
defaultMessage: 'The minimum number of runs in which the threshold must be met.',
|
||||
}
|
||||
);
|
||||
|
||||
const statusChangeThresholdLabel = i18n.translate(
|
||||
'alertsUIShared.ruleSettingsFlappingInputsProps.statusChangeThresholdLabel',
|
||||
{
|
||||
defaultMessage: 'Alert status change threshold',
|
||||
}
|
||||
);
|
||||
|
||||
const statusChangeThresholdHelp = i18n.translate(
|
||||
'alertsUIShared.ruleSettingsFlappingInputsProps.statusChangeThresholdHelp',
|
||||
{
|
||||
defaultMessage:
|
||||
'The minimum number of times an alert must switch states in the look back window.',
|
||||
}
|
||||
);
|
||||
|
||||
export interface RuleSettingsFlappingInputsProps {
|
||||
lookBackWindow: number;
|
||||
statusChangeThreshold: number;
|
||||
isDisabled?: boolean;
|
||||
onLookBackWindowChange: (value: number) => void;
|
||||
onStatusChangeThresholdChange: (value: number) => void;
|
||||
}
|
||||
|
||||
export const RuleSettingsFlappingInputs = (props: RuleSettingsFlappingInputsProps) => {
|
||||
const {
|
||||
lookBackWindow,
|
||||
statusChangeThreshold,
|
||||
isDisabled = false,
|
||||
onLookBackWindowChange,
|
||||
onStatusChangeThresholdChange,
|
||||
} = props;
|
||||
|
||||
const internalOnLookBackWindowChange = useCallback(
|
||||
(e) => {
|
||||
onLookBackWindowChange(parseInt(e.currentTarget.value, 10));
|
||||
},
|
||||
[onLookBackWindowChange]
|
||||
);
|
||||
|
||||
const internalOnStatusChangeThresholdChange = useCallback(
|
||||
(e) => {
|
||||
onStatusChangeThresholdChange(parseInt(e.currentTarget.value, 10));
|
||||
},
|
||||
[onStatusChangeThresholdChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
<RuleSettingsRangeInput
|
||||
fullWidth
|
||||
data-test-subj="lookBackWindowRangeInput"
|
||||
min={MIN_LOOK_BACK_WINDOW}
|
||||
max={MAX_LOOK_BACK_WINDOW}
|
||||
value={lookBackWindow}
|
||||
onChange={internalOnLookBackWindowChange}
|
||||
label={lookBackWindowLabel}
|
||||
labelPopoverText={lookBackWindowHelp}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<RuleSettingsRangeInput
|
||||
fullWidth
|
||||
data-test-subj="statusChangeThresholdRangeInput"
|
||||
min={MIN_STATUS_CHANGE_THRESHOLD}
|
||||
max={MAX_STATUS_CHANGE_THRESHOLD}
|
||||
value={statusChangeThreshold}
|
||||
onChange={internalOnStatusChangeThresholdChange}
|
||||
label={statusChangeThresholdLabel}
|
||||
labelPopoverText={statusChangeThresholdHelp}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { EuiText } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import React from 'react';
|
||||
|
||||
const getLookBackWindowLabelRuleRuns = (amount: number) => {
|
||||
return i18n.translate('alertsUIShared.ruleSettingsFlappingMessage.lookBackWindowLabelRuleRuns', {
|
||||
defaultMessage: '{amount, number} rule {amount, plural, one {run} other {runs}}',
|
||||
values: { amount },
|
||||
});
|
||||
};
|
||||
|
||||
const getStatusChangeThresholdRuleRuns = (amount: number) => {
|
||||
return i18n.translate('alertsUIShared.ruleSettingsFlappingMessage.statusChangeThresholdTimes', {
|
||||
defaultMessage: '{amount, number} {amount, plural, one {time} other {times}}',
|
||||
values: { amount },
|
||||
});
|
||||
};
|
||||
|
||||
export const flappingOffMessage = i18n.translate(
|
||||
'alertsUIShared.ruleSettingsFlappingMessage.flappingOffMessage',
|
||||
{
|
||||
defaultMessage:
|
||||
'Alert flapping detection is off. Alerts will be generated based on the rule interval, which might result in higher alert volumes.',
|
||||
}
|
||||
);
|
||||
|
||||
export interface RuleSettingsFlappingMessageProps {
|
||||
lookBackWindow: number;
|
||||
statusChangeThreshold: number;
|
||||
}
|
||||
|
||||
export const RuleSettingsFlappingMessage = (props: RuleSettingsFlappingMessageProps) => {
|
||||
const { lookBackWindow, statusChangeThreshold } = props;
|
||||
|
||||
return (
|
||||
<EuiText size="s" data-test-subj="ruleSettingsFlappingMessage">
|
||||
<FormattedMessage
|
||||
id="alertsUIShared.ruleSettingsFlappingMessage.flappingSettingsDescription"
|
||||
defaultMessage="An alert is flapping if it changes status at least {statusChangeThreshold} in the last {lookBackWindow}."
|
||||
values={{
|
||||
lookBackWindow: <b>{getLookBackWindowLabelRuleRuns(lookBackWindow)}</b>,
|
||||
statusChangeThreshold: <b>{getStatusChangeThresholdRuleRuns(statusChangeThreshold)}</b>,
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
);
|
||||
};
|
|
@ -1,25 +1,28 @@
|
|||
/*
|
||||
* 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.
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import { EuiFormRow, EuiFormRowProps, EuiIconTip, EuiRange, EuiRangeProps } from '@elastic/eui';
|
||||
|
||||
export interface RulesSettingsRangeProps {
|
||||
export interface RuleSettingsRangeInputProps {
|
||||
label: EuiFormRowProps['label'];
|
||||
labelPopoverText?: string;
|
||||
min: number;
|
||||
max: number;
|
||||
value: number;
|
||||
fullWidth?: EuiRangeProps['fullWidth'];
|
||||
disabled?: EuiRangeProps['disabled'];
|
||||
onChange?: EuiRangeProps['onChange'];
|
||||
}
|
||||
|
||||
export const RulesSettingsRange = memo((props: RulesSettingsRangeProps) => {
|
||||
const { label, labelPopoverText, min, max, value, disabled, onChange, ...rest } = props;
|
||||
export const RuleSettingsRangeInput = memo((props: RuleSettingsRangeInputProps) => {
|
||||
const { label, labelPopoverText, min, max, value, fullWidth, disabled, onChange, ...rest } =
|
||||
props;
|
||||
|
||||
const renderLabel = () => {
|
||||
return (
|
||||
|
@ -34,8 +37,9 @@ export const RulesSettingsRange = memo((props: RulesSettingsRangeProps) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<EuiFormRow label={renderLabel()}>
|
||||
<EuiFormRow label={renderLabel()} fullWidth={fullWidth}>
|
||||
<EuiRange
|
||||
fullWidth={fullWidth}
|
||||
min={min}
|
||||
max={max}
|
||||
step={1}
|
|
@ -4,6 +4,7 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export interface RulesSettingsModificationMetadata {
|
||||
createdBy: string | null;
|
||||
updatedBy: string | null;
|
||||
|
@ -17,6 +18,11 @@ export interface RulesSettingsFlappingProperties {
|
|||
statusChangeThreshold: number;
|
||||
}
|
||||
|
||||
export interface RuleSpecificFlappingProperties {
|
||||
lookBackWindow: number;
|
||||
statusChangeThreshold: number;
|
||||
}
|
||||
|
||||
export type RulesSettingsFlapping = RulesSettingsFlappingProperties &
|
||||
RulesSettingsModificationMetadata;
|
||||
|
||||
|
@ -37,10 +43,13 @@ export interface RulesSettings {
|
|||
queryDelay?: RulesSettingsQueryDelay;
|
||||
}
|
||||
|
||||
export const MIN_LOOK_BACK_WINDOW = 2;
|
||||
export const MAX_LOOK_BACK_WINDOW = 20;
|
||||
export const MIN_STATUS_CHANGE_THRESHOLD = 2;
|
||||
export const MAX_STATUS_CHANGE_THRESHOLD = 20;
|
||||
export {
|
||||
MIN_LOOK_BACK_WINDOW,
|
||||
MAX_LOOK_BACK_WINDOW,
|
||||
MIN_STATUS_CHANGE_THRESHOLD,
|
||||
MAX_STATUS_CHANGE_THRESHOLD,
|
||||
} from '@kbn/alerting-types';
|
||||
|
||||
export const MIN_QUERY_DELAY = 0;
|
||||
export const MAX_QUERY_DELAY = 60;
|
||||
|
||||
|
|
|
@ -46133,14 +46133,6 @@
|
|||
"xpack.triggersActionsUI.ruleSnoozeScheduler.untilDateSummary": "jusqu'au {date}",
|
||||
"xpack.triggersActionsUI.rulesSettings.flapping.alertFlappingDetection": "Détection de bagotement d'alerte",
|
||||
"xpack.triggersActionsUI.rulesSettings.flapping.alertFlappingDetectionDescription": "Modifiez la fréquence à laquelle une alerte peut passer de l'état actif à l'état récupéré au cours d'une période d'exécution de règle.",
|
||||
"xpack.triggersActionsUI.rulesSettings.flapping.flappingSettingsDescription": "Une alerte est instable si son statut change au moins {statusChangeThreshold} au cours des derniers/dernières {lookBackWindow}.",
|
||||
"xpack.triggersActionsUI.rulesSettings.flapping.flappingSettingsOffDescription": "La détection de bagotement d'alerte est désactivée. Les alertes seront générées en fonction de l'intervalle de la règle, ce qui peut entraîner des volumes d'alertes plus importants.",
|
||||
"xpack.triggersActionsUI.rulesSettings.flapping.lookBackWindowHelp": "Nombre minimal d'exécutions pour lesquelles le seuil doit être atteint.",
|
||||
"xpack.triggersActionsUI.rulesSettings.flapping.lookBackWindowLabel": "Fenêtre d'historique d'exécution de la règle",
|
||||
"xpack.triggersActionsUI.rulesSettings.flapping.lookBackWindowLabelRuleRuns": "{amount, number} règle {amount, plural, one {exécute} other {exécutent}}",
|
||||
"xpack.triggersActionsUI.rulesSettings.flapping.statusChangeThresholdHelp": "Nombre minimal de fois où une alerte doit changer d'état dans la fenêtre d'historique.",
|
||||
"xpack.triggersActionsUI.rulesSettings.flapping.statusChangeThresholdLabel": "Seuil de modification du statut d'alerte",
|
||||
"xpack.triggersActionsUI.rulesSettings.flapping.statusChangeThresholdTimes": "{amount, number} {amount, plural, other {fois}}",
|
||||
"xpack.triggersActionsUI.rulesSettings.link.title": "Paramètres",
|
||||
"xpack.triggersActionsUI.rulesSettings.modal.calloutMessage": "Appliquer à toutes les règles dans l'espace actuel.",
|
||||
"xpack.triggersActionsUI.rulesSettings.modal.cancelButton": "Annuler",
|
||||
|
|
|
@ -46114,14 +46114,6 @@
|
|||
"xpack.triggersActionsUI.ruleSnoozeScheduler.untilDateSummary": "{date}まで",
|
||||
"xpack.triggersActionsUI.rulesSettings.flapping.alertFlappingDetection": "アラートフラップ検出",
|
||||
"xpack.triggersActionsUI.rulesSettings.flapping.alertFlappingDetectionDescription": "ルールの実行期間中、アラートがアクティブと回復済みの間を遷移できる頻度を変更します。",
|
||||
"xpack.triggersActionsUI.rulesSettings.flapping.flappingSettingsDescription": "過去{lookBackWindow}に少なくとも{statusChangeThreshold}ステータスが変更された場合、アラートが作動しています。",
|
||||
"xpack.triggersActionsUI.rulesSettings.flapping.flappingSettingsOffDescription": "アラートフラップ検出がオフです。アラートはルール間隔に基づいて生成されるため、アラート量が多くなる可能性があります。",
|
||||
"xpack.triggersActionsUI.rulesSettings.flapping.lookBackWindowHelp": "しきい値を満たす必要がある最小実行回数。",
|
||||
"xpack.triggersActionsUI.rulesSettings.flapping.lookBackWindowLabel": "ルール実行ルックバック期間",
|
||||
"xpack.triggersActionsUI.rulesSettings.flapping.lookBackWindowLabelRuleRuns": "{amount, number}個のルール{amount, plural, other {が実行されます}}",
|
||||
"xpack.triggersActionsUI.rulesSettings.flapping.statusChangeThresholdHelp": "ルックバック期間でアラートの状態が切り替わる必要がある最小回数。",
|
||||
"xpack.triggersActionsUI.rulesSettings.flapping.statusChangeThresholdLabel": "アラートステータス変更しきい値",
|
||||
"xpack.triggersActionsUI.rulesSettings.flapping.statusChangeThresholdTimes": "{amount, number} {amount, plural, other {回}}",
|
||||
"xpack.triggersActionsUI.rulesSettings.link.title": "設定",
|
||||
"xpack.triggersActionsUI.rulesSettings.modal.calloutMessage": "現在のスペース内のすべてのルールに適用",
|
||||
"xpack.triggersActionsUI.rulesSettings.modal.cancelButton": "キャンセル",
|
||||
|
|
|
@ -46164,14 +46164,6 @@
|
|||
"xpack.triggersActionsUI.ruleSnoozeScheduler.untilDateSummary": "直到 {date}",
|
||||
"xpack.triggersActionsUI.rulesSettings.flapping.alertFlappingDetection": "告警摆动检测",
|
||||
"xpack.triggersActionsUI.rulesSettings.flapping.alertFlappingDetectionDescription": "修改在规则运行期间内告警可在“活动”和“已恢复”之间切换的频率。",
|
||||
"xpack.triggersActionsUI.rulesSettings.flapping.flappingSettingsDescription": "如果告警在过去 {lookBackWindow} 中更改状态至少 {statusChangeThreshold} 次,则表示它正在摆动。",
|
||||
"xpack.triggersActionsUI.rulesSettings.flapping.flappingSettingsOffDescription": "告警摆动检测已关闭。将根据规则时间间隔生成告警,这可能导致更高的告警量。",
|
||||
"xpack.triggersActionsUI.rulesSettings.flapping.lookBackWindowHelp": "必须在其间达到阈值的最小运行次数。",
|
||||
"xpack.triggersActionsUI.rulesSettings.flapping.lookBackWindowLabel": "规则运行回顾窗口",
|
||||
"xpack.triggersActionsUI.rulesSettings.flapping.lookBackWindowLabelRuleRuns": "{amount, number} 次规则{amount, plural, other {运行}}",
|
||||
"xpack.triggersActionsUI.rulesSettings.flapping.statusChangeThresholdHelp": "告警必须在回顾窗口中切换状态的最小次数。",
|
||||
"xpack.triggersActionsUI.rulesSettings.flapping.statusChangeThresholdLabel": "告警状态更改阈值",
|
||||
"xpack.triggersActionsUI.rulesSettings.flapping.statusChangeThresholdTimes": "{amount, number} {amount, plural, other {次}}",
|
||||
"xpack.triggersActionsUI.rulesSettings.link.title": "设置",
|
||||
"xpack.triggersActionsUI.rulesSettings.modal.calloutMessage": "应用于当前工作区内的所有规则。",
|
||||
"xpack.triggersActionsUI.rulesSettings.modal.cancelButton": "取消",
|
||||
|
|
|
@ -6,69 +6,14 @@
|
|||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle, EuiText, EuiPanel } from '@elastic/eui';
|
||||
import {
|
||||
RulesSettingsFlappingProperties,
|
||||
MIN_LOOK_BACK_WINDOW,
|
||||
MIN_STATUS_CHANGE_THRESHOLD,
|
||||
MAX_LOOK_BACK_WINDOW,
|
||||
MAX_STATUS_CHANGE_THRESHOLD,
|
||||
} from '@kbn/alerting-plugin/common';
|
||||
import { RulesSettingsRange } from '../rules_settings_range';
|
||||
import { RuleSettingsFlappingInputs } from '@kbn/alerts-ui-shared/src/rule_settings/rule_settings_flapping_inputs';
|
||||
import { RuleSettingsFlappingMessage } from '@kbn/alerts-ui-shared/src/rule_settings/rule_settings_flapping_message';
|
||||
import { RulesSettingsFlappingProperties } from '@kbn/alerting-plugin/common';
|
||||
|
||||
type OnChangeKey = keyof Omit<RulesSettingsFlappingProperties, 'enabled'>;
|
||||
|
||||
const lookBackWindowLabel = i18n.translate(
|
||||
'xpack.triggersActionsUI.rulesSettings.flapping.lookBackWindowLabel',
|
||||
{
|
||||
defaultMessage: 'Rule run look back window',
|
||||
}
|
||||
);
|
||||
|
||||
const lookBackWindowHelp = i18n.translate(
|
||||
'xpack.triggersActionsUI.rulesSettings.flapping.lookBackWindowHelp',
|
||||
{
|
||||
defaultMessage: 'The minimum number of runs in which the threshold must be met.',
|
||||
}
|
||||
);
|
||||
|
||||
const statusChangeThresholdLabel = i18n.translate(
|
||||
'xpack.triggersActionsUI.rulesSettings.flapping.statusChangeThresholdLabel',
|
||||
{
|
||||
defaultMessage: 'Alert status change threshold',
|
||||
}
|
||||
);
|
||||
|
||||
const statusChangeThresholdHelp = i18n.translate(
|
||||
'xpack.triggersActionsUI.rulesSettings.flapping.statusChangeThresholdHelp',
|
||||
{
|
||||
defaultMessage:
|
||||
'The minimum number of times an alert must switch states in the look back window.',
|
||||
}
|
||||
);
|
||||
|
||||
const getLookBackWindowLabelRuleRuns = (amount: number) => {
|
||||
return i18n.translate(
|
||||
'xpack.triggersActionsUI.rulesSettings.flapping.lookBackWindowLabelRuleRuns',
|
||||
{
|
||||
defaultMessage: '{amount, number} rule {amount, plural, one {run} other {runs}}',
|
||||
values: { amount },
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const getStatusChangeThresholdRuleRuns = (amount: number) => {
|
||||
return i18n.translate(
|
||||
'xpack.triggersActionsUI.rulesSettings.flapping.statusChangeThresholdTimes',
|
||||
{
|
||||
defaultMessage: '{amount, number} {amount, plural, one {time} other {times}}',
|
||||
values: { amount },
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const RulesSettingsFlappingTitle = () => {
|
||||
return (
|
||||
<EuiTitle size="xs">
|
||||
|
@ -123,44 +68,21 @@ export const RulesSettingsFlappingFormSection = memo(
|
|||
</EuiFlexItem>
|
||||
</>
|
||||
)}
|
||||
<EuiFlexItem grow={false}>
|
||||
<RulesSettingsRange
|
||||
data-test-subj="lookBackWindowRangeInput"
|
||||
min={MIN_LOOK_BACK_WINDOW}
|
||||
max={MAX_LOOK_BACK_WINDOW}
|
||||
value={lookBackWindow}
|
||||
onChange={(e) => onChange('lookBackWindow', parseInt(e.currentTarget.value, 10))}
|
||||
label={lookBackWindowLabel}
|
||||
labelPopoverText={lookBackWindowHelp}
|
||||
disabled={!canWrite}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<RulesSettingsRange
|
||||
data-test-subj="statusChangeThresholdRangeInput"
|
||||
min={MIN_STATUS_CHANGE_THRESHOLD}
|
||||
max={MAX_STATUS_CHANGE_THRESHOLD}
|
||||
value={statusChangeThreshold}
|
||||
onChange={(e) => onChange('statusChangeThreshold', parseInt(e.currentTarget.value, 10))}
|
||||
label={statusChangeThresholdLabel}
|
||||
labelPopoverText={statusChangeThresholdHelp}
|
||||
disabled={!canWrite}
|
||||
<EuiFlexItem>
|
||||
<RuleSettingsFlappingInputs
|
||||
lookBackWindow={lookBackWindow}
|
||||
statusChangeThreshold={statusChangeThreshold}
|
||||
isDisabled={!canWrite}
|
||||
onLookBackWindowChange={(value) => onChange('lookBackWindow', value)}
|
||||
onStatusChangeThresholdChange={(value) => onChange('statusChangeThreshold', value)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPanel borderRadius="none" color="subdued">
|
||||
<EuiText size="s">
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.rulesSettings.flapping.flappingSettingsDescription"
|
||||
defaultMessage="An alert is flapping if it changes status at least {statusChangeThreshold} in the last {lookBackWindow}."
|
||||
values={{
|
||||
lookBackWindow: <b>{getLookBackWindowLabelRuleRuns(lookBackWindow)}</b>,
|
||||
statusChangeThreshold: (
|
||||
<b>{getStatusChangeThresholdRuleRuns(statusChangeThreshold)}</b>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
<RuleSettingsFlappingMessage
|
||||
lookBackWindow={lookBackWindow}
|
||||
statusChangeThreshold={statusChangeThreshold}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
EuiText,
|
||||
EuiEmptyPrompt,
|
||||
} from '@elastic/eui';
|
||||
import { flappingOffMessage } from '@kbn/alerts-ui-shared/src/rule_settings/rule_settings_flapping_message';
|
||||
import {
|
||||
RulesSettingsFlappingFormSection,
|
||||
RulesSettingsFlappingFormSectionProps,
|
||||
|
@ -121,12 +122,7 @@ export const RulesSettingsFlappingFormRight = memo((props: RulesSettingsFlapping
|
|||
return (
|
||||
<EuiFlexItem data-test-subj="rulesSettingsFlappingOffPrompt">
|
||||
<EuiPanel borderRadius="none" color="subdued" grow={false}>
|
||||
<EuiText size="s">
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.rulesSettings.flapping.flappingSettingsOffDescription"
|
||||
defaultMessage="Alert flapping detection is off. Alerts will be generated based on the rule interval, which might result in higher alert volumes."
|
||||
/>
|
||||
</EuiText>
|
||||
<EuiText size="s">{flappingOffMessage}</EuiText>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
|
|
|
@ -22,7 +22,7 @@ import {
|
|||
EuiEmptyPrompt,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { RulesSettingsRange } from '../rules_settings_range';
|
||||
import { RuleSettingsRangeInput } from '@kbn/alerts-ui-shared/src/rule_settings/rule_settings_range_input';
|
||||
|
||||
const queryDelayDescription = i18n.translate(
|
||||
'xpack.triggersActionsUI.rulesSettings.modal.queryDelayDescription',
|
||||
|
@ -107,7 +107,7 @@ export const RulesSettingsQueryDelaySection = memo((props: RulesSettingsQueryDel
|
|||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow>
|
||||
<RulesSettingsRange
|
||||
<RuleSettingsRangeInput
|
||||
data-test-subj="queryDelayRangeInput"
|
||||
min={MIN_QUERY_DELAY}
|
||||
max={MAX_QUERY_DELAY}
|
||||
|
|
|
@ -12,7 +12,7 @@ import { getFlappingSettings } from '../lib/rule_api/get_flapping_settings';
|
|||
|
||||
interface UseGetFlappingSettingsProps {
|
||||
enabled: boolean;
|
||||
onSuccess: (settings: RulesSettingsFlapping) => void;
|
||||
onSuccess?: (settings: RulesSettingsFlapping) => void;
|
||||
}
|
||||
|
||||
export const useGetFlappingSettings = (props: UseGetFlappingSettingsProps) => {
|
||||
|
@ -23,7 +23,7 @@ export const useGetFlappingSettings = (props: UseGetFlappingSettingsProps) => {
|
|||
return getFlappingSettings({ http });
|
||||
};
|
||||
|
||||
const { data, isFetching, isError, isLoadingError, isLoading } = useQuery({
|
||||
const { data, isFetching, isError, isLoadingError, isLoading, isInitialLoading } = useQuery({
|
||||
queryKey: ['getFlappingSettings'],
|
||||
queryFn,
|
||||
onSuccess,
|
||||
|
@ -33,6 +33,7 @@ export const useGetFlappingSettings = (props: UseGetFlappingSettingsProps) => {
|
|||
});
|
||||
|
||||
return {
|
||||
isInitialLoading,
|
||||
isLoading: isLoading || isFetching,
|
||||
isError: isError || isLoadingError,
|
||||
data,
|
||||
|
|
|
@ -37,6 +37,7 @@ import { hasShowActionsCapability } from '../../lib/capabilities';
|
|||
import RuleAddFooter from './rule_add_footer';
|
||||
import { HealthContextProvider } from '../../context/health_context';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import { IS_RULE_SPECIFIC_FLAPPING_ENABLED } from '../../../common/constants';
|
||||
import { hasRuleChanged, haveRuleParamsChanged } from './has_rule_changed';
|
||||
import { getRuleWithInvalidatedFields } from '../../lib/value_validators';
|
||||
import { DEFAULT_RULE_INTERVAL, MULTI_CONSUMER_RULE_TYPE_IDS } from '../../constants';
|
||||
|
@ -253,11 +254,13 @@ const RuleAdd = <
|
|||
|
||||
async function onSaveRule(): Promise<Rule | undefined> {
|
||||
try {
|
||||
const { flapping, ...restRule } = rule;
|
||||
const newRule = await createRule({
|
||||
http,
|
||||
rule: {
|
||||
...rule,
|
||||
...restRule,
|
||||
...(selectableConsumer && selectedConsumer ? { consumer: selectedConsumer } : {}),
|
||||
...(IS_RULE_SPECIFIC_FLAPPING_ENABLED ? { flapping } : {}),
|
||||
} as CreateRuleBody,
|
||||
});
|
||||
toasts.addSuccess(
|
||||
|
|
|
@ -30,6 +30,7 @@ import { toMountPoint } from '@kbn/react-kibana-mount';
|
|||
import { parseRuleCircuitBreakerErrorMessage } from '@kbn/alerting-plugin/common';
|
||||
import { updateRule } from '@kbn/alerts-ui-shared/src/common/apis/update_rule';
|
||||
import { fetchUiConfig as triggersActionsUiConfig } from '@kbn/alerts-ui-shared/src/common/apis/fetch_ui_config';
|
||||
import { IS_RULE_SPECIFIC_FLAPPING_ENABLED } from '../../../common/constants';
|
||||
import {
|
||||
Rule,
|
||||
RuleFlyoutCloseReason,
|
||||
|
@ -204,7 +205,15 @@ export const RuleEdit = <
|
|||
isValidRule(rule, ruleErrors, ruleActionsErrors) &&
|
||||
!hasActionsWithBrokenConnector
|
||||
) {
|
||||
const newRule = await updateRule({ http, rule, id: rule.id });
|
||||
const { flapping, ...restRule } = rule;
|
||||
const newRule = await updateRule({
|
||||
http,
|
||||
rule: {
|
||||
...restRule,
|
||||
...(IS_RULE_SPECIFIC_FLAPPING_ENABLED ? { flapping } : {}),
|
||||
},
|
||||
id: rule.id,
|
||||
});
|
||||
toasts.addSuccess(
|
||||
i18n.translate('xpack.triggersActionsUI.sections.ruleEdit.saveSuccessNotificationText', {
|
||||
defaultMessage: "Updated ''{ruleName}''",
|
||||
|
|
|
@ -26,9 +26,19 @@ import {
|
|||
} from '../../../types';
|
||||
import { RuleForm } from './rule_form';
|
||||
import { coreMock } from '@kbn/core/public/mocks';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ALERTING_FEATURE_ID, RecoveredActionGroup } from '@kbn/alerting-plugin/common';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
cacheTime: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const toMapById = [
|
||||
(acc: Map<unknown, unknown>, val: { id: unknown }) => acc.set(val.id, val),
|
||||
new Map(),
|
||||
|
@ -225,19 +235,21 @@ describe('rule_form', () => {
|
|||
} as unknown as Rule;
|
||||
|
||||
wrapper = mountWithIntl(
|
||||
<RuleForm
|
||||
rule={initialRule}
|
||||
config={{
|
||||
isUsingSecurity: true,
|
||||
minimumScheduleInterval: { value: '1m', enforce: enforceMinimum },
|
||||
}}
|
||||
dispatch={() => {}}
|
||||
errors={{ name: [], 'schedule.interval': [], ruleTypeId: [], actionConnectors: [] }}
|
||||
operation="create"
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
ruleTypeRegistry={ruleTypeRegistry}
|
||||
onChangeMetaData={jest.fn()}
|
||||
/>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RuleForm
|
||||
rule={initialRule}
|
||||
config={{
|
||||
isUsingSecurity: true,
|
||||
minimumScheduleInterval: { value: '1m', enforce: enforceMinimum },
|
||||
}}
|
||||
dispatch={() => {}}
|
||||
errors={{ name: [], 'schedule.interval': [], ruleTypeId: [], actionConnectors: [] }}
|
||||
operation="create"
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
ruleTypeRegistry={ruleTypeRegistry}
|
||||
onChangeMetaData={jest.fn()}
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
|
@ -381,28 +393,30 @@ describe('rule_form', () => {
|
|||
} as unknown as Rule;
|
||||
|
||||
wrapper = mountWithIntl(
|
||||
<RuleForm
|
||||
canShowConsumerSelection
|
||||
rule={{
|
||||
...initialRule,
|
||||
...initialRuleOverwrite,
|
||||
}}
|
||||
config={{
|
||||
isUsingSecurity: true,
|
||||
minimumScheduleInterval: { value: '1m', enforce: enforceMinimum },
|
||||
}}
|
||||
dispatch={() => {}}
|
||||
errors={{ name: [], 'schedule.interval': [], ruleTypeId: [], actionConnectors: [] }}
|
||||
operation="create"
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
ruleTypeRegistry={ruleTypeRegistry}
|
||||
connectorFeatureId={featureId}
|
||||
onChangeMetaData={jest.fn()}
|
||||
validConsumers={validConsumers}
|
||||
setConsumer={mockSetConsumer}
|
||||
useRuleProducer={useRuleProducer}
|
||||
selectedConsumer={selectedConsumer}
|
||||
/>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RuleForm
|
||||
canShowConsumerSelection
|
||||
rule={{
|
||||
...initialRule,
|
||||
...initialRuleOverwrite,
|
||||
}}
|
||||
config={{
|
||||
isUsingSecurity: true,
|
||||
minimumScheduleInterval: { value: '1m', enforce: enforceMinimum },
|
||||
}}
|
||||
dispatch={() => {}}
|
||||
errors={{ name: [], 'schedule.interval': [], ruleTypeId: [], actionConnectors: [] }}
|
||||
operation="create"
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
ruleTypeRegistry={ruleTypeRegistry}
|
||||
connectorFeatureId={featureId}
|
||||
onChangeMetaData={jest.fn()}
|
||||
validConsumers={validConsumers}
|
||||
setConsumer={mockSetConsumer}
|
||||
useRuleProducer={useRuleProducer}
|
||||
selectedConsumer={selectedConsumer}
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
|
@ -1105,19 +1119,21 @@ describe('rule_form', () => {
|
|||
} as unknown as Rule;
|
||||
|
||||
wrapper = mountWithIntl(
|
||||
<RuleForm
|
||||
rule={initialRule}
|
||||
config={{
|
||||
isUsingSecurity: true,
|
||||
minimumScheduleInterval: { value: '1m', enforce: false },
|
||||
}}
|
||||
dispatch={() => {}}
|
||||
errors={{ name: [], 'schedule.interval': [], ruleTypeId: [], actionConnectors: [] }}
|
||||
operation="create"
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
ruleTypeRegistry={ruleTypeRegistry}
|
||||
onChangeMetaData={jest.fn()}
|
||||
/>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RuleForm
|
||||
rule={initialRule}
|
||||
config={{
|
||||
isUsingSecurity: true,
|
||||
minimumScheduleInterval: { value: '1m', enforce: false },
|
||||
}}
|
||||
dispatch={() => {}}
|
||||
errors={{ name: [], 'schedule.interval': [], ruleTypeId: [], actionConnectors: [] }}
|
||||
operation="create"
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
ruleTypeRegistry={ruleTypeRegistry}
|
||||
onChangeMetaData={jest.fn()}
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
|
@ -1164,19 +1180,21 @@ describe('rule_form', () => {
|
|||
} as unknown as Rule;
|
||||
|
||||
wrapper = mountWithIntl(
|
||||
<RuleForm
|
||||
rule={initialRule}
|
||||
config={{
|
||||
isUsingSecurity: true,
|
||||
minimumScheduleInterval: { value: '1m', enforce: false },
|
||||
}}
|
||||
dispatch={() => {}}
|
||||
errors={{ name: [], 'schedule.interval': [], ruleTypeId: [], actionConnectors: [] }}
|
||||
operation="create"
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
ruleTypeRegistry={ruleTypeRegistry}
|
||||
onChangeMetaData={jest.fn()}
|
||||
/>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RuleForm
|
||||
rule={initialRule}
|
||||
config={{
|
||||
isUsingSecurity: true,
|
||||
minimumScheduleInterval: { value: '1m', enforce: false },
|
||||
}}
|
||||
dispatch={() => {}}
|
||||
errors={{ name: [], 'schedule.interval': [], ruleTypeId: [], actionConnectors: [] }}
|
||||
operation="create"
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
ruleTypeRegistry={ruleTypeRegistry}
|
||||
onChangeMetaData={jest.fn()}
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
|
|
|
@ -62,6 +62,7 @@ import {
|
|||
isActionGroupDisabledForActionTypeId,
|
||||
RuleActionAlertsFilterProperty,
|
||||
RuleActionKey,
|
||||
RuleSpecificFlappingProperties,
|
||||
} from '@kbn/alerting-plugin/common';
|
||||
import { AlertingConnectorFeatureId } from '@kbn/actions-plugin/common';
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
|
@ -91,12 +92,16 @@ import {
|
|||
ruleTypeGroupCompare,
|
||||
ruleTypeUngroupedCompare,
|
||||
} from '../../lib/rule_type_compare';
|
||||
import { VIEW_LICENSE_OPTIONS_LINK } from '../../../common/constants';
|
||||
import {
|
||||
IS_RULE_SPECIFIC_FLAPPING_ENABLED,
|
||||
VIEW_LICENSE_OPTIONS_LINK,
|
||||
} from '../../../common/constants';
|
||||
import { MULTI_CONSUMER_RULE_TYPE_IDS } from '../../constants';
|
||||
import { SectionLoading } from '../../components/section_loading';
|
||||
import { RuleFormConsumerSelection, VALID_CONSUMERS } from './rule_form_consumer_selection';
|
||||
import { getInitialInterval } from './get_initial_interval';
|
||||
import { useLoadRuleTypesQuery } from '../../hooks/use_load_rule_types_query';
|
||||
import { RuleFormAdvancedOptions } from './rule_form_advanced_options';
|
||||
|
||||
const ENTER_KEY = 13;
|
||||
|
||||
|
@ -410,6 +415,16 @@ export const RuleForm = ({
|
|||
dispatch({ command: { type: 'setAlertDelayProperty' }, payload: { key, value } });
|
||||
};
|
||||
|
||||
const setFlapping = (flapping: RuleSpecificFlappingProperties | null) => {
|
||||
dispatch({ command: { type: 'setProperty' }, payload: { key: 'flapping', value: flapping } });
|
||||
};
|
||||
|
||||
const onAlertDelayChange = (value: string) => {
|
||||
const parsedValue = value === '' ? '' : parseInt(value, 10);
|
||||
setAlertDelayProperty('active', parsedValue || 1);
|
||||
setAlertDelay(parsedValue || undefined);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const searchValue = searchText ? searchText.trim().toLocaleLowerCase() : null;
|
||||
setFilteredRuleTypes(
|
||||
|
@ -617,20 +632,6 @@ export const RuleForm = ({
|
|||
</Fragment>
|
||||
));
|
||||
|
||||
const labelForRuleChecked = [
|
||||
i18n.translate('xpack.triggersActionsUI.sections.ruleForm.checkFieldLabel', {
|
||||
defaultMessage: 'Check every',
|
||||
}),
|
||||
<EuiIconTip
|
||||
position="right"
|
||||
type="questionInCircle"
|
||||
content={i18n.translate('xpack.triggersActionsUI.sections.ruleForm.checkWithTooltip', {
|
||||
defaultMessage:
|
||||
'Define how often to evaluate the condition. Checks are queued; they run as close to the defined value as capacity allows.',
|
||||
})}
|
||||
/>,
|
||||
];
|
||||
|
||||
const getHelpTextForInterval = () => {
|
||||
if (!config || !config.minimumScheduleInterval) {
|
||||
return '';
|
||||
|
@ -797,6 +798,27 @@ export const RuleForm = ({
|
|||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={
|
||||
<EuiFlexGroup gutterSize="xs">
|
||||
<EuiFlexItem>
|
||||
{i18n.translate('xpack.triggersActionsUI.sections.ruleForm.ruleScheduleLabel', {
|
||||
defaultMessage: 'Rule schedule',
|
||||
})}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIconTip
|
||||
content={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleForm.checkWithTooltip',
|
||||
{
|
||||
defaultMessage:
|
||||
'Define how often to evaluate the condition. Checks are queued; they run as close to the defined value as capacity allows.',
|
||||
}
|
||||
)}
|
||||
position="top"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
data-test-subj="intervalFormRow"
|
||||
display="rowCompressed"
|
||||
helpText={getHelpTextForInterval()}
|
||||
|
@ -806,7 +828,12 @@ export const RuleForm = ({
|
|||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem grow={2}>
|
||||
<EuiFieldNumber
|
||||
prepend={labelForRuleChecked}
|
||||
prepend={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleForm.checkFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Check every',
|
||||
}
|
||||
)}
|
||||
fullWidth
|
||||
min={1}
|
||||
isInvalid={!!errors['schedule.interval'].length}
|
||||
|
@ -855,45 +882,14 @@ export const RuleForm = ({
|
|||
</EuiText>
|
||||
}
|
||||
>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiFormRow fullWidth data-test-subj="alertDelayFormRow" display="rowCompressed">
|
||||
<EuiFieldNumber
|
||||
fullWidth
|
||||
min={1}
|
||||
value={alertDelay || ''}
|
||||
name="alertDelay"
|
||||
data-test-subj="alertDelayInput"
|
||||
prepend={[
|
||||
i18n.translate('xpack.triggersActionsUI.sections.ruleForm.alertDelayFieldLabel', {
|
||||
defaultMessage: 'Alert after',
|
||||
}),
|
||||
<EuiIconTip
|
||||
position="right"
|
||||
type="questionInCircle"
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.ruleForm.alertDelayFieldHelp"
|
||||
defaultMessage="An alert occurs only when the specified number of consecutive runs meet the rule conditions."
|
||||
/>
|
||||
}
|
||||
/>,
|
||||
]}
|
||||
append={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleForm.alertDelayFieldAppendLabel',
|
||||
{
|
||||
defaultMessage: 'consecutive matches',
|
||||
}
|
||||
)}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value === '' || INTEGER_REGEX.test(value)) {
|
||||
const parsedValue = value === '' ? '' : parseInt(value, 10);
|
||||
setAlertDelayProperty('active', parsedValue || 1);
|
||||
setAlertDelay(parsedValue || undefined);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="m" />
|
||||
<RuleFormAdvancedOptions
|
||||
alertDelay={alertDelay}
|
||||
flappingSettings={rule.flapping}
|
||||
onAlertDelayChange={onAlertDelayChange}
|
||||
onFlappingChange={setFlapping}
|
||||
enabledFlapping={IS_RULE_SPECIFIC_FLAPPING_ENABLED}
|
||||
/>
|
||||
</EuiAccordion>
|
||||
</EuiFlexItem>
|
||||
{shouldShowConsumerSelect && (
|
||||
|
|
|
@ -0,0 +1,222 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { httpServiceMock } from '@kbn/core/public/mocks';
|
||||
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
|
||||
import { RuleFormAdvancedOptions } from './rule_form_advanced_options';
|
||||
import { useKibana } from '../../../common/lib/kibana';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
jest.mock('../../../common/lib/kibana');
|
||||
|
||||
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const http = httpServiceMock.createStartContract();
|
||||
|
||||
const mockFlappingSettings = {
|
||||
lookBackWindow: 5,
|
||||
statusChangeThreshold: 5,
|
||||
};
|
||||
|
||||
const mockOnflappingChange = jest.fn();
|
||||
const mockAlertDelayChange = jest.fn();
|
||||
|
||||
describe('ruleFormAdvancedOptions', () => {
|
||||
beforeEach(() => {
|
||||
http.get.mockResolvedValue({
|
||||
look_back_window: 10,
|
||||
status_change_threshold: 3,
|
||||
enabled: true,
|
||||
});
|
||||
useKibanaMock().services.http = http;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should render correctly', async () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RuleFormAdvancedOptions
|
||||
enabledFlapping
|
||||
alertDelay={5}
|
||||
flappingSettings={mockFlappingSettings}
|
||||
onAlertDelayChange={mockAlertDelayChange}
|
||||
onFlappingChange={mockOnflappingChange}
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
expect(await screen.findByTestId('ruleFormAdvancedOptions')).toBeInTheDocument();
|
||||
expect(await screen.findByTestId('alertDelayFormRow')).toBeInTheDocument();
|
||||
expect(await screen.findByTestId('alertFlappingFormRow')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should initialize correctly when global flapping is on and override is not applied', async () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RuleFormAdvancedOptions
|
||||
enabledFlapping
|
||||
alertDelay={5}
|
||||
onAlertDelayChange={mockAlertDelayChange}
|
||||
onFlappingChange={mockOnflappingChange}
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
expect(await screen.findByText('ON')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('ruleFormAdvancedOptionsOverrideSwitch')).not.toBeChecked();
|
||||
expect(screen.queryByText('Override')).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('ruleSettingsFlappingMessage')).toHaveTextContent(
|
||||
'An alert is flapping if it changes status at least 3 times in the last 10 rule runs.'
|
||||
);
|
||||
|
||||
userEvent.click(screen.getByTestId('ruleFormAdvancedOptionsOverrideSwitch'));
|
||||
expect(mockOnflappingChange).toHaveBeenCalledWith({
|
||||
lookBackWindow: 10,
|
||||
statusChangeThreshold: 3,
|
||||
});
|
||||
});
|
||||
|
||||
test('should initialize correctly when global flapping is on and override is appplied', async () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RuleFormAdvancedOptions
|
||||
enabledFlapping
|
||||
alertDelay={5}
|
||||
flappingSettings={{
|
||||
lookBackWindow: 6,
|
||||
statusChangeThreshold: 4,
|
||||
}}
|
||||
onAlertDelayChange={mockAlertDelayChange}
|
||||
onFlappingChange={mockOnflappingChange}
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
expect(await screen.findByTestId('ruleFormAdvancedOptionsOverrideSwitch')).toBeChecked();
|
||||
expect(screen.getByText('Override')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('lookBackWindowRangeInput')).toHaveValue('6');
|
||||
expect(screen.getByTestId('statusChangeThresholdRangeInput')).toHaveValue('4');
|
||||
expect(screen.getByTestId('ruleSettingsFlappingMessage')).toHaveTextContent(
|
||||
'An alert is flapping if it changes status at least 4 times in the last 6 rule runs.'
|
||||
);
|
||||
|
||||
userEvent.click(screen.getByTestId('ruleFormAdvancedOptionsOverrideSwitch'));
|
||||
expect(mockOnflappingChange).toHaveBeenCalledWith(null);
|
||||
});
|
||||
|
||||
test('should not allow override when global flapping is off', async () => {
|
||||
http.get.mockResolvedValue({
|
||||
look_back_window: 10,
|
||||
status_change_threshold: 3,
|
||||
enabled: false,
|
||||
});
|
||||
useKibanaMock().services.http = http;
|
||||
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RuleFormAdvancedOptions
|
||||
enabledFlapping
|
||||
alertDelay={5}
|
||||
flappingSettings={{
|
||||
lookBackWindow: 6,
|
||||
statusChangeThreshold: 4,
|
||||
}}
|
||||
onAlertDelayChange={mockAlertDelayChange}
|
||||
onFlappingChange={mockOnflappingChange}
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
expect(await screen.findByText('OFF')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Override')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('ruleFormAdvancedOptionsOverrideSwitch')).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('ruleSettingsFlappingMessage')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should allow for flapping inputs to be modified', async () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RuleFormAdvancedOptions
|
||||
enabledFlapping
|
||||
alertDelay={5}
|
||||
flappingSettings={{
|
||||
lookBackWindow: 10,
|
||||
statusChangeThreshold: 10,
|
||||
}}
|
||||
onAlertDelayChange={mockAlertDelayChange}
|
||||
onFlappingChange={mockOnflappingChange}
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
expect(await screen.findByTestId('lookBackWindowRangeInput')).toBeInTheDocument();
|
||||
|
||||
const lookBackWindowInput = screen.getByTestId('lookBackWindowRangeInput');
|
||||
const statusChangeThresholdInput = screen.getByTestId('statusChangeThresholdRangeInput');
|
||||
|
||||
// Change lookBackWindow to a smaller value
|
||||
fireEvent.change(lookBackWindowInput, { target: { value: 5 } });
|
||||
// statusChangeThresholdInput gets pinned to be 5
|
||||
expect(mockOnflappingChange).toHaveBeenLastCalledWith({
|
||||
lookBackWindow: 5,
|
||||
statusChangeThreshold: 5,
|
||||
});
|
||||
|
||||
// Try making statusChangeThreshold bigger
|
||||
fireEvent.change(statusChangeThresholdInput, { target: { value: 20 } });
|
||||
// Still pinned
|
||||
expect(mockOnflappingChange).toHaveBeenLastCalledWith({
|
||||
lookBackWindow: 10,
|
||||
statusChangeThreshold: 10,
|
||||
});
|
||||
|
||||
fireEvent.change(statusChangeThresholdInput, { target: { value: 3 } });
|
||||
expect(mockOnflappingChange).toHaveBeenLastCalledWith({
|
||||
lookBackWindow: 10,
|
||||
statusChangeThreshold: 3,
|
||||
});
|
||||
});
|
||||
|
||||
test('should not render flapping if enableFlapping is false', () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RuleFormAdvancedOptions
|
||||
enabledFlapping={false}
|
||||
alertDelay={5}
|
||||
flappingSettings={{
|
||||
lookBackWindow: 10,
|
||||
statusChangeThreshold: 10,
|
||||
}}
|
||||
onAlertDelayChange={mockAlertDelayChange}
|
||||
onFlappingChange={mockOnflappingChange}
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId('alertFlappingFormRow')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,360 @@
|
|||
/*
|
||||
* 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, { useCallback, useMemo, useRef } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiBadge,
|
||||
EuiFieldNumber,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFormRow,
|
||||
EuiIconTip,
|
||||
EuiPanel,
|
||||
EuiSwitch,
|
||||
EuiText,
|
||||
useIsWithinMinBreakpoint,
|
||||
useEuiTheme,
|
||||
EuiHorizontalRule,
|
||||
EuiSpacer,
|
||||
EuiSplitPanel,
|
||||
EuiLoadingSpinner,
|
||||
EuiLink,
|
||||
} from '@elastic/eui';
|
||||
import { RuleSettingsFlappingInputs } from '@kbn/alerts-ui-shared/src/rule_settings/rule_settings_flapping_inputs';
|
||||
import { RuleSettingsFlappingMessage } from '@kbn/alerts-ui-shared/src/rule_settings/rule_settings_flapping_message';
|
||||
import { RuleSpecificFlappingProperties } from '@kbn/alerting-plugin/common';
|
||||
import { useGetFlappingSettings } from '../../hooks/use_get_flapping_settings';
|
||||
|
||||
const alertDelayFormRowLabel = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleForm.alertDelayLabel',
|
||||
{
|
||||
defaultMessage: 'Alert delay',
|
||||
}
|
||||
);
|
||||
|
||||
const alertDelayIconTipDescription = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleForm.alertDelayFieldHelp',
|
||||
{
|
||||
defaultMessage:
|
||||
'An alert occurs only when the specified number of consecutive runs meet the rule conditions.',
|
||||
}
|
||||
);
|
||||
|
||||
const alertDelayPrependLabel = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleForm.alertDelayFieldLabel',
|
||||
{
|
||||
defaultMessage: 'Alert after',
|
||||
}
|
||||
);
|
||||
|
||||
const alertDelayAppendLabel = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleForm.alertDelayFieldAppendLabel',
|
||||
{
|
||||
defaultMessage: 'consecutive matches',
|
||||
}
|
||||
);
|
||||
|
||||
const flappingLabel = i18n.translate(
|
||||
'xpack.triggersActionsUI.ruleFormAdvancedOptions.flappingLabel',
|
||||
{
|
||||
defaultMessage: 'Flapping Detection',
|
||||
}
|
||||
);
|
||||
|
||||
const flappingOnLabel = i18n.translate('xpack.triggersActionsUI.ruleFormAdvancedOptions.onLabel', {
|
||||
defaultMessage: 'ON',
|
||||
});
|
||||
|
||||
const flappingOffLabel = i18n.translate(
|
||||
'xpack.triggersActionsUI.ruleFormAdvancedOptions.offLabel',
|
||||
{
|
||||
defaultMessage: 'OFF',
|
||||
}
|
||||
);
|
||||
|
||||
const flappingOverrideLabel = i18n.translate(
|
||||
'xpack.triggersActionsUI.ruleFormAdvancedOptions.overrideLabel',
|
||||
{
|
||||
defaultMessage: 'Override',
|
||||
}
|
||||
);
|
||||
|
||||
const flappingOverrideConfiguration = i18n.translate(
|
||||
'xpack.triggersActionsUI.ruleFormAdvancedOptions.flappingOverrideConfiguration',
|
||||
{
|
||||
defaultMessage: 'Override Configuration',
|
||||
}
|
||||
);
|
||||
|
||||
const flappingExternalLinkLabel = i18n.translate(
|
||||
'xpack.triggersActionsUI.ruleFormAdvancedOptions.flappingExternalLinkLabel',
|
||||
{
|
||||
defaultMessage: "What's this?",
|
||||
}
|
||||
);
|
||||
|
||||
const flappingFormRowLabel = i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleForm.flappingLabel',
|
||||
{
|
||||
defaultMessage: 'Alert flapping detection',
|
||||
}
|
||||
);
|
||||
|
||||
const flappingIconTipDescription = i18n.translate(
|
||||
'xpack.triggersActionsUI.ruleFormAdvancedOptions.flappingIconTipDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'Detect alerts that switch quickly between active and recovered states and reduce unwanted noise for these flapping alerts.',
|
||||
}
|
||||
);
|
||||
|
||||
const clampFlappingValues = (flapping: RuleSpecificFlappingProperties) => {
|
||||
return {
|
||||
...flapping,
|
||||
statusChangeThreshold: Math.min(flapping.lookBackWindow, flapping.statusChangeThreshold),
|
||||
};
|
||||
};
|
||||
|
||||
const INTEGER_REGEX = /^[1-9][0-9]*$/;
|
||||
|
||||
export interface RuleFormAdvancedOptionsProps {
|
||||
alertDelay?: number;
|
||||
flappingSettings?: RuleSpecificFlappingProperties;
|
||||
onAlertDelayChange: (value: string) => void;
|
||||
onFlappingChange: (value: RuleSpecificFlappingProperties | null) => void;
|
||||
enabledFlapping?: boolean;
|
||||
}
|
||||
|
||||
export const RuleFormAdvancedOptions = (props: RuleFormAdvancedOptionsProps) => {
|
||||
const {
|
||||
alertDelay,
|
||||
flappingSettings,
|
||||
enabledFlapping = true,
|
||||
onAlertDelayChange,
|
||||
onFlappingChange,
|
||||
} = props;
|
||||
|
||||
const cachedFlappingSettings = useRef<RuleSpecificFlappingProperties>();
|
||||
|
||||
const isDesktop = useIsWithinMinBreakpoint('xl');
|
||||
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
const { data: spaceFlappingSettings, isInitialLoading } = useGetFlappingSettings({
|
||||
enabled: enabledFlapping,
|
||||
});
|
||||
|
||||
const internalOnAlertDelayChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
if (value === '' || INTEGER_REGEX.test(value)) {
|
||||
onAlertDelayChange(value);
|
||||
}
|
||||
},
|
||||
[onAlertDelayChange]
|
||||
);
|
||||
|
||||
const internalOnFlappingChange = useCallback(
|
||||
(flapping: RuleSpecificFlappingProperties) => {
|
||||
const clampedValue = clampFlappingValues(flapping);
|
||||
onFlappingChange(clampedValue);
|
||||
cachedFlappingSettings.current = clampedValue;
|
||||
},
|
||||
[onFlappingChange]
|
||||
);
|
||||
|
||||
const onLookBackWindowChange = useCallback(
|
||||
(value: number) => {
|
||||
if (!flappingSettings) {
|
||||
return;
|
||||
}
|
||||
internalOnFlappingChange({
|
||||
...flappingSettings,
|
||||
lookBackWindow: value,
|
||||
});
|
||||
},
|
||||
[flappingSettings, internalOnFlappingChange]
|
||||
);
|
||||
|
||||
const onStatusChangeThresholdChange = useCallback(
|
||||
(value: number) => {
|
||||
if (!flappingSettings) {
|
||||
return;
|
||||
}
|
||||
internalOnFlappingChange({
|
||||
...flappingSettings,
|
||||
statusChangeThreshold: value,
|
||||
});
|
||||
},
|
||||
[flappingSettings, internalOnFlappingChange]
|
||||
);
|
||||
|
||||
const onFlappingToggle = useCallback(() => {
|
||||
if (!spaceFlappingSettings) {
|
||||
return;
|
||||
}
|
||||
if (flappingSettings) {
|
||||
cachedFlappingSettings.current = flappingSettings;
|
||||
return onFlappingChange(null);
|
||||
}
|
||||
const initialFlappingSettings = cachedFlappingSettings.current || spaceFlappingSettings;
|
||||
onFlappingChange({
|
||||
lookBackWindow: initialFlappingSettings.lookBackWindow,
|
||||
statusChangeThreshold: initialFlappingSettings.statusChangeThreshold,
|
||||
});
|
||||
}, [spaceFlappingSettings, flappingSettings, onFlappingChange]);
|
||||
|
||||
const flappingFormHeader = useMemo(() => {
|
||||
if (!spaceFlappingSettings) {
|
||||
return null;
|
||||
}
|
||||
const { enabled } = spaceFlappingSettings;
|
||||
|
||||
return (
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup
|
||||
gutterSize="s"
|
||||
direction={isDesktop ? 'row' : 'column'}
|
||||
alignItems={isDesktop ? 'center' : undefined}
|
||||
>
|
||||
<EuiFlexItem style={{ flexDirection: 'row' }}>
|
||||
<EuiText size="s" style={{ marginRight: euiTheme.size.xs }}>
|
||||
{flappingLabel}
|
||||
</EuiText>
|
||||
<EuiBadge color={enabled ? 'success' : 'default'}>
|
||||
{enabled ? flappingOnLabel : flappingOffLabel}
|
||||
</EuiBadge>
|
||||
{flappingSettings && enabled && (
|
||||
<EuiBadge color="primary">{flappingOverrideLabel}</EuiBadge>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{enabled && (
|
||||
<EuiSwitch
|
||||
data-test-subj="ruleFormAdvancedOptionsOverrideSwitch"
|
||||
compressed
|
||||
checked={!!flappingSettings}
|
||||
label={flappingOverrideConfiguration}
|
||||
onChange={onFlappingToggle}
|
||||
/>
|
||||
)}
|
||||
{!enabled && (
|
||||
// TODO: Add the help link here
|
||||
<EuiLink href="" target="_blank">
|
||||
{flappingExternalLinkLabel}
|
||||
</EuiLink>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{flappingSettings && (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiHorizontalRule margin="none" />
|
||||
</>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}, [isDesktop, euiTheme, spaceFlappingSettings, flappingSettings, onFlappingToggle]);
|
||||
|
||||
const flappingFormBody = useMemo(() => {
|
||||
if (!flappingSettings) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<EuiFlexItem>
|
||||
<RuleSettingsFlappingInputs
|
||||
lookBackWindow={flappingSettings.lookBackWindow}
|
||||
statusChangeThreshold={flappingSettings.statusChangeThreshold}
|
||||
onLookBackWindowChange={onLookBackWindowChange}
|
||||
onStatusChangeThresholdChange={onStatusChangeThresholdChange}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}, [flappingSettings, onLookBackWindowChange, onStatusChangeThresholdChange]);
|
||||
|
||||
const flappingFormMessage = useMemo(() => {
|
||||
if (!spaceFlappingSettings || !spaceFlappingSettings.enabled) {
|
||||
return null;
|
||||
}
|
||||
const settingsToUse = flappingSettings || spaceFlappingSettings;
|
||||
return (
|
||||
<EuiSplitPanel.Inner
|
||||
color="subdued"
|
||||
style={{
|
||||
borderTop: euiTheme.border.thin,
|
||||
}}
|
||||
>
|
||||
<RuleSettingsFlappingMessage
|
||||
lookBackWindow={settingsToUse.lookBackWindow}
|
||||
statusChangeThreshold={settingsToUse.statusChangeThreshold}
|
||||
/>
|
||||
</EuiSplitPanel.Inner>
|
||||
);
|
||||
}, [spaceFlappingSettings, flappingSettings, euiTheme]);
|
||||
|
||||
return (
|
||||
<EuiPanel color="subdued" hasShadow={false} data-test-subj="ruleFormAdvancedOptions">
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={
|
||||
<EuiFlexGroup gutterSize="xs">
|
||||
<EuiFlexItem>{alertDelayFormRowLabel}</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIconTip content={alertDelayIconTipDescription} position="top" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
data-test-subj="alertDelayFormRow"
|
||||
display="rowCompressed"
|
||||
>
|
||||
<EuiFieldNumber
|
||||
fullWidth
|
||||
min={1}
|
||||
value={alertDelay || ''}
|
||||
name="alertDelay"
|
||||
data-test-subj="alertDelayInput"
|
||||
prepend={alertDelayPrependLabel}
|
||||
append={alertDelayAppendLabel}
|
||||
onChange={internalOnAlertDelayChange}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
{isInitialLoading && <EuiLoadingSpinner />}
|
||||
{spaceFlappingSettings && enabledFlapping && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={
|
||||
<EuiFlexGroup gutterSize="xs">
|
||||
<EuiFlexItem>{flappingFormRowLabel}</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIconTip content={flappingIconTipDescription} position="top" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
data-test-subj="alertFlappingFormRow"
|
||||
display="rowCompressed"
|
||||
>
|
||||
<EuiSplitPanel.Outer hasBorder>
|
||||
<EuiSplitPanel.Inner>
|
||||
<EuiFlexGroup direction="column">
|
||||
{flappingFormHeader}
|
||||
{flappingFormBody}
|
||||
</EuiFlexGroup>
|
||||
</EuiSplitPanel.Inner>
|
||||
{flappingFormMessage}
|
||||
</EuiSplitPanel.Outer>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
|
@ -26,6 +26,9 @@ export {
|
|||
I18N_WEEKDAY_OPTIONS_DDD,
|
||||
} from '@kbn/alerts-ui-shared/src/common/constants/i18n_weekdays';
|
||||
|
||||
// Feature flag for frontend rule specific flapping in rule flyout
|
||||
export const IS_RULE_SPECIFIC_FLAPPING_ENABLED = false;
|
||||
|
||||
export const builtInComparators: { [key: string]: Comparator } = {
|
||||
[COMPARATORS.GREATER_THAN]: {
|
||||
text: i18n.translate('xpack.triggersActionsUI.common.constants.comparators.isAboveLabel', {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue