From 07985db97a4f926e798fa9f3ca59085856d10aaa Mon Sep 17 00:00:00 2001
From: Jiawei Wu <74562234+JiaweiWu@users.noreply.github.com>
Date: Tue, 3 May 2022 15:57:25 -0700
Subject: [PATCH] [RAM] Add shareable rule status filter (#130705)
* rule state filter
* turn off experiment
* [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix'
* Status filter API call
* Fix tests
* rename state to status, added tests
* Address comments and fix tests
* Revert experiment flag
* Remove unused translations
* Addressed comments
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
---
.../public/hooks/use_fetch_rules.ts | 2 +-
.../translations/translations/fr-FR.json | 1 -
.../translations/translations/ja-JP.json | 1 -
.../translations/translations/zh-CN.json | 1 -
.../common/experimental_features.ts | 1 +
.../rule_status_filter_sandbox.tsx | 26 ++++
.../shareable_components_sandbox.tsx | 2 +
.../lib/rule_api/aggregate.test.ts | 80 +++++++++++
.../application/lib/rule_api/aggregate.ts | 13 +-
.../lib/rule_api/map_filters_to_kql.test.ts | 56 +++++++-
.../lib/rule_api/map_filters_to_kql.ts | 41 +++++-
.../application/lib/rule_api/rules.test.ts | 97 +++++++++++++
.../public/application/lib/rule_api/rules.ts | 13 +-
.../public/application/sections/index.tsx | 3 +
.../sections/rule_details/components/rule.tsx | 2 +-
.../rule_execution_status_filter.tsx | 107 +++++++++++++++
.../components/rule_status_filter.test.tsx | 64 +++++++++
.../components/rule_status_filter.tsx | 127 ++++++++----------
.../rules_list/components/rules_list.test.tsx | 41 ++++++
.../rules_list/components/rules_list.tsx | 32 ++++-
.../common/get_experimental_features.test.tsx | 5 +
.../public/common/get_rule_status_filter.tsx | 14 ++
.../public/common/types.ts | 2 +
.../triggers_actions_ui/public/mocks.ts | 4 +
.../triggers_actions_ui/public/plugin.ts | 6 +
.../triggers_actions_ui/public/types.ts | 6 +-
.../apps/triggers_actions_ui/alerts_list.ts | 63 ++++++++-
.../apps/triggers_actions_ui/index.ts | 1 +
.../triggers_actions_ui/rule_status_filter.ts | 54 ++++++++
x-pack/test/functional_with_es_ssl/config.ts | 1 +
.../lib/alert_api_actions.ts | 13 ++
31 files changed, 785 insertions(+), 94 deletions(-)
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
create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_filter.test.tsx
create mode 100644 x-pack/plugins/triggers_actions_ui/public/common/get_rule_status_filter.tsx
create mode 100644 x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rule_status_filter.ts
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/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json
index 66cae51a7b414..9bc497a66f473 100644
--- a/x-pack/plugins/translations/translations/fr-FR.json
+++ b/x-pack/plugins/translations/translations/fr-FR.json
@@ -29364,7 +29364,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 70b2f076d5f8c..9c22df3fdabb2 100644
--- a/x-pack/plugins/translations/translations/ja-JP.json
+++ b/x-pack/plugins/translations/translations/ja-JP.json
@@ -29433,7 +29433,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 e249662ac511b..6c6a5d60ed320 100644
--- a/x-pack/plugins/translations/translations/zh-CN.json
+++ b/x-pack/plugins/translations/translations/zh-CN.json
@@ -29468,7 +29468,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": "待处理",
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..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,6 +15,7 @@ export const allowedExperimentalValues = Object.freeze({
rulesListDatagrid: true,
internalAlertsTable: false,
internalShareableComponentsSandbox: false,
+ ruleStatusFilter: false,
rulesDetailLogs: true,
});
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 d756804bbd406..bedcbb03045a5 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,12 +7,14 @@
import React from 'react';
import { RuleStatusDropdownSandbox } from './rule_status_dropdown_sandbox';
+import { RuleStatusFilterSandbox } from './rule_status_filter_sandbox';
import { RuleTagBadgeSandbox } from './rule_tag_badge_sandbox';
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 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 c7bcd438ef697..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';
@@ -29,15 +29,22 @@ export async function loadRuleAggregations({
searchText,
typesFilter,
actionTypesFilter,
+ ruleExecutionStatusesFilter,
ruleStatusesFilter,
}: {
http: HttpSetup;
searchText?: string;
typesFilter?: string[];
actionTypesFilter?: string[];
- ruleStatusesFilter?: string[];
+ ruleExecutionStatusesFilter?: string[];
+ ruleStatusesFilter?: RuleStatus[];
}): Promise {
- const filters = mapFiltersToKql({ typesFilter, actionTypesFilter, ruleStatusesFilter });
+ 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/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..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
@@ -32,14 +32,62 @@ 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 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'],
+ })
+ ).toEqual([
+ 'alert.attributes.enabled:(true) or (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)',
+ ]);
+
+ expect(
+ mapFiltersToKql({
+ ruleStatusesFilter: ['disabled', 'snoozed'],
+ })
+ ).toEqual([
+ 'alert.attributes.enabled:(false) or (alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)',
+ ]);
+
+ expect(
+ mapFiltersToKql({
+ ruleStatusesFilter: ['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({
@@ -52,12 +100,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 d7b22a7a4aee4..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
@@ -5,16 +5,34 @@
* 2.0.
*/
+import { RuleStatus } from '../../../types';
+
+const getEnablementFilter = (ruleStatusFilter: RuleStatus[] = []) => {
+ const enablementFilters = ruleStatusFilter.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,
+ ruleExecutionStatusesFilter,
ruleStatusesFilter,
}: {
typesFilter?: string[];
actionTypesFilter?: string[];
- ruleStatusesFilter?: string[];
+ ruleExecutionStatusesFilter?: string[];
+ ruleStatusesFilter?: RuleStatus[];
}): string[] => {
const filters = [];
+
if (typesFilter && typesFilter.length) {
filters.push(`alert.attributes.alertTypeId:(${typesFilter.join(' or ')})`);
}
@@ -29,8 +47,27 @@ export const mapFiltersToKql = ({
].join('')
);
}
+ if (ruleExecutionStatusesFilter && ruleExecutionStatusesFilter.length) {
+ filters.push(
+ `alert.attributes.executionStatus.status:(${ruleExecutionStatusesFilter.join(' or ')})`
+ );
+ }
+
if (ruleStatusesFilter && ruleStatusesFilter.length) {
- filters.push(`alert.attributes.executionStatus.status:(${ruleStatusesFilter.join(' or ')})`);
+ const enablementFilter = getEnablementFilter(ruleStatusesFilter);
+ const snoozedFilter = `(alert.attributes.muteAll:true OR alert.attributes.snoozeEndTime > now)`;
+ const hasEnablement =
+ ruleStatusesFilter.includes('enabled') || ruleStatusesFilter.includes('disabled');
+ const hasSnoozed = ruleStatusesFilter.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..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
@@ -239,4 +239,101 @@ describe('loadRules', () => {
]
`);
});
+
+ test('should call find API with ruleStatusesilter', async () => {
+ const resolvedValue = {
+ page: 1,
+ per_page: 10,
+ total: 0,
+ data: [],
+ };
+ http.get.mockResolvedValue(resolvedValue);
+
+ let result = await loadRules({
+ http,
+ ruleStatusesFilter: ['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,
+ ruleStatusesFilter: ['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,
+ ruleStatusesFilter: ['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..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
@@ -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';
@@ -21,6 +21,7 @@ export async function loadRules({
searchText,
typesFilter,
actionTypesFilter,
+ ruleExecutionStatusesFilter,
ruleStatusesFilter,
sort = { field: 'name', direction: 'asc' },
}: {
@@ -29,7 +30,8 @@ export async function loadRules({
searchText?: string;
typesFilter?: string[];
actionTypesFilter?: string[];
- ruleStatusesFilter?: string[];
+ ruleExecutionStatusesFilter?: string[];
+ ruleStatusesFilter?: 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,
+ ruleExecutionStatusesFilter,
+ ruleStatusesFilter,
+ });
const res = await http.get<
AsApiContract<{
page: number;
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 0e59e3c8ca38f..e41c2a73a5124 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 RuleStatusFilter = suspendedComponentWithProps(
+ lazy(() => import('./rules_list/components/rule_status_filter'))
+);
export const RuleTagBadge = suspendedComponentWithProps(
lazy(() => import('./rules_list/components/rule_tag_badge'))
);
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 ad6ef32ab82be..9d62fc2f8e37a 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/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_status_filter.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_filter.test.tsx
new file mode 100644
index 0000000000000..f1f2957f9cada
--- /dev/null
+++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rules_list/components/rule_status_filter.test.tsx
@@ -0,0 +1,64 @@
+/*
+ * 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 { RuleStatusFilter } from './rule_status_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 statuses', () => {
+ const wrapper = mountWithIntl(
+
+ );
+
+ wrapper.find(EuiFilterButton).simulate('click');
+
+ wrapper.find(EuiFilterSelectItem).at(0).simulate('click');
+ expect(onChangeMock).toHaveBeenCalledWith(['enabled']);
+
+ wrapper.setProps({
+ selectedStatuses: ['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_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 727898d42a076..52c6e2d3ed149 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 status filter if the feature flag is off', async () => {
+ await setup();
+ expect(wrapper.find('[data-test-subj="ruleStatusFilter"]').exists()).toBeFalsy();
+ });
+
+ 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="ruleStatusFilter"]').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].ruleStatusesFilter).toEqual([]);
+
+ wrapper.find('[data-test-subj="ruleStatusFilterButton"] button').simulate('click');
+
+ wrapper.find('[data-test-subj="ruleStatusFilterOption-enabled"]').first().simulate('click');
+
+ expect(loadRules.mock.calls[1][0].ruleStatusesFilter).toEqual(['enabled']);
+
+ wrapper.find('[data-test-subj="ruleStatusFilterOption-snoozed"]').first().simulate('click');
+
+ expect(loadRules.mock.calls[2][0].ruleStatusesFilter).toEqual(['enabled', 'snoozed']);
+
+ wrapper.find('[data-test-subj="ruleStatusFilterOption-snoozed"]').first().simulate('click');
+
+ expect(loadRules.mock.calls[3][0].ruleStatusesFilter).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 57c59f3f09782..b1255600b68de 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
@@ -58,6 +58,7 @@ import {
RuleTableItem,
RuleType,
RuleTypeIndex,
+ RuleStatus,
Pagination,
Percentiles,
TriggersActionsUiConfig,
@@ -68,7 +69,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,
@@ -98,6 +99,8 @@ 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 { RuleStatusFilter } from './rule_status_filter';
+import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features';
const ENTER_KEY = 13;
@@ -153,7 +156,8 @@ export const RulesList: React.FunctionComponent = () => {
const [inputText, setInputText] = useState();
const [typesFilter, setTypesFilter] = useState([]);
const [actionTypesFilter, setActionTypesFilter] = useState([]);
- const [ruleStatusesFilter, setRuleStatusesFilter] = useState([]);
+ const [ruleExecutionStatusesFilter, setRuleExecutionStatusesFilter] = useState([]);
+ const [ruleStatusesFilter, setRuleStatusesFilter] = useState([]);
const [ruleFlyoutVisible, setRuleFlyoutVisibility] = useState(false);
const [editFlyoutVisible, setEditFlyoutVisibility] = useState(false);
const [currentRuleToEdit, setCurrentRuleToEdit] = useState(null);
@@ -163,6 +167,8 @@ export const RulesList: React.FunctionComponent = () => {
);
const [showErrors, setShowErrors] = useState(false);
+ const isRuleStatusFilterEnabled = getIsExperimentalFeatureEnabled('ruleStatusFilter');
+
useEffect(() => {
(async () => {
setConfig(await triggersActionsUiConfig({ http }));
@@ -225,6 +231,7 @@ export const RulesList: React.FunctionComponent = () => {
percentileOptions,
JSON.stringify(typesFilter),
JSON.stringify(actionTypesFilter),
+ JSON.stringify(ruleExecutionStatusesFilter),
JSON.stringify(ruleStatusesFilter),
]);
@@ -284,6 +291,7 @@ export const RulesList: React.FunctionComponent = () => {
searchText,
typesFilter,
actionTypesFilter,
+ ruleExecutionStatusesFilter,
ruleStatusesFilter,
sort,
});
@@ -302,6 +310,7 @@ export const RulesList: React.FunctionComponent = () => {
isEmpty(searchText) &&
isEmpty(typesFilter) &&
isEmpty(actionTypesFilter) &&
+ isEmpty(ruleExecutionStatusesFilter) &&
isEmpty(ruleStatusesFilter)
);
@@ -328,6 +337,7 @@ export const RulesList: React.FunctionComponent = () => {
searchText,
typesFilter,
actionTypesFilter,
+ ruleExecutionStatusesFilter,
ruleStatusesFilter,
});
if (rulesAggs?.ruleExecutionStatus) {
@@ -930,6 +940,15 @@ export const RulesList: React.FunctionComponent = () => {
);
};
+ const getRuleStatusFilter = () => {
+ if (isRuleStatusFilterEnabled) {
+ return [
+ ,
+ ];
+ }
+ return [];
+ };
+
const toolsRight = [
{
})
)}
/>,
+ ...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_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/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';
diff --git a/x-pack/plugins/triggers_actions_ui/public/mocks.ts b/x-pack/plugins/triggers_actions_ui/public/mocks.ts
index 959d959ef855a..cb79a1509a6c1 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 { getRuleStatusFilterLazy } from './common/get_rule_status_filter';
import { getRuleTagBadgeLazy } from './common/get_rule_tag_badge';
function createStartMock(): TriggersAndActionsUIPublicPluginStart {
@@ -65,6 +66,9 @@ function createStartMock(): TriggersAndActionsUIPublicPluginStart {
getRuleStatusDropdown: (props) => {
return getRuleStatusDropdownLazy(props);
},
+ getRuleStatusFilter: (props) => {
+ return getRuleStatusFilterLazy(props);
+ },
getRuleTagBadge: (props) => {
return getRuleTagBadgeLazy(props);
},
diff --git a/x-pack/plugins/triggers_actions_ui/public/plugin.ts b/x-pack/plugins/triggers_actions_ui/public/plugin.ts
index e2c3be96271b9..1d9c3c07e44ca 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 { getRuleStatusFilterLazy } from './common/get_rule_status_filter';
import { getRuleTagBadgeLazy } from './common/get_rule_tag_badge';
import { ExperimentalFeaturesService } from './common/experimental_features_service';
import {
@@ -47,6 +48,7 @@ import type {
ConnectorEditFlyoutProps,
AlertsTableProps,
RuleStatusDropdownProps,
+ RuleStatusFilterProps,
RuleTagBadgeProps,
AlertsTableConfigurationRegistry,
} from './types';
@@ -78,6 +80,7 @@ export interface TriggersAndActionsUIPublicPluginStart {
) => ReactElement;
getAlertsTable: (props: AlertsTableProps) => ReactElement;
getRuleStatusDropdown: (props: RuleStatusDropdownProps) => ReactElement;
+ getRuleStatusFilter: (props: RuleStatusFilterProps) => ReactElement;
getRuleTagBadge: (props: RuleTagBadgeProps) => ReactElement;
}
@@ -252,6 +255,9 @@ export class Plugin
getRuleStatusDropdown: (props: RuleStatusDropdownProps) => {
return getRuleStatusDropdownLazy(props);
},
+ getRuleStatusFilter: (props: RuleStatusFilterProps) => {
+ return getRuleStatusFilterLazy(props);
+ },
getRuleTagBadge: (props: RuleTagBadgeProps) => {
return getRuleTagBadgeLazy(props);
},
diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts
index c59692ebad271..25efbfb6ecc38 100644
--- a/x-pack/plugins/triggers_actions_ui/public/types.ts
+++ b/x-pack/plugins/triggers_actions_ui/public/types.ts
@@ -48,7 +48,8 @@ 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 { RuleTagBadgeProps } from './application/sections/rules_list/components/rule_tag_badge';
+import type { RuleStatusFilterProps } from './application/sections/rules_list/components/rule_status_filter';
+import type { RuleTagBadgeProps } from './application/sections/rules_list/components/rule_tag_badge';
// In Triggers and Actions we treat all `Alert`s as `SanitizedRule`
// so the `Params` is a black-box of Record
@@ -81,6 +82,7 @@ export type {
ResolvedRule,
SanitizedRule,
RuleStatusDropdownProps,
+ RuleStatusFilterProps,
RuleTagBadgeProps,
};
export type { ActionType, AsApiContract };
@@ -429,3 +431,5 @@ export interface AlertsTableConfigurationRegistry {
id: string;
columns: EuiDataGridColumn[];
}
+
+export type RuleStatus = 'enabled' | 'disabled' | 'snoozed';
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 270232d1aa0fd..9c57f29c6f707 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,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_status_filter'));
loadTestFile(require.resolve('./rule_tag_badge'));
});
};
diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rule_status_filter.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rule_status_filter.ts
new file mode 100644
index 0000000000000..0afdc932b0289
--- /dev/null
+++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/rule_status_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 status 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('ruleStatusFilter');
+ const exists = await testSubjects.exists('ruleStatusFilter');
+ expect(exists).to.be(true);
+ });
+
+ 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('ruleStatusFilter');
+ await testSubjects.click('ruleStatusFilterOption-enabled');
+
+ badge = await ruleStatusFilter.findByCssSelector('.euiFilterButton__notification');
+ expect(await badge.getVisibleText()).to.be('1');
+
+ await testSubjects.click('ruleStatusFilterOption-disabled');
+
+ badge = await ruleStatusFilter.findByCssSelector('.euiFilterButton__notification');
+ expect(await badge.getVisibleText()).to.be('2');
+
+ 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;
+}