From 19298ee3e6b39d832bd68170f9b9071459193bcf Mon Sep 17 00:00:00 2001 From: Jiawei Wu <74562234+JiaweiWu@users.noreply.github.com> Date: Fri, 6 May 2022 14:24:32 -0700 Subject: [PATCH] [RAM] Add shareable rule tag filter (#130710) * rule tag filter * lower case test subj * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * Tag filter and aggregation APIs * [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' * Fix and add more tests * Fix test * Fix test and add new function test * Addressed comments * Lint * Create new load tags function * bump bundle size Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- packages/kbn-optimizer/limits.yml | 2 +- x-pack/plugins/alerting/common/rule.ts | 1 + .../server/routes/aggregate_rules.test.ts | 7 + .../alerting/server/routes/aggregate_rules.ts | 2 + .../server/rules_client/rules_client.ts | 13 ++ .../rules_client/tests/aggregate.test.ts | 27 ++++ .../common/experimental_features.ts | 1 + .../rule_tag_filter_sandbox.tsx | 25 ++++ .../shareable_components_sandbox.tsx | 2 + .../lib/rule_api/aggregate.test.ts | 66 ++++++++- .../application/lib/rule_api/aggregate.ts | 23 +++ .../public/application/lib/rule_api/index.ts | 2 +- .../lib/rule_api/map_filters_to_kql.test.ts | 12 +- .../lib/rule_api/map_filters_to_kql.ts | 5 + .../application/lib/rule_api/rules.test.ts | 38 +++++ .../public/application/lib/rule_api/rules.ts | 3 + .../public/application/sections/index.tsx | 3 + .../components/rule_tag_filter.test.tsx | 77 ++++++++++ .../rules_list/components/rule_tag_filter.tsx | 133 ++++++++++++++++++ .../rules_list/components/rules_list.test.tsx | 44 +++++- .../rules_list/components/rules_list.tsx | 44 +++++- .../common/get_experimental_features.test.tsx | 5 + .../public/common/get_rule_tag_filter.tsx | 14 ++ .../triggers_actions_ui/public/mocks.ts | 4 + .../triggers_actions_ui/public/plugin.ts | 6 + .../triggers_actions_ui/public/types.ts | 2 + .../spaces_only/tests/alerting/aggregate.ts | 6 + .../apps/triggers_actions_ui/alerts_list.ts | 83 +++++++++-- .../apps/triggers_actions_ui/index.ts | 1 + .../triggers_actions_ui/rule_tag_filter.ts | 54 +++++++ x-pack/test/functional_with_es_ssl/config.ts | 1 + 31 files changed, 685 insertions(+), 21 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_tag_filter_sandbox.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/common/get_rule_tag_filter.tsx create mode 100644 x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rule_tag_filter.ts diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 9f73dcd620d30..9baed7a92a53e 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -57,7 +57,7 @@ pageLoadAssetSize: telemetry: 51957 telemetryManagementSection: 38586 transform: 41007 - triggersActionsUi: 103400 + triggersActionsUi: 104400 upgradeAssistant: 81241 urlForwarding: 32579 usageCollection: 39762 diff --git a/x-pack/plugins/alerting/common/rule.ts b/x-pack/plugins/alerting/common/rule.ts index c8f282bf695d7..4509a004c6e58 100644 --- a/x-pack/plugins/alerting/common/rule.ts +++ b/x-pack/plugins/alerting/common/rule.ts @@ -75,6 +75,7 @@ export interface RuleAggregations { ruleEnabledStatus: { enabled: number; disabled: number }; ruleMutedStatus: { muted: number; unmuted: number }; ruleSnoozedStatus: { snoozed: number }; + ruleTags: string[]; } export interface MappedParamsProperties { diff --git a/x-pack/plugins/alerting/server/routes/aggregate_rules.test.ts b/x-pack/plugins/alerting/server/routes/aggregate_rules.test.ts index 7123f1bf4ad6c..8c24b457df565 100644 --- a/x-pack/plugins/alerting/server/routes/aggregate_rules.test.ts +++ b/x-pack/plugins/alerting/server/routes/aggregate_rules.test.ts @@ -60,6 +60,7 @@ describe('aggregateRulesRoute', () => { ruleSnoozedStatus: { snoozed: 4, }, + ruleTags: ['a', 'b', 'c'], }; rulesClient.aggregate.mockResolvedValueOnce(aggregateResult); @@ -94,6 +95,11 @@ describe('aggregateRulesRoute', () => { "rule_snoozed_status": Object { "snoozed": 4, }, + "rule_tags": Array [ + "a", + "b", + "c", + ], }, } `); @@ -129,6 +135,7 @@ describe('aggregateRulesRoute', () => { rule_snoozed_status: { snoozed: 4, }, + rule_tags: ['a', 'b', 'c'], }, }); }); diff --git a/x-pack/plugins/alerting/server/routes/aggregate_rules.ts b/x-pack/plugins/alerting/server/routes/aggregate_rules.ts index 312def72dd65e..c48c74fc28754 100644 --- a/x-pack/plugins/alerting/server/routes/aggregate_rules.ts +++ b/x-pack/plugins/alerting/server/routes/aggregate_rules.ts @@ -50,6 +50,7 @@ const rewriteBodyRes: RewriteResponseCase = ({ ruleEnabledStatus, ruleMutedStatus, ruleSnoozedStatus, + ruleTags, ...rest }) => ({ ...rest, @@ -57,6 +58,7 @@ const rewriteBodyRes: RewriteResponseCase = ({ rule_enabled_status: ruleEnabledStatus, rule_muted_status: ruleMutedStatus, rule_snoozed_status: ruleSnoozedStatus, + rule_tags: ruleTags, }); export const aggregateRulesRoute = ( diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index 00f67437ae4f2..e229b15fcd1cd 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -133,6 +133,12 @@ export interface RuleAggregation { doc_count: number; }>; }; + tags: { + buckets: Array<{ + key: string; + doc_count: number; + }>; + }; } export interface ConstructorOptions { @@ -200,6 +206,7 @@ export interface AggregateResult { ruleEnabledStatus?: { enabled: number; disabled: number }; ruleMutedStatus?: { muted: number; unmuted: number }; ruleSnoozedStatus?: { snoozed: number }; + ruleTags?: string[]; } export interface FindResult { @@ -921,6 +928,9 @@ export class RulesClient { muted: { terms: { field: 'alert.attributes.muteAll' }, }, + tags: { + terms: { field: 'alert.attributes.tags', order: { _key: 'asc' } }, + }, snoozed: { date_range: { field: 'alert.attributes.snoozeEndTime', @@ -990,6 +1000,9 @@ export class RulesClient { snoozed: snoozedBuckets.reduce((acc, bucket) => acc + bucket.doc_count, 0), }; + const tagsBuckets = resp.aggregations.tags?.buckets || []; + ret.ruleTags = tagsBuckets.map((bucket) => bucket.key); + return ret; } diff --git a/x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts index b74059e4be3d6..1a3d203162bd6 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/aggregate.test.ts @@ -112,6 +112,22 @@ describe('aggregate()', () => { }, ], }, + tags: { + buckets: [ + { + key: 'a', + doc_count: 10, + }, + { + key: 'b', + doc_count: 20, + }, + { + key: 'c', + doc_count: 30, + }, + ], + }, }, }); @@ -160,6 +176,11 @@ describe('aggregate()', () => { "ruleSnoozedStatus": Object { "snoozed": 2, }, + "ruleTags": Array [ + "a", + "b", + "c", + ], } `); expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledTimes(1); @@ -187,6 +208,9 @@ describe('aggregate()', () => { ranges: [{ from: 'now' }], }, }, + tags: { + terms: { field: 'alert.attributes.tags', order: { _key: 'asc' } }, + }, }, }, ]); @@ -221,6 +245,9 @@ describe('aggregate()', () => { ranges: [{ from: 'now' }], }, }, + tags: { + terms: { field: 'alert.attributes.tags', order: { _key: 'asc' } }, + }, }, }, ]); 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..33f5fdc44afcd 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, + ruleTagFilter: false, ruleStatusFilter: false, rulesDetailLogs: true, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_tag_filter_sandbox.tsx b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_tag_filter_sandbox.tsx new file mode 100644 index 0000000000000..58603fdb8f178 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/internal/shareable_components_sandbox/rule_tag_filter_sandbox.tsx @@ -0,0 +1,25 @@ +/* + * 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 { EuiSpacer } from '@elastic/eui'; +import { getRuleTagFilterLazy } from '../../../common/get_rule_tag_filter'; + +export const RuleTagFilterSandbox = () => { + const [selectedTags, setSelectedTags] = useState([]); + + return ( +
+ {getRuleTagFilterLazy({ + tags: ['tag1', 'tag2', 'tag3', 'tag4'], + selectedTags, + onChange: setSelectedTags, + })} + +
selected tags: {JSON.stringify(selectedTags)}
+
+ ); +}; 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 bedcbb03045a5..668b1ccb5aa69 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,6 +7,7 @@ import React from 'react'; import { RuleStatusDropdownSandbox } from './rule_status_dropdown_sandbox'; +import { RuleTagFilterSandbox } from './rule_tag_filter_sandbox'; import { RuleStatusFilterSandbox } from './rule_status_filter_sandbox'; import { RuleTagBadgeSandbox } from './rule_tag_badge_sandbox'; @@ -14,6 +15,7 @@ export const InternalShareableComponentsSandbox: React.FC<{}> = () => { return ( <> + 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 ab8f1b565c888..5377e4269f46e 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 @@ -6,7 +6,7 @@ */ import { httpServiceMock } from '@kbn/core/public/mocks'; -import { loadRuleAggregations } from './aggregate'; +import { loadRuleAggregations, loadRuleTags } from './aggregate'; const http = httpServiceMock.createStartContract(); @@ -289,4 +289,68 @@ describe('loadRuleAggregations', () => { ] `); }); + + test('should call aggregate API with tagsFilter', async () => { + const resolvedValue = { + rule_execution_status: { + ok: 4, + active: 2, + error: 1, + pending: 1, + unknown: 0, + }, + }; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadRuleAggregations({ + http, + searchText: 'baz', + tagsFilter: ['a', 'b', 'c'], + }); + + 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.tags:(a or b or c)", + "search": "baz", + "search_fields": "[\\"name\\",\\"tags\\"]", + }, + }, + ] + `); + }); + + test('loadRuleTags should call the aggregate API with no filters', async () => { + const resolvedValue = { + rule_tags: ['a', 'b', 'c'], + }; + http.get.mockResolvedValueOnce(resolvedValue); + + const result = await loadRuleTags({ + http, + }); + + expect(result).toEqual({ + ruleTags: ['a', 'b', 'c'], + }); + + expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/internal/alerting/rules/_aggregate", + ] + `); + }); }); 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 9548445d0df9c..1df6177443657 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 @@ -10,11 +10,16 @@ import { RuleAggregations, RuleStatus } from '../../../types'; import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants'; import { mapFiltersToKql } from './map_filters_to_kql'; +export interface RuleTagsAggregations { + ruleTags: string[]; +} + const rewriteBodyRes: RewriteRequestCase = ({ rule_execution_status: ruleExecutionStatus, rule_enabled_status: ruleEnabledStatus, rule_muted_status: ruleMutedStatus, rule_snoozed_status: ruleSnoozedStatus, + rule_tags: ruleTags, ...rest }: any) => ({ ...rest, @@ -22,8 +27,23 @@ const rewriteBodyRes: RewriteRequestCase = ({ ruleEnabledStatus, ruleMutedStatus, ruleSnoozedStatus, + ruleTags, +}); + +const rewriteTagsBodyRes: RewriteRequestCase = ({ + rule_tags: ruleTags, +}: any) => ({ + ruleTags, }); +// TODO: https://github.com/elastic/kibana/issues/131682 +export async function loadRuleTags({ http }: { http: HttpSetup }): Promise { + const res = await http.get>( + `${INTERNAL_BASE_ALERTING_API_PATH}/rules/_aggregate` + ); + return rewriteTagsBodyRes(res); +} + export async function loadRuleAggregations({ http, searchText, @@ -31,6 +51,7 @@ export async function loadRuleAggregations({ actionTypesFilter, ruleExecutionStatusesFilter, ruleStatusesFilter, + tagsFilter, }: { http: HttpSetup; searchText?: string; @@ -38,12 +59,14 @@ export async function loadRuleAggregations({ actionTypesFilter?: string[]; ruleExecutionStatusesFilter?: string[]; ruleStatusesFilter?: RuleStatus[]; + tagsFilter?: string[]; }): Promise { const filters = mapFiltersToKql({ typesFilter, actionTypesFilter, ruleExecutionStatusesFilter, ruleStatusesFilter, + tagsFilter, }); 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/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts index 89ede79f4a21d..c9834dd140ea4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_api/index.ts @@ -7,7 +7,7 @@ export { alertingFrameworkHealth } from './health'; export { mapFiltersToKql } from './map_filters_to_kql'; -export { loadRuleAggregations } from './aggregate'; +export { loadRuleAggregations, loadRuleTags } from './aggregate'; export { createRule } from './create'; export { deleteRules } from './delete'; export { disableRule, disableRules } from './disable'; 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 df762d05e0eff..f67a27ef5409c 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 @@ -88,6 +88,14 @@ describe('mapFiltersToKql', () => { ]); }); + test('should handle tagsFilter', () => { + expect( + mapFiltersToKql({ + tagsFilter: ['a', 'b', 'c'], + }) + ).toEqual(['alert.attributes.tags:(a or b or c)']); + }); + test('should handle typesFilter and actionTypesFilter', () => { expect( mapFiltersToKql({ @@ -100,17 +108,19 @@ describe('mapFiltersToKql', () => { ]); }); - test('should handle typesFilter, actionTypesFilter and ruleExecutionStatusesFilter', () => { + test('should handle typesFilter, actionTypesFilter, ruleExecutionStatusesFilter, and tagsFilter', () => { expect( mapFiltersToKql({ typesFilter: ['type', 'filter'], actionTypesFilter: ['action', 'types', 'filter'], ruleExecutionStatusesFilter: ['alert', 'statuses', 'filter'], + tagsFilter: ['a', 'b', 'c'], }) ).toEqual([ 'alert.attributes.alertTypeId:(type or filter)', '(alert.attributes.actions:{ actionTypeId:action } OR alert.attributes.actions:{ actionTypeId:types } OR alert.attributes.actions:{ actionTypeId:filter })', 'alert.attributes.executionStatus.status:(alert or statuses or filter)', + 'alert.attributes.tags:(a or b or c)', ]); }); }); 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 0e64f5500454f..ff2a49e3a5e45 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 @@ -25,9 +25,11 @@ export const mapFiltersToKql = ({ actionTypesFilter, ruleExecutionStatusesFilter, ruleStatusesFilter, + tagsFilter, }: { typesFilter?: string[]; actionTypesFilter?: string[]; + tagsFilter?: string[]; ruleExecutionStatusesFilter?: string[]; ruleStatusesFilter?: RuleStatus[]; }): string[] => { @@ -68,6 +70,9 @@ export const mapFiltersToKql = ({ filters.push(`${enablementFilter} or ${snoozedFilter}`); } } + if (tagsFilter && tagsFilter.length) { + filters.push(`alert.attributes.tags:(${tagsFilter.join(' or ')})`); + } 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 8adc92738b7c6..2a20c9d9469f5 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 @@ -336,4 +336,42 @@ describe('loadRules', () => { ] `); }); + + test('should call find API with tagsFilter', async () => { + const resolvedValue = { + page: 1, + per_page: 10, + total: 0, + data: [], + }; + http.get.mockResolvedValueOnce(resolvedValue); + const result = await loadRules({ + http, + tagsFilter: ['a', 'b', 'c'], + 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.tags:(a or b or c)", + "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 bdbdcf2f094b2..6e527989cc91f 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 @@ -23,6 +23,7 @@ export async function loadRules({ actionTypesFilter, ruleExecutionStatusesFilter, ruleStatusesFilter, + tagsFilter, sort = { field: 'name', direction: 'asc' }, }: { http: HttpSetup; @@ -30,6 +31,7 @@ export async function loadRules({ searchText?: string; typesFilter?: string[]; actionTypesFilter?: string[]; + tagsFilter?: string[]; ruleExecutionStatusesFilter?: string[]; ruleStatusesFilter?: RuleStatus[]; sort?: Sorting; @@ -42,6 +44,7 @@ export async function loadRules({ const filters = mapFiltersToKql({ typesFilter, actionTypesFilter, + tagsFilter, ruleExecutionStatusesFilter, ruleStatusesFilter, }); 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 e41c2a73a5124..9ab31ae12402f 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,9 @@ export const ActionForm = suspendedComponentWithProps( export const RuleStatusDropdown = suspendedComponentWithProps( lazy(() => import('./rules_list/components/rule_status_dropdown')) ); +export const RuleTagFilter = suspendedComponentWithProps( + lazy(() => import('./rules_list/components/rule_tag_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_tag_filter.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.test.tsx new file mode 100644 index 0000000000000..a6b60b1099391 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.test.tsx @@ -0,0 +1,77 @@ +/* + * 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, EuiSelectable } from '@elastic/eui'; +import { RuleTagFilter } from './rule_tag_filter'; + +const onChangeMock = jest.fn(); + +const tags = ['a', 'b', 'c', 'd', 'e', 'f']; + +describe('rule_tag_filter', () => { + beforeEach(() => { + onChangeMock.mockReset(); + }); + + it('renders correctly', () => { + const wrapper = mountWithIntl( + + ); + + 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="ruleTagFilterSelectable"]').exists()).toBeFalsy(); + + wrapper.find(EuiFilterButton).simulate('click'); + + expect(wrapper.find('[data-test-subj="ruleTagFilterSelectable"]').exists()).toBeTruthy(); + expect(wrapper.find('li').length).toEqual(tags.length); + }); + + it('can select tags', () => { + const wrapper = mountWithIntl( + + ); + + wrapper.find(EuiFilterButton).simulate('click'); + + wrapper.find('[data-test-subj="ruleTagFilterOption-a"]').at(0).simulate('click'); + expect(onChangeMock).toHaveBeenCalledWith(['a']); + + wrapper.setProps({ + selectedTags: ['a'], + }); + + wrapper.find('[data-test-subj="ruleTagFilterOption-a"]').at(0).simulate('click'); + expect(onChangeMock).toHaveBeenCalledWith([]); + + wrapper.find('[data-test-subj="ruleTagFilterOption-b"]').at(0).simulate('click'); + expect(onChangeMock).toHaveBeenCalledWith(['a', 'b']); + }); + + it('renders selected tags even if they get deleted from the tags array', () => { + const selectedTags = ['g', 'h']; + const wrapper = mountWithIntl( + + ); + + wrapper.find(EuiFilterButton).simulate('click'); + + expect(wrapper.find(EuiSelectable).props().options.length).toEqual( + tags.length + selectedTags.length + ); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.tsx new file mode 100644 index 0000000000000..6aa8aa8c69213 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_tag_filter.tsx @@ -0,0 +1,133 @@ +/* + * 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, { useMemo, useState, useCallback } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { + EuiSelectable, + EuiFilterGroup, + EuiFilterButton, + EuiPopover, + EuiSelectableProps, + EuiSelectableOption, + EuiSpacer, +} from '@elastic/eui'; + +export interface RuleTagFilterProps { + tags: string[]; + selectedTags: string[]; + isLoading?: boolean; + loadingMessage?: EuiSelectableProps['loadingMessage']; + noMatchesMessage?: EuiSelectableProps['noMatchesMessage']; + emptyMessage?: EuiSelectableProps['emptyMessage']; + errorMessage?: EuiSelectableProps['errorMessage']; + dataTestSubj?: string; + selectableDataTestSubj?: string; + optionDataTestSubj?: (tag: string) => string; + buttonDataTestSubj?: string; + onChange: (tags: string[]) => void; +} + +const getOptionDataTestSubj = (tag: string) => `ruleTagFilterOption-${tag}`; + +export const RuleTagFilter = (props: RuleTagFilterProps) => { + const { + tags = [], + selectedTags = [], + isLoading = false, + loadingMessage, + noMatchesMessage, + emptyMessage, + errorMessage, + dataTestSubj = 'ruleTagFilter', + selectableDataTestSubj = 'ruleTagFilterSelectable', + optionDataTestSubj = getOptionDataTestSubj, + buttonDataTestSubj = 'ruleTagFilterButton', + onChange = () => {}, + } = props; + + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const allTags = useMemo(() => { + return [...new Set([...tags, ...selectedTags])].sort(); + }, [tags, selectedTags]); + + const options: EuiSelectableOption[] = useMemo( + () => + allTags.map((tag) => ({ + label: tag, + checked: selectedTags.includes(tag) ? 'on' : undefined, + 'data-test-subj': optionDataTestSubj(tag), + })), + [allTags, selectedTags, optionDataTestSubj] + ); + + const onChangeInternal = useCallback( + (newOptions: EuiSelectableOption[]) => { + const newSelectedTags = newOptions.reduce((result, option) => { + if (option.checked === 'on') { + result = [...result, option.label]; + } + return result; + }, []); + + onChange(newSelectedTags); + }, + [onChange] + ); + + const onClosePopover = () => { + setIsPopoverOpen(!isPopoverOpen); + }; + + const renderButton = () => { + return ( + 0} + numActiveFilters={selectedTags.length} + numFilters={selectedTags.length} + onClick={onClosePopover} + > + + + ); + }; + + return ( + + + + {(list, search) => ( + <> + {search} + + {list} + + )} + + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { RuleTagFilter 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 52c6e2d3ed149..12e1b0f1e4a6e 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 @@ -33,6 +33,7 @@ jest.mock('../../../lib/rule_api', () => ({ loadRules: jest.fn(), loadRuleTypes: jest.fn(), loadRuleAggregations: jest.fn(), + loadRuleTags: jest.fn(), alertingFrameworkHealth: jest.fn(() => ({ isSufficientlySecure: true, hasPermanentEncryptionKey: true, @@ -63,7 +64,10 @@ jest.mock('../../../lib/capabilities', () => ({ jest.mock('../../../../common/get_experimental_features', () => ({ getIsExperimentalFeatureEnabled: jest.fn(), })); -const { loadRules, loadRuleTypes, loadRuleAggregations } = + +const ruleTags = ['a', 'b', 'c', 'd']; + +const { loadRules, loadRuleTypes, loadRuleAggregations, loadRuleTags } = jest.requireMock('../../../lib/rule_api'); const { loadActionTypes, loadAllActions } = jest.requireMock('../../../lib/action_connector_api'); const actionTypeRegistry = actionTypeRegistryMock.create(); @@ -395,6 +399,10 @@ describe('rules_list component with items', () => { ruleEnabledStatus: { enabled: 2, disabled: 0 }, ruleExecutionStatus: { ok: 1, active: 2, error: 3, pending: 4, unknown: 5, warning: 6 }, ruleMutedStatus: { muted: 0, unmuted: 2 }, + ruleTags, + }); + loadRuleTags.mockResolvedValue({ + ruleTags, }); const ruleTypeMock: RuleTypeModel = { @@ -842,6 +850,40 @@ describe('rules_list component with items', () => { expect(loadRules.mock.calls[3][0].ruleStatusesFilter).toEqual(['enabled']); }); + + it('does not render the tag filter is the feature flag is off', async () => { + await setup(); + expect(wrapper.find('[data-test-subj="ruleTagFilter"]').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="ruleTagFilter"]').exists()).toBeTruthy(); + }); + + it('can filter by tags', async () => { + (getIsExperimentalFeatureEnabled as jest.Mock).mockImplementation(() => true); + loadRules.mockReset(); + await setup(); + + expect(loadRules.mock.calls[0][0].tagsFilter).toEqual([]); + + wrapper.find('[data-test-subj="ruleTagFilterButton"] button').simulate('click'); + + const tagFilterListItems = wrapper.find( + '[data-test-subj="ruleTagFilterSelectable"] .euiSelectableListItem' + ); + expect(tagFilterListItems.length).toEqual(ruleTags.length); + + tagFilterListItems.at(0).simulate('click'); + + expect(loadRules.mock.calls[1][0].tagsFilter).toEqual(['a']); + + tagFilterListItems.at(1).simulate('click'); + + expect(loadRules.mock.calls[2][0].tagsFilter).toEqual(['a', 'b']); + }); }); 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 b1255600b68de..a5b9661835131 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 @@ -73,6 +73,7 @@ import { RuleExecutionStatusFilter, getHealthColor } from './rule_execution_stat import { loadRules, loadRuleAggregations, + loadRuleTags, loadRuleTypes, disableRule, enableRule, @@ -99,6 +100,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 { RuleTagFilter } from './rule_tag_filter'; import { RuleStatusFilter } from './rule_status_filter'; import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features'; @@ -158,6 +160,8 @@ export const RulesList: React.FunctionComponent = () => { const [actionTypesFilter, setActionTypesFilter] = useState([]); const [ruleExecutionStatusesFilter, setRuleExecutionStatusesFilter] = useState([]); const [ruleStatusesFilter, setRuleStatusesFilter] = useState([]); + const [tags, setTags] = useState([]); + const [tagsFilter, setTagsFilter] = useState([]); const [ruleFlyoutVisible, setRuleFlyoutVisibility] = useState(false); const [editFlyoutVisible, setEditFlyoutVisibility] = useState(false); const [currentRuleToEdit, setCurrentRuleToEdit] = useState(null); @@ -167,6 +171,7 @@ export const RulesList: React.FunctionComponent = () => { ); const [showErrors, setShowErrors] = useState(false); + const isRuleTagFilterEnabled = getIsExperimentalFeatureEnabled('ruleTagFilter'); const isRuleStatusFilterEnabled = getIsExperimentalFeatureEnabled('ruleStatusFilter'); useEffect(() => { @@ -233,6 +238,7 @@ export const RulesList: React.FunctionComponent = () => { JSON.stringify(actionTypesFilter), JSON.stringify(ruleExecutionStatusesFilter), JSON.stringify(ruleStatusesFilter), + JSON.stringify(tagsFilter), ]); useEffect(() => { @@ -293,8 +299,10 @@ export const RulesList: React.FunctionComponent = () => { actionTypesFilter, ruleExecutionStatusesFilter, ruleStatusesFilter, + tagsFilter, sort, }); + await loadRuleTagsAggs(); await loadRuleAggs(); setRulesState({ isLoading: false, @@ -311,7 +319,8 @@ export const RulesList: React.FunctionComponent = () => { isEmpty(typesFilter) && isEmpty(actionTypesFilter) && isEmpty(ruleExecutionStatusesFilter) && - isEmpty(ruleStatusesFilter) + isEmpty(ruleStatusesFilter) && + isEmpty(tagsFilter) ); setNoData(rulesResponse.data.length === 0 && !isFilterApplied); @@ -339,6 +348,7 @@ export const RulesList: React.FunctionComponent = () => { actionTypesFilter, ruleExecutionStatusesFilter, ruleStatusesFilter, + tagsFilter, }); if (rulesAggs?.ruleExecutionStatus) { setRulesStatusesTotal(rulesAggs.ruleExecutionStatus); @@ -355,6 +365,24 @@ export const RulesList: React.FunctionComponent = () => { } } + async function loadRuleTagsAggs() { + if (!isRuleTagFilterEnabled) { + return; + } + try { + const ruleTagsAggs = await loadRuleTags({ http }); + if (ruleTagsAggs?.ruleTags) { + setTags(ruleTagsAggs.ruleTags); + } + } catch (e) { + toasts.addDanger({ + title: i18n.translate('xpack.triggersActionsUI.sections.rulesList.unableToLoadRuleTags', { + defaultMessage: 'Unable to load rule tags', + }), + }); + } + } + const renderRuleStatusDropdown = (ruleEnabled: boolean | undefined, item: RuleTableItem) => { return ( { sortable: false, width: '50px', 'data-test-subj': 'rulesTableCell-tagsPopover', - render: (tags: string[], item: RuleTableItem) => { - return tags.length > 0 ? ( + render: (ruleTags: string[], item: RuleTableItem) => { + return ruleTags.length > 0 ? ( setTagPopoverOpenIndex(item.index)} onClose={() => setTagPopoverOpenIndex(-1)} /> @@ -940,6 +968,13 @@ export const RulesList: React.FunctionComponent = () => { ); }; + const getRuleTagFilter = () => { + if (isRuleTagFilterEnabled) { + return []; + } + return []; + }; + const getRuleStatusFilter = () => { if (isRuleStatusFilterEnabled) { return [ @@ -960,6 +995,7 @@ export const RulesList: React.FunctionComponent = () => { }) )} />, + ...getRuleTagFilter(), ...getRuleStatusFilter(), { rulesListDatagrid: true, internalAlertsTable: true, rulesDetailLogs: true, + ruleTagFilter: true, ruleStatusFilter: true, internalShareableComponentsSandbox: true, }, @@ -39,6 +40,10 @@ describe('getIsExperimentalFeatureEnabled', () => { expect(result).toEqual(true); + result = getIsExperimentalFeatureEnabled('ruleTagFilter'); + + expect(result).toEqual(true); + result = getIsExperimentalFeatureEnabled('ruleStatusFilter'); expect(result).toEqual(true); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/get_rule_tag_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/common/get_rule_tag_filter.tsx new file mode 100644 index 0000000000000..ccca277ef10ba --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/get_rule_tag_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 { RuleTagFilter } from '../application/sections'; +import type { RuleTagFilterProps } from '../application/sections/rules_list/components/rule_tag_filter'; + +export const getRuleTagFilterLazy = (props: RuleTagFilterProps) => { + return ; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/mocks.ts b/x-pack/plugins/triggers_actions_ui/public/mocks.ts index cb79a1509a6c1..003748f7d421e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/mocks.ts +++ b/x-pack/plugins/triggers_actions_ui/public/mocks.ts @@ -26,6 +26,7 @@ import { } from './types'; import { getAlertsTableLazy } from './common/get_alerts_table'; import { getRuleStatusDropdownLazy } from './common/get_rule_status_dropdown'; +import { getRuleTagFilterLazy } from './common/get_rule_tag_filter'; import { getRuleStatusFilterLazy } from './common/get_rule_status_filter'; import { getRuleTagBadgeLazy } from './common/get_rule_tag_badge'; @@ -66,6 +67,9 @@ function createStartMock(): TriggersAndActionsUIPublicPluginStart { getRuleStatusDropdown: (props) => { return getRuleStatusDropdownLazy(props); }, + getRuleTagFilter: (props) => { + return getRuleTagFilterLazy(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 1d9c3c07e44ca..c95dd73102fd9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/plugin.ts +++ b/x-pack/plugins/triggers_actions_ui/public/plugin.ts @@ -31,6 +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 { getRuleTagFilterLazy } from './common/get_rule_tag_filter'; import { getRuleStatusFilterLazy } from './common/get_rule_status_filter'; import { getRuleTagBadgeLazy } from './common/get_rule_tag_badge'; import { ExperimentalFeaturesService } from './common/experimental_features_service'; @@ -48,6 +49,7 @@ import type { ConnectorEditFlyoutProps, AlertsTableProps, RuleStatusDropdownProps, + RuleTagFilterProps, RuleStatusFilterProps, RuleTagBadgeProps, AlertsTableConfigurationRegistry, @@ -80,6 +82,7 @@ export interface TriggersAndActionsUIPublicPluginStart { ) => ReactElement; getAlertsTable: (props: AlertsTableProps) => ReactElement; getRuleStatusDropdown: (props: RuleStatusDropdownProps) => ReactElement; + getRuleTagFilter: (props: RuleTagFilterProps) => ReactElement; getRuleStatusFilter: (props: RuleStatusFilterProps) => ReactElement; getRuleTagBadge: (props: RuleTagBadgeProps) => ReactElement; } @@ -255,6 +258,9 @@ export class Plugin getRuleStatusDropdown: (props: RuleStatusDropdownProps) => { return getRuleStatusDropdownLazy(props); }, + getRuleTagFilter: (props: RuleTagFilterProps) => { + return getRuleTagFilterLazy(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 25efbfb6ecc38..ef7ea7096961b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -48,6 +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 { RuleTagFilterProps } from './application/sections/rules_list/components/rule_tag_filter'; import type { RuleStatusFilterProps } from './application/sections/rules_list/components/rule_status_filter'; import type { RuleTagBadgeProps } from './application/sections/rules_list/components/rule_tag_badge'; @@ -82,6 +83,7 @@ export type { ResolvedRule, SanitizedRule, RuleStatusDropdownProps, + RuleTagFilterProps, RuleStatusFilterProps, RuleTagBadgeProps, }; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/aggregate.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/aggregate.ts index 588e7132f268c..4424175e36953 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/aggregate.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/aggregate.ts @@ -44,6 +44,7 @@ export default function createAggregateTests({ getService }: FtrProviderContext) rule_snoozed_status: { snoozed: 0, }, + rule_tags: [], }); }); @@ -122,6 +123,7 @@ export default function createAggregateTests({ getService }: FtrProviderContext) rule_snoozed_status: { snoozed: 0, }, + rule_tags: ['foo'], }); }); @@ -137,6 +139,7 @@ export default function createAggregateTests({ getService }: FtrProviderContext) { rule_type_id: 'test.noop', schedule: { interval: '1s' }, + tags: ['a', 'b'], }, 'ok' ); @@ -153,6 +156,7 @@ export default function createAggregateTests({ getService }: FtrProviderContext) params: { pattern: { instance: new Array(100).fill(true) }, }, + tags: ['a', 'c', 'f'], }, 'active' ); @@ -166,6 +170,7 @@ export default function createAggregateTests({ getService }: FtrProviderContext) { rule_type_id: 'test.throw', schedule: { interval: '1s' }, + tags: ['b', 'c', 'd'], }, 'error' ); @@ -202,6 +207,7 @@ export default function createAggregateTests({ getService }: FtrProviderContext) ruleSnoozedStatus: { snoozed: 0, }, + ruleTags: ['a', 'b', 'c', 'd', 'f'], }); }); }); 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 a921256a09148..a036c25e3d657 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 @@ -32,6 +32,13 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { } describe('rules list', function () { + const assertRulesLength = async (length: number) => { + return await retry.try(async () => { + const rules = await pageObjects.triggersActionsUI.getAlertsList(); + expect(rules.length).to.equal(length); + }); + }; + before(async () => { await pageObjects.common.navigateToApp('triggersActions'); await testSubjects.click('rulesTab'); @@ -603,13 +610,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); 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, @@ -640,34 +640,93 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.click('ruleStatusFilterButton'); await testSubjects.click('ruleStatusFilterOption-enabled'); await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); - await assertRulesLength(1); // Select disabled await testSubjects.click('ruleStatusFilterOption-enabled'); await testSubjects.click('ruleStatusFilterOption-disabled'); await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); - await assertRulesLength(1); // Select snoozed await testSubjects.click('ruleStatusFilterOption-disabled'); await testSubjects.click('ruleStatusFilterOption-snoozed'); await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); - await assertRulesLength(1); // Select disabled and snoozed await testSubjects.click('ruleStatusFilterOption-disabled'); await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); - await assertRulesLength(2); // Select all 3 await testSubjects.click('ruleStatusFilterOption-enabled'); await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); - await assertRulesLength(3); }); + + it('should filter alerts by the tag', async () => { + await createAlert({ + supertest, + objectRemover, + overwrites: { + tags: ['a'], + }, + }); + await createAlert({ + supertest, + objectRemover, + overwrites: { + tags: ['b'], + }, + }); + await createAlert({ + supertest, + objectRemover, + overwrites: { + tags: ['a', 'b'], + }, + }); + await createAlert({ + supertest, + objectRemover, + overwrites: { + tags: ['b', 'c'], + }, + }); + await createAlert({ + supertest, + objectRemover, + overwrites: { + tags: ['c'], + }, + }); + + await refreshAlertsList(); + await testSubjects.click('ruleTagFilter'); + + // Select a -> selected: a + await testSubjects.click('ruleTagFilterOption-a'); + await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); + await assertRulesLength(2); + + // Unselect a -> selected: none + await testSubjects.click('ruleTagFilterOption-a'); + await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); + await assertRulesLength(5); + + // Select a, b -> selected: a, b + await testSubjects.click('ruleTagFilterOption-a'); + await testSubjects.click('ruleTagFilterOption-b'); + await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); + await assertRulesLength(4); + + // Unselect a, b, select c -> selected: c + await testSubjects.click('ruleTagFilterOption-a'); + await testSubjects.click('ruleTagFilterOption-b'); + await testSubjects.click('ruleTagFilterOption-c'); + await find.waitForDeletedByCssSelector('.euiBasicTable-loading'); + await assertRulesLength(2); + }); }); }; 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 73b084c2ce0e4..3b2803e17e184 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 @@ -16,6 +16,7 @@ export default ({ loadTestFile, getService }: FtrProviderContext) => { loadTestFile(require.resolve('./connectors')); loadTestFile(require.resolve('./alerts_table')); loadTestFile(require.resolve('./rule_status_dropdown')); + loadTestFile(require.resolve('./rule_tag_filter')); loadTestFile(require.resolve('./rule_status_filter')); loadTestFile(require.resolve('./rule_tag_badge')); }); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rule_tag_filter.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rule_tag_filter.ts new file mode 100644 index 0000000000000..77d57e2819db5 --- /dev/null +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rule_tag_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 find = getService('find'); + const PageObjects = getPageObjects(['common', 'triggersActionsUI', 'header']); + const esArchiver = getService('esArchiver'); + + describe('Rule tag 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('shoud load from shareable lazy loader', async () => { + await testSubjects.find('ruleTagFilter'); + const exists = await testSubjects.exists('ruleTagFilter'); + expect(exists).to.be(true); + }); + + it('should allow tag filters to be selected', async () => { + let badge = await find.byCssSelector('.euiFilterButton__notification'); + expect(await badge.getVisibleText()).to.be('0'); + + await testSubjects.click('ruleTagFilter'); + await testSubjects.click('ruleTagFilterOption-tag1'); + + badge = await find.byCssSelector('.euiFilterButton__notification'); + expect(await badge.getVisibleText()).to.be('1'); + + await testSubjects.click('ruleTagFilterOption-tag2'); + + badge = await find.byCssSelector('.euiFilterButton__notification'); + expect(await badge.getVisibleText()).to.be('2'); + + await testSubjects.click('ruleTagFilterOption-tag1'); + 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 4872d2fd6fa38..62984ace526fb 100644 --- a/x-pack/test/functional_with_es_ssl/config.ts +++ b/x-pack/test/functional_with_es_ssl/config.ts @@ -76,6 +76,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { `--xpack.trigger_actions_ui.enableExperimental=${JSON.stringify([ 'internalAlertsTable', 'internalShareableComponentsSandbox', + 'ruleTagFilter', 'ruleStatusFilter', ])}`, `--xpack.alerting.rules.minimumScheduleInterval.value="2s"`,