diff --git a/packages/kbn-alerts-ui-shared/index.ts b/packages/kbn-alerts-ui-shared/index.ts index 25ef9bf8a66ca..88445d10a463d 100644 --- a/packages/kbn-alerts-ui-shared/index.ts +++ b/packages/kbn-alerts-ui-shared/index.ts @@ -17,3 +17,5 @@ export { AlertsSearchBar } from './src/alerts_search_bar'; export type { AlertsSearchBarProps } from './src/alerts_search_bar/types'; export * from './src/alert_fields_table'; + +export * from './src/rule_type_modal'; diff --git a/packages/kbn-alerts-ui-shared/src/alerts_search_bar/apis/index.ts b/packages/kbn-alerts-ui-shared/src/alerts_search_bar/apis/index.ts index e3defec5c5003..4f4cae09d5d3a 100644 --- a/packages/kbn-alerts-ui-shared/src/alerts_search_bar/apis/index.ts +++ b/packages/kbn-alerts-ui-shared/src/alerts_search_bar/apis/index.ts @@ -9,4 +9,3 @@ export * from './fetch_aad_fields'; export * from './fetch_alert_fields'; export * from './fetch_alert_index_names'; -export * from './fetch_rule_types'; diff --git a/packages/kbn-alerts-ui-shared/src/alerts_search_bar/constants.ts b/packages/kbn-alerts-ui-shared/src/alerts_search_bar/constants.ts index 5fbdaf4fa7455..c81588af58bf9 100644 --- a/packages/kbn-alerts-ui-shared/src/alerts_search_bar/constants.ts +++ b/packages/kbn-alerts-ui-shared/src/alerts_search_bar/constants.ts @@ -10,6 +10,4 @@ import type { DataView, DataViewField } from '@kbn/data-views-plugin/common'; export const NO_INDEX_PATTERNS: DataView[] = []; export const EMPTY_AAD_FIELDS: DataViewField[] = []; -export const ALERTS_FEATURE_ID = 'alerts'; -export const BASE_ALERTING_API_PATH = '/api/alerting'; -export const BASE_RAC_ALERTS_API_PATH = '/internal/rac/alerts'; +export * from '../common/constants'; diff --git a/packages/kbn-alerts-ui-shared/src/alerts_search_bar/hooks/index.ts b/packages/kbn-alerts-ui-shared/src/alerts_search_bar/hooks/index.ts index bee311f222fbc..057b8ad60b999 100644 --- a/packages/kbn-alerts-ui-shared/src/alerts_search_bar/hooks/index.ts +++ b/packages/kbn-alerts-ui-shared/src/alerts_search_bar/hooks/index.ts @@ -7,5 +7,4 @@ */ export * from './use_alert_data_view'; -export * from './use_load_rule_types_query'; export * from './use_rule_aad_fields'; diff --git a/packages/kbn-alerts-ui-shared/src/alerts_search_bar/hooks/use_load_rule_types_query.ts b/packages/kbn-alerts-ui-shared/src/alerts_search_bar/hooks/use_load_rule_types_query.ts deleted file mode 100644 index c2396622a6bca..0000000000000 --- a/packages/kbn-alerts-ui-shared/src/alerts_search_bar/hooks/use_load_rule_types_query.ts +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; -import { useQuery } from '@tanstack/react-query'; -import type { RuleType, RuleTypeIndex } from '@kbn/triggers-actions-ui-types'; -import type { ToastsStart, HttpStart } from '@kbn/core/public'; -import { ALERTS_FEATURE_ID } from '../constants'; -import { fetchRuleTypes } from '../apis/fetch_rule_types'; - -export interface UseLoadRuleTypesQueryProps { - filteredRuleTypes: string[]; - enabled?: boolean; - http: HttpStart; - toasts: ToastsStart; -} - -const getFilteredIndex = (data: Array>, filteredRuleTypes: string[]) => { - const index: RuleTypeIndex = new Map(); - for (const ruleType of data) { - index.set(ruleType.id, ruleType); - } - let filteredIndex = index; - if (filteredRuleTypes?.length) { - filteredIndex = new Map( - [...index].filter(([k, v]) => { - return filteredRuleTypes.includes(v.id); - }) - ); - } - return filteredIndex; -}; - -export const useLoadRuleTypesQuery = (props: UseLoadRuleTypesQueryProps) => { - const { filteredRuleTypes, enabled = true, http, toasts } = props; - - const queryFn = () => { - return fetchRuleTypes({ http }); - }; - - const onErrorFn = () => { - toasts.addDanger( - i18n.translate('alertsUIShared.hooks.useLoadRuleTypesQuery.unableToLoadRuleTypesMessage', { - defaultMessage: 'Unable to load rule types', - }) - ); - }; - - const { data, isSuccess, isFetching, isInitialLoading, isLoading } = useQuery({ - queryKey: ['loadRuleTypes'], - queryFn, - onError: onErrorFn, - refetchOnWindowFocus: false, - enabled, - }); - - const filteredIndex = data ? getFilteredIndex(data, filteredRuleTypes) : new Map(); - - const hasAnyAuthorizedRuleType = filteredIndex.size > 0; - const authorizedRuleTypes = [...filteredIndex.values()]; - const authorizedToCreateAnyRules = authorizedRuleTypes.some( - (ruleType) => ruleType.authorizedConsumers[ALERTS_FEATURE_ID]?.all - ); - const authorizedToReadAnyRules = - authorizedToCreateAnyRules || - authorizedRuleTypes.some((ruleType) => ruleType.authorizedConsumers[ALERTS_FEATURE_ID]?.read); - - return { - ruleTypesState: { - initialLoad: isLoading || isInitialLoading, - isLoading: isLoading || isFetching, - data: filteredIndex, - }, - hasAnyAuthorizedRuleType, - authorizedRuleTypes, - authorizedToReadAnyRules, - authorizedToCreateAnyRules, - isSuccess, - }; -}; diff --git a/packages/kbn-alerts-ui-shared/src/alerts_search_bar/index.tsx b/packages/kbn-alerts-ui-shared/src/alerts_search_bar/index.tsx index 4e42c33e7c153..13bca396cb401 100644 --- a/packages/kbn-alerts-ui-shared/src/alerts_search_bar/index.tsx +++ b/packages/kbn-alerts-ui-shared/src/alerts_search_bar/index.tsx @@ -13,9 +13,9 @@ import { AlertConsumers } from '@kbn/rule-data-utils'; import { NO_INDEX_PATTERNS } from './constants'; import { SEARCH_BAR_PLACEHOLDER } from './translations'; import type { AlertsSearchBarProps, QueryLanguageType } from './types'; +import { useLoadRuleTypesQuery } from '../common/hooks/use_load_rule_types_query'; import { useAlertDataView } from './hooks/use_alert_data_view'; import { useRuleAADFields } from './hooks/use_rule_aad_fields'; -import { useLoadRuleTypesQuery } from './hooks/use_load_rule_types_query'; const SA_ALERTS = { type: 'alerts', fields: {} } as SuggestionsAbstraction; diff --git a/packages/kbn-alerts-ui-shared/src/alerts_search_bar/apis/fetch_rule_types.ts b/packages/kbn-alerts-ui-shared/src/common/apis/fetch_rule_types.ts similarity index 100% rename from packages/kbn-alerts-ui-shared/src/alerts_search_bar/apis/fetch_rule_types.ts rename to packages/kbn-alerts-ui-shared/src/common/apis/fetch_rule_types.ts diff --git a/packages/kbn-alerts-ui-shared/src/common/constants.ts b/packages/kbn-alerts-ui-shared/src/common/constants.ts new file mode 100644 index 0000000000000..de25fbe1e76f9 --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/common/constants.ts @@ -0,0 +1,11 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const ALERTS_FEATURE_ID = 'alerts'; +export const BASE_ALERTING_API_PATH = '/api/alerting'; +export const BASE_RAC_ALERTS_API_PATH = '/internal/rac/alerts'; diff --git a/packages/kbn-alerts-ui-shared/src/common/hooks/index.ts b/packages/kbn-alerts-ui-shared/src/common/hooks/index.ts new file mode 100644 index 0000000000000..027c825d1cee6 --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/common/hooks/index.ts @@ -0,0 +1,9 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './use_load_rule_types_query'; diff --git a/packages/kbn-alerts-ui-shared/src/common/hooks/use_load_rule_types_query.ts b/packages/kbn-alerts-ui-shared/src/common/hooks/use_load_rule_types_query.ts new file mode 100644 index 0000000000000..d5c66e5c502fc --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/common/hooks/use_load_rule_types_query.ts @@ -0,0 +1,118 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { keyBy } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { useMemo } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import type { HttpStart } from '@kbn/core-http-browser'; +import type { ToastsStart } from '@kbn/core-notifications-browser'; +import type { RuleType } from '@kbn/triggers-actions-ui-types'; +import { ALERTS_FEATURE_ID } from '../constants'; +import { fetchRuleTypes } from '../apis/fetch_rule_types'; +import { RuleTypeIndexWithDescriptions, RuleTypeWithDescription } from '../types'; + +export interface UseRuleTypesProps { + http: HttpStart; + toasts: ToastsStart; + filteredRuleTypes: string[]; + registeredRuleTypes?: Array<{ id: string; description: string }>; + enabled?: boolean; +} + +const getFilteredIndex = ( + data: Array>, + filteredRuleTypes: string[], + registeredRuleTypes: UseRuleTypesProps['registeredRuleTypes'] +) => { + const index: RuleTypeIndexWithDescriptions = new Map(); + const registeredRuleTypesDictionary = registeredRuleTypes ? keyBy(registeredRuleTypes, 'id') : {}; + for (const ruleType of data) { + const ruleTypeRecord: RuleType & { description?: string } = { ...ruleType }; + if (!registeredRuleTypes) { + // If rule type registry is not provided, don't use it for filtering + index.set(ruleType.id, ruleTypeRecord); + } else if (registeredRuleTypesDictionary[ruleType.id]) { + // Filter out unregistered rule types, and add descriptions to registered rule types + ruleTypeRecord.description = registeredRuleTypesDictionary[ruleType.id].description; + index.set(ruleType.id, ruleTypeRecord); + } + } + let filteredIndex = index; + if (filteredRuleTypes?.length) { + filteredIndex = new Map( + [...index].filter(([k, v]) => { + return filteredRuleTypes.includes(v.id); + }) + ); + } + return filteredIndex; +}; + +export const useLoadRuleTypesQuery = ({ + http, + toasts, + filteredRuleTypes, + registeredRuleTypes, + enabled = true, +}: UseRuleTypesProps) => { + const queryFn = () => { + return fetchRuleTypes({ http }); + }; + + const onErrorFn = (error: Error) => { + if (error) { + toasts.addDanger( + i18n.translate('alertsUIShared.hooks.useLoadRuleTypesQuery.unableToLoadRuleTypesMessage', { + defaultMessage: 'Unable to load rule types', + }) + ); + } + }; + const { data, isSuccess, isFetching, isInitialLoading, isLoading, error } = useQuery({ + queryKey: ['loadRuleTypes'], + queryFn, + onError: onErrorFn, + refetchOnWindowFocus: false, + // Leveraging TanStack Query's caching system to avoid duplicated requests as + // other state-sharing solutions turned out to be overly complex and less readable + staleTime: 60 * 1000, + enabled, + }); + + const filteredIndex = useMemo( + () => + data + ? getFilteredIndex(data, filteredRuleTypes, registeredRuleTypes) + : new Map(), + [data, filteredRuleTypes, registeredRuleTypes] + ); + + const hasAnyAuthorizedRuleType = filteredIndex.size > 0; + const authorizedRuleTypes = useMemo(() => [...filteredIndex.values()], [filteredIndex]); + const authorizedToCreateAnyRules = authorizedRuleTypes.some( + (ruleType) => ruleType.authorizedConsumers[ALERTS_FEATURE_ID]?.all + ); + const authorizedToReadAnyRules = + authorizedToCreateAnyRules || + authorizedRuleTypes.some((ruleType) => ruleType.authorizedConsumers[ALERTS_FEATURE_ID]?.read); + + return { + ruleTypesState: { + initialLoad: isLoading || isInitialLoading, + isLoading: isLoading || isFetching, + data: filteredIndex, + error, + }, + hasAnyAuthorizedRuleType, + authorizedRuleTypes, + authorizedToReadAnyRules, + authorizedToCreateAnyRules, + isSuccess, + }; +}; diff --git a/packages/kbn-alerts-ui-shared/src/common/i18n.ts b/packages/kbn-alerts-ui-shared/src/common/i18n.ts new file mode 100644 index 0000000000000..040a3fb90da68 --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/common/i18n.ts @@ -0,0 +1,45 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; + +export const PRODUCER_DISPLAY_NAMES = { + apm: i18n.translate('alertsUIShared.producerDisplayNames.apm', { + defaultMessage: 'APM and User Experience', + }), + uptime: i18n.translate('alertsUIShared.producerDisplayNames.uptime', { + defaultMessage: 'Synthetics and Uptime', + }), + stackAlerts: i18n.translate('alertsUIShared.producerDisplayNames.stackAlerts', { + defaultMessage: 'Stack Alerts', + }), + metrics: i18n.translate('alertsUIShared.producerDisplayNames.metrics', { + defaultMessage: 'Metrics', + }), + logs: i18n.translate('alertsUIShared.producerDisplayNames.logs', { + defaultMessage: 'Logs', + }), + siem: i18n.translate('alertsUIShared.producerDisplayNames.siem', { + defaultMessage: 'Security', + }), + observability: i18n.translate('alertsUIShared.producerDisplayNames.observability', { + defaultMessage: 'Observability', + }), + ml: i18n.translate('alertsUIShared.producerDisplayNames.ml', { + defaultMessage: 'Machine Learning', + }), + slo: i18n.translate('alertsUIShared.producerDisplayNames.slo', { + defaultMessage: 'SLOs', + }), + infrastructure: i18n.translate('alertsUIShared.producerDisplayNames.infrastructure', { + defaultMessage: 'Infrastructure', + }), + monitoring: i18n.translate('alertsUIShared.producerDisplayNames.monitoring', { + defaultMessage: 'Stack Monitoring', + }), +}; diff --git a/packages/kbn-alerts-ui-shared/src/common/types.ts b/packages/kbn-alerts-ui-shared/src/common/types.ts new file mode 100644 index 0000000000000..d7808487772c4 --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/common/types.ts @@ -0,0 +1,13 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { RuleType } from '@kbn/triggers-actions-ui-types'; + +export type RuleTypeWithDescription = RuleType & { description?: string }; + +export type RuleTypeIndexWithDescriptions = Map; diff --git a/packages/kbn-alerts-ui-shared/src/rule_type_modal/components/helpers/filter_and_count_rule_types.test.ts b/packages/kbn-alerts-ui-shared/src/rule_type_modal/components/helpers/filter_and_count_rule_types.test.ts new file mode 100644 index 0000000000000..8c062b090e078 --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_type_modal/components/helpers/filter_and_count_rule_types.test.ts @@ -0,0 +1,115 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { RuleTypeWithDescription } from '../../types'; +import { filterAndCountRuleTypes } from './filter_and_count_rule_types'; + +const mockRuleType: ( + name: string, + producer: string, + description: string +) => RuleTypeWithDescription = (name, producer, description) => ({ + id: name, + name, + producer, + description, + authorizedConsumers: {}, + actionVariables: { params: [] }, + enabledInLicense: true, + minimumLicenseRequired: 'basic', + recoveryActionGroup: { id: 'recovered', name: 'recovered' }, + actionGroups: [], + defaultActionGroupId: 'default', +}); + +const pickRuleTypeName = (ruleType: RuleTypeWithDescription) => ({ name: ruleType.name }); + +describe('filterAndCountRuleTypes', () => { + const ruleTypeIndex = new Map([ + ['rule1', mockRuleType('rule1', 'producer1', 'first rule type')], + ['rule2', mockRuleType('rule2', 'producer1', 'second rule type')], + ['rule3', mockRuleType('rule3', 'producer2', 'third rule type')], + ['rule4', mockRuleType('rule4', 'producer2', 'fourth rule type')], + ['rule5', mockRuleType('rule5', 'producer2', 'fifth rule type')], + ['rule6', mockRuleType('rule6', 'producer1', 'sixth rule type')], + ]); + + it('should return an empty array and total 0 when ruleTypeIndex is empty', () => { + const [ruleTypes, ruleTypeCountsByProducer] = filterAndCountRuleTypes(new Map(), null, ''); + expect(ruleTypes).toEqual([]); + expect(ruleTypeCountsByProducer).toEqual({ total: 0 }); + }); + + it('should return all rule types when no producer or search string is provided', () => { + const [ruleTypes, ruleTypeCountsByProducer] = filterAndCountRuleTypes(ruleTypeIndex, null, ''); + expect(ruleTypes.map(pickRuleTypeName)).toEqual([ + { name: 'rule1' }, + { name: 'rule2' }, + { name: 'rule3' }, + { name: 'rule4' }, + { name: 'rule5' }, + { name: 'rule6' }, + ]); + expect(ruleTypeCountsByProducer).toEqual({ producer1: 3, producer2: 3, total: 6 }); + }); + + it('should filter titles by search string', () => { + const [ruleTypes, ruleTypeCountsByProducer] = filterAndCountRuleTypes(ruleTypeIndex, null, '1'); + expect(ruleTypes.map(pickRuleTypeName)).toEqual([{ name: 'rule1' }]); + expect(ruleTypeCountsByProducer).toEqual({ producer1: 1, total: 1 }); + }); + + it('should filter descriptions by search string', () => { + const [ruleTypes, ruleTypeCountsByProducer] = filterAndCountRuleTypes( + ruleTypeIndex, + null, + 'second' + ); + expect(ruleTypes.map(pickRuleTypeName)).toEqual([{ name: 'rule2' }]); + expect(ruleTypeCountsByProducer).toEqual({ producer1: 1, total: 1 }); + }); + + it('should filter by producer without applying this filter to the total counts', () => { + const [ruleTypes, ruleTypeCountsByProducer] = filterAndCountRuleTypes( + ruleTypeIndex, + 'producer1', + '' + ); + expect(ruleTypes.map(pickRuleTypeName)).toEqual([ + { name: 'rule1' }, + { name: 'rule2' }, + { name: 'rule6' }, + ]); + expect(ruleTypeCountsByProducer).toEqual({ producer1: 3, producer2: 3, total: 6 }); + }); + + it('should filter by producer and search string', () => { + const [ruleTypes, ruleTypeCountsByProducer] = filterAndCountRuleTypes( + ruleTypeIndex, + 'producer1', + 'second' + ); + expect(ruleTypes.map(pickRuleTypeName)).toEqual([{ name: 'rule2' }]); + expect(ruleTypeCountsByProducer).toEqual({ producer1: 1, total: 1 }); + }); + + it('should filter by search string before calculating counts', () => { + const [ruleTypes, ruleTypeCountsByProducer] = filterAndCountRuleTypes( + ruleTypeIndex, + null, + 'th' // Filter out all but rules with third, fourth, fifth, sixth in description + ); + expect(ruleTypes.map(pickRuleTypeName)).toEqual([ + { name: 'rule3' }, + { name: 'rule4' }, + { name: 'rule5' }, + { name: 'rule6' }, + ]); + expect(ruleTypeCountsByProducer).toEqual({ producer1: 1, producer2: 3, total: 4 }); + }); +}); diff --git a/packages/kbn-alerts-ui-shared/src/rule_type_modal/components/helpers/filter_and_count_rule_types.ts b/packages/kbn-alerts-ui-shared/src/rule_type_modal/components/helpers/filter_and_count_rule_types.ts new file mode 100644 index 0000000000000..dad133f82f900 --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_type_modal/components/helpers/filter_and_count_rule_types.ts @@ -0,0 +1,50 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { countBy } from 'lodash'; +import { + RuleTypeIndexWithDescriptions, + RuleTypeCountsByProducer, + RuleTypeWithDescription, +} from '../../types'; + +export const filterAndCountRuleTypes: ( + ruleTypeIndex: RuleTypeIndexWithDescriptions, + selectedProducer: string | null, + searchString: string +) => [RuleTypeWithDescription[], RuleTypeCountsByProducer] = ( + ruleTypeIndex, + selectedProducer, + searchString +) => { + const ruleTypeValues = [...ruleTypeIndex.values()]; + if (!ruleTypeValues.length) return [[], { total: 0 }]; + + // Filter by search first to preserve totals in the facets + const ruleTypesFilteredBySearch = ruleTypeValues.filter((ruleType) => { + if (searchString) { + const lowerCaseSearchString = searchString.toLowerCase(); + return ( + ruleType.name.toLowerCase().includes(lowerCaseSearchString) || + (ruleType.description && ruleType.description.toLowerCase().includes(lowerCaseSearchString)) + ); + } + return true; + }); + const ruleTypesFilteredBySearchAndProducer = ruleTypesFilteredBySearch.filter((ruleType) => { + if (selectedProducer && ruleType.producer !== selectedProducer) return false; + return true; + }); + return [ + ruleTypesFilteredBySearchAndProducer, + { + ...countBy(ruleTypesFilteredBySearch, 'producer'), + total: ruleTypesFilteredBySearch.length, + }, + ]; +}; diff --git a/packages/kbn-alerts-ui-shared/src/rule_type_modal/components/index.tsx b/packages/kbn-alerts-ui-shared/src/rule_type_modal/components/index.tsx new file mode 100644 index 0000000000000..e7d89c76e6650 --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_type_modal/components/index.tsx @@ -0,0 +1,63 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useMemo, useState } from 'react'; +import type { HttpStart } from '@kbn/core-http-browser'; +import type { ToastsStart } from '@kbn/core-notifications-browser'; +import { useLoadRuleTypesQuery } from '../../common/hooks'; +import { RuleTypeModal, type RuleTypeModalProps } from './rule_type_modal'; +import { filterAndCountRuleTypes } from './helpers/filter_and_count_rule_types'; + +export interface RuleTypeModalComponentProps { + http: HttpStart; + toasts: ToastsStart; + filteredRuleTypes: string[]; + registeredRuleTypes: Array<{ id: string; description: string }>; + onClose: RuleTypeModalProps['onClose']; + onSelectRuleType: RuleTypeModalProps['onSelectRuleType']; +} + +const EMPTY_ARRAY: string[] = []; + +export const RuleTypeModalComponent: React.FC = ({ + http, + toasts, + filteredRuleTypes = EMPTY_ARRAY, + registeredRuleTypes, + ...rest +}) => { + const [selectedProducer, setSelectedProducer] = useState(null); + const [searchString, setSearchString] = useState(''); + + const { + ruleTypesState: { data: ruleTypeIndex, isLoading: ruleTypesLoading }, + } = useLoadRuleTypesQuery({ + http, + toasts, + filteredRuleTypes, + registeredRuleTypes, + }); + + const [ruleTypes, ruleTypeCountsByProducer] = useMemo( + () => filterAndCountRuleTypes(ruleTypeIndex, selectedProducer, searchString), + [ruleTypeIndex, searchString, selectedProducer] + ); + + return ( + + ); +}; diff --git a/packages/kbn-alerts-ui-shared/src/rule_type_modal/components/rule_type_list.tsx b/packages/kbn-alerts-ui-shared/src/rule_type_modal/components/rule_type_list.tsx new file mode 100644 index 0000000000000..7eb1ead13c727 --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_type_modal/components/rule_type_list.tsx @@ -0,0 +1,156 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React, { useMemo, useCallback } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiFacetGroup, + EuiFacetButton, + EuiCard, + EuiSpacer, + EuiText, + EuiEmptyPrompt, + EuiButton, + useEuiTheme, +} from '@elastic/eui'; +import { omit } from 'lodash'; +import { PRODUCER_DISPLAY_NAMES } from '../../common/i18n'; +import { RuleTypeWithDescription, RuleTypeCountsByProducer } from '../types'; + +interface RuleTypeListProps { + ruleTypes: RuleTypeWithDescription[]; + onSelectRuleType: (ruleTypeId: string) => void; + onFilterByProducer: (producer: string | null) => void; + selectedProducer: string | null; + ruleTypeCountsByProducer: RuleTypeCountsByProducer; + onClearFilters: () => void; +} + +const producerToDisplayName = (producer: string) => { + return Reflect.get(PRODUCER_DISPLAY_NAMES, producer) ?? producer; +}; + +export const RuleTypeList: React.FC = ({ + ruleTypes, + onSelectRuleType, + onFilterByProducer, + selectedProducer, + ruleTypeCountsByProducer, + onClearFilters, +}) => { + const ruleTypesList = [...ruleTypes].sort((a, b) => a.name.localeCompare(b.name)); + const { euiTheme } = useEuiTheme(); + + const facetList = useMemo( + () => + Object.entries(omit(ruleTypeCountsByProducer, 'total')) + .sort(([, aCount], [, bCount]) => bCount - aCount) + .map(([producer, count]) => ( + onFilterByProducer(producer)} + isSelected={selectedProducer === producer} + > + {producerToDisplayName(producer)} + + )), + [ruleTypeCountsByProducer, onFilterByProducer, selectedProducer] + ); + + return ( + + + + onFilterByProducer(null), [onFilterByProducer])} + isSelected={!selectedProducer} + > + All + + {facetList} + + + + {ruleTypesList.length === 0 && ( + + {i18n.translate('alertsUIShared.components.ruleTypeModal.noRuleTypesErrorTitle', { + defaultMessage: 'No rule types found', + })} + + } + body={ +

+ {i18n.translate('alertsUIShared.components.ruleTypeModal.noRuleTypesErrorBody', { + defaultMessage: 'Try a different search or change your filter settings', + })} + . +

+ } + actions={ + + Clear filters + + } + /> + )} + {ruleTypesList.map((rule) => ( + + onSelectRuleType(rule.id)} + description={ + <> + {rule.description} + {rule.description && } + + {producerToDisplayName(rule.producer)} + + + } + style={{ marginRight: '8px', flexGrow: 0 }} + data-test-subj={`${rule.id}-SelectOption`} + /> + + + ))} +
+
+ ); +}; diff --git a/packages/kbn-alerts-ui-shared/src/rule_type_modal/components/rule_type_modal.tsx b/packages/kbn-alerts-ui-shared/src/rule_type_modal/components/rule_type_modal.tsx new file mode 100644 index 0000000000000..169ae289513c3 --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_type_modal/components/rule_type_modal.tsx @@ -0,0 +1,141 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import React, { useCallback } from 'react'; +import { + EuiPageHeader, + EuiModal, + EuiPanel, + EuiPageHeaderSection, + EuiTitle, + EuiFieldSearch, + EuiSpacer, + useEuiTheme, + useCurrentEuiBreakpoint, + EuiEmptyPrompt, + EuiLoadingSpinner, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import { RuleTypeList } from './rule_type_list'; +import { RuleTypeWithDescription, RuleTypeCountsByProducer } from '../types'; + +export interface RuleTypeModalProps { + onClose: () => void; + onSelectRuleType: (ruleTypeId: string) => void; + onFilterByProducer: (producer: string | null) => void; + onChangeSearch: (search: string) => void; + searchString: string; + selectedProducer: string | null; +} + +export interface RuleTypeModalState { + ruleTypes: RuleTypeWithDescription[]; + ruleTypesLoading: boolean; + ruleTypeCountsByProducer: RuleTypeCountsByProducer; +} + +const loadingPrompt = ( + + {i18n.translate('alertsUIShared.components.ruleTypeModal.loadingRuleTypes', { + defaultMessage: 'Loading rule types', + })} + + } + icon={} + /> +); + +export const RuleTypeModal: React.FC = ({ + onClose, + onSelectRuleType, + onFilterByProducer, + onChangeSearch, + ruleTypes, + ruleTypesLoading, + ruleTypeCountsByProducer, + searchString, + selectedProducer, +}) => { + const { euiTheme } = useEuiTheme(); + const currentBreakpoint = useCurrentEuiBreakpoint() ?? 'm'; + const isFullscreenPortrait = ['s', 'xs'].includes(currentBreakpoint); + + const onClearFilters = useCallback(() => { + onFilterByProducer(null); + onChangeSearch(''); + }, [onFilterByProducer, onChangeSearch]); + + return ( + + + + + + + +

+ {i18n.translate('alertsUIShared.components.ruleTypeModal.title', { + defaultMessage: 'Select rule type', + })} +

+
+ + + onChangeSearch(value)} + fullWidth + /> + +
+
+
+ + {ruleTypesLoading ? ( + loadingPrompt + ) : ( + + )} + +
+
+
+ ); +}; diff --git a/packages/kbn-alerts-ui-shared/src/rule_type_modal/index.tsx b/packages/kbn-alerts-ui-shared/src/rule_type_modal/index.tsx new file mode 100644 index 0000000000000..d7c9e0b5bef59 --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_type_modal/index.tsx @@ -0,0 +1,9 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { RuleTypeModalComponent as RuleTypeModal } from './components'; diff --git a/packages/kbn-alerts-ui-shared/src/rule_type_modal/types.ts b/packages/kbn-alerts-ui-shared/src/rule_type_modal/types.ts new file mode 100644 index 0000000000000..7faf0faab3bb7 --- /dev/null +++ b/packages/kbn-alerts-ui-shared/src/rule_type_modal/types.ts @@ -0,0 +1,13 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export type { RuleTypeWithDescription, RuleTypeIndexWithDescriptions } from '../common/types'; +export interface RuleTypeCountsByProducer { + total: number; + [x: string]: number; +} diff --git a/packages/kbn-alerts-ui-shared/tsconfig.json b/packages/kbn-alerts-ui-shared/tsconfig.json index f2347b579acef..6c042ab98b5ea 100644 --- a/packages/kbn-alerts-ui-shared/tsconfig.json +++ b/packages/kbn-alerts-ui-shared/tsconfig.json @@ -29,5 +29,7 @@ "@kbn/unified-search-plugin", "@kbn/es-query", "@kbn/ui-theme", + "@kbn/core-http-browser", + "@kbn/core-notifications-browser", ] } diff --git a/x-pack/plugins/observability_solution/observability/public/pages/rules/rules.tsx b/x-pack/plugins/observability_solution/observability/public/pages/rules/rules.tsx index e15818e8ea3ae..1d04ebc45f339 100644 --- a/x-pack/plugins/observability_solution/observability/public/pages/rules/rules.tsx +++ b/x-pack/plugins/observability_solution/observability/public/pages/rules/rules.tsx @@ -6,6 +6,7 @@ */ import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { RuleTypeModal } from '@kbn/alerts-ui-shared'; import { ALERTING_FEATURE_ID } from '@kbn/alerting-plugin/common'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -33,10 +34,17 @@ export function RulesPage({ activeTab = RULES_TAB_NAME }: RulesPageProps) { const { http, docLinks, - triggersActionsUi: { getAddRuleFlyout: AddRuleFlyout, getRulesSettingsLink: RulesSettingsLink }, + notifications: { toasts }, + triggersActionsUi: { + ruleTypeRegistry, + getAddRuleFlyout: AddRuleFlyout, + getRulesSettingsLink: RulesSettingsLink, + }, } = useKibana().services; const { ObservabilityPageTemplate } = usePluginContext(); const history = useHistory(); + const [ruleTypeModalVisibility, setRuleTypeModalVisibility] = useState(false); + const [ruleTypeIdToCreate, setRuleTypeIdToCreate] = useState(undefined); const [addRuleFlyoutVisibility, setAddRuleFlyoutVisibility] = useState(false); const [stateRefresh, setRefresh] = useState(new Date()); @@ -96,7 +104,7 @@ export function RulesPage({ activeTab = RULES_TAB_NAME }: RulesPageProps) { fill iconType="plusInCircle" key="create-alert" - onClick={() => setAddRuleFlyoutVisibility(true)} + onClick={() => setRuleTypeModalVisibility(true)} > + {ruleTypeModalVisibility && ( + setRuleTypeModalVisibility(false)} + onSelectRuleType={(ruleTypeId) => { + setRuleTypeIdToCreate(ruleTypeId); + setRuleTypeModalVisibility(false); + setAddRuleFlyoutVisibility(true); + }} + http={http} + toasts={toasts} + registeredRuleTypes={ruleTypeRegistry.list()} + filteredRuleTypes={filteredRuleTypes} + /> + )} + {addRuleFlyoutVisibility && ( { renderWithProviders(); const createRuleEl = await screen.findByText('Create rule'); - expect(screen.queryByTestId('addRuleFlyoutTitle')).not.toBeInTheDocument(); + expect(screen.queryByTestId('ruleTypeModal')).not.toBeInTheDocument(); fireEvent.click(createRuleEl); - expect(await screen.findByTestId('addRuleFlyoutTitle')).toBeInTheDocument(); + expect(await screen.findByTestId('ruleTypeModal')).toBeInTheDocument(); }); }); 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 b1cdc5330401f..92c2239f349a4 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 @@ -13,6 +13,7 @@ import { KueryNode } from '@kbn/es-query'; import { FormattedMessage } from '@kbn/i18n-react'; import { toMountPoint } from '@kbn/kibana-react-plugin/public'; import { parseRuleCircuitBreakerErrorMessage } from '@kbn/alerting-plugin/common'; +import { RuleTypeModal } from '@kbn/alerts-ui-shared'; import React, { lazy, useEffect, @@ -196,6 +197,8 @@ export const RulesList = ({ const [page, setPage] = useState({ index: 0, size: DEFAULT_SEARCH_PAGE_SIZE }); const [inputText, setInputText] = useState(searchFilter); + const [ruleTypeModalVisible, setRuleTypeModalVisibility] = useState(false); + const [ruleTypeIdToCreate, setRuleTypeIdToCreate] = useState(undefined); const [ruleFlyoutVisible, setRuleFlyoutVisibility] = useState(false); const [editFlyoutVisible, setEditFlyoutVisibility] = useState(false); const [currentRuleToEdit, setCurrentRuleToEdit] = useState(null); @@ -651,13 +654,13 @@ export const RulesList = ({ } }; - const openFlyout = useCallback(() => { - setRuleFlyoutVisibility(true); + const openRuleTypeModal = useCallback(() => { + setRuleTypeModalVisibility(true); }, []); useEffect(() => { setHeaderActions?.([ - ...(authorizedToCreateAnyRules ? [] : []), + ...(authorizedToCreateAnyRules ? [] : []), , , ]); @@ -756,7 +759,7 @@ export const RulesList = ({ showCreateFirstRulePrompt={showCreateFirstRulePrompt} showCreateRuleButtonInPrompt={showCreateRuleButtonInPrompt} showSpinner={showSpinner} - onCreateRulesClick={openFlyout} + onCreateRulesClick={openRuleTypeModal} /> {isDeleteModalFlyoutVisible && ( @@ -996,6 +999,20 @@ export const RulesList = ({ )} )} + {ruleTypeModalVisible && ( + setRuleTypeModalVisibility(false)} + onSelectRuleType={(ruleTypeId) => { + setRuleTypeIdToCreate(ruleTypeId); + setRuleTypeModalVisibility(false); + setRuleFlyoutVisibility(true); + }} + http={http} + toasts={toasts} + registeredRuleTypes={ruleTypeRegistry.list()} + filteredRuleTypes={filteredRuleTypes} + /> + )} {ruleFlyoutVisible && ( }> )} diff --git a/x-pack/test/functional/services/rules/common.ts b/x-pack/test/functional/services/rules/common.ts index 42d32b8337c03..bb1d1464fb509 100644 --- a/x-pack/test/functional/services/rules/common.ts +++ b/x-pack/test/functional/services/rules/common.ts @@ -42,9 +42,9 @@ export function RulesCommonServiceProvider({ getService, getPageObject }: FtrPro async defineIndexThresholdAlert(alertName: string) { await browser.refresh(); await this.clickCreateAlertButton(); + await testSubjects.click(`.index-threshold-SelectOption`); await testSubjects.scrollIntoView('ruleNameInput'); await testSubjects.setValue('ruleNameInput', alertName); - await testSubjects.click(`.index-threshold-SelectOption`); await testSubjects.scrollIntoView('selectIndexExpression'); await testSubjects.click('selectIndexExpression'); await comboBox.set('thresholdIndexesComboBox', 'k'); diff --git a/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/discover/search_source_alert.ts b/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/discover/search_source_alert.ts index 3053f776aede7..ec192185829b5 100644 --- a/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/discover/search_source_alert.ts +++ b/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/discover/search_source_alert.ts @@ -550,14 +550,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.click('createRuleButton'); await PageObjects.header.waitUntilLoadingHasFinished(); + await testSubjects.click('.es-query-SelectOption'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await retry.waitFor('rule name value is correct', async () => { await testSubjects.setValue('ruleNameInput', newAlert); const ruleName = await testSubjects.getAttribute('ruleNameInput', 'value'); return ruleName === newAlert; }); - await testSubjects.click('.es-query-SelectOption'); - await PageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.click('queryFormType_searchSource'); await PageObjects.header.waitUntilLoadingHasFinished(); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts index 5d11dd17695d0..9e816e8222dfd 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts @@ -18,7 +18,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const supertest = getService('supertest'); const find = getService('find'); const retry = getService('retry'); - const browser = getService('browser'); const rules = getService('rules'); const toasts = getService('toasts'); @@ -55,8 +54,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { async function defineEsQueryAlert(alertName: string) { await pageObjects.triggersActionsUI.clickCreateAlertButton(); - await testSubjects.setValue('ruleNameInput', alertName); await testSubjects.click(`.es-query-SelectOption`); + await testSubjects.setValue('ruleNameInput', alertName); await testSubjects.click('queryFormType_esQuery'); await testSubjects.click('selectIndexExpression'); await comboBox.set('thresholdIndexesComboBox', 'k'); @@ -74,8 +73,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { async function defineAlwaysFiringAlert(alertName: string) { await pageObjects.triggersActionsUI.clickCreateAlertButton(); - await testSubjects.setValue('ruleNameInput', alertName); await testSubjects.click('test.always-firing-SelectOption'); + await testSubjects.setValue('ruleNameInput', alertName); } async function discardNewRuleCreation() { @@ -287,10 +286,12 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('should show discard confirmation before closing flyout without saving', async () => { await pageObjects.triggersActionsUI.clickCreateAlertButton(); + await testSubjects.click(`.es-query-SelectOption`); await testSubjects.click('cancelSaveRuleButton'); await testSubjects.missingOrFail('confirmRuleCloseModal'); await pageObjects.triggersActionsUI.clickCreateAlertButton(); + await testSubjects.click(`.es-query-SelectOption`); await testSubjects.setValue('ruleNameInput', 'alertName'); await testSubjects.click('cancelSaveRuleButton'); await testSubjects.existOrFail('confirmRuleCloseModal'); @@ -339,26 +340,5 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await discardNewRuleCreation(); }); - - it('should show all rule types on click euiFormControlLayoutClearButton', async () => { - await pageObjects.triggersActionsUI.clickCreateAlertButton(); - await testSubjects.setValue('ruleNameInput', 'alertName'); - const ruleTypeSearchBox = await testSubjects.find('ruleSearchField'); - await ruleTypeSearchBox.type('notexisting rule type'); - await ruleTypeSearchBox.pressKeys(browser.keys.ENTER); - - const ruleTypes = await find.allByCssSelector('.triggersActionsUI__ruleTypeNodeHeading'); - expect(ruleTypes).to.have.length(0); - - const searchClearButton = await find.byCssSelector('.euiFormControlLayoutClearButton'); - await searchClearButton.click(); - - const ruleTypesClearFilter = await find.allByCssSelector( - '.triggersActionsUI__ruleTypeNodeHeading' - ); - expect(ruleTypesClearFilter.length).to.above(0); - - await discardNewRuleCreation(); - }); }); }; diff --git a/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts b/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts index ca1a3f5c622df..ca202b2a9e28d 100644 --- a/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts +++ b/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts @@ -56,8 +56,13 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { }; const selectAndFillInEsQueryRule = async (ruleName: string) => { - await testSubjects.setValue('ruleNameInput', ruleName); await testSubjects.click(`.es-query-SelectOption`); + await retry.waitFor( + 'Create Rule flyout is visible', + async () => await testSubjects.exists('addRuleFlyoutTitle') + ); + + await testSubjects.setValue('ruleNameInput', ruleName); await testSubjects.click('queryFormType_esQuery'); await testSubjects.click('selectIndexExpression'); const indexComboBox = await find.byCssSelector('#indexSelectSearchBox'); @@ -91,8 +96,8 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { ); await observability.alerts.rulesPage.clickCreateRuleButton(); await retry.waitFor( - 'Create Rule flyout is visible', - async () => await testSubjects.exists('addRuleFlyoutTitle') + 'Rule Type Modal is visible', + async () => await testSubjects.exists('ruleTypeModal') ); }; @@ -115,7 +120,7 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => { }); describe('Create rule button', () => { - it('Show Create Rule flyout when Create Rule button is clicked', async () => { + it('Show Rule Type Modal when Create Rule button is clicked', async () => { await navigateAndOpenCreateRuleFlyout(); }); }); diff --git a/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts b/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts index 9c654b518d6f2..a28e4eae3913a 100644 --- a/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts +++ b/x-pack/test_serverless/functional/test_suites/common/discover_ml_uptime/discover/search_source_alert.ts @@ -602,14 +602,15 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await testSubjects.click('createRuleButton'); await PageObjects.header.waitUntilLoadingHasFinished(); + await testSubjects.click('.es-query-SelectOption'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await retry.waitFor('rule name value is correct', async () => { await testSubjects.setValue('ruleNameInput', newAlert); const ruleName = await testSubjects.getAttribute('ruleNameInput', 'value'); return ruleName === newAlert; }); - await testSubjects.click('.es-query-SelectOption'); - await PageObjects.header.waitUntilLoadingHasFinished(); await testSubjects.click('queryFormType_searchSource'); await PageObjects.header.waitUntilLoadingHasFinished();