mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -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 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 const RuleAction = t.exact(
|
||||
t.intersection([
|
||||
|
@ -38,7 +71,7 @@ export const RuleAction = t.exact(
|
|||
action_type_id: RuleActionTypeId,
|
||||
params: RuleActionParams,
|
||||
}),
|
||||
t.partial({ uuid: RuleActionUuid }),
|
||||
t.partial({ uuid: RuleActionUuid, alerts_filter: RuleActionAlertsFilter }),
|
||||
])
|
||||
);
|
||||
|
||||
|
@ -54,7 +87,7 @@ export const RuleActionCamel = t.exact(
|
|||
actionTypeId: RuleActionTypeId,
|
||||
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 RuleActionParam = SavedObjectAttribute;
|
||||
|
||||
export type IsoWeekday = 1 | 2 | 3 | 4 | 5 | 6 | 7;
|
||||
export interface AlertsFilterTimeframe extends SavedObjectAttributes {
|
||||
days: Array<1 | 2 | 3 | 4 | 5 | 6 | 7>;
|
||||
days: IsoWeekday[];
|
||||
timezone: string;
|
||||
hours: {
|
||||
start: string;
|
||||
|
@ -94,6 +95,8 @@ export interface AlertsFilter extends SavedObjectAttributes {
|
|||
timeframe: null | AlertsFilterTimeframe;
|
||||
}
|
||||
|
||||
export type RuleActionAlertsFilterProperty = AlertsFilterTimeframe | RuleActionParam;
|
||||
|
||||
export interface RuleAction {
|
||||
uuid?: string;
|
||||
group: string;
|
||||
|
|
|
@ -9,7 +9,12 @@ import { omit } from 'lodash';
|
|||
import { schema } from '@kbn/config-schema';
|
||||
import { IRouter } from '@kbn/core/server';
|
||||
import { ILicenseState } from '../lib';
|
||||
import { verifyAccessAndContext, RewriteResponseCase, rewriteRuleLastRun } from './lib';
|
||||
import {
|
||||
verifyAccessAndContext,
|
||||
RewriteResponseCase,
|
||||
rewriteRuleLastRun,
|
||||
rewriteActionsRes,
|
||||
} from './lib';
|
||||
import {
|
||||
RuleTypeParams,
|
||||
AlertingRequestHandlerContext,
|
||||
|
@ -61,21 +66,7 @@ const rewriteBodyRes: RewriteResponseCase<SanitizedRule<RuleTypeParams>> = ({
|
|||
last_execution_date: executionStatus.lastExecutionDate,
|
||||
last_duration: executionStatus.lastDuration,
|
||||
},
|
||||
actions: actions.map(({ group, id, actionTypeId, params, frequency, uuid, alertsFilter }) => ({
|
||||
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 }),
|
||||
})),
|
||||
actions: rewriteActionsRes(actions),
|
||||
...(lastRun ? { last_run: rewriteRuleLastRun(lastRun) } : {}),
|
||||
...(nextRun ? { next_run: nextRun } : {}),
|
||||
...(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(),
|
||||
}));
|
||||
|
||||
jest.mock('@kbn/kibana-react-plugin/public/ui_settings/use_ui_setting', () => ({
|
||||
useUiSetting: jest.fn().mockImplementation((_, defaultValue) => defaultValue),
|
||||
}));
|
||||
|
||||
const initLegacyShims = () => {
|
||||
const triggersActionsUi = {
|
||||
actionTypeRegistry: actionTypeRegistryMock.create(),
|
||||
|
@ -237,10 +241,13 @@ describe('alert_form', () => {
|
|||
initialAlert.actions[index].id = id;
|
||||
}}
|
||||
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 })
|
||||
}
|
||||
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 })
|
||||
}
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
|
|
|
@ -16,11 +16,13 @@ export const transformRuleToAlertAction = ({
|
|||
action_type_id: actionTypeId,
|
||||
params,
|
||||
uuid,
|
||||
alerts_filter: alertsFilter,
|
||||
}: RuleAlertAction): RuleAction => ({
|
||||
group,
|
||||
id,
|
||||
params,
|
||||
actionTypeId,
|
||||
...(alertsFilter && { alertsFilter }),
|
||||
...(uuid && { uuid }),
|
||||
});
|
||||
|
||||
|
@ -30,11 +32,13 @@ export const transformAlertToRuleAction = ({
|
|||
actionTypeId,
|
||||
params,
|
||||
uuid,
|
||||
alertsFilter,
|
||||
}: RuleAction): RuleAlertAction => ({
|
||||
group,
|
||||
id,
|
||||
params,
|
||||
action_type_id: actionTypeId,
|
||||
...(alertsFilter && { alerts_filter: alertsFilter }),
|
||||
...(uuid && { uuid }),
|
||||
});
|
||||
|
||||
|
|
|
@ -7,8 +7,9 @@
|
|||
|
||||
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;
|
||||
alerts_filter?: RuleAction['alertsFilter'];
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -13,7 +13,11 @@ import ReactMarkdown from 'react-markdown';
|
|||
import styled from 'styled-components';
|
||||
|
||||
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 type { FieldHook } from '../../../../shared_imports';
|
||||
import { useFormContext } from '../../../../shared_imports';
|
||||
|
@ -131,6 +135,23 @@ export const RuleActionsField: React.FC<Props> = ({ field, messageVariables }) =
|
|||
[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(
|
||||
() =>
|
||||
getActionForm({
|
||||
|
@ -141,10 +162,12 @@ export const RuleActionsField: React.FC<Props> = ({ field, messageVariables }) =
|
|||
setActions: setAlertActionsProperty,
|
||||
setActionParamsProperty,
|
||||
setActionFrequencyProperty: () => {},
|
||||
setActionAlertsFilterProperty,
|
||||
featureId: SecurityConnectorFeatureId,
|
||||
defaultActionMessage: DEFAULT_ACTION_MESSAGE,
|
||||
hideActionHeader: true,
|
||||
hideNotifyWhen: true,
|
||||
showActionAlertsFilter: true,
|
||||
}),
|
||||
[
|
||||
actions,
|
||||
|
@ -153,6 +176,7 @@ export const RuleActionsField: React.FC<Props> = ({ field, messageVariables }) =
|
|||
setActionIdByIndex,
|
||||
setActionParamsProperty,
|
||||
setAlertActionsProperty,
|
||||
setActionAlertsFilterProperty,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ const transformAction: RewriteRequestCase<RuleAction> = ({
|
|||
connector_type_id: actionTypeId,
|
||||
params,
|
||||
frequency,
|
||||
alerts_filter: alertsFilter,
|
||||
}) => ({
|
||||
group,
|
||||
id,
|
||||
|
@ -29,6 +30,7 @@ const transformAction: RewriteRequestCase<RuleAction> = ({
|
|||
},
|
||||
}
|
||||
: {}),
|
||||
...(alertsFilter ? { alertsFilter } : {}),
|
||||
...(uuid && { uuid }),
|
||||
});
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ const rewriteBodyRequest: RewriteResponseCase<RuleCreateBody> = ({
|
|||
}): any => ({
|
||||
...res,
|
||||
rule_type_id: ruleTypeId,
|
||||
actions: actions.map(({ group, id, params, frequency }) => ({
|
||||
actions: actions.map(({ group, id, params, frequency, alertsFilter }) => ({
|
||||
group,
|
||||
id,
|
||||
params,
|
||||
|
@ -36,6 +36,7 @@ const rewriteBodyRequest: RewriteResponseCase<RuleCreateBody> = ({
|
|||
throttle: frequency!.throttle,
|
||||
summary: frequency!.summary,
|
||||
},
|
||||
alertsFilter,
|
||||
})),
|
||||
});
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ type RuleUpdatesBody = Pick<
|
|||
>;
|
||||
const rewriteBodyRequest: RewriteResponseCase<RuleUpdatesBody> = ({ actions, ...res }): any => ({
|
||||
...res,
|
||||
actions: actions.map(({ group, id, params, frequency, uuid }) => ({
|
||||
actions: actions.map(({ group, id, params, frequency, uuid, alertsFilter }) => ({
|
||||
group,
|
||||
id,
|
||||
params,
|
||||
|
@ -26,6 +26,7 @@ const rewriteBodyRequest: RewriteResponseCase<RuleUpdatesBody> = ({ actions, ...
|
|||
throttle: frequency!.throttle,
|
||||
summary: frequency!.summary,
|
||||
},
|
||||
alertsFilter,
|
||||
...(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 },
|
||||
})
|
||||
}
|
||||
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}
|
||||
setHasActionsWithBrokenConnector={setHasActionsWithBrokenConnector}
|
||||
/>
|
||||
|
|
|
@ -19,7 +19,11 @@ import {
|
|||
EuiToolTip,
|
||||
EuiLink,
|
||||
} 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 { loadActionTypes, loadAllActions as loadConnectors } from '../../lib/action_connector_api';
|
||||
import {
|
||||
|
@ -56,6 +60,11 @@ export interface ActionAccordionFormProps {
|
|||
setActions: (actions: RuleAction[]) => void;
|
||||
setActionParamsProperty: (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;
|
||||
messageVariables?: ActionVariables;
|
||||
setHasActionsDisabled?: (value: boolean) => void;
|
||||
|
@ -68,6 +77,7 @@ export interface ActionAccordionFormProps {
|
|||
defaultSummaryMessage?: string;
|
||||
hasSummary?: boolean;
|
||||
minimumThrottleInterval?: [number | undefined, string];
|
||||
showActionAlertsFilter?: boolean;
|
||||
}
|
||||
|
||||
interface ActiveActionConnectorState {
|
||||
|
@ -83,6 +93,7 @@ export const ActionForm = ({
|
|||
setActions,
|
||||
setActionParamsProperty,
|
||||
setActionFrequencyProperty,
|
||||
setActionAlertsFilterProperty,
|
||||
featureId,
|
||||
messageVariables,
|
||||
actionGroups,
|
||||
|
@ -97,6 +108,7 @@ export const ActionForm = ({
|
|||
defaultSummaryMessage,
|
||||
hasSummary,
|
||||
minimumThrottleInterval,
|
||||
showActionAlertsFilter,
|
||||
}: ActionAccordionFormProps) => {
|
||||
const {
|
||||
http,
|
||||
|
@ -373,6 +385,7 @@ export const ActionForm = ({
|
|||
key={`action-form-action-at-${index}`}
|
||||
setActionParamsProperty={setActionParamsProperty}
|
||||
setActionFrequencyProperty={setActionFrequencyProperty}
|
||||
setActionAlertsFilterProperty={setActionAlertsFilterProperty}
|
||||
actionTypesIndex={actionTypesIndex}
|
||||
connectors={connectors}
|
||||
defaultActionGroupId={defaultActionGroupId}
|
||||
|
@ -402,6 +415,7 @@ export const ActionForm = ({
|
|||
defaultSummaryMessage={defaultSummaryMessage}
|
||||
hasSummary={hasSummary}
|
||||
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', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
@ -376,6 +380,7 @@ function getActionTypeForm({
|
|||
onDeleteAction,
|
||||
onConnectorSelected,
|
||||
setActionFrequencyProperty,
|
||||
setActionAlertsFilterProperty,
|
||||
hasSummary = true,
|
||||
messageVariables = { context: [], state: [], params: [] },
|
||||
}: {
|
||||
|
@ -389,6 +394,7 @@ function getActionTypeForm({
|
|||
onDeleteAction?: () => void;
|
||||
onConnectorSelected?: (id: string) => void;
|
||||
setActionFrequencyProperty?: () => void;
|
||||
setActionAlertsFilterProperty?: () => void;
|
||||
hasSummary?: boolean;
|
||||
messageVariables?: ActionVariables;
|
||||
}) {
|
||||
|
@ -469,6 +475,7 @@ function getActionTypeForm({
|
|||
defaultActionGroupId={defaultActionGroupId ?? 'default'}
|
||||
setActionParamsProperty={jest.fn()}
|
||||
setActionFrequencyProperty={setActionFrequencyProperty ?? jest.fn()}
|
||||
setActionAlertsFilterProperty={setActionAlertsFilterProperty ?? jest.fn()}
|
||||
index={index ?? 1}
|
||||
actionTypesIndex={actionTypeIndex ?? actionTypeIndexDefault}
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
|
|
|
@ -30,7 +30,11 @@ import {
|
|||
EuiCallOut,
|
||||
} from '@elastic/eui';
|
||||
import { isEmpty, partition, some } from 'lodash';
|
||||
import { ActionVariable, RuleActionParam } from '@kbn/alerting-plugin/common';
|
||||
import {
|
||||
ActionVariable,
|
||||
RuleActionAlertsFilterProperty,
|
||||
RuleActionParam,
|
||||
} from '@kbn/alerting-plugin/common';
|
||||
import {
|
||||
getDurationNumberInItsUnit,
|
||||
getDurationUnitValue,
|
||||
|
@ -54,6 +58,7 @@ import { useKibana } from '../../../common/lib/kibana';
|
|||
import { ConnectorsSelection } from './connectors_selection';
|
||||
import { ActionNotifyWhen } from './action_notify_when';
|
||||
import { validateParamsForWarnings } from '../../lib/validate_params_for_warnings';
|
||||
import { ActionAlertsFilterTimeframe } from './action_alerts_filter_timeframe';
|
||||
|
||||
export type ActionTypeFormProps = {
|
||||
actionItem: RuleAction;
|
||||
|
@ -64,6 +69,11 @@ export type ActionTypeFormProps = {
|
|||
onDeleteAction: () => void;
|
||||
setActionParamsProperty: (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;
|
||||
connectors: ActionConnector[];
|
||||
actionTypeRegistry: ActionTypeRegistryContract;
|
||||
|
@ -72,6 +82,7 @@ export type ActionTypeFormProps = {
|
|||
hideNotifyWhen?: boolean;
|
||||
hasSummary?: boolean;
|
||||
minimumThrottleInterval?: [number | undefined, string];
|
||||
showActionAlertsFilter?: boolean;
|
||||
} & Pick<
|
||||
ActionAccordionFormProps,
|
||||
| 'defaultActionGroupId'
|
||||
|
@ -99,6 +110,7 @@ export const ActionTypeForm = ({
|
|||
onDeleteAction,
|
||||
setActionParamsProperty,
|
||||
setActionFrequencyProperty,
|
||||
setActionAlertsFilterProperty,
|
||||
actionTypesIndex,
|
||||
connectors,
|
||||
defaultActionGroupId,
|
||||
|
@ -113,6 +125,7 @@ export const ActionTypeForm = ({
|
|||
defaultSummaryMessage,
|
||||
hasSummary,
|
||||
minimumThrottleInterval,
|
||||
showActionAlertsFilter,
|
||||
}: ActionTypeFormProps) => {
|
||||
const {
|
||||
application: { capabilities },
|
||||
|
@ -143,6 +156,7 @@ export const ActionTypeForm = ({
|
|||
const [warning, setWarning] = useState<string | null>(null);
|
||||
|
||||
const [useDefaultMessage, setUseDefaultMessage] = useState(false);
|
||||
|
||||
const isSummaryAction = actionItem.frequency?.summary;
|
||||
|
||||
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 color="plain">
|
||||
{ParamsFieldsComponent ? (
|
||||
|
|
|
@ -48,6 +48,7 @@ import {
|
|||
ALERTS_FEATURE_ID,
|
||||
RecoveredActionGroup,
|
||||
isActionGroupDisabledForActionTypeId,
|
||||
RuleActionAlertsFilterProperty,
|
||||
} from '@kbn/alerting-plugin/common';
|
||||
import { AlertingConnectorFeatureId } from '@kbn/actions-plugin/common';
|
||||
import { RuleReducerAction, InitialRule } from './rule_reducer';
|
||||
|
@ -291,6 +292,13 @@ export const RuleForm = ({
|
|||
[dispatch]
|
||||
);
|
||||
|
||||
const setActionAlertsFilterProperty = useCallback(
|
||||
(key: string, value: RuleActionAlertsFilterProperty, index: number) => {
|
||||
dispatch({ command: { type: 'setRuleActionAlertsFilter' }, payload: { key, value, index } });
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const searchValue = searchText ? searchText.trim().toLocaleLowerCase() : null;
|
||||
setFilteredRuleTypes(
|
||||
|
@ -677,6 +685,7 @@ export const RuleForm = ({
|
|||
setActionParamsProperty={setActionParamsProperty}
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
setActionFrequencyProperty={setActionFrequencyProperty}
|
||||
setActionAlertsFilterProperty={setActionAlertsFilterProperty}
|
||||
defaultSummaryMessage={ruleTypeModel?.defaultSummaryMessage || summaryMessage}
|
||||
minimumThrottleInterval={[ruleInterval, ruleIntervalUnit]}
|
||||
/>
|
||||
|
|
|
@ -8,7 +8,11 @@
|
|||
import { SavedObjectAttribute } from '@kbn/core/public';
|
||||
import { isEqual } from 'lodash';
|
||||
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 { DEFAULT_FREQUENCY } from '../../../common/constants';
|
||||
|
||||
|
@ -24,6 +28,7 @@ interface CommandType<
|
|||
| 'setRuleActionParams'
|
||||
| 'setRuleActionProperty'
|
||||
| 'setRuleActionFrequency'
|
||||
| 'setRuleActionAlertsFilter'
|
||||
> {
|
||||
type: T;
|
||||
}
|
||||
|
@ -84,6 +89,10 @@ export type RuleReducerAction =
|
|||
| {
|
||||
command: CommandType<'setRuleActionFrequency'>;
|
||||
payload: Payload<string, RuleActionParam>;
|
||||
}
|
||||
| {
|
||||
command: CommandType<'setRuleActionAlertsFilter'>;
|
||||
payload: Payload<string, RuleActionAlertsFilterProperty>;
|
||||
};
|
||||
|
||||
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': {
|
||||
const { key, value, index } = action.payload as RuleActionPayload<keyof RuleAction>;
|
||||
if (index === undefined || isEqual(rule.actions[index][key], value)) {
|
||||
|
|
|
@ -6,10 +6,9 @@
|
|||
*/
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { invert, mapValues } from 'lodash';
|
||||
import moment from 'moment';
|
||||
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 = [
|
||||
{ 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> = {
|
||||
1: 'MO',
|
||||
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 CONNECTORS_PLUGIN_ID = 'triggersActionsConnectors';
|
||||
export * from './i18n_weekdays';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue