From b337f96a109a85c9b842fecdb4962fb46d92aaf7 Mon Sep 17 00:00:00 2001 From: Jiawei Wu Date: Wed, 20 Apr 2022 09:51:43 -0700 Subject: [PATCH 01/10] rule state filter --- .../common/experimental_features.ts | 2 +- .../rule_state_filter_sandbox.tsx | 26 +++++ .../shareable_components_sandbox.tsx | 2 + .../public/application/sections/index.tsx | 3 + .../components/rule_state_filter.test.tsx | 73 +++++++++++++ .../components/rule_state_filter.tsx | 101 ++++++++++++++++++ .../public/common/get_rule_state_filter.tsx | 14 +++ .../triggers_actions_ui/public/mocks.ts | 4 + .../triggers_actions_ui/public/plugin.ts | 6 ++ .../triggers_actions_ui/public/types.ts | 3 +- 10 files changed, 232 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_state_filter_sandbox.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_state_filter.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_state_filter.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/common/get_rule_state_filter.tsx diff --git a/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts b/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts index 21835a5977216..aef0137efcf7d 100644 --- a/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts +++ b/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts @@ -14,7 +14,7 @@ export type ExperimentalFeatures = typeof allowedExperimentalValues; export const allowedExperimentalValues = Object.freeze({ rulesListDatagrid: true, internalAlertsTable: false, - internalShareableComponentsSandbox: false, + internalShareableComponentsSandbox: true, rulesDetailLogs: true, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_state_filter_sandbox.tsx b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_state_filter_sandbox.tsx new file mode 100644 index 0000000000000..4dad738edbe5f --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_state_filter_sandbox.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { RuleStateFilterProps } from '../../../../public/types'; +import { getRuleStateFilterLazy } from '../../../common/get_rule_state_filter'; + +export const RuleStateFilterSandbox = () => { + const [selectedStates, setSelectedStates] = useState([]); + + return ( +
+ {getRuleStateFilterLazy({ + selectedStates, + onChange: setSelectedStates, + })} +
+ Selected states: {JSON.stringify(selectedStates)} +
+
+ ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/shareable_components_sandbox.tsx b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/shareable_components_sandbox.tsx index 97366832bda0e..49bdcde156fe4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/shareable_components_sandbox.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/shareable_components_sandbox.tsx @@ -7,11 +7,13 @@ import React from 'react'; import { RuleStatusDropdownSandbox } from './rule_status_dropdown_sandbox'; +import { RuleStateFilterSandbox } from './rule_state_filter_sandbox'; export const InternalShareableComponentsSandbox: React.FC<{}> = () => { return ( <> + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx index 0aaa3195b7c52..59721e2a3d0d5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx @@ -32,3 +32,6 @@ export const ActionForm = suspendedComponentWithProps( export const RuleStatusDropdown = suspendedComponentWithProps( lazy(() => import('./rules_list/components/rule_status_dropdown')) ); +export const RuleStateFilter = suspendedComponentWithProps( + lazy(() => import('./rules_list/components/rule_state_filter')) +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_state_filter.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_state_filter.test.tsx new file mode 100644 index 0000000000000..e7fb340462a76 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_state_filter.test.tsx @@ -0,0 +1,73 @@ +/* + * 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 } from '@kbn/test-jest-helpers'; +import { EuiFilterButton, EuiFilterSelectItem } from '@elastic/eui'; +import { RuleStateFilter } from './rule_state_filter'; + +const onChangeMock = jest.fn(); + +describe('rule_state_filter', () => { + beforeEach(() => { + onChangeMock.mockReset(); + }); + + it('renders correctly', () => { + const wrapper = mountWithIntl( + + ); + + expect(wrapper.find(EuiFilterSelectItem).exists()).toBeFalsy(); + expect(wrapper.find(EuiFilterButton).exists()).toBeTruthy(); + + expect(wrapper.find('.euiNotificationBadge').text()).toEqual('0'); + }); + + it('can open the popover correctly', () => { + const wrapper = mountWithIntl( + + ); + + expect(wrapper.find('[data-test-subj="ruleStateFilterSelect"]').exists()).toBeFalsy(); + + wrapper.find(EuiFilterButton).simulate('click'); + + const statusItems = wrapper.find(EuiFilterSelectItem); + expect(statusItems.length).toEqual(3); + }); + + it('can select states', () => { + const wrapper = mountWithIntl( + + ); + + wrapper.find(EuiFilterButton).simulate('click'); + + wrapper.find(EuiFilterSelectItem).at(0).simulate('click'); + expect(onChangeMock).toHaveBeenCalledWith(['enabled']); + + wrapper.setProps({ + selectedStates: ['enabled'], + }); + + wrapper.find(EuiFilterSelectItem).at(0).simulate('click'); + expect(onChangeMock).toHaveBeenCalledWith([]); + + wrapper.find(EuiFilterSelectItem).at(1).simulate('click'); + expect(onChangeMock).toHaveBeenCalledWith(['enabled', 'disabled']); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_state_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_state_filter.tsx new file mode 100644 index 0000000000000..ec6673640e26f --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_state_filter.tsx @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useState, useCallback } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiFilterButton, EuiPopover, EuiFilterGroup, EuiFilterSelectItem } from '@elastic/eui'; + +type State = 'enabled' | 'muted' | 'disabled'; + +const states: State[] = [ + 'enabled', + 'disabled', + 'muted', +]; + +const optionStyles = { + textTransform: 'capitalize' as const, +}; + +const getOptionDataTestSubj = (state: State) => `ruleStateFilterOption-${state}`; + +export interface RuleStateFilterProps { + selectedStates: State[], + dataTestSubj?: string, + buttonDataTestSubj?: string, + optionDataTestSubj?: (state: State) => string; + onChange: (selectedStates: State[]) => void; +} + +export const RuleStateFilter = (props: RuleStateFilterProps) => { + const { + selectedStates = [], + dataTestSubj = 'ruleStateFilterSelect', + buttonDataTestSubj = 'ruleStateFilterButton', + optionDataTestSubj = getOptionDataTestSubj, + onChange = () => {}, + } = props; + + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const onFilterItemClick = useCallback( + (newOption: State) => () => { + if (selectedStates.includes(newOption)) { + onChange(selectedStates.filter((option) => option !== newOption)); + return; + } + onChange([...selectedStates, newOption]); + }, + [selectedStates, onChange] + ); + + const onClick = useCallback(() => { + setIsPopoverOpen((prevIsOpen) => !prevIsOpen); + }, [setIsPopoverOpen]); + + return ( + + setIsPopoverOpen(false)} + button={ + 0} + numActiveFilters={selectedStates.length} + numFilters={selectedStates.length} + onClick={onClick} + > + + + } + > +
+ {states.map((state) => { + return ( + + {state} + + ); + })} +
+
+
+ ); +}; + +// eslint-disable-next-line import/no-default-export +export { RuleStateFilter as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/get_rule_state_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/common/get_rule_state_filter.tsx new file mode 100644 index 0000000000000..f1b539fa5bc00 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/get_rule_state_filter.tsx @@ -0,0 +1,14 @@ +/* + * 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 { RuleStateFilter } from '../application/sections'; +import type { RuleStateFilterProps } from '../application/sections/rules_list/components/rule_state_filter'; + +export const getRuleStateFilterLazy = (props: RuleStateFilterProps) => { + return ; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/mocks.ts b/x-pack/plugins/triggers_actions_ui/public/mocks.ts index 79edc1f08ac97..50038b9946225 100644 --- a/x-pack/plugins/triggers_actions_ui/public/mocks.ts +++ b/x-pack/plugins/triggers_actions_ui/public/mocks.ts @@ -25,6 +25,7 @@ import { } from './types'; import { getAlertsTableLazy } from './common/get_alerts_table'; import { getRuleStatusDropdownLazy } from './common/get_rule_status_dropdown'; +import { getRuleStateFilterLazy } from './common/get_rule_state_filter'; function createStartMock(): TriggersAndActionsUIPublicPluginStart { const actionTypeRegistry = new TypeRegistry(); @@ -63,6 +64,9 @@ function createStartMock(): TriggersAndActionsUIPublicPluginStart { getRuleStatusDropdown: (props) => { return getRuleStatusDropdownLazy(props); }, + getRuleStateFilter: (props) => { + return getRuleStateFilterLazy(props); + }, }; } diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts index ba2e869c82e0f..3ab38bdb52858 100644 --- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts @@ -30,6 +30,7 @@ import { getAddAlertFlyoutLazy } from './common/get_add_alert_flyout'; import { getEditAlertFlyoutLazy } from './common/get_edit_alert_flyout'; import { getAlertsTableLazy } from './common/get_alerts_table'; import { getRuleStatusDropdownLazy } from './common/get_rule_status_dropdown'; +import { getRuleStateFilterLazy } from './common/get_rule_state_filter'; import { ExperimentalFeaturesService } from './common/experimental_features_service'; import { ExperimentalFeatures, @@ -45,6 +46,7 @@ import type { ConnectorEditFlyoutProps, AlertsTableProps, RuleStatusDropdownProps, + RuleStateFilterProps, AlertsTableConfigurationRegistry, } from './types'; import { TriggersActionsUiConfigType } from '../common/types'; @@ -75,6 +77,7 @@ export interface TriggersAndActionsUIPublicPluginStart { ) => ReactElement; getAlertsTable: (props: AlertsTableProps) => ReactElement; getRuleStatusDropdown: (props: RuleStatusDropdownProps) => ReactElement; + getRuleStateFilter: (props: RuleStateFilterProps) => ReactElement; } interface PluginsSetup { @@ -244,6 +247,9 @@ export class Plugin getRuleStatusDropdown: (props: RuleStatusDropdownProps) => { return getRuleStatusDropdownLazy(props); }, + getRuleStateFilter: (props: RuleStateFilterProps) => { + return getRuleStateFilterLazy(props); + }, }; } diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 2d4268252a353..9b4ec9aa8c3c3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -48,7 +48,7 @@ import { import { RuleRegistrySearchRequestPagination } from '@kbn/rule-registry-plugin/common'; import { TypeRegistry } from './application/type_registry'; import type { ComponentOpts as RuleStatusDropdownProps } from './application/sections/rules_list/components/rule_status_dropdown'; - +import type { RuleStateFilterProps } from './application/sections/rules_list/components/rule_state_filter'; // In Triggers and Actions we treat all `Alert`s as `SanitizedRule` // so the `Params` is a black-box of Record type SanitizedRule = Omit< @@ -80,6 +80,7 @@ export type { ResolvedRule, SanitizedRule, RuleStatusDropdownProps, + RuleStateFilterProps, }; export type { ActionType, AsApiContract }; export { From 0576f5092d283f5461e55c8a423168b3d31cd61a Mon Sep 17 00:00:00 2001 From: Jiawei Wu Date: Wed, 20 Apr 2022 09:52:38 -0700 Subject: [PATCH 02/10] turn off experiment --- .../plugins/triggers_actions_ui/common/experimental_features.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts b/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts index aef0137efcf7d..21835a5977216 100644 --- a/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts +++ b/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts @@ -14,7 +14,7 @@ export type ExperimentalFeatures = typeof allowedExperimentalValues; export const allowedExperimentalValues = Object.freeze({ rulesListDatagrid: true, internalAlertsTable: false, - internalShareableComponentsSandbox: true, + internalShareableComponentsSandbox: false, rulesDetailLogs: true, }); From 31aa11b18959d835c14ef5a5dccba7e3f7b6687e Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 20 Apr 2022 17:46:07 +0000 Subject: [PATCH 03/10] [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' --- .../rule_state_filter_sandbox.tsx | 6 ++---- .../components/rule_state_filter.test.tsx | 21 +++---------------- .../components/rule_state_filter.tsx | 12 ++++------- 3 files changed, 9 insertions(+), 30 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_state_filter_sandbox.tsx b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_state_filter_sandbox.tsx index 4dad738edbe5f..17fb6af19cd1b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_state_filter_sandbox.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_state_filter_sandbox.tsx @@ -6,7 +6,7 @@ */ import React, { useState } from 'react'; -import { RuleStateFilterProps } from '../../../../public/types'; +import { RuleStateFilterProps } from '../../../types'; import { getRuleStateFilterLazy } from '../../../common/get_rule_state_filter'; export const RuleStateFilterSandbox = () => { @@ -18,9 +18,7 @@ export const RuleStateFilterSandbox = () => { selectedStates, onChange: setSelectedStates, })} -
- Selected states: {JSON.stringify(selectedStates)} -
+
Selected states: {JSON.stringify(selectedStates)}
); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_state_filter.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_state_filter.test.tsx index e7fb340462a76..506c328a3fcdb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_state_filter.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_state_filter.test.tsx @@ -18,12 +18,7 @@ describe('rule_state_filter', () => { }); it('renders correctly', () => { - const wrapper = mountWithIntl( - - ); + const wrapper = mountWithIntl(); expect(wrapper.find(EuiFilterSelectItem).exists()).toBeFalsy(); expect(wrapper.find(EuiFilterButton).exists()).toBeTruthy(); @@ -32,12 +27,7 @@ describe('rule_state_filter', () => { }); it('can open the popover correctly', () => { - const wrapper = mountWithIntl( - - ); + const wrapper = mountWithIntl(); expect(wrapper.find('[data-test-subj="ruleStateFilterSelect"]').exists()).toBeFalsy(); @@ -48,12 +38,7 @@ describe('rule_state_filter', () => { }); it('can select states', () => { - const wrapper = mountWithIntl( - - ); + const wrapper = mountWithIntl(); wrapper.find(EuiFilterButton).simulate('click'); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_state_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_state_filter.tsx index ec6673640e26f..0fa7f6759c899 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_state_filter.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_state_filter.tsx @@ -10,11 +10,7 @@ import { EuiFilterButton, EuiPopover, EuiFilterGroup, EuiFilterSelectItem } from type State = 'enabled' | 'muted' | 'disabled'; -const states: State[] = [ - 'enabled', - 'disabled', - 'muted', -]; +const states: State[] = ['enabled', 'disabled', 'muted']; const optionStyles = { textTransform: 'capitalize' as const, @@ -23,9 +19,9 @@ const optionStyles = { const getOptionDataTestSubj = (state: State) => `ruleStateFilterOption-${state}`; export interface RuleStateFilterProps { - selectedStates: State[], - dataTestSubj?: string, - buttonDataTestSubj?: string, + selectedStates: State[]; + dataTestSubj?: string; + buttonDataTestSubj?: string; optionDataTestSubj?: (state: State) => string; onChange: (selectedStates: State[]) => void; } From b42c959f3186409ca7ddcf14574d5b65992efe70 Mon Sep 17 00:00:00 2001 From: Jiawei Wu Date: Mon, 25 Apr 2022 23:27:35 -0700 Subject: [PATCH 04/10] Status filter API call --- .../common/experimental_features.ts | 1 + .../lib/rule_api/map_filters_to_kql.test.ts | 26 +++++ .../lib/rule_api/map_filters_to_kql.ts | 35 +++++++ .../application/lib/rule_api/rules.test.ts | 97 +++++++++++++++++++ .../public/application/lib/rule_api/rules.ts | 11 ++- .../components/rule_state_filter.tsx | 23 ++--- .../rules_list/components/rules_list.tsx | 20 +++- .../triggers_actions_ui/public/types.ts | 2 + .../apps/triggers_actions_ui/index.ts | 1 + .../triggers_actions_ui/rule_state_filter.ts | 54 +++++++++++ 10 files changed, 256 insertions(+), 14 deletions(-) create mode 100644 x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rule_state_filter.ts diff --git a/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts b/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts index 21835a5977216..e9db2890b0b8b 100644 --- a/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts +++ b/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts @@ -15,6 +15,7 @@ export const allowedExperimentalValues = Object.freeze({ rulesListDatagrid: true, internalAlertsTable: false, internalShareableComponentsSandbox: false, + ruleStateFilter: false, rulesDetailLogs: true, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.test.ts index e1dd14a7a9fde..9433fd92f0492 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.test.ts @@ -40,6 +40,32 @@ describe('mapFiltersToKql', () => { ).toEqual(['alert.attributes.executionStatus.status:(alert or statuses or filter)']); }); + test('should handle ruleStateFilter', () => { + expect( + mapFiltersToKql({ + ruleStateFilter: ['enabled', 'snoozed'], + }) + ).toEqual([ + 'alert.attributes.enabled:(true) or (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)', + ]); + + expect( + mapFiltersToKql({ + ruleStateFilter: ['enabled'], + }) + ).toEqual([ + 'alert.attributes.enabled:(true) and not (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)', + ]); + + expect( + mapFiltersToKql({ + ruleStateFilter: ['enabled', 'disabled', 'snoozed'], + }) + ).toEqual([ + 'alert.attributes.enabled:(true or false) or (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)', + ]); + }); + test('should handle typesFilter and actionTypesFilter', () => { expect( mapFiltersToKql({ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.ts index d7b22a7a4aee4..38f35b74697d2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.ts @@ -5,16 +5,34 @@ * 2.0. */ +import { RuleStatus } from '../../../types'; + +const getEnablementFilter = (ruleStateFilter: RuleStatus[] = []) => { + const enablementFilters = ruleStateFilter.reduce((result, filter) => { + if (filter === 'enabled') { + return [...result, 'true']; + } + if (filter === 'disabled') { + return [...result, 'false']; + } + return result; + }, []); + return `alert.attributes.enabled:(${enablementFilters.join(' or ')})`; +}; + export const mapFiltersToKql = ({ typesFilter, actionTypesFilter, ruleStatusesFilter, + ruleStateFilter, }: { typesFilter?: string[]; actionTypesFilter?: string[]; ruleStatusesFilter?: string[]; + ruleStateFilter?: RuleStatus[]; }): string[] => { const filters = []; + if (typesFilter && typesFilter.length) { filters.push(`alert.attributes.alertTypeId:(${typesFilter.join(' or ')})`); } @@ -32,5 +50,22 @@ export const mapFiltersToKql = ({ if (ruleStatusesFilter && ruleStatusesFilter.length) { filters.push(`alert.attributes.executionStatus.status:(${ruleStatusesFilter.join(' or ')})`); } + + if (ruleStateFilter && ruleStateFilter.length) { + const enablementFilter = getEnablementFilter(ruleStateFilter); + const snoozedFilter = `(alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)`; + const hasEnablement = + ruleStateFilter.includes('enabled') || ruleStateFilter.includes('disabled'); + const hasSnoozed = ruleStateFilter.includes('snoozed'); + + if (hasEnablement && !hasSnoozed) { + filters.push(`${enablementFilter} and not ${snoozedFilter}`); + } else if (!hasEnablement && hasSnoozed) { + filters.push(snoozedFilter); + } else { + filters.push(`${enablementFilter} or ${snoozedFilter}`); + } + } + return filters; }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.test.ts index 5f6c6e938a0a7..6723047768ca5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.test.ts @@ -239,4 +239,101 @@ describe('loadRules', () => { ] `); }); + + test('should call find API with ruleStateFilter', async () => { + const resolvedValue = { + page: 1, + per_page: 10, + total: 0, + data: [], + }; + http.get.mockResolvedValue(resolvedValue); + + let result = await loadRules({ + http, + ruleStateFilter: ['enabled', 'snoozed'], + page: { index: 0, size: 10 }, + }); + expect(result).toEqual({ + page: 1, + perPage: 10, + total: 0, + data: [], + }); + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/internal/alerting/rules/_find", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": "alert.attributes.enabled:(true) or (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)", + "page": 1, + "per_page": 10, + "search": undefined, + "search_fields": undefined, + "sort_field": "name", + "sort_order": "asc", + }, + }, + ] + `); + + result = await loadRules({ + http, + ruleStateFilter: ['disabled'], + page: { index: 0, size: 10 }, + }); + expect(result).toEqual({ + page: 1, + perPage: 10, + total: 0, + data: [], + }); + expect(http.get.mock.calls[1]).toMatchInlineSnapshot(` + Array [ + "/internal/alerting/rules/_find", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": "alert.attributes.enabled:(false) and not (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)", + "page": 1, + "per_page": 10, + "search": undefined, + "search_fields": undefined, + "sort_field": "name", + "sort_order": "asc", + }, + }, + ] + `); + + result = await loadRules({ + http, + ruleStateFilter: ['enabled', 'disabled', 'snoozed'], + page: { index: 0, size: 10 }, + }); + expect(result).toEqual({ + page: 1, + perPage: 10, + total: 0, + data: [], + }); + expect(http.get.mock.calls[2]).toMatchInlineSnapshot(` + Array [ + "/internal/alerting/rules/_find", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": "alert.attributes.enabled:(true or false) or (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)", + "page": 1, + "per_page": 10, + "search": undefined, + "search_fields": undefined, + "sort_field": "name", + "sort_order": "asc", + }, + }, + ] + `); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.ts index 52ba09a5c0adf..c386ee2c8aa92 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.ts @@ -7,7 +7,7 @@ import { HttpSetup } from '@kbn/core/public'; import { AsApiContract } from '@kbn/actions-plugin/common'; import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; -import { Rule, Pagination, Sorting } from '../../../types'; +import { Rule, Pagination, Sorting, RuleStatus } from '../../../types'; import { mapFiltersToKql } from './map_filters_to_kql'; import { transformRule } from './common_transformations'; @@ -22,6 +22,7 @@ export async function loadRules({ typesFilter, actionTypesFilter, ruleStatusesFilter, + ruleStateFilter, sort = { field: 'name', direction: 'asc' }, }: { http: HttpSetup; @@ -30,6 +31,7 @@ export async function loadRules({ typesFilter?: string[]; actionTypesFilter?: string[]; ruleStatusesFilter?: string[]; + ruleStateFilter?: RuleStatus[]; sort?: Sorting; }): Promise<{ page: number; @@ -37,7 +39,12 @@ export async function loadRules({ total: number; data: Rule[]; }> { - const filters = mapFiltersToKql({ typesFilter, actionTypesFilter, ruleStatusesFilter }); + const filters = mapFiltersToKql({ + typesFilter, + actionTypesFilter, + ruleStatusesFilter, + ruleStateFilter, + }); const res = await http.get< AsApiContract<{ page: number; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_state_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_state_filter.tsx index 0fa7f6759c899..8a4c6a3a0dd87 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_state_filter.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_state_filter.tsx @@ -7,29 +7,30 @@ import React, { useState, useCallback } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiFilterButton, EuiPopover, EuiFilterGroup, EuiFilterSelectItem } from '@elastic/eui'; +import { RuleStatus } from '../../../../types'; -type State = 'enabled' | 'muted' | 'disabled'; - -const states: State[] = ['enabled', 'disabled', 'muted']; +const states: RuleStatus[] = ['enabled', 'disabled', 'snoozed']; const optionStyles = { textTransform: 'capitalize' as const, }; -const getOptionDataTestSubj = (state: State) => `ruleStateFilterOption-${state}`; +const getOptionDataTestSubj = (state: RuleStatus) => `ruleStateFilterOption-${state}`; export interface RuleStateFilterProps { - selectedStates: State[]; + selectedStates: RuleStatus[]; dataTestSubj?: string; + selectDataTestSubj?: string; buttonDataTestSubj?: string; - optionDataTestSubj?: (state: State) => string; - onChange: (selectedStates: State[]) => void; + optionDataTestSubj?: (state: RuleStatus) => string; + onChange: (selectedStates: RuleStatus[]) => void; } export const RuleStateFilter = (props: RuleStateFilterProps) => { const { selectedStates = [], - dataTestSubj = 'ruleStateFilterSelect', + dataTestSubj = 'ruleStateFilter', + selectDataTestSubj = 'ruleStateFilterSelect', buttonDataTestSubj = 'ruleStateFilterButton', optionDataTestSubj = getOptionDataTestSubj, onChange = () => {}, @@ -38,7 +39,7 @@ export const RuleStateFilter = (props: RuleStateFilterProps) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const onFilterItemClick = useCallback( - (newOption: State) => () => { + (newOption: RuleStatus) => () => { if (selectedStates.includes(newOption)) { onChange(selectedStates.filter((option) => option !== newOption)); return; @@ -53,7 +54,7 @@ export const RuleStateFilter = (props: RuleStateFilterProps) => { }, [setIsPopoverOpen]); return ( - + setIsPopoverOpen(false)} @@ -73,7 +74,7 @@ export const RuleStateFilter = (props: RuleStateFilterProps) => { } > -
+
{states.map((state) => { return ( { const [typesFilter, setTypesFilter] = useState([]); const [actionTypesFilter, setActionTypesFilter] = useState([]); const [ruleStatusesFilter, setRuleStatusesFilter] = useState([]); + const [ruleStateFilter, setRuleStateFilter] = useState([]); const [ruleFlyoutVisible, setRuleFlyoutVisibility] = useState(false); const [editFlyoutVisible, setEditFlyoutVisibility] = useState(false); const [currentRuleToEdit, setCurrentRuleToEdit] = useState(null); @@ -165,6 +169,8 @@ export const RulesList: React.FunctionComponent = () => { ); const [showErrors, setShowErrors] = useState(false); + const isRuleStateFilterEnabled = getIsExperimentalFeatureEnabled('ruleStateFilter'); + useEffect(() => { (async () => { setConfig(await triggersActionsUiConfig({ http })); @@ -228,6 +234,7 @@ export const RulesList: React.FunctionComponent = () => { JSON.stringify(typesFilter), JSON.stringify(actionTypesFilter), JSON.stringify(ruleStatusesFilter), + JSON.stringify(ruleStateFilter), ]); useEffect(() => { @@ -287,6 +294,7 @@ export const RulesList: React.FunctionComponent = () => { typesFilter, actionTypesFilter, ruleStatusesFilter, + ruleStateFilter, sort, }); await loadRuleAggs(); @@ -304,7 +312,9 @@ export const RulesList: React.FunctionComponent = () => { isEmpty(searchText) && isEmpty(typesFilter) && isEmpty(actionTypesFilter) && - isEmpty(ruleStatusesFilter) + isEmpty(ruleStatusesFilter) && + isRuleStateFilterEnabled && + isEmpty(ruleStateFilter) ); setNoData(rulesResponse.data.length === 0 && !isFilterApplied); @@ -960,6 +970,13 @@ export const RulesList: React.FunctionComponent = () => { ); }; + const getRuleStateFilter = () => { + if (isRuleStateFilterEnabled) { + return []; + } + return []; + }; + const toolsRight = [ { }) )} />, + ...getRuleStateFilter(), { loadTestFile(require.resolve('./connectors')); loadTestFile(require.resolve('./alerts_table')); loadTestFile(require.resolve('./rule_status_dropdown')); + loadTestFile(require.resolve('./rule_state_filter')); }); }; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rule_state_filter.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rule_state_filter.ts new file mode 100644 index 0000000000000..216a56765bcc9 --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rule_state_filter.ts @@ -0,0 +1,54 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default ({ getPageObjects, getService }: FtrProviderContext) => { + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['common', 'triggersActionsUI', 'header']); + const esArchiver = getService('esArchiver'); + + describe('Rule state filter', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/observability/alerts'); + await PageObjects.common.navigateToUrlWithBrowserHistory( + 'triggersActions', + '/__components_sandbox' + ); + }); + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/observability/alerts'); + }); + + it('should load from the shareable lazy loader', async () => { + await testSubjects.find('ruleStateFilter'); + const exists = await testSubjects.exists('ruleStateFilter'); + expect(exists).to.be(true); + }); + + it('should allow rule states to be filtered', async () => { + const ruleStateFilter = await testSubjects.find('ruleStateFilter'); + let badge = await ruleStateFilter.findByCssSelector('.euiFilterButton__notification'); + expect(await badge.getVisibleText()).to.be('0'); + + await testSubjects.click('ruleStateFilter'); + await testSubjects.click('ruleStateFilterOption-enabled'); + + badge = await ruleStateFilter.findByCssSelector('.euiFilterButton__notification'); + expect(await badge.getVisibleText()).to.be('1'); + + await testSubjects.click('ruleStateFilterOption-disabled'); + + badge = await ruleStateFilter.findByCssSelector('.euiFilterButton__notification'); + expect(await badge.getVisibleText()).to.be('2'); + + await testSubjects.click('ruleStateFilterOption-enabled'); + expect(await badge.getVisibleText()).to.be('1'); + }); + }); +}; From 10ece1c2d1978c13b7fc227b838e8c2198760d43 Mon Sep 17 00:00:00 2001 From: Jiawei Wu Date: Tue, 26 Apr 2022 13:20:26 -0700 Subject: [PATCH 05/10] Fix tests --- .../rules_list/components/rules_list.test.tsx | 41 +++++++++++++++++++ .../rules_list/components/rules_list.tsx | 1 - 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx index f87cee4c6547f..4f207e28a1881 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx @@ -20,6 +20,7 @@ import { parseDuration, } from '@kbn/alerting-plugin/common'; import { getFormattedDuration, getFormattedMilliseconds } from '../../../lib/monitoring_utils'; +import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features'; import { useKibana } from '../../../../common/lib/kibana'; jest.mock('../../../../common/lib/kibana'); @@ -59,6 +60,9 @@ jest.mock('../../../lib/capabilities', () => ({ hasShowActionsCapability: jest.fn(() => true), hasExecuteActionsCapability: jest.fn(() => true), })); +jest.mock('../../../../common/get_experimental_features', () => ({ + getIsExperimentalFeatureEnabled: jest.fn(), +})); const { loadRules, loadRuleTypes, loadRuleAggregations } = jest.requireMock('../../../lib/rule_api'); const { loadActionTypes, loadAllActions } = jest.requireMock('../../../lib/action_connector_api'); @@ -95,6 +99,10 @@ ruleTypeRegistry.list.mockReturnValue([ruleType]); actionTypeRegistry.list.mockReturnValue([]); const useKibanaMock = useKibana as jest.Mocked; +beforeEach(() => { + (getIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(() => false); +}); + describe('rules_list component empty', () => { let wrapper: ReactWrapper; async function setup() { @@ -801,6 +809,39 @@ describe('rules_list component with items', () => { 'Warning: 6' ); }); + + it('does not render the state filter if the feature flag is off', async () => { + await setup(); + expect(wrapper.find('[data-test-subj="ruleStateFilter"]').exists()).toBeFalsy(); + }); + + it('renders the tag filter if the experiment is on', async () => { + (getIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(() => true); + await setup(); + expect(wrapper.find('[data-test-subj="ruleStateFilter"]').exists()).toBeTruthy(); + }); + + it('can filter by rule states', async () => { + (getIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(() => true); + loadRules.mockReset(); + await setup(); + + expect(loadRules.mock.calls[0][0].ruleStateFilter).toEqual([]); + + wrapper.find('[data-test-subj="ruleStateFilterButton"] button').simulate('click'); + + wrapper.find('[data-test-subj="ruleStateFilterOption-enabled"]').first().simulate('click'); + + expect(loadRules.mock.calls[1][0].ruleStateFilter).toEqual(['enabled']); + + wrapper.find('[data-test-subj="ruleStateFilterOption-snoozed"]').first().simulate('click'); + + expect(loadRules.mock.calls[2][0].ruleStateFilter).toEqual(['enabled', 'snoozed']); + + wrapper.find('[data-test-subj="ruleStateFilterOption-snoozed"]').first().simulate('click'); + + expect(loadRules.mock.calls[3][0].ruleStateFilter).toEqual(['enabled']); + }); }); describe('rules_list component empty with show only capability', () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx index 3d2087ceaebc1..261384164ee5f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx @@ -313,7 +313,6 @@ export const RulesList: React.FunctionComponent = () => { isEmpty(typesFilter) && isEmpty(actionTypesFilter) && isEmpty(ruleStatusesFilter) && - isRuleStateFilterEnabled && isEmpty(ruleStateFilter) ); From c18a5829565a46fa2bde748409d26dc6f24f8f57 Mon Sep 17 00:00:00 2001 From: Jiawei Wu Date: Thu, 28 Apr 2022 16:01:22 -0700 Subject: [PATCH 06/10] rename state to status, added tests --- .../public/hooks/use_fetch_rules.ts | 2 +- .../common/experimental_features.ts | 2 +- .../rule_state_filter_sandbox.tsx | 24 ---- .../rule_status_filter_sandbox.tsx | 26 ++++ .../shareable_components_sandbox.tsx | 4 +- .../application/lib/rule_api/aggregate.ts | 6 +- .../lib/rule_api/map_filters_to_kql.test.ts | 16 +-- .../lib/rule_api/map_filters_to_kql.ts | 24 ++-- .../application/lib/rule_api/rules.test.ts | 8 +- .../public/application/lib/rule_api/rules.ts | 8 +- .../public/application/sections/index.tsx | 4 +- .../rule_execution_status_filter.tsx | 107 +++++++++++++++ .../components/rule_state_filter.tsx | 98 -------------- ...r.test.tsx => rule_status_filter.test.tsx} | 18 ++- .../components/rule_status_filter.tsx | 127 ++++++++---------- .../rules_list/components/rules_list.test.tsx | 24 ++-- .../rules_list/components/rules_list.tsx | 38 +++--- .../common/get_experimental_features.test.tsx | 5 + .../public/common/get_rule_state_filter.tsx | 14 -- .../public/common/get_rule_status_filter.tsx | 14 ++ .../triggers_actions_ui/public/mocks.ts | 6 +- .../triggers_actions_ui/public/plugin.ts | 10 +- .../triggers_actions_ui/public/types.ts | 4 +- .../apps/triggers_actions_ui/alerts_list.ts | 63 ++++++++- .../apps/triggers_actions_ui/index.ts | 2 +- ..._state_filter.ts => rule_status_filter.ts} | 24 ++-- x-pack/test/functional_with_es_ssl/config.ts | 1 + .../lib/alert_api_actions.ts | 13 ++ 28 files changed, 391 insertions(+), 301 deletions(-) delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_state_filter_sandbox.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_status_filter_sandbox.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_execution_status_filter.tsx delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_state_filter.tsx rename x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/{rule_state_filter.test.tsx => rule_status_filter.test.tsx} (76%) delete mode 100644 x-pack/plugins/triggers_actions_ui/public/common/get_rule_state_filter.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/common/get_rule_status_filter.tsx rename x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/{rule_state_filter.ts => rule_status_filter.ts} (61%) diff --git a/x-pack/plugins/observability/public/hooks/use_fetch_rules.ts b/x-pack/plugins/observability/public/hooks/use_fetch_rules.ts index 00cb58e504bdc..a09626654e6f8 100644 --- a/x-pack/plugins/observability/public/hooks/use_fetch_rules.ts +++ b/x-pack/plugins/observability/public/hooks/use_fetch_rules.ts @@ -42,7 +42,7 @@ export function useFetchRules({ page, searchText, typesFilter: typesFilter.length > 0 ? typesFilter : OBSERVABILITY_RULE_TYPES, - ruleStatusesFilter: ruleLastResponseFilter, + ruleExecutionStatusesFilter: ruleLastResponseFilter, sort, }); setRulesState((oldState) => ({ diff --git a/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts b/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts index e9db2890b0b8b..0a0b8cdeab208 100644 --- a/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts +++ b/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts @@ -15,7 +15,7 @@ export const allowedExperimentalValues = Object.freeze({ rulesListDatagrid: true, internalAlertsTable: false, internalShareableComponentsSandbox: false, - ruleStateFilter: false, + ruleStatusFilter: false, rulesDetailLogs: true, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_state_filter_sandbox.tsx b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_state_filter_sandbox.tsx deleted file mode 100644 index 17fb6af19cd1b..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_state_filter_sandbox.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useState } from 'react'; -import { RuleStateFilterProps } from '../../../types'; -import { getRuleStateFilterLazy } from '../../../common/get_rule_state_filter'; - -export const RuleStateFilterSandbox = () => { - const [selectedStates, setSelectedStates] = useState([]); - - return ( -
- {getRuleStateFilterLazy({ - selectedStates, - onChange: setSelectedStates, - })} -
Selected states: {JSON.stringify(selectedStates)}
-
- ); -}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_status_filter_sandbox.tsx b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_status_filter_sandbox.tsx new file mode 100644 index 0000000000000..99ddd8daf16ac --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_status_filter_sandbox.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { RuleStatusFilterProps } from '../../../types'; +import { getRuleStatusFilterLazy } from '../../../common/get_rule_status_filter'; + +export const RuleStatusFilterSandbox = () => { + const [selectedStatuses, setSelectedStatuses] = useState< + RuleStatusFilterProps['selectedStatuses'] + >([]); + + return ( +
+ {getRuleStatusFilterLazy({ + selectedStatuses, + onChange: setSelectedStatuses, + })} +
Selected states: {JSON.stringify(selectedStatuses)}
+
+ ); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/shareable_components_sandbox.tsx b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/shareable_components_sandbox.tsx index 49bdcde156fe4..8cce1bd85afb4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/shareable_components_sandbox.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/shareable_components_sandbox.tsx @@ -7,13 +7,13 @@ import React from 'react'; import { RuleStatusDropdownSandbox } from './rule_status_dropdown_sandbox'; -import { RuleStateFilterSandbox } from './rule_state_filter_sandbox'; +import { RuleStatusFilterSandbox } from './rule_status_filter_sandbox'; export const InternalShareableComponentsSandbox: React.FC<{}> = () => { return ( <> - + ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts index c7bcd438ef697..68abcb62448f3 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts @@ -29,15 +29,15 @@ export async function loadRuleAggregations({ searchText, typesFilter, actionTypesFilter, - ruleStatusesFilter, + ruleExecutionStatusesFilter, }: { http: HttpSetup; searchText?: string; typesFilter?: string[]; actionTypesFilter?: string[]; - ruleStatusesFilter?: string[]; + ruleExecutionStatusesFilter?: string[]; }): Promise { - const filters = mapFiltersToKql({ typesFilter, actionTypesFilter, ruleStatusesFilter }); + const filters = mapFiltersToKql({ typesFilter, actionTypesFilter, ruleExecutionStatusesFilter }); const res = await http.get>( `${INTERNAL_BASE_ALERTING_API_PATH}/rules/_aggregate`, { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.test.ts index 9433fd92f0492..583990f1f0f08 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.test.ts @@ -32,18 +32,18 @@ describe('mapFiltersToKql', () => { ]); }); - test('should handle ruleStatusesFilter', () => { + test('should handle ruleExecutionStatusesFilter', () => { expect( mapFiltersToKql({ - ruleStatusesFilter: ['alert', 'statuses', 'filter'], + ruleExecutionStatusesFilter: ['alert', 'statuses', 'filter'], }) ).toEqual(['alert.attributes.executionStatus.status:(alert or statuses or filter)']); }); - test('should handle ruleStateFilter', () => { + test('should handle ruleStatusesFilter', () => { expect( mapFiltersToKql({ - ruleStateFilter: ['enabled', 'snoozed'], + ruleStatusesFilter: ['enabled', 'snoozed'], }) ).toEqual([ 'alert.attributes.enabled:(true) or (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)', @@ -51,7 +51,7 @@ describe('mapFiltersToKql', () => { expect( mapFiltersToKql({ - ruleStateFilter: ['enabled'], + ruleStatusesFilter: ['enabled'], }) ).toEqual([ 'alert.attributes.enabled:(true) and not (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)', @@ -59,7 +59,7 @@ describe('mapFiltersToKql', () => { expect( mapFiltersToKql({ - ruleStateFilter: ['enabled', 'disabled', 'snoozed'], + ruleStatusesFilter: ['enabled', 'disabled', 'snoozed'], }) ).toEqual([ 'alert.attributes.enabled:(true or false) or (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)', @@ -78,12 +78,12 @@ describe('mapFiltersToKql', () => { ]); }); - test('should handle typesFilter, actionTypesFilter and ruleStatusesFilter', () => { + test('should handle typesFilter, actionTypesFilter and ruleExecutionStatusesFilter', () => { expect( mapFiltersToKql({ typesFilter: ['type', 'filter'], actionTypesFilter: ['action', 'types', 'filter'], - ruleStatusesFilter: ['alert', 'statuses', 'filter'], + ruleExecutionStatusesFilter: ['alert', 'statuses', 'filter'], }) ).toEqual([ 'alert.attributes.alertTypeId:(type or filter)', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.ts index 38f35b74697d2..0e64f5500454f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.ts @@ -7,8 +7,8 @@ import { RuleStatus } from '../../../types'; -const getEnablementFilter = (ruleStateFilter: RuleStatus[] = []) => { - const enablementFilters = ruleStateFilter.reduce((result, filter) => { +const getEnablementFilter = (ruleStatusFilter: RuleStatus[] = []) => { + const enablementFilters = ruleStatusFilter.reduce((result, filter) => { if (filter === 'enabled') { return [...result, 'true']; } @@ -23,13 +23,13 @@ const getEnablementFilter = (ruleStateFilter: RuleStatus[] = []) => { export const mapFiltersToKql = ({ typesFilter, actionTypesFilter, + ruleExecutionStatusesFilter, ruleStatusesFilter, - ruleStateFilter, }: { typesFilter?: string[]; actionTypesFilter?: string[]; - ruleStatusesFilter?: string[]; - ruleStateFilter?: RuleStatus[]; + ruleExecutionStatusesFilter?: string[]; + ruleStatusesFilter?: RuleStatus[]; }): string[] => { const filters = []; @@ -47,16 +47,18 @@ export const mapFiltersToKql = ({ ].join('') ); } - if (ruleStatusesFilter && ruleStatusesFilter.length) { - filters.push(`alert.attributes.executionStatus.status:(${ruleStatusesFilter.join(' or ')})`); + if (ruleExecutionStatusesFilter && ruleExecutionStatusesFilter.length) { + filters.push( + `alert.attributes.executionStatus.status:(${ruleExecutionStatusesFilter.join(' or ')})` + ); } - if (ruleStateFilter && ruleStateFilter.length) { - const enablementFilter = getEnablementFilter(ruleStateFilter); + if (ruleStatusesFilter && ruleStatusesFilter.length) { + const enablementFilter = getEnablementFilter(ruleStatusesFilter); const snoozedFilter = `(alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)`; const hasEnablement = - ruleStateFilter.includes('enabled') || ruleStateFilter.includes('disabled'); - const hasSnoozed = ruleStateFilter.includes('snoozed'); + ruleStatusesFilter.includes('enabled') || ruleStatusesFilter.includes('disabled'); + const hasSnoozed = ruleStatusesFilter.includes('snoozed'); if (hasEnablement && !hasSnoozed) { filters.push(`${enablementFilter} and not ${snoozedFilter}`); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.test.ts index 6723047768ca5..8adc92738b7c6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.test.ts @@ -240,7 +240,7 @@ describe('loadRules', () => { `); }); - test('should call find API with ruleStateFilter', async () => { + test('should call find API with ruleStatusesilter', async () => { const resolvedValue = { page: 1, per_page: 10, @@ -251,7 +251,7 @@ describe('loadRules', () => { let result = await loadRules({ http, - ruleStateFilter: ['enabled', 'snoozed'], + ruleStatusesFilter: ['enabled', 'snoozed'], page: { index: 0, size: 10 }, }); expect(result).toEqual({ @@ -280,7 +280,7 @@ describe('loadRules', () => { result = await loadRules({ http, - ruleStateFilter: ['disabled'], + ruleStatusesFilter: ['disabled'], page: { index: 0, size: 10 }, }); expect(result).toEqual({ @@ -309,7 +309,7 @@ describe('loadRules', () => { result = await loadRules({ http, - ruleStateFilter: ['enabled', 'disabled', 'snoozed'], + ruleStatusesFilter: ['enabled', 'disabled', 'snoozed'], page: { index: 0, size: 10 }, }); expect(result).toEqual({ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.ts index c386ee2c8aa92..bdbdcf2f094b2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/rules.ts @@ -21,8 +21,8 @@ export async function loadRules({ searchText, typesFilter, actionTypesFilter, + ruleExecutionStatusesFilter, ruleStatusesFilter, - ruleStateFilter, sort = { field: 'name', direction: 'asc' }, }: { http: HttpSetup; @@ -30,8 +30,8 @@ export async function loadRules({ searchText?: string; typesFilter?: string[]; actionTypesFilter?: string[]; - ruleStatusesFilter?: string[]; - ruleStateFilter?: RuleStatus[]; + ruleExecutionStatusesFilter?: string[]; + ruleStatusesFilter?: RuleStatus[]; sort?: Sorting; }): Promise<{ page: number; @@ -42,8 +42,8 @@ export async function loadRules({ const filters = mapFiltersToKql({ typesFilter, actionTypesFilter, + ruleExecutionStatusesFilter, ruleStatusesFilter, - ruleStateFilter, }); const res = await http.get< AsApiContract<{ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx index 59721e2a3d0d5..befdde7127cac 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/index.tsx @@ -32,6 +32,6 @@ export const ActionForm = suspendedComponentWithProps( export const RuleStatusDropdown = suspendedComponentWithProps( lazy(() => import('./rules_list/components/rule_status_dropdown')) ); -export const RuleStateFilter = suspendedComponentWithProps( - lazy(() => import('./rules_list/components/rule_state_filter')) +export const RuleStatusFilter = suspendedComponentWithProps( + lazy(() => import('./rules_list/components/rule_status_filter')) ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_execution_status_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_execution_status_filter.tsx new file mode 100644 index 0000000000000..9acb8489fa09a --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_execution_status_filter.tsx @@ -0,0 +1,107 @@ +/* + * 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, { useEffect, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { + EuiFilterGroup, + EuiPopover, + EuiFilterButton, + EuiFilterSelectItem, + EuiHealth, +} from '@elastic/eui'; +import { RuleExecutionStatuses, RuleExecutionStatusValues } from '@kbn/alerting-plugin/common'; +import { rulesStatusesTranslationsMapping } from '../translations'; + +interface RuleExecutionStatusFilterProps { + selectedStatuses: string[]; + onChange?: (selectedRuleStatusesIds: string[]) => void; +} + +export const RuleExecutionStatusFilter: React.FunctionComponent = ({ + selectedStatuses, + onChange, +}: RuleExecutionStatusFilterProps) => { + const [selectedValues, setSelectedValues] = useState(selectedStatuses); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + useEffect(() => { + if (onChange) { + onChange(selectedValues); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedValues]); + + useEffect(() => { + setSelectedValues(selectedStatuses); + }, [selectedStatuses]); + + return ( + + setIsPopoverOpen(false)} + button={ + 0} + numActiveFilters={selectedValues.length} + numFilters={selectedValues.length} + onClick={() => setIsPopoverOpen(!isPopoverOpen)} + data-test-subj="ruleExecutionStatusFilterButton" + > + + + } + > +
+ {[...RuleExecutionStatusValues].sort().map((item: RuleExecutionStatuses) => { + const healthColor = getHealthColor(item); + return ( + { + const isPreviouslyChecked = selectedValues.includes(item); + if (isPreviouslyChecked) { + setSelectedValues(selectedValues.filter((val) => val !== item)); + } else { + setSelectedValues(selectedValues.concat(item)); + } + }} + checked={selectedValues.includes(item) ? 'on' : undefined} + data-test-subj={`ruleExecutionStatus${item}FilterOption`} + > + {rulesStatusesTranslationsMapping[item]} + + ); + })} +
+
+
+ ); +}; + +export function getHealthColor(status: RuleExecutionStatuses) { + switch (status) { + case 'active': + return 'success'; + case 'error': + return 'danger'; + case 'ok': + return 'primary'; + case 'pending': + return 'accent'; + case 'warning': + return 'warning'; + default: + return 'subdued'; + } +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_state_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_state_filter.tsx deleted file mode 100644 index 8a4c6a3a0dd87..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_state_filter.tsx +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import React, { useState, useCallback } from 'react'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiFilterButton, EuiPopover, EuiFilterGroup, EuiFilterSelectItem } from '@elastic/eui'; -import { RuleStatus } from '../../../../types'; - -const states: RuleStatus[] = ['enabled', 'disabled', 'snoozed']; - -const optionStyles = { - textTransform: 'capitalize' as const, -}; - -const getOptionDataTestSubj = (state: RuleStatus) => `ruleStateFilterOption-${state}`; - -export interface RuleStateFilterProps { - selectedStates: RuleStatus[]; - dataTestSubj?: string; - selectDataTestSubj?: string; - buttonDataTestSubj?: string; - optionDataTestSubj?: (state: RuleStatus) => string; - onChange: (selectedStates: RuleStatus[]) => void; -} - -export const RuleStateFilter = (props: RuleStateFilterProps) => { - const { - selectedStates = [], - dataTestSubj = 'ruleStateFilter', - selectDataTestSubj = 'ruleStateFilterSelect', - buttonDataTestSubj = 'ruleStateFilterButton', - optionDataTestSubj = getOptionDataTestSubj, - onChange = () => {}, - } = props; - - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - - const onFilterItemClick = useCallback( - (newOption: RuleStatus) => () => { - if (selectedStates.includes(newOption)) { - onChange(selectedStates.filter((option) => option !== newOption)); - return; - } - onChange([...selectedStates, newOption]); - }, - [selectedStates, onChange] - ); - - const onClick = useCallback(() => { - setIsPopoverOpen((prevIsOpen) => !prevIsOpen); - }, [setIsPopoverOpen]); - - return ( - - setIsPopoverOpen(false)} - button={ - 0} - numActiveFilters={selectedStates.length} - numFilters={selectedStates.length} - onClick={onClick} - > - - - } - > -
- {states.map((state) => { - return ( - - {state} - - ); - })} -
-
-
- ); -}; - -// eslint-disable-next-line import/no-default-export -export { RuleStateFilter as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_state_filter.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_filter.test.tsx similarity index 76% rename from x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_state_filter.test.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_filter.test.tsx index 506c328a3fcdb..f1f2957f9cada 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_state_filter.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_filter.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { EuiFilterButton, EuiFilterSelectItem } from '@elastic/eui'; -import { RuleStateFilter } from './rule_state_filter'; +import { RuleStatusFilter } from './rule_status_filter'; const onChangeMock = jest.fn(); @@ -18,7 +18,9 @@ describe('rule_state_filter', () => { }); it('renders correctly', () => { - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + + ); expect(wrapper.find(EuiFilterSelectItem).exists()).toBeFalsy(); expect(wrapper.find(EuiFilterButton).exists()).toBeTruthy(); @@ -27,7 +29,9 @@ describe('rule_state_filter', () => { }); it('can open the popover correctly', () => { - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + + ); expect(wrapper.find('[data-test-subj="ruleStateFilterSelect"]').exists()).toBeFalsy(); @@ -37,8 +41,10 @@ describe('rule_state_filter', () => { expect(statusItems.length).toEqual(3); }); - it('can select states', () => { - const wrapper = mountWithIntl(); + it('can select statuses', () => { + const wrapper = mountWithIntl( + + ); wrapper.find(EuiFilterButton).simulate('click'); @@ -46,7 +52,7 @@ describe('rule_state_filter', () => { expect(onChangeMock).toHaveBeenCalledWith(['enabled']); wrapper.setProps({ - selectedStates: ['enabled'], + selectedStatuses: ['enabled'], }); wrapper.find(EuiFilterSelectItem).at(0).simulate('click'); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_filter.tsx index cbb1a7f5455da..6d286ec6d09d7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_filter.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_filter.tsx @@ -4,82 +4,87 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import React, { useEffect, useState } from 'react'; +import React, { useState, useCallback } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { - EuiFilterGroup, - EuiPopover, - EuiFilterButton, - EuiFilterSelectItem, - EuiHealth, -} from '@elastic/eui'; -import { RuleExecutionStatuses, RuleExecutionStatusValues } from '@kbn/alerting-plugin/common'; -import { rulesStatusesTranslationsMapping } from '../translations'; +import { EuiFilterButton, EuiPopover, EuiFilterGroup, EuiFilterSelectItem } from '@elastic/eui'; +import { RuleStatus } from '../../../../types'; + +const statuses: RuleStatus[] = ['enabled', 'disabled', 'snoozed']; + +const optionStyles = { + textTransform: 'capitalize' as const, +}; -interface RuleStatusFilterProps { - selectedStatuses: string[]; - onChange?: (selectedRuleStatusesIds: string[]) => void; +const getOptionDataTestSubj = (status: RuleStatus) => `ruleStatusFilterOption-${status}`; + +export interface RuleStatusFilterProps { + selectedStatuses: RuleStatus[]; + dataTestSubj?: string; + selectDataTestSubj?: string; + buttonDataTestSubj?: string; + optionDataTestSubj?: (status: RuleStatus) => string; + onChange: (selectedStatuses: RuleStatus[]) => void; } -export const RuleStatusFilter: React.FunctionComponent = ({ - selectedStatuses, - onChange, -}: RuleStatusFilterProps) => { - const [selectedValues, setSelectedValues] = useState(selectedStatuses); +export const RuleStatusFilter = (props: RuleStatusFilterProps) => { + const { + selectedStatuses = [], + dataTestSubj = 'ruleStatusFilter', + selectDataTestSubj = 'ruleStatusFilterSelect', + buttonDataTestSubj = 'ruleStatusFilterButton', + optionDataTestSubj = getOptionDataTestSubj, + onChange = () => {}, + } = props; + const [isPopoverOpen, setIsPopoverOpen] = useState(false); - useEffect(() => { - if (onChange) { - onChange(selectedValues); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedValues]); + const onFilterItemClick = useCallback( + (newOption: RuleStatus) => () => { + if (selectedStatuses.includes(newOption)) { + onChange(selectedStatuses.filter((option) => option !== newOption)); + return; + } + onChange([...selectedStatuses, newOption]); + }, + [selectedStatuses, onChange] + ); - useEffect(() => { - setSelectedValues(selectedStatuses); - }, [selectedStatuses]); + const onClick = useCallback(() => { + setIsPopoverOpen((prevIsOpen) => !prevIsOpen); + }, [setIsPopoverOpen]); return ( - + setIsPopoverOpen(false)} button={ 0} - numActiveFilters={selectedValues.length} - numFilters={selectedValues.length} - onClick={() => setIsPopoverOpen(!isPopoverOpen)} - data-test-subj="ruleStatusFilterButton" + hasActiveFilters={selectedStatuses.length > 0} + numActiveFilters={selectedStatuses.length} + numFilters={selectedStatuses.length} + onClick={onClick} > } > -
- {[...RuleExecutionStatusValues].sort().map((item: RuleExecutionStatuses) => { - const healthColor = getHealthColor(item); +
+ {statuses.map((status) => { return ( { - const isPreviouslyChecked = selectedValues.includes(item); - if (isPreviouslyChecked) { - setSelectedValues(selectedValues.filter((val) => val !== item)); - } else { - setSelectedValues(selectedValues.concat(item)); - } - }} - checked={selectedValues.includes(item) ? 'on' : undefined} - data-test-subj={`ruleStatus${item}FilerOption`} + key={status} + style={optionStyles} + data-test-subj={optionDataTestSubj(status)} + onClick={onFilterItemClick(status)} + checked={selectedStatuses.includes(status) ? 'on' : undefined} > - {rulesStatusesTranslationsMapping[item]} + {status} ); })} @@ -89,19 +94,5 @@ export const RuleStatusFilter: React.FunctionComponent = ); }; -export function getHealthColor(status: RuleExecutionStatuses) { - switch (status) { - case 'active': - return 'success'; - case 'error': - return 'danger'; - case 'ok': - return 'primary'; - case 'pending': - return 'accent'; - case 'warning': - return 'warning'; - default: - return 'subdued'; - } -} +// eslint-disable-next-line import/no-default-export +export { RuleStatusFilter as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx index 4f207e28a1881..46ab8f3080b4a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.test.tsx @@ -810,15 +810,15 @@ describe('rules_list component with items', () => { ); }); - it('does not render the state filter if the feature flag is off', async () => { + it('does not render the status filter if the feature flag is off', async () => { await setup(); - expect(wrapper.find('[data-test-subj="ruleStateFilter"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="ruleStatusFilter"]').exists()).toBeFalsy(); }); - it('renders the tag filter if the experiment is on', async () => { + it('renders the status filter if the experiment is on', async () => { (getIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(() => true); await setup(); - expect(wrapper.find('[data-test-subj="ruleStateFilter"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="ruleStatusFilter"]').exists()).toBeTruthy(); }); it('can filter by rule states', async () => { @@ -826,21 +826,21 @@ describe('rules_list component with items', () => { loadRules.mockReset(); await setup(); - expect(loadRules.mock.calls[0][0].ruleStateFilter).toEqual([]); + expect(loadRules.mock.calls[0][0].ruleStatusesFilter).toEqual([]); - wrapper.find('[data-test-subj="ruleStateFilterButton"] button').simulate('click'); + wrapper.find('[data-test-subj="ruleStatusFilterButton"] button').simulate('click'); - wrapper.find('[data-test-subj="ruleStateFilterOption-enabled"]').first().simulate('click'); + wrapper.find('[data-test-subj="ruleStatusFilterOption-enabled"]').first().simulate('click'); - expect(loadRules.mock.calls[1][0].ruleStateFilter).toEqual(['enabled']); + expect(loadRules.mock.calls[1][0].ruleStatusesFilter).toEqual(['enabled']); - wrapper.find('[data-test-subj="ruleStateFilterOption-snoozed"]').first().simulate('click'); + wrapper.find('[data-test-subj="ruleStatusFilterOption-snoozed"]').first().simulate('click'); - expect(loadRules.mock.calls[2][0].ruleStateFilter).toEqual(['enabled', 'snoozed']); + expect(loadRules.mock.calls[2][0].ruleStatusesFilter).toEqual(['enabled', 'snoozed']); - wrapper.find('[data-test-subj="ruleStateFilterOption-snoozed"]').first().simulate('click'); + wrapper.find('[data-test-subj="ruleStatusFilterOption-snoozed"]').first().simulate('click'); - expect(loadRules.mock.calls[3][0].ruleStateFilter).toEqual(['enabled']); + expect(loadRules.mock.calls[3][0].ruleStatusesFilter).toEqual(['enabled']); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx index 261384164ee5f..d9d27f7e5e34e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx @@ -72,7 +72,7 @@ import { RuleQuickEditButtonsWithApi as RuleQuickEditButtons } from '../../commo import { CollapsedItemActionsWithApi as CollapsedItemActions } from './collapsed_item_actions'; import { TypeFilter } from './type_filter'; import { ActionTypeFilter } from './action_type_filter'; -import { RuleStatusFilter, getHealthColor } from './rule_status_filter'; +import { RuleExecutionStatusFilter, getHealthColor } from './rule_execution_status_filter'; import { loadRules, loadRuleAggregations, @@ -101,7 +101,7 @@ import { RuleDurationFormat } from './rule_duration_format'; import { shouldShowDurationWarning } from '../../../lib/execution_duration_utils'; import { getFormattedSuccessRatio } from '../../../lib/monitoring_utils'; import { triggersActionsUiConfig } from '../../../../common/lib/config_api'; -import { RuleStateFilter } from './rule_state_filter'; +import { RuleStatusFilter } from './rule_status_filter'; import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features'; const ENTER_KEY = 13; @@ -158,8 +158,8 @@ export const RulesList: React.FunctionComponent = () => { const [inputText, setInputText] = useState(); const [typesFilter, setTypesFilter] = useState([]); const [actionTypesFilter, setActionTypesFilter] = useState([]); - const [ruleStatusesFilter, setRuleStatusesFilter] = useState([]); - const [ruleStateFilter, setRuleStateFilter] = useState([]); + const [ruleExecutionStatusesFilter, setRuleExecutionStatusesFilter] = useState([]); + const [ruleStatusesFilter, setRuleStatusesFilter] = useState([]); const [ruleFlyoutVisible, setRuleFlyoutVisibility] = useState(false); const [editFlyoutVisible, setEditFlyoutVisibility] = useState(false); const [currentRuleToEdit, setCurrentRuleToEdit] = useState(null); @@ -169,7 +169,7 @@ export const RulesList: React.FunctionComponent = () => { ); const [showErrors, setShowErrors] = useState(false); - const isRuleStateFilterEnabled = getIsExperimentalFeatureEnabled('ruleStateFilter'); + const isRuleStatusFilterEnabled = getIsExperimentalFeatureEnabled('ruleStatusFilter'); useEffect(() => { (async () => { @@ -233,8 +233,8 @@ export const RulesList: React.FunctionComponent = () => { percentileOptions, JSON.stringify(typesFilter), JSON.stringify(actionTypesFilter), + JSON.stringify(ruleExecutionStatusesFilter), JSON.stringify(ruleStatusesFilter), - JSON.stringify(ruleStateFilter), ]); useEffect(() => { @@ -293,8 +293,8 @@ export const RulesList: React.FunctionComponent = () => { searchText, typesFilter, actionTypesFilter, + ruleExecutionStatusesFilter, ruleStatusesFilter, - ruleStateFilter, sort, }); await loadRuleAggs(); @@ -312,8 +312,8 @@ export const RulesList: React.FunctionComponent = () => { isEmpty(searchText) && isEmpty(typesFilter) && isEmpty(actionTypesFilter) && - isEmpty(ruleStatusesFilter) && - isEmpty(ruleStateFilter) + isEmpty(ruleExecutionStatusesFilter) && + isEmpty(ruleStatusesFilter) ); setNoData(rulesResponse.data.length === 0 && !isFilterApplied); @@ -339,7 +339,7 @@ export const RulesList: React.FunctionComponent = () => { searchText, typesFilter, actionTypesFilter, - ruleStatusesFilter, + ruleExecutionStatusesFilter, }); if (rulesAggs?.ruleExecutionStatus) { setRulesStatusesTotal(rulesAggs.ruleExecutionStatus); @@ -969,9 +969,11 @@ export const RulesList: React.FunctionComponent = () => { ); }; - const getRuleStateFilter = () => { - if (isRuleStateFilterEnabled) { - return []; + const getRuleStatusFilter = () => { + if (isRuleStatusFilterEnabled) { + return [ + , + ]; } return []; }; @@ -987,16 +989,16 @@ export const RulesList: React.FunctionComponent = () => { }) )} />, - ...getRuleStateFilter(), + ...getRuleStatusFilter(), setActionTypesFilter(ids)} />, - setRuleStatusesFilter(ids)} + selectedStatuses={ruleExecutionStatusesFilter} + onChange={(ids: string[]) => setRuleExecutionStatusesFilter(ids)} />, { }} />   - setRuleStatusesFilter(['error'])}> + setRuleExecutionStatusesFilter(['error'])}> { rulesListDatagrid: true, internalAlertsTable: true, rulesDetailLogs: true, + ruleStatusFilter: true, internalShareableComponentsSandbox: true, }, }); @@ -38,6 +39,10 @@ describe('getIsExperimentalFeatureEnabled', () => { expect(result).toEqual(true); + result = getIsExperimentalFeatureEnabled('ruleStatusFilter'); + + expect(result).toEqual(true); + expect(() => getIsExperimentalFeatureEnabled('doesNotExist' as any)).toThrowError( `Invalid enable value doesNotExist. Allowed values are: ${allowedExperimentalValueKeys.join( ', ' diff --git a/x-pack/plugins/triggers_actions_ui/public/common/get_rule_state_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/common/get_rule_state_filter.tsx deleted file mode 100644 index f1b539fa5bc00..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/common/get_rule_state_filter.tsx +++ /dev/null @@ -1,14 +0,0 @@ -/* - * 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 { RuleStateFilter } from '../application/sections'; -import type { RuleStateFilterProps } from '../application/sections/rules_list/components/rule_state_filter'; - -export const getRuleStateFilterLazy = (props: RuleStateFilterProps) => { - return ; -}; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/get_rule_status_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/common/get_rule_status_filter.tsx new file mode 100644 index 0000000000000..77ac3fc51d703 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/get_rule_status_filter.tsx @@ -0,0 +1,14 @@ +/* + * 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 { RuleStatusFilter } from '../application/sections'; +import type { RuleStatusFilterProps } from '../application/sections/rules_list/components/rule_status_filter'; + +export const getRuleStatusFilterLazy = (props: RuleStatusFilterProps) => { + return ; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/mocks.ts b/x-pack/plugins/triggers_actions_ui/public/mocks.ts index f05c3e301ac3b..7117c233a227e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/mocks.ts +++ b/x-pack/plugins/triggers_actions_ui/public/mocks.ts @@ -26,7 +26,7 @@ import { } from './types'; import { getAlertsTableLazy } from './common/get_alerts_table'; import { getRuleStatusDropdownLazy } from './common/get_rule_status_dropdown'; -import { getRuleStateFilterLazy } from './common/get_rule_state_filter'; +import { getRuleStatusFilterLazy } from './common/get_rule_status_filter'; function createStartMock(): TriggersAndActionsUIPublicPluginStart { const actionTypeRegistry = new TypeRegistry(); @@ -65,8 +65,8 @@ function createStartMock(): TriggersAndActionsUIPublicPluginStart { getRuleStatusDropdown: (props) => { return getRuleStatusDropdownLazy(props); }, - getRuleStateFilter: (props) => { - return getRuleStateFilterLazy(props); + getRuleStatusFilter: (props) => { + return getRuleStatusFilterLazy(props); }, }; } diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts index 6880ea945f27b..e739804fc920a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts @@ -31,7 +31,7 @@ import { getAddAlertFlyoutLazy } from './common/get_add_alert_flyout'; import { getEditAlertFlyoutLazy } from './common/get_edit_alert_flyout'; import { getAlertsTableLazy } from './common/get_alerts_table'; import { getRuleStatusDropdownLazy } from './common/get_rule_status_dropdown'; -import { getRuleStateFilterLazy } from './common/get_rule_state_filter'; +import { getRuleStatusFilterLazy } from './common/get_rule_status_filter'; import { ExperimentalFeaturesService } from './common/experimental_features_service'; import { ExperimentalFeatures, @@ -47,7 +47,7 @@ import type { ConnectorEditFlyoutProps, AlertsTableProps, RuleStatusDropdownProps, - RuleStateFilterProps, + RuleStatusFilterProps, AlertsTableConfigurationRegistry, } from './types'; import { TriggersActionsUiConfigType } from '../common/types'; @@ -78,7 +78,7 @@ export interface TriggersAndActionsUIPublicPluginStart { ) => ReactElement; getAlertsTable: (props: AlertsTableProps) => ReactElement; getRuleStatusDropdown: (props: RuleStatusDropdownProps) => ReactElement; - getRuleStateFilter: (props: RuleStateFilterProps) => ReactElement; + getRuleStatusFilter: (props: RuleStatusFilterProps) => ReactElement; } interface PluginsSetup { @@ -252,8 +252,8 @@ export class Plugin getRuleStatusDropdown: (props: RuleStatusDropdownProps) => { return getRuleStatusDropdownLazy(props); }, - getRuleStateFilter: (props: RuleStateFilterProps) => { - return getRuleStateFilterLazy(props); + getRuleStatusFilter: (props: RuleStatusFilterProps) => { + return getRuleStatusFilterLazy(props); }, }; } diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 1e4478e264857..8f833f2ae85ec 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -48,7 +48,7 @@ import { import { RuleRegistrySearchRequestPagination } from '@kbn/rule-registry-plugin/common'; import { TypeRegistry } from './application/type_registry'; import type { ComponentOpts as RuleStatusDropdownProps } from './application/sections/rules_list/components/rule_status_dropdown'; -import type { RuleStateFilterProps } from './application/sections/rules_list/components/rule_state_filter'; +import type { RuleStatusFilterProps } from './application/sections/rules_list/components/rule_status_filter'; // In Triggers and Actions we treat all `Alert`s as `SanitizedRule` // so the `Params` is a black-box of Record type SanitizedRule = Omit< @@ -80,7 +80,7 @@ export type { ResolvedRule, SanitizedRule, RuleStatusDropdownProps, - RuleStateFilterProps, + RuleStatusFilterProps, }; export type { ActionType, AsApiContract }; export { diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts index 581edecc3d8bc..49a3fada3dbef 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts @@ -14,6 +14,7 @@ import { createFailingAlert, disableAlert, muteAlert, + snoozeAlert, } from '../../lib/alert_api_actions'; import { ObjectRemover } from '../../lib/object_remover'; import { generateUniqueKey } from '../../lib/get_test_data'; @@ -462,8 +463,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const refreshResults = await pageObjects.triggersActionsUI.getAlertsListWithStatus(); expect(refreshResults.map((item: any) => item.status).sort()).to.eql(['Error', 'Ok']); }); - await testSubjects.click('ruleStatusFilterButton'); - await testSubjects.click('ruleStatuserrorFilerOption'); // select Error status filter + await testSubjects.click('ruleExecutionStatusFilterButton'); + await testSubjects.click('ruleExecutionStatuserrorFilterOption'); // select Error status filter await retry.try(async () => { const filterErrorOnlyResults = await pageObjects.triggersActionsUI.getAlertsListWithStatus(); @@ -600,5 +601,63 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.missingOrFail('centerJustifiedSpinner'); }); + + it('should filter alerts by the rule status', async () => { + const assertRulesLength = async (length: number) => { + return await retry.try(async () => { + const rules = await pageObjects.triggersActionsUI.getAlertsList(); + expect(rules.length).to.equal(length); + }); + }; + + // Enabled alert + await createAlert({ + supertest, + objectRemover, + }); + const disabledAlert = await createAlert({ + supertest, + objectRemover, + }); + const snoozedAlert = await createAlert({ + supertest, + objectRemover, + }); + + await disableAlert({ + supertest, + alertId: disabledAlert.id, + }); + await snoozeAlert({ + supertest, + alertId: snoozedAlert.id, + }); + + await refreshAlertsList(); + await assertRulesLength(3); + + // Select enabled + await testSubjects.click('ruleStatusFilterButton'); + await testSubjects.click('ruleStatusFilterOption-enabled'); + await assertRulesLength(1); + + // Select disabled + await testSubjects.click('ruleStatusFilterOption-enabled'); + await testSubjects.click('ruleStatusFilterOption-disabled'); + await assertRulesLength(1); + + // Select snoozed + await testSubjects.click('ruleStatusFilterOption-disabled'); + await testSubjects.click('ruleStatusFilterOption-snoozed'); + await assertRulesLength(1); + + // Select disabled and snoozed + await testSubjects.click('ruleStatusFilterOption-disabled'); + await assertRulesLength(2); + + // Select all 3 + await testSubjects.click('ruleStatusFilterOption-enabled'); + await assertRulesLength(3); + }); }); }; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts index 3424bf3176665..7c171c9f8086e 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/index.ts @@ -17,6 +17,6 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => { loadTestFile(require.resolve('./connectors')); loadTestFile(require.resolve('./alerts_table')); loadTestFile(require.resolve('./rule_status_dropdown')); - loadTestFile(require.resolve('./rule_state_filter')); + loadTestFile(require.resolve('./rule_status_filter')); }); }; diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rule_state_filter.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rule_status_filter.ts similarity index 61% rename from x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rule_state_filter.ts rename to x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rule_status_filter.ts index 216a56765bcc9..0afdc932b0289 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rule_state_filter.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rule_status_filter.ts @@ -13,7 +13,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const PageObjects = getPageObjects(['common', 'triggersActionsUI', 'header']); const esArchiver = getService('esArchiver'); - describe('Rule state filter', () => { + describe('Rule status filter', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/observability/alerts'); await PageObjects.common.navigateToUrlWithBrowserHistory( @@ -26,28 +26,28 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('should load from the shareable lazy loader', async () => { - await testSubjects.find('ruleStateFilter'); - const exists = await testSubjects.exists('ruleStateFilter'); + await testSubjects.find('ruleStatusFilter'); + const exists = await testSubjects.exists('ruleStatusFilter'); expect(exists).to.be(true); }); - it('should allow rule states to be filtered', async () => { - const ruleStateFilter = await testSubjects.find('ruleStateFilter'); - let badge = await ruleStateFilter.findByCssSelector('.euiFilterButton__notification'); + it('should allow rule statuses to be filtered', async () => { + const ruleStatusFilter = await testSubjects.find('ruleStatusFilter'); + let badge = await ruleStatusFilter.findByCssSelector('.euiFilterButton__notification'); expect(await badge.getVisibleText()).to.be('0'); - await testSubjects.click('ruleStateFilter'); - await testSubjects.click('ruleStateFilterOption-enabled'); + await testSubjects.click('ruleStatusFilter'); + await testSubjects.click('ruleStatusFilterOption-enabled'); - badge = await ruleStateFilter.findByCssSelector('.euiFilterButton__notification'); + badge = await ruleStatusFilter.findByCssSelector('.euiFilterButton__notification'); expect(await badge.getVisibleText()).to.be('1'); - await testSubjects.click('ruleStateFilterOption-disabled'); + await testSubjects.click('ruleStatusFilterOption-disabled'); - badge = await ruleStateFilter.findByCssSelector('.euiFilterButton__notification'); + badge = await ruleStatusFilter.findByCssSelector('.euiFilterButton__notification'); expect(await badge.getVisibleText()).to.be('2'); - await testSubjects.click('ruleStateFilterOption-enabled'); + await testSubjects.click('ruleStatusFilterOption-enabled'); expect(await badge.getVisibleText()).to.be('1'); }); }); diff --git a/x-pack/test/functional_with_es_ssl/config.ts b/x-pack/test/functional_with_es_ssl/config.ts index 5243b97898578..4783ad683c0cf 100644 --- a/x-pack/test/functional_with_es_ssl/config.ts +++ b/x-pack/test/functional_with_es_ssl/config.ts @@ -74,6 +74,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { `--xpack.trigger_actions_ui.enableExperimental=${JSON.stringify([ 'internalAlertsTable', 'internalShareableComponentsSandbox', + 'ruleStatusFilter', ])}`, `--xpack.alerting.rules.minimumScheduleInterval.value="2s"`, `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, diff --git a/x-pack/test/functional_with_es_ssl/lib/alert_api_actions.ts b/x-pack/test/functional_with_es_ssl/lib/alert_api_actions.ts index 40e567c299826..ab15d4b2ec3f4 100644 --- a/x-pack/test/functional_with_es_ssl/lib/alert_api_actions.ts +++ b/x-pack/test/functional_with_es_ssl/lib/alert_api_actions.ts @@ -8,6 +8,8 @@ import type { ObjectRemover } from './object_remover'; import { getTestAlertData, getTestActionData } from './get_test_data'; +const FUTURE_SNOOZE_TIME = '9999-12-31T06:00:00.000Z'; + export async function createAlertManualCleanup({ supertest, overwrites = {}, @@ -85,3 +87,14 @@ export async function disableAlert({ supertest, alertId }: { supertest: any; ale .set('kbn-xsrf', 'foo'); return alert; } + +export async function snoozeAlert({ supertest, alertId }: { supertest: any; alertId: string }) { + const { body: alert } = await supertest + .post(`/internal/alerting/rule/${alertId}/_snooze`) + .set('kbn-xsrf', 'foo') + .set('content-type', 'application/json') + .send({ + snooze_end_time: FUTURE_SNOOZE_TIME, + }); + return alert; +} From 6882021564664dee473e804df4ebda95005f633f Mon Sep 17 00:00:00 2001 From: Jiawei Wu Date: Fri, 29 Apr 2022 11:04:25 -0700 Subject: [PATCH 07/10] Address comments and fix tests --- .../common/experimental_features.ts | 2 +- .../lib/rule_api/aggregate.test.ts | 80 +++++++++++++++++++ .../application/lib/rule_api/aggregate.ts | 11 ++- .../sections/rule_details/components/rule.tsx | 2 +- .../rules_list/components/rules_list.tsx | 1 + .../public/common/types.ts | 2 + 6 files changed, 94 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts b/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts index 0a0b8cdeab208..dce87288ab15b 100644 --- a/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts +++ b/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts @@ -15,7 +15,7 @@ export const allowedExperimentalValues = Object.freeze({ rulesListDatagrid: true, internalAlertsTable: false, internalShareableComponentsSandbox: false, - ruleStatusFilter: false, + ruleStatusFilter: true, rulesDetailLogs: true, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.test.ts index 46653e5bc3911..ab8f1b565c888 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.test.ts @@ -209,4 +209,84 @@ describe('loadRuleAggregations', () => { ] `); }); + + test('should call aggregate API with ruleStatusesFilter', async () => { + const resolvedValue = { + rule_execution_status: { + ok: 4, + active: 2, + error: 1, + pending: 1, + unknown: 0, + }, + }; + http.get.mockResolvedValue(resolvedValue); + + let result = await loadRuleAggregations({ + http, + ruleStatusesFilter: ['enabled'], + }); + + expect(result).toEqual({ + ruleExecutionStatus: { + ok: 4, + active: 2, + error: 1, + pending: 1, + unknown: 0, + }, + }); + + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/internal/alerting/rules/_aggregate", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": "alert.attributes.enabled:(true) and not (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)", + "search": undefined, + "search_fields": undefined, + }, + }, + ] + `); + + result = await loadRuleAggregations({ + http, + ruleStatusesFilter: ['enabled', 'snoozed'], + }); + + expect(http.get.mock.calls[1]).toMatchInlineSnapshot(` + Array [ + "/internal/alerting/rules/_aggregate", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": "alert.attributes.enabled:(true) or (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)", + "search": undefined, + "search_fields": undefined, + }, + }, + ] + `); + + result = await loadRuleAggregations({ + http, + ruleStatusesFilter: ['enabled', 'disabled', 'snoozed'], + }); + + expect(http.get.mock.calls[1]).toMatchInlineSnapshot(` + Array [ + "/internal/alerting/rules/_aggregate", + Object { + "query": Object { + "default_search_operator": "AND", + "filter": "alert.attributes.enabled:(true) or (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)", + "search": undefined, + "search_fields": undefined, + }, + }, + ] + `); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts index 68abcb62448f3..9548445d0df9c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/aggregate.ts @@ -6,7 +6,7 @@ */ import { HttpSetup } from '@kbn/core/public'; import { AsApiContract, RewriteRequestCase } from '@kbn/actions-plugin/common'; -import { RuleAggregations } from '../../../types'; +import { RuleAggregations, RuleStatus } from '../../../types'; import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; import { mapFiltersToKql } from './map_filters_to_kql'; @@ -30,14 +30,21 @@ export async function loadRuleAggregations({ typesFilter, actionTypesFilter, ruleExecutionStatusesFilter, + ruleStatusesFilter, }: { http: HttpSetup; searchText?: string; typesFilter?: string[]; actionTypesFilter?: string[]; ruleExecutionStatusesFilter?: string[]; + ruleStatusesFilter?: RuleStatus[]; }): Promise { - const filters = mapFiltersToKql({ typesFilter, actionTypesFilter, ruleExecutionStatusesFilter }); + const filters = mapFiltersToKql({ + typesFilter, + actionTypesFilter, + ruleExecutionStatusesFilter, + ruleStatusesFilter, + }); const res = await http.get>( `${INTERNAL_BASE_ALERTING_API_PATH}/rules/_aggregate`, { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx index b70eaf20a051d..6ee16ea08ae65 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_details/components/rule.tsx @@ -33,7 +33,7 @@ import { withBulkRuleOperations, } from '../../common/components/with_bulk_rule_api_operations'; import './rule.scss'; -import { getHealthColor } from '../../rules_list/components/rule_status_filter'; +import { getHealthColor } from '../../rules_list/components/rule_execution_status_filter'; import { rulesStatusesTranslationsMapping, ALERT_STATUS_LICENSE_ERROR, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx index e04821c3757e7..e12ee75c609f8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rules_list.tsx @@ -338,6 +338,7 @@ export const RulesList: React.FunctionComponent = () => { typesFilter, actionTypesFilter, ruleExecutionStatusesFilter, + ruleStatusesFilter, }); if (rulesAggs?.ruleExecutionStatus) { setRulesStatusesTotal(rulesAggs.ruleExecutionStatus); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/types.ts b/x-pack/plugins/triggers_actions_ui/public/common/types.ts index 4aca07ad5482e..610962706661a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/types.ts @@ -24,3 +24,5 @@ export interface GroupByType { value: string; validNormalizedTypes: string[]; } + +export type { RuleStatus } from '../types'; From 82170a2719d332ca18dd1df6f4e88a3021248e3e Mon Sep 17 00:00:00 2001 From: Jiawei Wu Date: Fri, 29 Apr 2022 11:04:49 -0700 Subject: [PATCH 08/10] Revert experiment flag --- .../plugins/triggers_actions_ui/common/experimental_features.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts b/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts index dce87288ab15b..0a0b8cdeab208 100644 --- a/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts +++ b/x-pack/plugins/triggers_actions_ui/common/experimental_features.ts @@ -15,7 +15,7 @@ export const allowedExperimentalValues = Object.freeze({ rulesListDatagrid: true, internalAlertsTable: false, internalShareableComponentsSandbox: false, - ruleStatusFilter: true, + ruleStatusFilter: false, rulesDetailLogs: true, }); From 1084bbbfbc57869ea78f368e11d34854adfe0851 Mon Sep 17 00:00:00 2001 From: Jiawei Wu Date: Fri, 29 Apr 2022 12:59:29 -0700 Subject: [PATCH 09/10] Remove unused translations --- x-pack/plugins/translations/translations/fr-FR.json | 1 - x-pack/plugins/translations/translations/ja-JP.json | 1 - x-pack/plugins/translations/translations/zh-CN.json | 1 - 3 files changed, 3 deletions(-) diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index cb59649d7710c..c1ff631000ee0 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -29266,7 +29266,6 @@ "xpack.triggersActionsUI.sections.rulesList.ruleStatusActive": "Actif", "xpack.triggersActionsUI.sections.rulesList.ruleStatusDropdownMenuLabel": "Modifier le statut de la règle ou répéter", "xpack.triggersActionsUI.sections.rulesList.ruleStatusError": "Erreur", - "xpack.triggersActionsUI.sections.rulesList.ruleStatusFilterLabel": "Dernière réponse", "xpack.triggersActionsUI.sections.rulesList.ruleStatusLicenseError": "Erreur de licence", "xpack.triggersActionsUI.sections.rulesList.ruleStatusOk": "Ok", "xpack.triggersActionsUI.sections.rulesList.ruleStatusPending": "En attente", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index e62c4f671e4f1..a5432066aa7ce 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -29450,7 +29450,6 @@ "xpack.triggersActionsUI.sections.rulesList.ruleStatusActive": "アクティブ", "xpack.triggersActionsUI.sections.rulesList.ruleStatusDropdownMenuLabel": "ルールステータスまたはスヌーズを変更", "xpack.triggersActionsUI.sections.rulesList.ruleStatusError": "エラー", - "xpack.triggersActionsUI.sections.rulesList.ruleStatusFilterLabel": "前回の応答", "xpack.triggersActionsUI.sections.rulesList.ruleStatusLicenseError": "ライセンスエラー", "xpack.triggersActionsUI.sections.rulesList.ruleStatusOk": "OK", "xpack.triggersActionsUI.sections.rulesList.ruleStatusPending": "保留中", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 75c0098fa1f77..d7fd5949c51c5 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -29484,7 +29484,6 @@ "xpack.triggersActionsUI.sections.rulesList.ruleStatusActive": "活动", "xpack.triggersActionsUI.sections.rulesList.ruleStatusDropdownMenuLabel": "更改规则状态或暂停", "xpack.triggersActionsUI.sections.rulesList.ruleStatusError": "错误", - "xpack.triggersActionsUI.sections.rulesList.ruleStatusFilterLabel": "上次响应", "xpack.triggersActionsUI.sections.rulesList.ruleStatusLicenseError": "许可证错误", "xpack.triggersActionsUI.sections.rulesList.ruleStatusOk": "确定", "xpack.triggersActionsUI.sections.rulesList.ruleStatusPending": "待处理", From c59e3d85fbab1b51ccdc73072330bcfb960ceeff Mon Sep 17 00:00:00 2001 From: Jiawei Wu Date: Tue, 3 May 2022 13:56:42 -0700 Subject: [PATCH 10/10] Addressed comments --- .../lib/rule_api/map_filters_to_kql.test.ts | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.test.ts index 583990f1f0f08..df762d05e0eff 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/map_filters_to_kql.test.ts @@ -41,6 +41,28 @@ describe('mapFiltersToKql', () => { }); test('should handle ruleStatusesFilter', () => { + expect( + mapFiltersToKql({ + ruleStatusesFilter: ['enabled'], + }) + ).toEqual([ + 'alert.attributes.enabled:(true) and not (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)', + ]); + + expect( + mapFiltersToKql({ + ruleStatusesFilter: ['disabled'], + }) + ).toEqual([ + 'alert.attributes.enabled:(false) and not (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)', + ]); + + expect( + mapFiltersToKql({ + ruleStatusesFilter: ['snoozed'], + }) + ).toEqual(['(alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)']); + expect( mapFiltersToKql({ ruleStatusesFilter: ['enabled', 'snoozed'], @@ -51,10 +73,10 @@ describe('mapFiltersToKql', () => { expect( mapFiltersToKql({ - ruleStatusesFilter: ['enabled'], + ruleStatusesFilter: ['disabled', 'snoozed'], }) ).toEqual([ - 'alert.attributes.enabled:(true) and not (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)', + 'alert.attributes.enabled:(false) or (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)', ]); expect(