mirror of
https://github.com/elastic/kibana.git
synced 2025-06-28 11:05:39 -04:00
[RAM] [FE] Add conditional actions UI for timeframe (#153944)
## Summary Adds the conditional timeframe to the actions UI. **Currently only enabled in the Security Solution alerts UI using the `showActionAlertsFilter` prop** Part of https://github.com/elastic/kibana/issues/152026 and https://github.com/elastic/kibana/issues/152611 <img width="880" alt="Screenshot 2023-03-29 at 4 15 24 PM" src="https://user-images.githubusercontent.com/1445834/228567116-a0fa80ac-7664-411f-9757-41aa81b52857.png"> ### UPDATED UI <img width="857" alt="Screenshot 2023-03-31 at 3 58 12 PM" src="https://user-images.githubusercontent.com/1445834/229140790-11aeea9b-6db9-46bc-8f35-47f9afabb606.png"> ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [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 - [x] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [x] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
e469ece932
commit
56796db2c0
21 changed files with 574 additions and 35 deletions
|
@ -29,6 +29,39 @@ export const RuleActionUuid = NonEmptyString;
|
||||||
export type RuleActionParams = t.TypeOf<typeof RuleActionParams>;
|
export type RuleActionParams = t.TypeOf<typeof RuleActionParams>;
|
||||||
export const RuleActionParams = saved_object_attributes;
|
export const RuleActionParams = saved_object_attributes;
|
||||||
|
|
||||||
|
export const RuleActionAlertsFilter = t.strict({
|
||||||
|
query: t.union([
|
||||||
|
t.null,
|
||||||
|
t.intersection([
|
||||||
|
t.strict({
|
||||||
|
kql: t.string,
|
||||||
|
}),
|
||||||
|
t.partial({ dsl: t.string }),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
timeframe: t.union([
|
||||||
|
t.null,
|
||||||
|
t.strict({
|
||||||
|
timezone: t.string,
|
||||||
|
days: t.array(
|
||||||
|
t.union([
|
||||||
|
t.literal(1),
|
||||||
|
t.literal(2),
|
||||||
|
t.literal(3),
|
||||||
|
t.literal(4),
|
||||||
|
t.literal(5),
|
||||||
|
t.literal(6),
|
||||||
|
t.literal(7),
|
||||||
|
])
|
||||||
|
),
|
||||||
|
hours: t.strict({
|
||||||
|
start: t.string,
|
||||||
|
end: t.string,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
export type RuleAction = t.TypeOf<typeof RuleAction>;
|
export type RuleAction = t.TypeOf<typeof RuleAction>;
|
||||||
export const RuleAction = t.exact(
|
export const RuleAction = t.exact(
|
||||||
t.intersection([
|
t.intersection([
|
||||||
|
@ -38,7 +71,7 @@ export const RuleAction = t.exact(
|
||||||
action_type_id: RuleActionTypeId,
|
action_type_id: RuleActionTypeId,
|
||||||
params: RuleActionParams,
|
params: RuleActionParams,
|
||||||
}),
|
}),
|
||||||
t.partial({ uuid: RuleActionUuid }),
|
t.partial({ uuid: RuleActionUuid, alerts_filter: RuleActionAlertsFilter }),
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -54,7 +87,7 @@ export const RuleActionCamel = t.exact(
|
||||||
actionTypeId: RuleActionTypeId,
|
actionTypeId: RuleActionTypeId,
|
||||||
params: RuleActionParams,
|
params: RuleActionParams,
|
||||||
}),
|
}),
|
||||||
t.partial({ uuid: RuleActionUuid }),
|
t.partial({ uuid: RuleActionUuid, alertsFilter: RuleActionAlertsFilter }),
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -77,8 +77,9 @@ export interface RuleExecutionStatus {
|
||||||
export type RuleActionParams = SavedObjectAttributes;
|
export type RuleActionParams = SavedObjectAttributes;
|
||||||
export type RuleActionParam = SavedObjectAttribute;
|
export type RuleActionParam = SavedObjectAttribute;
|
||||||
|
|
||||||
|
export type IsoWeekday = 1 | 2 | 3 | 4 | 5 | 6 | 7;
|
||||||
export interface AlertsFilterTimeframe extends SavedObjectAttributes {
|
export interface AlertsFilterTimeframe extends SavedObjectAttributes {
|
||||||
days: Array<1 | 2 | 3 | 4 | 5 | 6 | 7>;
|
days: IsoWeekday[];
|
||||||
timezone: string;
|
timezone: string;
|
||||||
hours: {
|
hours: {
|
||||||
start: string;
|
start: string;
|
||||||
|
@ -94,6 +95,8 @@ export interface AlertsFilter extends SavedObjectAttributes {
|
||||||
timeframe: null | AlertsFilterTimeframe;
|
timeframe: null | AlertsFilterTimeframe;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type RuleActionAlertsFilterProperty = AlertsFilterTimeframe | RuleActionParam;
|
||||||
|
|
||||||
export interface RuleAction {
|
export interface RuleAction {
|
||||||
uuid?: string;
|
uuid?: string;
|
||||||
group: string;
|
group: string;
|
||||||
|
|
|
@ -9,7 +9,12 @@ import { omit } from 'lodash';
|
||||||
import { schema } from '@kbn/config-schema';
|
import { schema } from '@kbn/config-schema';
|
||||||
import { IRouter } from '@kbn/core/server';
|
import { IRouter } from '@kbn/core/server';
|
||||||
import { ILicenseState } from '../lib';
|
import { ILicenseState } from '../lib';
|
||||||
import { verifyAccessAndContext, RewriteResponseCase, rewriteRuleLastRun } from './lib';
|
import {
|
||||||
|
verifyAccessAndContext,
|
||||||
|
RewriteResponseCase,
|
||||||
|
rewriteRuleLastRun,
|
||||||
|
rewriteActionsRes,
|
||||||
|
} from './lib';
|
||||||
import {
|
import {
|
||||||
RuleTypeParams,
|
RuleTypeParams,
|
||||||
AlertingRequestHandlerContext,
|
AlertingRequestHandlerContext,
|
||||||
|
@ -61,21 +66,7 @@ const rewriteBodyRes: RewriteResponseCase<SanitizedRule<RuleTypeParams>> = ({
|
||||||
last_execution_date: executionStatus.lastExecutionDate,
|
last_execution_date: executionStatus.lastExecutionDate,
|
||||||
last_duration: executionStatus.lastDuration,
|
last_duration: executionStatus.lastDuration,
|
||||||
},
|
},
|
||||||
actions: actions.map(({ group, id, actionTypeId, params, frequency, uuid, alertsFilter }) => ({
|
actions: rewriteActionsRes(actions),
|
||||||
group,
|
|
||||||
id,
|
|
||||||
params,
|
|
||||||
connector_type_id: actionTypeId,
|
|
||||||
frequency: frequency
|
|
||||||
? {
|
|
||||||
summary: frequency.summary,
|
|
||||||
notify_when: frequency.notifyWhen,
|
|
||||||
throttle: frequency.throttle,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
...(uuid && { uuid }),
|
|
||||||
...(alertsFilter && { alerts_filter: alertsFilter }),
|
|
||||||
})),
|
|
||||||
...(lastRun ? { last_run: rewriteRuleLastRun(lastRun) } : {}),
|
...(lastRun ? { last_run: rewriteRuleLastRun(lastRun) } : {}),
|
||||||
...(nextRun ? { next_run: nextRun } : {}),
|
...(nextRun ? { next_run: nextRun } : {}),
|
||||||
...(viewInAppRelativeUrl ? { view_in_app_relative_url: viewInAppRelativeUrl } : {}),
|
...(viewInAppRelativeUrl ? { view_in_app_relative_url: viewInAppRelativeUrl } : {}),
|
||||||
|
|
|
@ -47,6 +47,10 @@ jest.mock('@kbn/triggers-actions-ui-plugin/public/application/lib/rule_api', ()
|
||||||
loadAlertTypes: jest.fn(),
|
loadAlertTypes: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('@kbn/kibana-react-plugin/public/ui_settings/use_ui_setting', () => ({
|
||||||
|
useUiSetting: jest.fn().mockImplementation((_, defaultValue) => defaultValue),
|
||||||
|
}));
|
||||||
|
|
||||||
const initLegacyShims = () => {
|
const initLegacyShims = () => {
|
||||||
const triggersActionsUi = {
|
const triggersActionsUi = {
|
||||||
actionTypeRegistry: actionTypeRegistryMock.create(),
|
actionTypeRegistry: actionTypeRegistryMock.create(),
|
||||||
|
@ -237,10 +241,13 @@ describe('alert_form', () => {
|
||||||
initialAlert.actions[index].id = id;
|
initialAlert.actions[index].id = id;
|
||||||
}}
|
}}
|
||||||
setActions={(_updatedActions: AlertAction[]) => {}}
|
setActions={(_updatedActions: AlertAction[]) => {}}
|
||||||
setActionParamsProperty={(key: string, value: any, index: number) =>
|
setActionParamsProperty={(key: string, value: unknown, index: number) =>
|
||||||
(initialAlert.actions[index] = { ...initialAlert.actions[index], [key]: value })
|
(initialAlert.actions[index] = { ...initialAlert.actions[index], [key]: value })
|
||||||
}
|
}
|
||||||
setActionFrequencyProperty={(key: string, value: any, index: number) =>
|
setActionFrequencyProperty={(key: string, value: unknown, index: number) =>
|
||||||
|
(initialAlert.actions[index] = { ...initialAlert.actions[index], [key]: value })
|
||||||
|
}
|
||||||
|
setActionAlertsFilterProperty={(key: string, value: unknown, index: number) =>
|
||||||
(initialAlert.actions[index] = { ...initialAlert.actions[index], [key]: value })
|
(initialAlert.actions[index] = { ...initialAlert.actions[index], [key]: value })
|
||||||
}
|
}
|
||||||
actionTypeRegistry={actionTypeRegistry}
|
actionTypeRegistry={actionTypeRegistry}
|
||||||
|
|
|
@ -16,11 +16,13 @@ export const transformRuleToAlertAction = ({
|
||||||
action_type_id: actionTypeId,
|
action_type_id: actionTypeId,
|
||||||
params,
|
params,
|
||||||
uuid,
|
uuid,
|
||||||
|
alerts_filter: alertsFilter,
|
||||||
}: RuleAlertAction): RuleAction => ({
|
}: RuleAlertAction): RuleAction => ({
|
||||||
group,
|
group,
|
||||||
id,
|
id,
|
||||||
params,
|
params,
|
||||||
actionTypeId,
|
actionTypeId,
|
||||||
|
...(alertsFilter && { alertsFilter }),
|
||||||
...(uuid && { uuid }),
|
...(uuid && { uuid }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -30,11 +32,13 @@ export const transformAlertToRuleAction = ({
|
||||||
actionTypeId,
|
actionTypeId,
|
||||||
params,
|
params,
|
||||||
uuid,
|
uuid,
|
||||||
|
alertsFilter,
|
||||||
}: RuleAction): RuleAlertAction => ({
|
}: RuleAction): RuleAlertAction => ({
|
||||||
group,
|
group,
|
||||||
id,
|
id,
|
||||||
params,
|
params,
|
||||||
action_type_id: actionTypeId,
|
action_type_id: actionTypeId,
|
||||||
|
...(alertsFilter && { alerts_filter: alertsFilter }),
|
||||||
...(uuid && { uuid }),
|
...(uuid && { uuid }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -7,8 +7,9 @@
|
||||||
|
|
||||||
import type { RuleAction } from '@kbn/alerting-plugin/common';
|
import type { RuleAction } from '@kbn/alerting-plugin/common';
|
||||||
|
|
||||||
export type RuleAlertAction = Omit<RuleAction, 'actionTypeId'> & {
|
export type RuleAlertAction = Omit<RuleAction, 'actionTypeId' | 'alertsFilter'> & {
|
||||||
action_type_id: string;
|
action_type_id: string;
|
||||||
|
alerts_filter?: RuleAction['alertsFilter'];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -13,7 +13,11 @@ import ReactMarkdown from 'react-markdown';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import type { ActionVariables } from '@kbn/triggers-actions-ui-plugin/public';
|
import type { ActionVariables } from '@kbn/triggers-actions-ui-plugin/public';
|
||||||
import type { RuleAction, RuleActionParam } from '@kbn/alerting-plugin/common';
|
import type {
|
||||||
|
RuleAction,
|
||||||
|
RuleActionAlertsFilterProperty,
|
||||||
|
RuleActionParam,
|
||||||
|
} from '@kbn/alerting-plugin/common';
|
||||||
import { SecurityConnectorFeatureId } from '@kbn/actions-plugin/common';
|
import { SecurityConnectorFeatureId } from '@kbn/actions-plugin/common';
|
||||||
import type { FieldHook } from '../../../../shared_imports';
|
import type { FieldHook } from '../../../../shared_imports';
|
||||||
import { useFormContext } from '../../../../shared_imports';
|
import { useFormContext } from '../../../../shared_imports';
|
||||||
|
@ -131,6 +135,23 @@ export const RuleActionsField: React.FC<Props> = ({ field, messageVariables }) =
|
||||||
[field, isInitializingAction]
|
[field, isInitializingAction]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const setActionAlertsFilterProperty = useCallback(
|
||||||
|
(key: string, value: RuleActionAlertsFilterProperty, index: number) => {
|
||||||
|
field.setValue((prevValue: RuleAction[]) => {
|
||||||
|
const updatedActions = [...prevValue];
|
||||||
|
updatedActions[index] = {
|
||||||
|
...updatedActions[index],
|
||||||
|
alertsFilter: {
|
||||||
|
...(updatedActions[index].alertsFilter ?? { query: null, timeframe: null }),
|
||||||
|
[key]: value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return updatedActions;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[field]
|
||||||
|
);
|
||||||
|
|
||||||
const actionForm = useMemo(
|
const actionForm = useMemo(
|
||||||
() =>
|
() =>
|
||||||
getActionForm({
|
getActionForm({
|
||||||
|
@ -141,10 +162,12 @@ export const RuleActionsField: React.FC<Props> = ({ field, messageVariables }) =
|
||||||
setActions: setAlertActionsProperty,
|
setActions: setAlertActionsProperty,
|
||||||
setActionParamsProperty,
|
setActionParamsProperty,
|
||||||
setActionFrequencyProperty: () => {},
|
setActionFrequencyProperty: () => {},
|
||||||
|
setActionAlertsFilterProperty,
|
||||||
featureId: SecurityConnectorFeatureId,
|
featureId: SecurityConnectorFeatureId,
|
||||||
defaultActionMessage: DEFAULT_ACTION_MESSAGE,
|
defaultActionMessage: DEFAULT_ACTION_MESSAGE,
|
||||||
hideActionHeader: true,
|
hideActionHeader: true,
|
||||||
hideNotifyWhen: true,
|
hideNotifyWhen: true,
|
||||||
|
showActionAlertsFilter: true,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
actions,
|
actions,
|
||||||
|
@ -153,6 +176,7 @@ export const RuleActionsField: React.FC<Props> = ({ field, messageVariables }) =
|
||||||
setActionIdByIndex,
|
setActionIdByIndex,
|
||||||
setActionParamsProperty,
|
setActionParamsProperty,
|
||||||
setAlertActionsProperty,
|
setAlertActionsProperty,
|
||||||
|
setActionAlertsFilterProperty,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ const transformAction: RewriteRequestCase<RuleAction> = ({
|
||||||
connector_type_id: actionTypeId,
|
connector_type_id: actionTypeId,
|
||||||
params,
|
params,
|
||||||
frequency,
|
frequency,
|
||||||
|
alerts_filter: alertsFilter,
|
||||||
}) => ({
|
}) => ({
|
||||||
group,
|
group,
|
||||||
id,
|
id,
|
||||||
|
@ -29,6 +30,7 @@ const transformAction: RewriteRequestCase<RuleAction> = ({
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
|
...(alertsFilter ? { alertsFilter } : {}),
|
||||||
...(uuid && { uuid }),
|
...(uuid && { uuid }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ const rewriteBodyRequest: RewriteResponseCase<RuleCreateBody> = ({
|
||||||
}): any => ({
|
}): any => ({
|
||||||
...res,
|
...res,
|
||||||
rule_type_id: ruleTypeId,
|
rule_type_id: ruleTypeId,
|
||||||
actions: actions.map(({ group, id, params, frequency }) => ({
|
actions: actions.map(({ group, id, params, frequency, alertsFilter }) => ({
|
||||||
group,
|
group,
|
||||||
id,
|
id,
|
||||||
params,
|
params,
|
||||||
|
@ -36,6 +36,7 @@ const rewriteBodyRequest: RewriteResponseCase<RuleCreateBody> = ({
|
||||||
throttle: frequency!.throttle,
|
throttle: frequency!.throttle,
|
||||||
summary: frequency!.summary,
|
summary: frequency!.summary,
|
||||||
},
|
},
|
||||||
|
alertsFilter,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ type RuleUpdatesBody = Pick<
|
||||||
>;
|
>;
|
||||||
const rewriteBodyRequest: RewriteResponseCase<RuleUpdatesBody> = ({ actions, ...res }): any => ({
|
const rewriteBodyRequest: RewriteResponseCase<RuleUpdatesBody> = ({ actions, ...res }): any => ({
|
||||||
...res,
|
...res,
|
||||||
actions: actions.map(({ group, id, params, frequency, uuid }) => ({
|
actions: actions.map(({ group, id, params, frequency, uuid, alertsFilter }) => ({
|
||||||
group,
|
group,
|
||||||
id,
|
id,
|
||||||
params,
|
params,
|
||||||
|
@ -26,6 +26,7 @@ const rewriteBodyRequest: RewriteResponseCase<RuleUpdatesBody> = ({ actions, ...
|
||||||
throttle: frequency!.throttle,
|
throttle: frequency!.throttle,
|
||||||
summary: frequency!.summary,
|
summary: frequency!.summary,
|
||||||
},
|
},
|
||||||
|
alertsFilter,
|
||||||
...(uuid && { uuid }),
|
...(uuid && { uuid }),
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,135 @@
|
||||||
|
/*
|
||||||
|
* 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 { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { ActionAlertsFilterTimeframe } from './action_alerts_filter_timeframe';
|
||||||
|
import { AlertsFilterTimeframe } from '@kbn/alerting-plugin/common';
|
||||||
|
import { Moment } from 'moment';
|
||||||
|
|
||||||
|
jest.mock('@kbn/kibana-react-plugin/public/ui_settings/use_ui_setting', () => ({
|
||||||
|
useUiSetting: jest.fn().mockImplementation((_, defaultValue) => defaultValue),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('action_alerts_filter_timeframe', () => {
|
||||||
|
async function setup(timeframe: AlertsFilterTimeframe | null) {
|
||||||
|
const wrapper = mountWithIntl(
|
||||||
|
<ActionAlertsFilterTimeframe state={timeframe} onChange={() => {}} />
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wait for active space to resolve before requesting the component to update
|
||||||
|
await act(async () => {
|
||||||
|
await nextTick();
|
||||||
|
wrapper.update();
|
||||||
|
});
|
||||||
|
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
it('renders an unchecked switch when passed a null timeframe', async () => {
|
||||||
|
const wrapper = await setup(null);
|
||||||
|
|
||||||
|
const alertsFilterTimeframeToggle = wrapper.find(
|
||||||
|
'[data-test-subj="alertsFilterTimeframeToggle"]'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(alertsFilterTimeframeToggle.first().props().checked).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the passed in timeframe', async () => {
|
||||||
|
const wrapper = await setup({
|
||||||
|
days: [6, 7],
|
||||||
|
timezone: 'America/Chicago',
|
||||||
|
hours: { start: '10:00', end: '20:00' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const alertsFilterTimeframeToggle = wrapper.find(
|
||||||
|
'[data-test-subj="alertsFilterTimeframeToggle"]'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(alertsFilterTimeframeToggle.first().props().checked).toBeTruthy();
|
||||||
|
|
||||||
|
const alertsFilterTimeframeWeekdayButtons = wrapper.find(
|
||||||
|
'[data-test-subj="alertsFilterTimeframeWeekdayButtons"]'
|
||||||
|
);
|
||||||
|
expect(alertsFilterTimeframeWeekdayButtons.exists()).toBeTruthy();
|
||||||
|
// Use Reflect.get to avoid typescript errors
|
||||||
|
expect(
|
||||||
|
Reflect.get(
|
||||||
|
alertsFilterTimeframeWeekdayButtons.find('[data-test-subj="1"]').first().props(),
|
||||||
|
'isSelected'
|
||||||
|
)
|
||||||
|
).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
Reflect.get(
|
||||||
|
alertsFilterTimeframeWeekdayButtons.find('[data-test-subj="2"]').first().props(),
|
||||||
|
'isSelected'
|
||||||
|
)
|
||||||
|
).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
Reflect.get(
|
||||||
|
alertsFilterTimeframeWeekdayButtons.find('[data-test-subj="3"]').first().props(),
|
||||||
|
'isSelected'
|
||||||
|
)
|
||||||
|
).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
Reflect.get(
|
||||||
|
alertsFilterTimeframeWeekdayButtons.find('[data-test-subj="4"]').first().props(),
|
||||||
|
'isSelected'
|
||||||
|
)
|
||||||
|
).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
Reflect.get(
|
||||||
|
alertsFilterTimeframeWeekdayButtons.find('[data-test-subj="5"]').first().props(),
|
||||||
|
'isSelected'
|
||||||
|
)
|
||||||
|
).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
Reflect.get(
|
||||||
|
alertsFilterTimeframeWeekdayButtons.find('[data-test-subj="6"]').first().props(),
|
||||||
|
'isSelected'
|
||||||
|
)
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
Reflect.get(
|
||||||
|
alertsFilterTimeframeWeekdayButtons.find('[data-test-subj="6"]').first().props(),
|
||||||
|
'isSelected'
|
||||||
|
)
|
||||||
|
).toBeTruthy();
|
||||||
|
|
||||||
|
const alertsFilterTimeframeStart = wrapper.find(
|
||||||
|
'[data-test-subj="alertsFilterTimeframeStart"]'
|
||||||
|
);
|
||||||
|
expect(alertsFilterTimeframeStart.exists()).toBeTruthy();
|
||||||
|
{
|
||||||
|
const selectedDate: Moment = Reflect.get(
|
||||||
|
alertsFilterTimeframeStart.first().props(),
|
||||||
|
'selected'
|
||||||
|
);
|
||||||
|
expect(selectedDate.format('HH:mm')).toEqual('10:00');
|
||||||
|
}
|
||||||
|
|
||||||
|
const alertsFilterTimeframeEnd = wrapper.find('[data-test-subj="alertsFilterTimeframeEnd"]');
|
||||||
|
expect(alertsFilterTimeframeEnd.exists()).toBeTruthy();
|
||||||
|
{
|
||||||
|
const selectedDate: Moment = Reflect.get(
|
||||||
|
alertsFilterTimeframeEnd.first().props(),
|
||||||
|
'selected'
|
||||||
|
);
|
||||||
|
expect(selectedDate.format('HH:mm')).toEqual('20:00');
|
||||||
|
}
|
||||||
|
|
||||||
|
const alertsFilterTimeframeTimezone = wrapper.find(
|
||||||
|
'[data-test-subj="alertsFilterTimeframeTimezone"]'
|
||||||
|
);
|
||||||
|
expect(alertsFilterTimeframeTimezone.exists()).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
Reflect.get(alertsFilterTimeframeTimezone.first().props(), 'selectedOptions')[0].label
|
||||||
|
).toEqual('America/Chicago');
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,221 @@
|
||||||
|
/*
|
||||||
|
* 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 moment, { Moment } from 'moment';
|
||||||
|
import React, { useState, useCallback, useMemo, useEffect } from 'react';
|
||||||
|
import { useUiSetting } from '@kbn/kibana-react-plugin/public';
|
||||||
|
import { i18n } from '@kbn/i18n';
|
||||||
|
import {
|
||||||
|
EuiFlexGroup,
|
||||||
|
EuiFlexItem,
|
||||||
|
EuiSwitch,
|
||||||
|
EuiButtonGroup,
|
||||||
|
EuiSpacer,
|
||||||
|
EuiDatePickerRange,
|
||||||
|
EuiDatePicker,
|
||||||
|
EuiComboBox,
|
||||||
|
} from '@elastic/eui';
|
||||||
|
import deepEqual from 'fast-deep-equal';
|
||||||
|
import { AlertsFilterTimeframe, IsoWeekday } from '@kbn/alerting-plugin/common';
|
||||||
|
import { I18N_WEEKDAY_OPTIONS_DDD, ISO_WEEKDAYS } from '../../../common/constants';
|
||||||
|
|
||||||
|
interface ActionAlertsFilterTimeframeProps {
|
||||||
|
state: AlertsFilterTimeframe | null;
|
||||||
|
onChange: (update: AlertsFilterTimeframe | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TIMEZONE_OPTIONS = moment.tz?.names().map((n) => ({ label: n })) ?? [{ label: 'UTC' }];
|
||||||
|
|
||||||
|
const useSortedWeekdayOptions = () => {
|
||||||
|
const kibanaDow: string = useUiSetting('dateFormat:dow');
|
||||||
|
const startDow = kibanaDow ?? 'Sunday';
|
||||||
|
const startDowIndex = I18N_WEEKDAY_OPTIONS_DDD.findIndex((o) => o.label.startsWith(startDow));
|
||||||
|
return [
|
||||||
|
...I18N_WEEKDAY_OPTIONS_DDD.slice(startDowIndex),
|
||||||
|
...I18N_WEEKDAY_OPTIONS_DDD.slice(0, startDowIndex),
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
const useDefaultTimezone = () => {
|
||||||
|
const kibanaTz: string = useUiSetting('dateFormat:tz');
|
||||||
|
if (!kibanaTz || kibanaTz === 'Browser') return moment.tz?.guess() ?? 'UTC';
|
||||||
|
return kibanaTz;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useTimeframe = (initialTimeframe: AlertsFilterTimeframe | null) => {
|
||||||
|
const timezone = useDefaultTimezone();
|
||||||
|
const DEFAULT_TIMEFRAME = {
|
||||||
|
days: [],
|
||||||
|
timezone,
|
||||||
|
hours: {
|
||||||
|
start: '00:00',
|
||||||
|
end: '24:00',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return useState<AlertsFilterTimeframe>(initialTimeframe || DEFAULT_TIMEFRAME);
|
||||||
|
};
|
||||||
|
const useTimeFormat = () => {
|
||||||
|
const dateFormatScaled: Array<[string, string]> = useUiSetting('dateFormat:scaled') ?? [
|
||||||
|
['PT1M', 'HH:mm'],
|
||||||
|
];
|
||||||
|
const [, PT1M] = dateFormatScaled.find(([key]) => key === 'PT1M') ?? ['', 'HH:mm'];
|
||||||
|
return PT1M;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ActionAlertsFilterTimeframe: React.FC<ActionAlertsFilterTimeframeProps> = ({
|
||||||
|
state,
|
||||||
|
onChange,
|
||||||
|
}) => {
|
||||||
|
const timeFormat = useTimeFormat();
|
||||||
|
const [timeframe, setTimeframe] = useTimeframe(state);
|
||||||
|
const [selectedTimezone, setSelectedTimezone] = useState([{ label: timeframe.timezone }]);
|
||||||
|
|
||||||
|
const timeframeEnabled = useMemo(() => Boolean(state), [state]);
|
||||||
|
|
||||||
|
const weekdayOptions = useSortedWeekdayOptions();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const nextState = timeframeEnabled ? timeframe : null;
|
||||||
|
if (!deepEqual(state, nextState)) onChange(nextState);
|
||||||
|
}, [timeframeEnabled, timeframe, state, onChange]);
|
||||||
|
|
||||||
|
const toggleTimeframe = useCallback(
|
||||||
|
() => onChange(state ? null : timeframe),
|
||||||
|
[state, timeframe, onChange]
|
||||||
|
);
|
||||||
|
const updateTimeframe = useCallback(
|
||||||
|
(update: Partial<AlertsFilterTimeframe>) => {
|
||||||
|
setTimeframe({
|
||||||
|
...timeframe,
|
||||||
|
...update,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[timeframe, setTimeframe]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onChangeHours = useCallback(
|
||||||
|
(startOrEnd: 'start' | 'end') => (date: Moment) => {
|
||||||
|
updateTimeframe({
|
||||||
|
hours: { ...timeframe.hours, [startOrEnd]: date.format('HH:mm') },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[updateTimeframe, timeframe]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onToggleWeekday = useCallback(
|
||||||
|
(id: string) => {
|
||||||
|
if (!timeframe) return;
|
||||||
|
const day = Number(id) as IsoWeekday;
|
||||||
|
const previouslyHasDay = timeframe.days.includes(day);
|
||||||
|
const newDays = previouslyHasDay
|
||||||
|
? timeframe.days.filter((d) => d !== day)
|
||||||
|
: [...timeframe.days, day];
|
||||||
|
updateTimeframe({ days: newDays });
|
||||||
|
},
|
||||||
|
[timeframe, updateTimeframe]
|
||||||
|
);
|
||||||
|
const selectedWeekdays = useMemo(
|
||||||
|
() =>
|
||||||
|
ISO_WEEKDAYS.reduce(
|
||||||
|
(result, day) => ({ ...result, [day]: timeframe.days.includes(day) }),
|
||||||
|
{}
|
||||||
|
),
|
||||||
|
[timeframe]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onChangeTimezone = useCallback(
|
||||||
|
(value) => {
|
||||||
|
setSelectedTimezone(value);
|
||||||
|
if (value[0].label) updateTimeframe({ timezone: value[0].label });
|
||||||
|
},
|
||||||
|
[updateTimeframe, setSelectedTimezone]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [startH, startM] = useMemo(() => timeframe.hours.start.split(':').map(Number), [timeframe]);
|
||||||
|
const [endH, endM] = useMemo(() => timeframe.hours.end.split(':').map(Number), [timeframe]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<EuiSwitch
|
||||||
|
label={i18n.translate(
|
||||||
|
'xpack.triggersActionsUI.sections.actionTypeForm.ActionAlertsFilterTimeframeToggleLabel',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Send alert notification within the selected time frame only',
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
checked={timeframeEnabled}
|
||||||
|
onChange={toggleTimeframe}
|
||||||
|
data-test-subj="alertsFilterTimeframeToggle"
|
||||||
|
/>
|
||||||
|
{timeframeEnabled && (
|
||||||
|
<>
|
||||||
|
<EuiSpacer size="s" />
|
||||||
|
<EuiFlexItem>
|
||||||
|
<EuiButtonGroup
|
||||||
|
isFullWidth
|
||||||
|
legend={i18n.translate(
|
||||||
|
'xpack.triggersActionsUI.sections.actionTypeForm.ActionAlertsFilterTimeframeWeekdays',
|
||||||
|
{
|
||||||
|
defaultMessage: 'Days of week',
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
options={weekdayOptions}
|
||||||
|
idToSelectedMap={selectedWeekdays}
|
||||||
|
type="multi"
|
||||||
|
onChange={onToggleWeekday}
|
||||||
|
data-test-subj="alertsFilterTimeframeWeekdayButtons"
|
||||||
|
/>
|
||||||
|
</EuiFlexItem>
|
||||||
|
<EuiSpacer size="s" />
|
||||||
|
<EuiFlexGroup alignItems="center">
|
||||||
|
<EuiFlexItem grow={2}>
|
||||||
|
<EuiDatePickerRange
|
||||||
|
fullWidth
|
||||||
|
startDateControl={
|
||||||
|
<EuiDatePicker
|
||||||
|
showTimeSelect
|
||||||
|
showTimeSelectOnly
|
||||||
|
dateFormat={timeFormat}
|
||||||
|
timeFormat={timeFormat}
|
||||||
|
selected={moment().set('hour', startH).set('minute', startM)}
|
||||||
|
onChange={onChangeHours('start')}
|
||||||
|
data-test-subj="alertsFilterTimeframeStart"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
endDateControl={
|
||||||
|
<EuiDatePicker
|
||||||
|
showTimeSelect
|
||||||
|
showTimeSelectOnly
|
||||||
|
dateFormat={timeFormat}
|
||||||
|
timeFormat={timeFormat}
|
||||||
|
selected={moment().set('hour', endH).set('minute', endM)}
|
||||||
|
onChange={onChangeHours('end')}
|
||||||
|
data-test-subj="alertsFilterTimeframeEnd"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</EuiFlexItem>
|
||||||
|
<EuiFlexItem grow={1}>
|
||||||
|
<EuiComboBox
|
||||||
|
prepend={i18n.translate(
|
||||||
|
'xpack.triggersActionsUI.sections.actionTypeForm.ActionAlertsFilterTimeframeTimezoneLabel',
|
||||||
|
{ defaultMessage: 'Timezone' }
|
||||||
|
)}
|
||||||
|
singleSelection={{ asPlainText: true }}
|
||||||
|
options={TIMEZONE_OPTIONS}
|
||||||
|
selectedOptions={selectedTimezone}
|
||||||
|
onChange={onChangeTimezone}
|
||||||
|
isClearable={false}
|
||||||
|
data-test-subj="alertsFilterTimeframeTimezone"
|
||||||
|
/>
|
||||||
|
</EuiFlexItem>
|
||||||
|
</EuiFlexGroup>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -347,6 +347,15 @@ describe('action_form', () => {
|
||||||
frequency: { ...initialAlert.actions[index].frequency!, [key]: value },
|
frequency: { ...initialAlert.actions[index].frequency!, [key]: value },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
setActionAlertsFilterProperty={(key: string, value: any, index: number) =>
|
||||||
|
(initialAlert.actions[index] = {
|
||||||
|
...initialAlert.actions[index],
|
||||||
|
alertsFilter: {
|
||||||
|
...(initialAlert.actions[index].alertsFilter ?? { query: null, timeframe: null }),
|
||||||
|
[key]: value,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
actionTypeRegistry={actionTypeRegistry}
|
actionTypeRegistry={actionTypeRegistry}
|
||||||
setHasActionsWithBrokenConnector={setHasActionsWithBrokenConnector}
|
setHasActionsWithBrokenConnector={setHasActionsWithBrokenConnector}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -19,7 +19,11 @@ import {
|
||||||
EuiToolTip,
|
EuiToolTip,
|
||||||
EuiLink,
|
EuiLink,
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
import { ActionGroup, RuleActionParam } from '@kbn/alerting-plugin/common';
|
import {
|
||||||
|
ActionGroup,
|
||||||
|
RuleActionAlertsFilterProperty,
|
||||||
|
RuleActionParam,
|
||||||
|
} from '@kbn/alerting-plugin/common';
|
||||||
import { betaBadgeProps } from './beta_badge_props';
|
import { betaBadgeProps } from './beta_badge_props';
|
||||||
import { loadActionTypes, loadAllActions as loadConnectors } from '../../lib/action_connector_api';
|
import { loadActionTypes, loadAllActions as loadConnectors } from '../../lib/action_connector_api';
|
||||||
import {
|
import {
|
||||||
|
@ -56,6 +60,11 @@ export interface ActionAccordionFormProps {
|
||||||
setActions: (actions: RuleAction[]) => void;
|
setActions: (actions: RuleAction[]) => void;
|
||||||
setActionParamsProperty: (key: string, value: RuleActionParam, index: number) => void;
|
setActionParamsProperty: (key: string, value: RuleActionParam, index: number) => void;
|
||||||
setActionFrequencyProperty: (key: string, value: RuleActionParam, index: number) => void;
|
setActionFrequencyProperty: (key: string, value: RuleActionParam, index: number) => void;
|
||||||
|
setActionAlertsFilterProperty: (
|
||||||
|
key: string,
|
||||||
|
value: RuleActionAlertsFilterProperty,
|
||||||
|
index: number
|
||||||
|
) => void;
|
||||||
featureId: string;
|
featureId: string;
|
||||||
messageVariables?: ActionVariables;
|
messageVariables?: ActionVariables;
|
||||||
setHasActionsDisabled?: (value: boolean) => void;
|
setHasActionsDisabled?: (value: boolean) => void;
|
||||||
|
@ -68,6 +77,7 @@ export interface ActionAccordionFormProps {
|
||||||
defaultSummaryMessage?: string;
|
defaultSummaryMessage?: string;
|
||||||
hasSummary?: boolean;
|
hasSummary?: boolean;
|
||||||
minimumThrottleInterval?: [number | undefined, string];
|
minimumThrottleInterval?: [number | undefined, string];
|
||||||
|
showActionAlertsFilter?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ActiveActionConnectorState {
|
interface ActiveActionConnectorState {
|
||||||
|
@ -83,6 +93,7 @@ export const ActionForm = ({
|
||||||
setActions,
|
setActions,
|
||||||
setActionParamsProperty,
|
setActionParamsProperty,
|
||||||
setActionFrequencyProperty,
|
setActionFrequencyProperty,
|
||||||
|
setActionAlertsFilterProperty,
|
||||||
featureId,
|
featureId,
|
||||||
messageVariables,
|
messageVariables,
|
||||||
actionGroups,
|
actionGroups,
|
||||||
|
@ -97,6 +108,7 @@ export const ActionForm = ({
|
||||||
defaultSummaryMessage,
|
defaultSummaryMessage,
|
||||||
hasSummary,
|
hasSummary,
|
||||||
minimumThrottleInterval,
|
minimumThrottleInterval,
|
||||||
|
showActionAlertsFilter,
|
||||||
}: ActionAccordionFormProps) => {
|
}: ActionAccordionFormProps) => {
|
||||||
const {
|
const {
|
||||||
http,
|
http,
|
||||||
|
@ -373,6 +385,7 @@ export const ActionForm = ({
|
||||||
key={`action-form-action-at-${index}`}
|
key={`action-form-action-at-${index}`}
|
||||||
setActionParamsProperty={setActionParamsProperty}
|
setActionParamsProperty={setActionParamsProperty}
|
||||||
setActionFrequencyProperty={setActionFrequencyProperty}
|
setActionFrequencyProperty={setActionFrequencyProperty}
|
||||||
|
setActionAlertsFilterProperty={setActionAlertsFilterProperty}
|
||||||
actionTypesIndex={actionTypesIndex}
|
actionTypesIndex={actionTypesIndex}
|
||||||
connectors={connectors}
|
connectors={connectors}
|
||||||
defaultActionGroupId={defaultActionGroupId}
|
defaultActionGroupId={defaultActionGroupId}
|
||||||
|
@ -402,6 +415,7 @@ export const ActionForm = ({
|
||||||
defaultSummaryMessage={defaultSummaryMessage}
|
defaultSummaryMessage={defaultSummaryMessage}
|
||||||
hasSummary={hasSummary}
|
hasSummary={hasSummary}
|
||||||
minimumThrottleInterval={minimumThrottleInterval}
|
minimumThrottleInterval={minimumThrottleInterval}
|
||||||
|
showActionAlertsFilter={showActionAlertsFilter}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -36,6 +36,10 @@ jest.mock('../../lib/action_variables', () => {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
jest.mock('@kbn/kibana-react-plugin/public/ui_settings/use_ui_setting', () => ({
|
||||||
|
useUiSetting: jest.fn().mockImplementation((_, defaultValue) => defaultValue),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('action_type_form', () => {
|
describe('action_type_form', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
|
@ -376,6 +380,7 @@ function getActionTypeForm({
|
||||||
onDeleteAction,
|
onDeleteAction,
|
||||||
onConnectorSelected,
|
onConnectorSelected,
|
||||||
setActionFrequencyProperty,
|
setActionFrequencyProperty,
|
||||||
|
setActionAlertsFilterProperty,
|
||||||
hasSummary = true,
|
hasSummary = true,
|
||||||
messageVariables = { context: [], state: [], params: [] },
|
messageVariables = { context: [], state: [], params: [] },
|
||||||
}: {
|
}: {
|
||||||
|
@ -389,6 +394,7 @@ function getActionTypeForm({
|
||||||
onDeleteAction?: () => void;
|
onDeleteAction?: () => void;
|
||||||
onConnectorSelected?: (id: string) => void;
|
onConnectorSelected?: (id: string) => void;
|
||||||
setActionFrequencyProperty?: () => void;
|
setActionFrequencyProperty?: () => void;
|
||||||
|
setActionAlertsFilterProperty?: () => void;
|
||||||
hasSummary?: boolean;
|
hasSummary?: boolean;
|
||||||
messageVariables?: ActionVariables;
|
messageVariables?: ActionVariables;
|
||||||
}) {
|
}) {
|
||||||
|
@ -469,6 +475,7 @@ function getActionTypeForm({
|
||||||
defaultActionGroupId={defaultActionGroupId ?? 'default'}
|
defaultActionGroupId={defaultActionGroupId ?? 'default'}
|
||||||
setActionParamsProperty={jest.fn()}
|
setActionParamsProperty={jest.fn()}
|
||||||
setActionFrequencyProperty={setActionFrequencyProperty ?? jest.fn()}
|
setActionFrequencyProperty={setActionFrequencyProperty ?? jest.fn()}
|
||||||
|
setActionAlertsFilterProperty={setActionAlertsFilterProperty ?? jest.fn()}
|
||||||
index={index ?? 1}
|
index={index ?? 1}
|
||||||
actionTypesIndex={actionTypeIndex ?? actionTypeIndexDefault}
|
actionTypesIndex={actionTypeIndex ?? actionTypeIndexDefault}
|
||||||
actionTypeRegistry={actionTypeRegistry}
|
actionTypeRegistry={actionTypeRegistry}
|
||||||
|
|
|
@ -30,7 +30,11 @@ import {
|
||||||
EuiCallOut,
|
EuiCallOut,
|
||||||
} from '@elastic/eui';
|
} from '@elastic/eui';
|
||||||
import { isEmpty, partition, some } from 'lodash';
|
import { isEmpty, partition, some } from 'lodash';
|
||||||
import { ActionVariable, RuleActionParam } from '@kbn/alerting-plugin/common';
|
import {
|
||||||
|
ActionVariable,
|
||||||
|
RuleActionAlertsFilterProperty,
|
||||||
|
RuleActionParam,
|
||||||
|
} from '@kbn/alerting-plugin/common';
|
||||||
import {
|
import {
|
||||||
getDurationNumberInItsUnit,
|
getDurationNumberInItsUnit,
|
||||||
getDurationUnitValue,
|
getDurationUnitValue,
|
||||||
|
@ -54,6 +58,7 @@ import { useKibana } from '../../../common/lib/kibana';
|
||||||
import { ConnectorsSelection } from './connectors_selection';
|
import { ConnectorsSelection } from './connectors_selection';
|
||||||
import { ActionNotifyWhen } from './action_notify_when';
|
import { ActionNotifyWhen } from './action_notify_when';
|
||||||
import { validateParamsForWarnings } from '../../lib/validate_params_for_warnings';
|
import { validateParamsForWarnings } from '../../lib/validate_params_for_warnings';
|
||||||
|
import { ActionAlertsFilterTimeframe } from './action_alerts_filter_timeframe';
|
||||||
|
|
||||||
export type ActionTypeFormProps = {
|
export type ActionTypeFormProps = {
|
||||||
actionItem: RuleAction;
|
actionItem: RuleAction;
|
||||||
|
@ -64,6 +69,11 @@ export type ActionTypeFormProps = {
|
||||||
onDeleteAction: () => void;
|
onDeleteAction: () => void;
|
||||||
setActionParamsProperty: (key: string, value: RuleActionParam, index: number) => void;
|
setActionParamsProperty: (key: string, value: RuleActionParam, index: number) => void;
|
||||||
setActionFrequencyProperty: (key: string, value: RuleActionParam, index: number) => void;
|
setActionFrequencyProperty: (key: string, value: RuleActionParam, index: number) => void;
|
||||||
|
setActionAlertsFilterProperty: (
|
||||||
|
key: string,
|
||||||
|
value: RuleActionAlertsFilterProperty,
|
||||||
|
index: number
|
||||||
|
) => void;
|
||||||
actionTypesIndex: ActionTypeIndex;
|
actionTypesIndex: ActionTypeIndex;
|
||||||
connectors: ActionConnector[];
|
connectors: ActionConnector[];
|
||||||
actionTypeRegistry: ActionTypeRegistryContract;
|
actionTypeRegistry: ActionTypeRegistryContract;
|
||||||
|
@ -72,6 +82,7 @@ export type ActionTypeFormProps = {
|
||||||
hideNotifyWhen?: boolean;
|
hideNotifyWhen?: boolean;
|
||||||
hasSummary?: boolean;
|
hasSummary?: boolean;
|
||||||
minimumThrottleInterval?: [number | undefined, string];
|
minimumThrottleInterval?: [number | undefined, string];
|
||||||
|
showActionAlertsFilter?: boolean;
|
||||||
} & Pick<
|
} & Pick<
|
||||||
ActionAccordionFormProps,
|
ActionAccordionFormProps,
|
||||||
| 'defaultActionGroupId'
|
| 'defaultActionGroupId'
|
||||||
|
@ -99,6 +110,7 @@ export const ActionTypeForm = ({
|
||||||
onDeleteAction,
|
onDeleteAction,
|
||||||
setActionParamsProperty,
|
setActionParamsProperty,
|
||||||
setActionFrequencyProperty,
|
setActionFrequencyProperty,
|
||||||
|
setActionAlertsFilterProperty,
|
||||||
actionTypesIndex,
|
actionTypesIndex,
|
||||||
connectors,
|
connectors,
|
||||||
defaultActionGroupId,
|
defaultActionGroupId,
|
||||||
|
@ -113,6 +125,7 @@ export const ActionTypeForm = ({
|
||||||
defaultSummaryMessage,
|
defaultSummaryMessage,
|
||||||
hasSummary,
|
hasSummary,
|
||||||
minimumThrottleInterval,
|
minimumThrottleInterval,
|
||||||
|
showActionAlertsFilter,
|
||||||
}: ActionTypeFormProps) => {
|
}: ActionTypeFormProps) => {
|
||||||
const {
|
const {
|
||||||
application: { capabilities },
|
application: { capabilities },
|
||||||
|
@ -143,6 +156,7 @@ export const ActionTypeForm = ({
|
||||||
const [warning, setWarning] = useState<string | null>(null);
|
const [warning, setWarning] = useState<string | null>(null);
|
||||||
|
|
||||||
const [useDefaultMessage, setUseDefaultMessage] = useState(false);
|
const [useDefaultMessage, setUseDefaultMessage] = useState(false);
|
||||||
|
|
||||||
const isSummaryAction = actionItem.frequency?.summary;
|
const isSummaryAction = actionItem.frequency?.summary;
|
||||||
|
|
||||||
const getDefaultParams = async () => {
|
const getDefaultParams = async () => {
|
||||||
|
@ -380,6 +394,15 @@ export const ActionTypeForm = ({
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{showActionAlertsFilter && (
|
||||||
|
<>
|
||||||
|
{!hideNotifyWhen && <EuiSpacer size="xl" />}
|
||||||
|
<ActionAlertsFilterTimeframe
|
||||||
|
state={actionItem.alertsFilter?.timeframe ?? null}
|
||||||
|
onChange={(timeframe) => setActionAlertsFilterProperty('timeframe', timeframe, index)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</EuiSplitPanel.Inner>
|
</EuiSplitPanel.Inner>
|
||||||
<EuiSplitPanel.Inner color="plain">
|
<EuiSplitPanel.Inner color="plain">
|
||||||
{ParamsFieldsComponent ? (
|
{ParamsFieldsComponent ? (
|
||||||
|
|
|
@ -48,6 +48,7 @@ import {
|
||||||
ALERTS_FEATURE_ID,
|
ALERTS_FEATURE_ID,
|
||||||
RecoveredActionGroup,
|
RecoveredActionGroup,
|
||||||
isActionGroupDisabledForActionTypeId,
|
isActionGroupDisabledForActionTypeId,
|
||||||
|
RuleActionAlertsFilterProperty,
|
||||||
} from '@kbn/alerting-plugin/common';
|
} from '@kbn/alerting-plugin/common';
|
||||||
import { AlertingConnectorFeatureId } from '@kbn/actions-plugin/common';
|
import { AlertingConnectorFeatureId } from '@kbn/actions-plugin/common';
|
||||||
import { RuleReducerAction, InitialRule } from './rule_reducer';
|
import { RuleReducerAction, InitialRule } from './rule_reducer';
|
||||||
|
@ -291,6 +292,13 @@ export const RuleForm = ({
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const setActionAlertsFilterProperty = useCallback(
|
||||||
|
(key: string, value: RuleActionAlertsFilterProperty, index: number) => {
|
||||||
|
dispatch({ command: { type: 'setRuleActionAlertsFilter' }, payload: { key, value, index } });
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const searchValue = searchText ? searchText.trim().toLocaleLowerCase() : null;
|
const searchValue = searchText ? searchText.trim().toLocaleLowerCase() : null;
|
||||||
setFilteredRuleTypes(
|
setFilteredRuleTypes(
|
||||||
|
@ -677,6 +685,7 @@ export const RuleForm = ({
|
||||||
setActionParamsProperty={setActionParamsProperty}
|
setActionParamsProperty={setActionParamsProperty}
|
||||||
actionTypeRegistry={actionTypeRegistry}
|
actionTypeRegistry={actionTypeRegistry}
|
||||||
setActionFrequencyProperty={setActionFrequencyProperty}
|
setActionFrequencyProperty={setActionFrequencyProperty}
|
||||||
|
setActionAlertsFilterProperty={setActionAlertsFilterProperty}
|
||||||
defaultSummaryMessage={ruleTypeModel?.defaultSummaryMessage || summaryMessage}
|
defaultSummaryMessage={ruleTypeModel?.defaultSummaryMessage || summaryMessage}
|
||||||
minimumThrottleInterval={[ruleInterval, ruleIntervalUnit]}
|
minimumThrottleInterval={[ruleInterval, ruleIntervalUnit]}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -8,7 +8,11 @@
|
||||||
import { SavedObjectAttribute } from '@kbn/core/public';
|
import { SavedObjectAttribute } from '@kbn/core/public';
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
import { Reducer } from 'react';
|
import { Reducer } from 'react';
|
||||||
import { RuleActionParam, IntervalSchedule } from '@kbn/alerting-plugin/common';
|
import {
|
||||||
|
RuleActionParam,
|
||||||
|
IntervalSchedule,
|
||||||
|
RuleActionAlertsFilterProperty,
|
||||||
|
} from '@kbn/alerting-plugin/common';
|
||||||
import { Rule, RuleAction } from '../../../types';
|
import { Rule, RuleAction } from '../../../types';
|
||||||
import { DEFAULT_FREQUENCY } from '../../../common/constants';
|
import { DEFAULT_FREQUENCY } from '../../../common/constants';
|
||||||
|
|
||||||
|
@ -24,6 +28,7 @@ interface CommandType<
|
||||||
| 'setRuleActionParams'
|
| 'setRuleActionParams'
|
||||||
| 'setRuleActionProperty'
|
| 'setRuleActionProperty'
|
||||||
| 'setRuleActionFrequency'
|
| 'setRuleActionFrequency'
|
||||||
|
| 'setRuleActionAlertsFilter'
|
||||||
> {
|
> {
|
||||||
type: T;
|
type: T;
|
||||||
}
|
}
|
||||||
|
@ -84,6 +89,10 @@ export type RuleReducerAction =
|
||||||
| {
|
| {
|
||||||
command: CommandType<'setRuleActionFrequency'>;
|
command: CommandType<'setRuleActionFrequency'>;
|
||||||
payload: Payload<string, RuleActionParam>;
|
payload: Payload<string, RuleActionParam>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
command: CommandType<'setRuleActionAlertsFilter'>;
|
||||||
|
payload: Payload<string, RuleActionAlertsFilterProperty>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type InitialRuleReducer = Reducer<{ rule: InitialRule }, RuleReducerAction>;
|
export type InitialRuleReducer = Reducer<{ rule: InitialRule }, RuleReducerAction>;
|
||||||
|
@ -215,6 +224,36 @@ export const ruleReducer = <RulePhase extends InitialRule | Rule>(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case 'setRuleActionAlertsFilter': {
|
||||||
|
const { key, value, index } = action.payload as Payload<
|
||||||
|
keyof RuleAction,
|
||||||
|
SavedObjectAttribute
|
||||||
|
>;
|
||||||
|
if (
|
||||||
|
index === undefined ||
|
||||||
|
rule.actions[index] == null ||
|
||||||
|
(!!rule.actions[index][key] && isEqual(rule.actions[index][key], value))
|
||||||
|
) {
|
||||||
|
return state;
|
||||||
|
} else {
|
||||||
|
const oldAction = rule.actions.splice(index, 1)[0];
|
||||||
|
const updatedAction = {
|
||||||
|
...oldAction,
|
||||||
|
alertsFilter: {
|
||||||
|
...(oldAction.alertsFilter ?? { timeframe: null, query: null }),
|
||||||
|
[key]: value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
rule.actions.splice(index, 0, updatedAction);
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
rule: {
|
||||||
|
...rule,
|
||||||
|
actions: [...rule.actions],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
case 'setRuleActionProperty': {
|
case 'setRuleActionProperty': {
|
||||||
const { key, value, index } = action.payload as RuleActionPayload<keyof RuleAction>;
|
const { key, value, index } = action.payload as RuleActionPayload<keyof RuleAction>;
|
||||||
if (index === undefined || isEqual(rule.actions[index][key], value)) {
|
if (index === undefined || isEqual(rule.actions[index][key], value)) {
|
||||||
|
|
|
@ -6,10 +6,9 @@
|
||||||
*/
|
*/
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { invert, mapValues } from 'lodash';
|
import { invert, mapValues } from 'lodash';
|
||||||
import moment from 'moment';
|
|
||||||
import { RRuleFrequency } from '../../../../../../types';
|
import { RRuleFrequency } from '../../../../../../types';
|
||||||
|
|
||||||
export const ISO_WEEKDAYS = [1, 2, 3, 4, 5, 6, 7];
|
export { ISO_WEEKDAYS, I18N_WEEKDAY_OPTIONS } from '../../../../../../common/constants';
|
||||||
|
|
||||||
export const RECURRENCE_END_OPTIONS = [
|
export const RECURRENCE_END_OPTIONS = [
|
||||||
{ id: 'never', label: 'Never' },
|
{ id: 'never', label: 'Never' },
|
||||||
|
@ -65,11 +64,6 @@ export const DEFAULT_RRULE_PRESETS = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const I18N_WEEKDAY_OPTIONS = ISO_WEEKDAYS.map((n) => ({
|
|
||||||
id: String(n),
|
|
||||||
label: moment().isoWeekday(n).format('dd'),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const ISO_WEEKDAYS_TO_RRULE: Record<number, string> = {
|
export const ISO_WEEKDAYS_TO_RRULE: Record<number, string> = {
|
||||||
1: 'MO',
|
1: 'MO',
|
||||||
2: 'TU',
|
2: 'TU',
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
/*
|
||||||
|
* 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 { IsoWeekday } from '@kbn/alerting-plugin/common';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
export const ISO_WEEKDAYS: IsoWeekday[] = [1, 2, 3, 4, 5, 6, 7];
|
||||||
|
|
||||||
|
export const I18N_WEEKDAY_OPTIONS = ISO_WEEKDAYS.map((n) => ({
|
||||||
|
id: String(n),
|
||||||
|
label: moment().isoWeekday(n).format('dd'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const I18N_WEEKDAY_OPTIONS_DDD = ISO_WEEKDAYS.map((n) => ({
|
||||||
|
id: String(n),
|
||||||
|
label: moment().isoWeekday(n).format('ddd'),
|
||||||
|
}));
|
|
@ -14,3 +14,4 @@ export const VIEW_LICENSE_OPTIONS_LINK = 'https://www.elastic.co/subscriptions';
|
||||||
|
|
||||||
export const PLUGIN_ID = 'triggersActions';
|
export const PLUGIN_ID = 'triggersActions';
|
||||||
export const CONNECTORS_PLUGIN_ID = 'triggersActionsConnectors';
|
export const CONNECTORS_PLUGIN_ID = 'triggersActionsConnectors';
|
||||||
|
export * from './i18n_weekdays';
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue