[8.17] [ML] Transforms: Support wildcards in the alerting rule flyout (#204226) (#204713)

# Backport

This will backport the following commits from `main` to `8.17`:
- [[ML] Transforms: Support wildcards in the alerting rule flyout
(#204226)](https://github.com/elastic/kibana/pull/204226)

<!--- Backport version: 8.9.8 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Dima
Arnautov","email":"dmitrii.arnautov@elastic.co"},"sourceCommit":{"committedDate":"2024-12-17T13:28:39Z","message":"[ML]
Transforms: Support wildcards in the alerting rule flyout
(#204226)\n\n## Summary\r\n\r\n\r\nCloses #166810\r\n\r\n- Adds
wildcards support for the tranform health alerting rule. \r\n- Populates
transforms with alerting rules based on wildcard\r\nexpressions.\r\n-
Excludes `alerting_rules` from the JSON tab. \r\n\r\n###
Checklist\r\n\r\n- [x] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[
]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas
added for features that require explanation or tutorials\r\n- [x] [Unit
or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios","sha":"fd986432e896d4804ede18024ce1202c6ef77d6d","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:enhancement",":ml","Feature:Transforms","v9.0.0","Team:ML","backport:version","v8.18.0","v8.17.1"],"number":204226,"url":"https://github.com/elastic/kibana/pull/204226","mergeCommit":{"message":"[ML]
Transforms: Support wildcards in the alerting rule flyout
(#204226)\n\n## Summary\r\n\r\n\r\nCloses #166810\r\n\r\n- Adds
wildcards support for the tranform health alerting rule. \r\n- Populates
transforms with alerting rules based on wildcard\r\nexpressions.\r\n-
Excludes `alerting_rules` from the JSON tab. \r\n\r\n###
Checklist\r\n\r\n- [x] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[
]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas
added for features that require explanation or tutorials\r\n- [x] [Unit
or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios","sha":"fd986432e896d4804ede18024ce1202c6ef77d6d"}},"sourceBranch":"main","suggestedTargetBranches":["8.17"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","labelRegex":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/204226","number":204226,"mergeCommit":{"message":"[ML]
Transforms: Support wildcards in the alerting rule flyout
(#204226)\n\n## Summary\r\n\r\n\r\nCloses #166810\r\n\r\n- Adds
wildcards support for the tranform health alerting rule. \r\n- Populates
transforms with alerting rules based on wildcard\r\nexpressions.\r\n-
Excludes `alerting_rules` from the JSON tab. \r\n\r\n###
Checklist\r\n\r\n- [x] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[
]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas
added for features that require explanation or tutorials\r\n- [x] [Unit
or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common
scenarios","sha":"fd986432e896d4804ede18024ce1202c6ef77d6d"}},{"branch":"8.x","label":"v8.18.0","labelRegex":"^v8.18.0$","isSourceBranch":false,"url":"https://github.com/elastic/kibana/pull/204575","number":204575,"state":"MERGED","mergeCommit":{"sha":"dff3cc2943ec1ea9981dab84326a6e948456cd07","message":"[8.x]
[ML] Transforms: Support wildcards in the alerting rule flyout (#204226)
(#204575)\n\n# Backport\n\nThis will backport the following commits from
`main` to `8.x`:\n- [[ML] Transforms: Support wildcards in the alerting
rule
flyout\n(#204226)](https://github.com/elastic/kibana/pull/204226)\n\n<!---
Backport version: 9.4.3 -->\n\n### Questions ?\nPlease refer to the
[Backport
tool\ndocumentation](https://github.com/sqren/backport)\n\n<!--BACKPORT
[{\"author\":{\"name\":\"Dima\nArnautov\",\"email\":\"dmitrii.arnautov@elastic.co\"},\"sourceCommit\":{\"committedDate\":\"2024-12-17T13:28:39Z\",\"message\":\"[ML]\nTransforms:
Support wildcards in the alerting rule flyout\n(#204226)\\n\\n##
Summary\\r\\n\\r\\n\\r\\nCloses #166810\\r\\n\\r\\n- Adds\nwildcards
support for the tranform health alerting rule. \\r\\n-
Populates\ntransforms with alerting rules based on
wildcard\\r\\nexpressions.\\r\\n-\nExcludes `alerting_rules` from the
JSON tab. \\r\\n\\r\\n###\nChecklist\\r\\n\\r\\n- [x] Any text added
follows
[EUI's\nwriting\\r\\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),\nuses\\r\\nsentence
case text and
includes\n[i18n\\r\\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\\r\\n-\n[\n]\\r\\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\\r\\nwas\nadded
for features that require explanation or tutorials\\r\\n- [x]
[Unit\nor\nfunctional\\r\\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\\r\\nwere\nupdated
or added to match the most
common\nscenarios\",\"sha\":\"fd986432e896d4804ede18024ce1202c6ef77d6d\",\"branchLabelMapping\":{\"^v9.0.0$\":\"main\",\"^v8.18.0$\":\"8.x\",\"^v(\\\\d+).(\\\\d+).\\\\d+$\":\"$1.$2\"}},\"sourcePullRequest\":{\"labels\":[\"release_note:enhancement\",\":ml\",\"Feature:Transforms\",\"v9.0.0\",\"Team:ML\",\"backport:version\",\"v8.18.0\",\"v8.17.1\"],\"title\":\"[ML]\nTransforms:
Support wildcards in the alerting
rule\nflyout\",\"number\":204226,\"url\":\"https://github.com/elastic/kibana/pull/204226\",\"mergeCommit\":{\"message\":\"[ML]\nTransforms:
Support wildcards in the alerting rule flyout\n(#204226)\\n\\n##
Summary\\r\\n\\r\\n\\r\\nCloses #166810\\r\\n\\r\\n- Adds\nwildcards
support for the tranform health alerting rule. \\r\\n-
Populates\ntransforms with alerting rules based on
wildcard\\r\\nexpressions.\\r\\n-\nExcludes `alerting_rules` from the
JSON tab. \\r\\n\\r\\n###\nChecklist\\r\\n\\r\\n- [x] Any text added
follows
[EUI's\nwriting\\r\\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),\nuses\\r\\nsentence
case text and
includes\n[i18n\\r\\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\\r\\n-\n[\n]\\r\\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\\r\\nwas\nadded
for features that require explanation or tutorials\\r\\n- [x]
[Unit\nor\nfunctional\\r\\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\\r\\nwere\nupdated
or added to match the most
common\nscenarios\",\"sha\":\"fd986432e896d4804ede18024ce1202c6ef77d6d\"}},\"sourceBranch\":\"main\",\"suggestedTargetBranches\":[\"8.x\",\"8.17\"],\"targetPullRequestStates\":[{\"branch\":\"main\",\"label\":\"v9.0.0\",\"branchLabelMappingKey\":\"^v9.0.0$\",\"isSourceBranch\":true,\"state\":\"MERGED\",\"url\":\"https://github.com/elastic/kibana/pull/204226\",\"number\":204226,\"mergeCommit\":{\"message\":\"[ML]\nTransforms:
Support wildcards in the alerting rule flyout\n(#204226)\\n\\n##
Summary\\r\\n\\r\\n\\r\\nCloses #166810\\r\\n\\r\\n- Adds\nwildcards
support for the tranform health alerting rule. \\r\\n-
Populates\ntransforms with alerting rules based on
wildcard\\r\\nexpressions.\\r\\n-\nExcludes `alerting_rules` from the
JSON tab. \\r\\n\\r\\n###\nChecklist\\r\\n\\r\\n- [x] Any text added
follows
[EUI's\nwriting\\r\\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),\nuses\\r\\nsentence
case text and
includes\n[i18n\\r\\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\\r\\n-\n[\n]\\r\\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\\r\\nwas\nadded
for features that require explanation or tutorials\\r\\n- [x]
[Unit\nor\nfunctional\\r\\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\\r\\nwere\nupdated
or added to match the most
common\nscenarios\",\"sha\":\"fd986432e896d4804ede18024ce1202c6ef77d6d\"}},{\"branch\":\"8.x\",\"label\":\"v8.18.0\",\"branchLabelMappingKey\":\"^v8.18.0$\",\"isSourceBranch\":false,\"state\":\"NOT_CREATED\"},{\"branch\":\"8.17\",\"label\":\"v8.17.1\",\"branchLabelMappingKey\":\"^v(\\\\d+).(\\\\d+).\\\\d+$\",\"isSourceBranch\":false,\"state\":\"NOT_CREATED\"}]}]\nBACKPORT-->\n\nCo-authored-by:
Dima Arnautov
<dmitrii.arnautov@elastic.co>"}},{"branch":"8.17","label":"v8.17.1","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->
This commit is contained in:
Dima Arnautov 2024-12-18 13:34:58 +01:00 committed by GitHub
parent 624406005f
commit 50701e0495
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 343 additions and 67 deletions

View file

@ -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(<TransformSelectorControl {...defaultProps} />);
expect(getByLabelText('Select Transforms')).toBeInTheDocument();
});
it('displays options correctly', () => {
const { getByText } = render(<TransformSelectorControl {...defaultProps} />);
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(<TransformSelectorControl {...defaultProps} />);
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(<TransformSelectorControl {...defaultProps} />);
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(<TransformSelectorControl {...errorProps} />);
expect(getByText('Error message')).toBeInTheDocument();
});
});

View file

@ -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<TransformSelectorControlProps> = ({
options,
allowSelectAll = false,
}) => {
const [allowCustomOptions, setAllowCustomOptions] = useState(false);
const onSelectionChange: EuiComboBoxProps<string>['onChange'] = ((selectionUpdate) => {
if (!selectionUpdate?.length) {
onChange([]);
@ -50,6 +53,12 @@ export const TransformSelectorControl: FC<TransformSelectorControlProps> = ({
);
}) as Exclude<EuiComboBoxProps<string>['onChange'], undefined>;
const onCreateOption = allowCustomOptions
? (((searchValue) => {
onChange([...selectedOptions, searchValue]);
}) as EuiComboBoxOptionsListProps<string>['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<TransformSelectorControlProps> = ({
return (
<EuiFormRow fullWidth label={label} isInvalid={!!errors?.length} error={errors}>
<EuiComboBox<string>
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}

View file

@ -16,6 +16,12 @@ interface Props {
}
export const ExpandedRowJsonPane: FC<Props> = ({ json }) => {
// exclude alerting rules from the JSON
if ('alerting_rules' in json) {
const { alerting_rules: alertingRules, ...rest } = json;
json = rest;
}
return (
<div data-test-subj="transformJsonTabContent">
<EuiFlexGroup>

View file

@ -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<ElasticsearchClient>;
@ -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<TransformHealthRuleParams>);
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<TransformHealthRuleParams>);
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]],
},
]);
});
});
});

View file

@ -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<Set<string>> => {
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[];
// 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);
}
});
}
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<TransformWithAlertingRules[]> {
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<TransformHealthRuleParams>({
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;
},
};
}

View file

@ -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