From fd986432e896d4804ede18024ce1202c6ef77d6d Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 17 Dec 2024 14:28:39 +0100 Subject: [PATCH] [ML] Transforms: Support wildcards in the alerting rule flyout (#204226) ## Summary Closes #166810 - Adds wildcards support for the tranform health alerting rule. - Populates transforms with alerting rules based on wildcard expressions. - Excludes `alerting_rules` from the JSON tab. ### Checklist - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../transform_selector_control.test.tsx | 62 ++++++ .../transform_selector_control.tsx | 24 +- .../transform_list/expanded_row_json_pane.tsx | 6 + .../transform_health_service.test.ts | 209 ++++++++++++++++-- .../transform_health_service.ts | 93 ++++---- .../transform_health/rule.ts | 18 +- 6 files changed, 344 insertions(+), 68 deletions(-) create mode 100644 x-pack/platform/plugins/private/transform/public/alerting/transform_health_rule_type/transform_selector_control.test.tsx diff --git a/x-pack/platform/plugins/private/transform/public/alerting/transform_health_rule_type/transform_selector_control.test.tsx b/x-pack/platform/plugins/private/transform/public/alerting/transform_health_rule_type/transform_selector_control.test.tsx new file mode 100644 index 000000000000..79d21ca95d4e --- /dev/null +++ b/x-pack/platform/plugins/private/transform/public/alerting/transform_health_rule_type/transform_selector_control.test.tsx @@ -0,0 +1,62 @@ +/* + * 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 { render, fireEvent } from '@testing-library/react'; +import type { TransformSelectorControlProps } from './transform_selector_control'; +import { TransformSelectorControl } from './transform_selector_control'; + +describe('TransformSelectorControl', () => { + const defaultProps: TransformSelectorControlProps = { + label: 'Select Transforms', + errors: [], + onChange: jest.fn(), + selectedOptions: [], + options: ['transform1', 'transform2'], + allowSelectAll: true, + }; + + it('renders without crashing', () => { + const { getByLabelText } = render(); + expect(getByLabelText('Select Transforms')).toBeInTheDocument(); + }); + + it('displays options correctly', () => { + const { getByText } = render(); + fireEvent.click(getByText('Select Transforms')); + expect(getByText('transform1')).toBeInTheDocument(); + expect(getByText('transform2')).toBeInTheDocument(); + expect(getByText('*')).toBeInTheDocument(); + }); + + it('calls onChange with selected options', () => { + const { getByText } = render(); + fireEvent.click(getByText('Select Transforms')); + fireEvent.click(getByText('transform1')); + expect(defaultProps.onChange).toHaveBeenCalledWith(['transform1']); + }); + + it('only allows wildcards as custom options', () => { + const { getByText, getByTestId } = render(); + fireEvent.click(getByText('Select Transforms')); + const input = getByTestId('comboBoxSearchInput'); + + fireEvent.change(input, { target: { value: 'custom' } }); + fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' }); + expect(defaultProps.onChange).not.toHaveBeenCalledWith(['custom']); + + fireEvent.change(input, { target: { value: 'custom*' } }); + fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' }); + expect(defaultProps.onChange).toHaveBeenCalledWith(['custom*']); + }); + + it('displays errors correctly', () => { + const errorProps = { ...defaultProps, errors: ['Error message'] }; + const { getByText } = render(); + expect(getByText('Error message')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/platform/plugins/private/transform/public/alerting/transform_health_rule_type/transform_selector_control.tsx b/x-pack/platform/plugins/private/transform/public/alerting/transform_health_rule_type/transform_selector_control.tsx index 97c7c74823c0..bbb1c77e50ed 100644 --- a/x-pack/platform/plugins/private/transform/public/alerting/transform_health_rule_type/transform_selector_control.tsx +++ b/x-pack/platform/plugins/private/transform/public/alerting/transform_health_rule_type/transform_selector_control.tsx @@ -5,11 +5,12 @@ * 2.0. */ -import type { EuiComboBoxProps } from '@elastic/eui'; +import type { EuiComboBoxOptionsListProps, EuiComboBoxProps } from '@elastic/eui'; import { EuiComboBox, EuiFormRow } from '@elastic/eui'; import type { FC } from 'react'; -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import { isDefined } from '@kbn/ml-is-defined'; +import { i18n } from '@kbn/i18n'; import { ALL_TRANSFORMS_SELECTION } from '../../../common/constants'; export interface TransformSelectorControlProps { @@ -33,6 +34,8 @@ export const TransformSelectorControl: FC = ({ options, allowSelectAll = false, }) => { + const [allowCustomOptions, setAllowCustomOptions] = useState(false); + const onSelectionChange: EuiComboBoxProps['onChange'] = ((selectionUpdate) => { if (!selectionUpdate?.length) { onChange([]); @@ -50,6 +53,12 @@ export const TransformSelectorControl: FC = ({ ); }) as Exclude['onChange'], undefined>; + const onCreateOption = allowCustomOptions + ? (((searchValue) => { + onChange([...selectedOptions, searchValue]); + }) as EuiComboBoxOptionsListProps['onCreateOption']) + : undefined; + const selectedOptionsEui = useMemo(() => convertToEuiOptions(selectedOptions), [selectedOptions]); const optionsEui = useMemo(() => { return convertToEuiOptions(allowSelectAll ? [ALL_TRANSFORMS_SELECTION, ...options] : options); @@ -58,6 +67,17 @@ export const TransformSelectorControl: FC = ({ return ( + onSearchChange={(searchValue, hasMatchingOption) => { + setAllowCustomOptions(!hasMatchingOption && searchValue.includes('*')); + }} + onCreateOption={onCreateOption} + customOptionText={i18n.translate( + 'xpack.transform.alertTypes.transformHealth.customOptionText', + { + defaultMessage: 'Include {searchValuePlaceholder} wildcard', + values: { searchValuePlaceholder: '{searchValue}' }, + } + )} singleSelection={false} selectedOptions={selectedOptionsEui} options={optionsEui} diff --git a/x-pack/platform/plugins/private/transform/public/app/sections/transform_management/components/transform_list/expanded_row_json_pane.tsx b/x-pack/platform/plugins/private/transform/public/app/sections/transform_management/components/transform_list/expanded_row_json_pane.tsx index 48e17d915722..60c47ecc81f8 100644 --- a/x-pack/platform/plugins/private/transform/public/app/sections/transform_management/components/transform_list/expanded_row_json_pane.tsx +++ b/x-pack/platform/plugins/private/transform/public/app/sections/transform_management/components/transform_list/expanded_row_json_pane.tsx @@ -16,6 +16,12 @@ interface Props { } export const ExpandedRowJsonPane: FC = ({ json }) => { + // exclude alerting rules from the JSON + if ('alerting_rules' in json) { + const { alerting_rules: alertingRules, ...rest } = json; + json = rest; + } + return (
diff --git a/x-pack/platform/plugins/private/transform/server/lib/alerting/transform_health_rule_type/transform_health_service.test.ts b/x-pack/platform/plugins/private/transform/server/lib/alerting/transform_health_rule_type/transform_health_service.test.ts index ff502c9fcdc2..a799fbe0499f 100644 --- a/x-pack/platform/plugins/private/transform/server/lib/alerting/transform_health_rule_type/transform_health_service.test.ts +++ b/x-pack/platform/plugins/private/transform/server/lib/alerting/transform_health_rule_type/transform_health_service.test.ts @@ -5,16 +5,18 @@ * 2.0. */ -import { transformHealthServiceProvider } from './transform_health_service'; -import type { ElasticsearchClient } from '@kbn/core/server'; -import type { RulesClient } from '@kbn/alerting-plugin/server'; -import type { FieldFormatsRegistry } from '@kbn/field-formats-plugin/common'; -import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; -import { rulesClientMock } from '@kbn/alerting-plugin/server/rules_client.mock'; import type { TransformGetTransformResponse, TransformGetTransformStatsResponse, + TransformGetTransformTransformSummary, } from '@elastic/elasticsearch/lib/api/types'; +import type { FindResult, RulesClient } from '@kbn/alerting-plugin/server'; +import { rulesClientMock } from '@kbn/alerting-plugin/server/rules_client.mock'; +import { elasticsearchServiceMock } from '@kbn/core-elasticsearch-server-mocks'; +import type { ElasticsearchClient } from '@kbn/core/server'; +import type { FieldFormatsRegistry } from '@kbn/field-formats-plugin/common'; +import { transformHealthServiceProvider } from './transform_health_service'; +import type { TransformHealthRuleParams } from './schema'; describe('transformHealthServiceProvider', () => { let esClient: jest.Mocked; @@ -24,20 +26,48 @@ describe('transformHealthServiceProvider', () => { beforeEach(() => { esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; - (esClient.transform.getTransform as jest.Mock).mockResolvedValue({ - count: 3, - transforms: [ - // Mock continuous transforms - ...new Array(102).fill(null).map((_, i) => ({ - id: `transform${i}`, - sync: true, - })), - { - id: 'transform102', - sync: false, - }, - ], - } as unknown as TransformGetTransformResponse); + (esClient.transform.getTransform as jest.Mock).mockImplementation( + async ({ transform_id: transformId }) => { + if (transformId === 'transform4,transform6,transform6*') { + // arrangement for exclude transforms + return { + transforms: [ + { + id: `transform4`, + sync: true, + }, + { + id: `transform6`, + sync: true, + }, + ...new Array(10).fill(null).map((_, i) => ({ + id: `transform6${i}`, + sync: true, + })), + ], + } as unknown as TransformGetTransformResponse; + } else { + return { + transforms: [ + // Mock continuous transforms + ...new Array(102).fill(null).map((_, i) => ({ + id: `transform${i}`, + sync: { + time: { + field: 'order_date', + delay: '60s', + }, + }, + })), + { + id: 'transform102', + }, + ], + } as unknown as TransformGetTransformResponse; + } + } + ); + (esClient.transform.getTransformStats as jest.Mock).mockResolvedValue({ count: 2, transforms: [{}], @@ -57,19 +87,27 @@ describe('transformHealthServiceProvider', () => { const service = transformHealthServiceProvider({ esClient, rulesClient, fieldFormatsRegistry }); const result = await service.getHealthChecksResults({ includeTransforms: ['*'], - excludeTransforms: ['transform4', 'transform6', 'transform62'], + excludeTransforms: ['transform4', 'transform6', 'transform6*'], testsConfig: null, }); + expect(esClient.transform.getTransform).toHaveBeenCalledTimes(2); + + expect(esClient.transform.getTransform).toHaveBeenCalledWith({ + allow_no_match: true, + size: 1000, + }); expect(esClient.transform.getTransform).toHaveBeenCalledWith({ + transform_id: 'transform4,transform6,transform6*', allow_no_match: true, size: 1000, }); + expect(esClient.transform.getTransformStats).toHaveBeenCalledTimes(1); expect(esClient.transform.getTransformStats).toHaveBeenNthCalledWith(1, { basic: true, transform_id: - 'transform0,transform1,transform2,transform3,transform5,transform7,transform8,transform9,transform10,transform11,transform12,transform13,transform14,transform15,transform16,transform17,transform18,transform19,transform20,transform21,transform22,transform23,transform24,transform25,transform26,transform27,transform28,transform29,transform30,transform31,transform32,transform33,transform34,transform35,transform36,transform37,transform38,transform39,transform40,transform41,transform42,transform43,transform44,transform45,transform46,transform47,transform48,transform49,transform50,transform51,transform52,transform53,transform54,transform55,transform56,transform57,transform58,transform59,transform60,transform61,transform63,transform64,transform65,transform66,transform67,transform68,transform69,transform70,transform71,transform72,transform73,transform74,transform75,transform76,transform77,transform78,transform79,transform80,transform81,transform82,transform83,transform84,transform85,transform86,transform87,transform88,transform89,transform90,transform91,transform92,transform93,transform94,transform95,transform96,transform97,transform98,transform99,transform100,transform101', + 'transform0,transform1,transform2,transform3,transform5,transform7,transform8,transform9,transform10,transform11,transform12,transform13,transform14,transform15,transform16,transform17,transform18,transform19,transform20,transform21,transform22,transform23,transform24,transform25,transform26,transform27,transform28,transform29,transform30,transform31,transform32,transform33,transform34,transform35,transform36,transform37,transform38,transform39,transform40,transform41,transform42,transform43,transform44,transform45,transform46,transform47,transform48,transform49,transform50,transform51,transform52,transform53,transform54,transform55,transform56,transform57,transform58,transform59,transform70,transform71,transform72,transform73,transform74,transform75,transform76,transform77,transform78,transform79,transform80,transform81,transform82,transform83,transform84,transform85,transform86,transform87,transform88,transform89,transform90,transform91,transform92,transform93,transform94,transform95,transform96,transform97,transform98,transform99,transform100,transform101', }); expect(result).toBeDefined(); @@ -126,4 +164,131 @@ describe('transformHealthServiceProvider', () => { 'Transform transform_with_a_very_long_id_that_result_in_long_url_for_sure_0, transform_with_a_very_long_id_that_result_in_long_url_for_sure_1, transform_with_a_very_long_id_that_result_in_long_url_for_sure_2, transform_with_a_very_long_id_that_result_in_long_url_for_sure_3, transform_with_a_very_long_id_that_result_in_long_url_for_sure_4, transform_with_a_very_long_id_that_result_in_long_url_for_sure_5, transform_with_a_very_long_id_that_result_in_long_url_for_sure_6, transform_with_a_very_long_id_that_result_in_long_url_for_sure_7, transform_with_a_very_long_id_that_result_in_long_url_for_sure_8, transform_with_a_very_long_id_that_result_in_long_url_for_sure_9, transform_with_a_very_long_id_that_result_in_long_url_for_sure_10, transform_with_a_very_long_id_that_result_in_long_url_for_sure_11, transform_with_a_very_long_id_that_result_in_long_url_for_sure_12, transform_with_a_very_long_id_that_result_in_long_url_for_sure_13, transform_with_a_very_long_id_that_result_in_long_url_for_sure_14, transform_with_a_very_long_id_that_result_in_long_url_for_sure_15, transform_with_a_very_long_id_that_result_in_long_url_for_sure_16, transform_with_a_very_long_id_that_result_in_long_url_for_sure_17, transform_with_a_very_long_id_that_result_in_long_url_for_sure_18, transform_with_a_very_long_id_that_result_in_long_url_for_sure_19, transform_with_a_very_long_id_that_result_in_long_url_for_sure_20, transform_with_a_very_long_id_that_result_in_long_url_for_sure_21, transform_with_a_very_long_id_that_result_in_long_url_for_sure_22, transform_with_a_very_long_id_that_result_in_long_url_for_sure_23, transform_with_a_very_long_id_that_result_in_long_url_for_sure_24, transform_with_a_very_long_id_that_result_in_long_url_for_sure_25, transform_with_a_very_long_id_that_result_in_long_url_for_sure_26, transform_with_a_very_long_id_that_result_in_long_url_for_sure_27, transform_with_a_very_long_id_that_result_in_long_url_for_sure_28, transform_with_a_very_long_id_that_result_in_long_url_for_sure_29, transform_with_a_very_long_id_that_result_in_long_url_for_sure_30, transform_with_a_very_long_id_that_result_in_long_url_for_sure_31, transform_with_a_very_long_id_that_result_in_long_url_for_sure_32, transform_with_a_very_long_id_that_result_in_long_url_for_sure_33, transform_with_a_very_long_id_that_result_in_long_url_for_sure_34, transform_with_a_very_long_id_that_result_in_long_url_for_sure_35, transform_with_a_very_long_id_that_result_in_long_url_for_sure_36, transform_with_a_very_long_id_that_result_in_long_url_for_sure_37, transform_with_a_very_long_id_that_result_in_long_url_for_sure_38, transform_with_a_very_long_id_that_result_in_long_url_for_sure_39, transform_with_a_very_long_id_that_result_in_long_url_for_sure_40, transform_with_a_very_long_id_that_result_in_long_url_for_sure_41, transform_with_a_very_long_id_that_result_in_long_url_for_sure_42, transform_with_a_very_long_id_that_result_in_long_url_for_sure_43, transform_with_a_very_long_id_that_result_in_long_url_for_sure_44, transform_with_a_very_long_id_that_result_in_long_url_for_sure_45, transform_with_a_very_long_id_that_result_in_long_url_for_sure_46, transform_with_a_very_long_id_that_result_in_long_url_for_sure_47, transform_with_a_very_long_id_that_result_in_long_url_for_sure_48, transform_with_a_very_long_id_that_result_in_long_url_for_sure_49, transform_with_a_very_long_id_that_result_in_long_url_for_sure_50, transform_with_a_very_long_id_that_result_in_long_url_for_sure_51, transform_with_a_very_long_id_that_result_in_long_url_for_sure_52, transform_with_a_very_long_id_that_result_in_long_url_for_sure_53, transform_with_a_very_long_id_that_result_in_long_url_for_sure_54, transform_with_a_very_long_id_that_result_in_long_url_for_sure_55, transform_with_a_very_long_id_that_result_in_long_url_for_sure_56, transform_with_a_very_long_id_that_result_in_long_url_for_sure_57, transform_with_a_very_long_id_that_result_in_long_url_for_sure_58, transform_with_a_very_long_id_that_result_in_long_url_for_sure_59 are not started.' ); }); + + describe('populateTransformsWithAssignedRules', () => { + it('should throw an error if rulesClient is missing', async () => { + const service = transformHealthServiceProvider({ esClient, fieldFormatsRegistry }); + + await expect(service.populateTransformsWithAssignedRules([])).rejects.toThrow( + 'Rules client is missing' + ); + }); + + it('should return an empty list if no transforms are provided', async () => { + const service = transformHealthServiceProvider({ + esClient, + rulesClient, + fieldFormatsRegistry, + }); + + const result = await service.populateTransformsWithAssignedRules([]); + expect(result).toEqual([]); + }); + + it('should return transforms with associated alerting rules', async () => { + const transforms = [ + { id: 'transform1', sync: {} }, + { id: 'transform2', sync: {} }, + { id: 'transform3', sync: {} }, + ] as TransformGetTransformTransformSummary[]; + + const rules = [ + { + id: 'rule1', + params: { + includeTransforms: ['transform1', 'transform2'], + excludeTransforms: [], + }, + }, + { + id: 'rule2', + params: { + includeTransforms: ['transform3'], + excludeTransforms: null, + }, + }, + ]; + + rulesClient.find.mockResolvedValue({ data: rules } as FindResult); + + const service = transformHealthServiceProvider({ + esClient, + rulesClient, + fieldFormatsRegistry, + }); + + const result = await service.populateTransformsWithAssignedRules(transforms); + + expect(result).toEqual([ + { + id: 'transform1', + sync: {}, + alerting_rules: [rules[0]], + }, + { + id: 'transform2', + sync: {}, + alerting_rules: [rules[0]], + }, + { + id: 'transform3', + sync: {}, + alerting_rules: [rules[1]], + }, + ]); + }); + + it('should exclude transforms based on excludeTransforms parameter', async () => { + const transforms = [ + { id: 'transform1', sync: {} }, + { id: 'transform2', sync: {} }, + { id: 'transform3', sync: {} }, + ] as TransformGetTransformTransformSummary[]; + + const rules = [ + { + id: 'rule1', + params: { + includeTransforms: ['transform*'], + excludeTransforms: ['transform2'], + }, + }, + { + id: 'rule2', + params: { + includeTransforms: ['*'], + excludeTransforms: [], + }, + }, + ]; + + rulesClient.find.mockResolvedValue({ data: rules } as FindResult); + + const service = transformHealthServiceProvider({ + esClient, + rulesClient, + fieldFormatsRegistry, + }); + + const result = await service.populateTransformsWithAssignedRules(transforms); + + expect(result).toEqual([ + { + id: 'transform1', + sync: {}, + alerting_rules: [rules[0], rules[1]], + }, + { + id: 'transform2', + sync: {}, + alerting_rules: [rules[1]], + }, + { + id: 'transform3', + sync: {}, + alerting_rules: [rules[0], rules[1]], + }, + ]); + }); + }); }); diff --git a/x-pack/platform/plugins/private/transform/server/lib/alerting/transform_health_rule_type/transform_health_service.ts b/x-pack/platform/plugins/private/transform/server/lib/alerting/transform_health_rule_type/transform_health_service.ts index 939d4e01a6f1..288cb86842db 100644 --- a/x-pack/platform/plugins/private/transform/server/lib/alerting/transform_health_rule_type/transform_health_service.ts +++ b/x-pack/platform/plugins/private/transform/server/lib/alerting/transform_health_rule_type/transform_health_service.ts @@ -38,11 +38,7 @@ interface TestResult { context: TransformHealthAlertContext; } -type Transform = estypes.TransformGetTransformTransformSummary & { - id: string; - description?: string; - sync: object; -}; +type Transform = estypes.TransformGetTransformTransformSummary; type TransformWithAlertingRules = Transform & { alerting_rules: TransformHealthAlertRule[] }; @@ -63,40 +59,44 @@ export function transformHealthServiceProvider({ * Resolves result transform selection. Only continuously running transforms are included. * @param includeTransforms * @param excludeTransforms - * @param skipIDsCheck */ const getResultsTransformIds = async ( includeTransforms: string[], - excludeTransforms: string[] | null, - skipIDsCheck = false + excludeTransforms: string[] | null ): Promise> => { const includeAll = includeTransforms.some((id) => id === ALL_TRANSFORMS_SELECTION); let resultTransformIds: string[] = []; - if (skipIDsCheck) { - resultTransformIds = includeTransforms; - } else { - // Fetch transforms to make sure assigned transforms exists. - const transformsResponse = ( - await esClient.transform.getTransform({ - ...(includeAll ? {} : { transform_id: includeTransforms.join(',') }), - allow_no_match: true, - size: 1000, - }) - ).transforms as Transform[]; - - transformsResponse.forEach((t) => { - transformsDict.set(t.id, t); - // Include only continuously running transforms. - if (t.sync) { - resultTransformIds.push(t.id); - } - }); - } + // Fetch transforms to make sure assigned transforms exists. + const transformsResponse = ( + await esClient.transform.getTransform({ + ...(includeAll ? {} : { transform_id: includeTransforms.join(',') }), + allow_no_match: true, + size: 1000, + }) + ).transforms as Transform[]; + + transformsResponse.forEach((t) => { + transformsDict.set(t.id, t); + // Include only continuously running transforms. + if (isContinuousTransform(t)) { + resultTransformIds.push(t.id); + } + }); if (excludeTransforms && excludeTransforms.length > 0) { - const excludeIdsSet = new Set(excludeTransforms); + let excludeIdsSet = new Set(excludeTransforms); + if (excludeTransforms.some((id) => id.includes('*'))) { + const excludeTransformResponse = ( + await esClient.transform.getTransform({ + transform_id: excludeTransforms.join(','), + allow_no_match: true, + size: 1000, + }) + ).transforms as Transform[]; + excludeIdsSet = new Set(excludeTransformResponse.map((t) => t.id)); + } resultTransformIds = resultTransformIds.filter((id) => !excludeIdsSet.has(id)); } @@ -381,13 +381,19 @@ export function transformHealthServiceProvider({ async populateTransformsWithAssignedRules( transforms: Transform[] ): Promise { - const newList = transforms.filter(isContinuousTransform) as TransformWithAlertingRules[]; + const continuousTransforms = transforms.filter( + isContinuousTransform + ) as TransformWithAlertingRules[]; if (!rulesClient) { throw new Error('Rules client is missing'); } - const transformMap = keyBy(newList, 'id'); + if (!continuousTransforms.length) { + return transforms as TransformWithAlertingRules[]; + } + + const transformMap = keyBy(continuousTransforms, 'id'); const transformAlertingRules = await rulesClient.find({ options: { @@ -398,12 +404,23 @@ export function transformHealthServiceProvider({ for (const ruleInstance of transformAlertingRules.data) { // Retrieve result transform IDs - const resultTransformIds = await getResultsTransformIds( - ruleInstance.params.includeTransforms.includes(ALL_TRANSFORMS_SELECTION) - ? Object.keys(transformMap) - : ruleInstance.params.includeTransforms, - ruleInstance.params.excludeTransforms, - true + const { includeTransforms, excludeTransforms } = ruleInstance.params; + + const resultTransformIds = new Set( + transforms + .filter( + (t) => + includeTransforms.some((includedTransformId) => + new RegExp(includedTransformId.replace(/\*/g, '.*')).test(t.id) + ) && + (Array.isArray(excludeTransforms) && excludeTransforms.length > 0 + ? excludeTransforms.every( + (excludedTransformId) => + new RegExp(excludedTransformId.replace(/\*/g, '.*')).test(t.id) === false + ) + : true) + ) + .map((t) => t.id) ); resultTransformIds.forEach((transformId) => { @@ -419,7 +436,7 @@ export function transformHealthServiceProvider({ }); } - return newList; + return continuousTransforms; }, }; } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/transform_rule_types/transform_health/rule.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/transform_rule_types/transform_health/rule.ts index f760ac26f40c..ca0ba9a92f64 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/transform_rule_types/transform_health/rule.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group2/transform_rule_types/transform_health/rule.ts @@ -82,7 +82,6 @@ export default function ruleTests({ getService }: FtrProviderContext) { const objectRemover = new ObjectRemover(supertest); let connectorId: string; const transformId = 'test_transform_01'; - const destinationIndex = generateDestIndex(transformId); beforeEach(async () => { await esTestIndexTool.destroy(); @@ -98,8 +97,11 @@ export default function ruleTests({ getService }: FtrProviderContext) { connectorId = await createConnector(); - await transform.api.createIndices(destinationIndex); await createTransform(transformId); + + // Create additional transforms to exclude from the rule + await createTransform('exclude_transform_01'); + await createTransform('exclude_transform_02'); }); afterEach(async () => { @@ -112,10 +114,12 @@ export default function ruleTests({ getService }: FtrProviderContext) { it('runs correctly', async () => { await stopTransform(transformId); + await stopTransform('exclude_transform_01'); const ruleId = await createRule({ name: 'Test all transforms', includeTransforms: ['*'], + excludeTransforms: ['exclude_transform_*'], }); log.debug('Checking created alerts...'); @@ -160,6 +164,8 @@ export default function ruleTests({ getService }: FtrProviderContext) { } async function createTransform(id: string) { + const destinationIndex = generateDestIndex(id); + await transform.api.createIndices(destinationIndex); const config = generateTransformConfig(id); await transform.api.createAndRunTransform(id, config); } @@ -183,20 +189,20 @@ export default function ruleTests({ getService }: FtrProviderContext) { }, }; + const { name, ...transformHealthRuleParams } = params; + const { status, body: createdRule } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) .set('kbn-xsrf', 'foo') .send({ - name: params.name, + name, consumer: 'alerts', enabled: true, rule_type_id: RULE_TYPE_ID, schedule: { interval: '1d' }, actions: [action], notify_when: 'onActiveAlert', - params: { - includeTransforms: params.includeTransforms, - }, + params: transformHealthRuleParams, }); // will print the error body, if an error occurred