diff --git a/.github/workflows/create-deploy-tag.yml b/.github/workflows/create-deploy-tag.yml index 85e226d384cc2..76bf21c0b723d 100644 --- a/.github/workflows/create-deploy-tag.yml +++ b/.github/workflows/create-deploy-tag.yml @@ -102,7 +102,8 @@ jobs: "", "", " (use Elastic Cloud Staging VPN)", - "", + "", + "", "" ] - name: Post Slack failure message diff --git a/packages/kbn-alerts-ui-shared/src/maintenance_window_callout/api.ts b/packages/kbn-alerts-ui-shared/src/maintenance_window_callout/api.ts index fede1a30c9035..13f86bd2996c4 100644 --- a/packages/kbn-alerts-ui-shared/src/maintenance_window_callout/api.ts +++ b/packages/kbn-alerts-ui-shared/src/maintenance_window_callout/api.ts @@ -6,15 +6,45 @@ * Side Public License, v 1. */ -import { KibanaServices } from './types'; +import { AsApiContract } from '@kbn/actions-plugin/common'; +import type { KibanaServices, MaintenanceWindow } from './types'; + +const rewriteMaintenanceWindowRes = ({ + expiration_date: expirationDate, + r_rule: rRule, + created_by: createdBy, + updated_by: updatedBy, + created_at: createdAt, + updated_at: updatedAt, + event_start_time: eventStartTime, + event_end_time: eventEndTime, + category_ids: categoryIds, + ...rest +}: AsApiContract): MaintenanceWindow => ({ + ...rest, + expirationDate, + rRule, + createdBy, + updatedBy, + createdAt, + updatedAt, + eventStartTime, + eventEndTime, + categoryIds, +}); export const fetchActiveMaintenanceWindows = async ( http: KibanaServices['http'], signal?: AbortSignal -) => - http.fetch(INTERNAL_ALERTING_API_GET_ACTIVE_MAINTENANCE_WINDOWS_PATH, { - method: 'GET', - signal, - }); +): Promise => { + const result = await http.fetch>>( + INTERNAL_ALERTING_API_GET_ACTIVE_MAINTENANCE_WINDOWS_PATH, + { + method: 'GET', + signal, + } + ); + return result.map((mw) => rewriteMaintenanceWindowRes(mw)); +}; const INTERNAL_ALERTING_API_GET_ACTIVE_MAINTENANCE_WINDOWS_PATH = `/internal/alerting/rules/maintenance_window/_active`; diff --git a/packages/kbn-alerts-ui-shared/src/maintenance_window_callout/index.test.tsx b/packages/kbn-alerts-ui-shared/src/maintenance_window_callout/index.test.tsx index 9a05a6bd09222..7ac73ddc26c19 100644 --- a/packages/kbn-alerts-ui-shared/src/maintenance_window_callout/index.test.tsx +++ b/packages/kbn-alerts-ui-shared/src/maintenance_window_callout/index.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { I18nProvider } from '@kbn/i18n-react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { render, waitFor, cleanup } from '@testing-library/react'; +import { render, waitFor, cleanup, screen } from '@testing-library/react'; import { MAINTENANCE_WINDOW_FEATURE_ID } from '@kbn/alerting-plugin/common'; import { MaintenanceWindowCallout } from '.'; import { fetchActiveMaintenanceWindows } from './api'; @@ -215,4 +215,56 @@ describe('MaintenanceWindowCallout', () => { expect(await findByText('Maintenance window is running')).toBeInTheDocument(); }); + + it('should display the callout if the category ids contains the specified category', async () => { + fetchActiveMaintenanceWindowsMock.mockResolvedValue([ + { + ...RUNNING_MAINTENANCE_WINDOW_1, + categoryIds: ['observability'], + }, + ]); + + render( + , + { + wrapper: TestProviders, + } + ); + + await waitFor(() => { + expect(screen.queryByTestId('maintenanceWindowCallout')).not.toBeInTheDocument(); + }); + + fetchActiveMaintenanceWindowsMock.mockResolvedValue([ + { + ...RUNNING_MAINTENANCE_WINDOW_1, + categoryIds: ['securitySolution'], + }, + ]); + + render( + , + { + wrapper: TestProviders, + } + ); + + await waitFor(() => { + expect(screen.getByTestId('maintenanceWindowCallout')).toBeInTheDocument(); + }); + + render(, { + wrapper: TestProviders, + }); + + await waitFor(() => { + expect(screen.getByTestId('maintenanceWindowCallout')).toBeInTheDocument(); + }); + }); }); diff --git a/packages/kbn-alerts-ui-shared/src/maintenance_window_callout/index.tsx b/packages/kbn-alerts-ui-shared/src/maintenance_window_callout/index.tsx index ab432dce15667..295c74cc00e8d 100644 --- a/packages/kbn-alerts-ui-shared/src/maintenance_window_callout/index.tsx +++ b/packages/kbn-alerts-ui-shared/src/maintenance_window_callout/index.tsx @@ -6,10 +6,10 @@ * Side Public License, v 1. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiCallOut } from '@elastic/eui'; -import { MaintenanceWindow, MaintenanceWindowStatus, KibanaServices } from './types'; +import { MaintenanceWindowStatus, KibanaServices } from './types'; import { useFetchActiveMaintenanceWindows } from './use_fetch_active_maintenance_windows'; const MAINTENANCE_WINDOW_FEATURE_ID = 'maintenanceWindow'; @@ -28,8 +28,10 @@ const MAINTENANCE_WINDOW_RUNNING_DESCRIPTION = i18n.translate( export function MaintenanceWindowCallout({ kibanaServices, + categories, }: { kibanaServices: KibanaServices; + categories?: string[]; }): JSX.Element | null { const { application: { capabilities }, @@ -38,28 +40,48 @@ export function MaintenanceWindowCallout({ const isMaintenanceWindowDisabled = !capabilities[MAINTENANCE_WINDOW_FEATURE_ID].show && !capabilities[MAINTENANCE_WINDOW_FEATURE_ID].save; - const { data } = useFetchActiveMaintenanceWindows(kibanaServices, { + const { data: activeMaintenanceWindows = [] } = useFetchActiveMaintenanceWindows(kibanaServices, { enabled: !isMaintenanceWindowDisabled, }); + const shouldShowMaintenanceWindowCallout = useMemo(() => { + if (!activeMaintenanceWindows) { + return false; + } + if (activeMaintenanceWindows.length === 0) { + return false; + } + if (!Array.isArray(categories)) { + return true; + } + + return activeMaintenanceWindows.some(({ status, categoryIds }) => { + if (status !== MaintenanceWindowStatus.Running) { + return false; + } + if (!Array.isArray(categoryIds)) { + return true; + } + return categoryIds.some((category) => categories.includes(category)); + }); + }, [categories, activeMaintenanceWindows]); + if (isMaintenanceWindowDisabled) { return null; } - const activeMaintenanceWindows = (data as MaintenanceWindow[]) || []; - - if (activeMaintenanceWindows.some(({ status }) => status === MaintenanceWindowStatus.Running)) { - return ( - - {MAINTENANCE_WINDOW_RUNNING_DESCRIPTION} - - ); + if (!shouldShowMaintenanceWindowCallout) { + return null; } - return null; + return ( + + {MAINTENANCE_WINDOW_RUNNING_DESCRIPTION} + + ); } diff --git a/packages/kbn-alerts-ui-shared/src/maintenance_window_callout/types.ts b/packages/kbn-alerts-ui-shared/src/maintenance_window_callout/types.ts index 52c5dc4d48c8a..6810a79223125 100644 --- a/packages/kbn-alerts-ui-shared/src/maintenance_window_callout/types.ts +++ b/packages/kbn-alerts-ui-shared/src/maintenance_window_callout/types.ts @@ -34,6 +34,7 @@ export interface MaintenanceWindowSOProperties { expirationDate: string; events: DateRange[]; rRule: RRuleParams; + categoryIds?: string[]; } export type MaintenanceWindowSOAttributes = MaintenanceWindowSOProperties & diff --git a/packages/kbn-alerts-ui-shared/tsconfig.json b/packages/kbn-alerts-ui-shared/tsconfig.json index 7573505ebcd3f..a7c2a19a6bba2 100644 --- a/packages/kbn-alerts-ui-shared/tsconfig.json +++ b/packages/kbn-alerts-ui-shared/tsconfig.json @@ -21,6 +21,7 @@ "@kbn/core", "@kbn/i18n-react", "@kbn/alerting-plugin", - "@kbn/rrule" + "@kbn/rrule", + "@kbn/actions-plugin" ] } diff --git a/src/core/server/integration_tests/saved_objects/migrations/group5/dot_kibana_split.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group5/dot_kibana_split.test.ts index 6a4bc23a814b1..3c41eafb6102c 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/group5/dot_kibana_split.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/group5/dot_kibana_split.test.ts @@ -29,7 +29,6 @@ import { } from '../kibana_migrator_test_kit'; import { delay, parseLogFile } from '../test_utils'; import '../jest_matchers'; -import { getElasticsearchClientWrapperFactory } from '../elasticsearch_client_wrapper'; // define a type => index distribution const RELOCATE_TYPES: Record = { @@ -59,210 +58,6 @@ describe('split .kibana index into multiple system indices', () => { await clearLog(logFilePathSecondRun); }); - describe('failure cases', () => { - const getFailingKibanaMigratorTestKit = async ({ - logFilePath, - failOn, - delaySeconds, - }: { - logFilePath: string; - failOn: (methodName: string, methodArgs: any[]) => boolean; - delaySeconds?: number; - }) => { - const clientWrapperFactory = getElasticsearchClientWrapperFactory({ - failOn, - errorDelaySeconds: delaySeconds, - }); - - return await getKibanaMigratorTestKit({ - types: typeRegistry.getAllTypes(), - kibanaIndex: MAIN_SAVED_OBJECT_INDEX, - defaultIndexTypesMap: DEFAULT_INDEX_TYPES_MAP, - logFilePath, - clientWrapperFactory, - }); - }; - - beforeEach(async () => { - esServer = await startElasticsearch({ - dataArchive: Path.join(__dirname, '..', 'archives', '7.7.2_xpack_100k_obj.zip'), - }); - }); - - describe('when the .kibana_task_manager migrator fails on the TRANSFORMED_DOCUMENTS_BULK_INDEX state, after the other ones have finished', () => { - it('is capable of completing the .kibana_task_manager migration in subsequent restart', async () => { - const { runMigrations: firstRun } = await getFailingKibanaMigratorTestKit({ - logFilePath: logFilePathFirstRun, - failOn: (methodName, methodArgs) => { - // fail on esClient.bulk({ index: '.kibana_task_manager_1' }) which supposedly causes - // the .kibana_task_manager migrator to fail on the TRANSFORMED_DOCUMENTS_BULK_INDEX state - return methodName === 'bulk' && methodArgs[0].index === '.kibana_task_manager_1'; - }, - delaySeconds: 90, // give the other migrators enough time to finish before failing - }); - - try { - await firstRun(); - throw new Error('First run should have thrown an error but it did not'); - } catch (error) { - expect(error.message).toEqual( - 'Unable to complete saved object migrations for the [.kibana_task_manager] index. Error: esClient.bulk() failed unexpectedly' - ); - } - }); - }); - - describe('when the .kibana migrator fails on the REINDEX_SOURCE_TO_TEMP_INDEX_BULK state', () => { - it('is capable of successfully performing the split migration in subsequent restart', async () => { - const { runMigrations: firstRun } = await getFailingKibanaMigratorTestKit({ - logFilePath: logFilePathFirstRun, - failOn: (methodName, methodArgs) => { - // fail on esClient.bulk({ index: '.kibana_8.11.0_reindex_temp_alias' }) which supposedly causes - // the .kibana migrator to fail on the REINDEX_SOURCE_TO_TEMP_INDEX_BULK - return ( - methodName === 'bulk' && - methodArgs[0].index === `.kibana_${currentVersion}_reindex_temp_alias` - ); - }, - delaySeconds: 10, // give the .kibana_task_manager migrator enough time to finish before failing - }); - - try { - await firstRun(); - throw new Error('First run should have thrown an error but it did not'); - } catch (error) { - expect(error.message).toEqual( - 'Unable to complete saved object migrations for the [.kibana] index. Error: esClient.bulk() failed unexpectedly' - ); - } - }); - }); - - describe('when the .kibana migrator fails on the CLONE_TEMP_TO_TARGET state', () => { - it('is capable of successfully performing the split migration in subsequent restart', async () => { - const { runMigrations: firstRun } = await getFailingKibanaMigratorTestKit({ - logFilePath: logFilePathFirstRun, - failOn: (methodName, methodArgs) => { - // fail on esClient.indices.clone({ index: '.kibana_8.11.0_reindex_temp', target: ... }) which supposedly causes - // the .kibana migrator to fail on the CLONE_TEMP_TO_TARGET - return ( - methodName === 'indices.clone' && - methodArgs[0].index === `.kibana_${currentVersion}_reindex_temp` && - methodArgs[0].target === `.kibana_${currentVersion}_001` - ); - }, - delaySeconds: 15, // give the other migrators enough time to finish before failing - }); - - try { - await firstRun(); - throw new Error('First run should have thrown an error but it did not'); - } catch (error) { - expect(error.message).toEqual( - 'Unable to complete saved object migrations for the [.kibana] index. Error: esClient.indices.clone() failed unexpectedly' - ); - } - }); - }); - - describe('when the .kibana migrator fails on the UPDATE_TARGET_MAPPINGS_PROPERTIES state', () => { - it('is capable of successfully performing the split migration in subsequent restart', async () => { - const { runMigrations: firstRun } = await getFailingKibanaMigratorTestKit({ - logFilePath: logFilePathFirstRun, - failOn: (methodName, methodArgs) => { - // fail on esClient.updateByQuery({ index: '.kibana_8.11.0_001' }) which supposedly causes - // the .kibana migrator to fail on the UPDATE_TARGET_MAPPINGS_PROPERTIES (pickup mappings' changes) - return ( - methodName === 'updateByQuery' && - methodArgs[0].index === `.kibana_${currentVersion}_001` - ); - }, - delaySeconds: 10, // give the other migrators enough time to finish before failing - }); - - try { - await firstRun(); - throw new Error('First run should have thrown an error but it did not'); - } catch (error) { - expect(error.message).toEqual( - 'Unable to complete saved object migrations for the [.kibana] index. Error: esClient.updateByQuery() failed unexpectedly' - ); - } - }); - }); - - describe('when the .kibana_analytics migrator fails on the CLONE_TEMP_TO_TARGET state', () => { - it('is capable of successfully performing the split migration in subsequent restart', async () => { - const { runMigrations: firstRun } = await getFailingKibanaMigratorTestKit({ - logFilePath: logFilePathFirstRun, - failOn: (methodName, methodArgs) => { - // fail on esClient.indices.clone({ index: '.kibana_8.11.0_reindex_temp', target: ... }) which supposedly causes - // the .kibana migrator to fail on the CLONE_TEMP_TO_TARGET - return ( - methodName === 'indices.clone' && - methodArgs[0].index === `.kibana_analytics_${currentVersion}_reindex_temp` && - methodArgs[0].target === `.kibana_analytics_${currentVersion}_001` - ); - }, - delaySeconds: 15, // give the other migrators enough time to finish before failing - }); - - try { - await firstRun(); - throw new Error('First run should have thrown an error but it did not'); - } catch (error) { - expect(error.message).toEqual( - 'Unable to complete saved object migrations for the [.kibana_analytics] index. Error: esClient.indices.clone() failed unexpectedly' - ); - } - }); - }); - - describe('when the .kibana_analytics migrator fails on the UPDATE_TARGET_MAPPINGS_PROPERTIES state', () => { - it('is capable of successfully performing the split migration in subsequent restart', async () => { - const { runMigrations: firstRun } = await getFailingKibanaMigratorTestKit({ - logFilePath: logFilePathFirstRun, - failOn: (methodName, methodArgs) => { - // fail on esClient.updateByQuery({ index: '.kibana_8.11.0_001' }) which supposedly causes - // the .kibana migrator to fail on the UPDATE_TARGET_MAPPINGS_PROPERTIES (pickup mappings' changes) - return ( - methodName === 'updateByQuery' && - methodArgs[0].index === `.kibana_analytics_${currentVersion}_001` - ); - }, - delaySeconds: 10, // give the other migrators enough time to finish before failing - }); - - try { - await firstRun(); - throw new Error('First run should have thrown an error but it did not'); - } catch (error) { - expect(error.message).toEqual( - 'Unable to complete saved object migrations for the [.kibana_analytics] index. Error: esClient.updateByQuery() failed unexpectedly' - ); - } - }); - }); - - afterEach(async () => { - const { runMigrations: secondRun } = await getKibanaMigratorTestKit({ - types: typeRegistry.getAllTypes(), - logFilePath: logFilePathSecondRun, - kibanaIndex: MAIN_SAVED_OBJECT_INDEX, - defaultIndexTypesMap: DEFAULT_INDEX_TYPES_MAP, - }); - const results = await secondRun(); - expect( - results - .flat() - .every((result) => result.status === 'migrated' || result.status === 'patched') - ).toEqual(true); - - await esServer?.stop(); - await delay(2); - }); - }); - describe('when migrating from a legacy version', () => { let migratorTestKitFactory: (logFilePath: string) => Promise; diff --git a/src/core/server/integration_tests/saved_objects/migrations/group6/jest.integration.config.js b/src/core/server/integration_tests/saved_objects/migrations/group6/jest.integration.config.js new file mode 100644 index 0000000000000..52f118ddf906f --- /dev/null +++ b/src/core/server/integration_tests/saved_objects/migrations/group6/jest.integration.config.js @@ -0,0 +1,19 @@ +/* + * 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. + */ + +module.exports = { + // TODO replace the line below with + // preset: '@kbn/test/jest_integration_node + // to do so, we must fix all integration tests first + // see https://github.com/elastic/kibana/pull/130255/ + preset: '@kbn/test/jest_integration', + rootDir: '../../../../../../..', + roots: ['/src/core/server/integration_tests/saved_objects/migrations/group6'], + // must override to match all test given there is no `integration_tests` subfolder + testMatch: ['**/*.test.{js,mjs,ts,tsx}'], +}; diff --git a/src/core/server/integration_tests/saved_objects/migrations/group6/single_migrator_failures.test.ts b/src/core/server/integration_tests/saved_objects/migrations/group6/single_migrator_failures.test.ts new file mode 100644 index 0000000000000..7f78f3d033ba7 --- /dev/null +++ b/src/core/server/integration_tests/saved_objects/migrations/group6/single_migrator_failures.test.ts @@ -0,0 +1,246 @@ +/* + * 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 Path from 'path'; +import type { TestElasticsearchUtils } from '@kbn/core-test-helpers-kbn-server'; +import { + type ISavedObjectTypeRegistry, + MAIN_SAVED_OBJECT_INDEX, +} from '@kbn/core-saved-objects-server'; +import { DEFAULT_INDEX_TYPES_MAP } from '@kbn/core-saved-objects-base-server-internal'; +import { + clearLog, + startElasticsearch, + getKibanaMigratorTestKit, + getCurrentVersionTypeRegistry, + currentVersion, +} from '../kibana_migrator_test_kit'; +import { delay } from '../test_utils'; +import '../jest_matchers'; +import { getElasticsearchClientWrapperFactory } from '../elasticsearch_client_wrapper'; + +export const logFilePathFirstRun = Path.join(__dirname, 'dot_kibana_split_1st_run.test.log'); +export const logFilePathSecondRun = Path.join(__dirname, 'dot_kibana_split_2nd_run.test.log'); + +describe('split .kibana index into multiple system indices', () => { + let esServer: TestElasticsearchUtils['es']; + let typeRegistry: ISavedObjectTypeRegistry; + + beforeAll(async () => { + typeRegistry = await getCurrentVersionTypeRegistry({ oss: false }); + }); + + beforeEach(async () => { + await clearLog(logFilePathFirstRun); + await clearLog(logFilePathSecondRun); + }); + + describe('failure cases', () => { + const getFailingKibanaMigratorTestKit = async ({ + logFilePath, + failOn, + delaySeconds, + }: { + logFilePath: string; + failOn: (methodName: string, methodArgs: any[]) => boolean; + delaySeconds?: number; + }) => { + const clientWrapperFactory = getElasticsearchClientWrapperFactory({ + failOn, + errorDelaySeconds: delaySeconds, + }); + + return await getKibanaMigratorTestKit({ + types: typeRegistry.getAllTypes(), + kibanaIndex: MAIN_SAVED_OBJECT_INDEX, + defaultIndexTypesMap: DEFAULT_INDEX_TYPES_MAP, + logFilePath, + clientWrapperFactory, + }); + }; + + beforeEach(async () => { + esServer = await startElasticsearch({ + dataArchive: Path.join(__dirname, '..', 'archives', '7.7.2_xpack_100k_obj.zip'), + }); + }); + + describe('when the .kibana_task_manager migrator fails on the TRANSFORMED_DOCUMENTS_BULK_INDEX state, after the other ones have finished', () => { + it('is capable of completing the .kibana_task_manager migration in subsequent restart', async () => { + const { runMigrations: firstRun } = await getFailingKibanaMigratorTestKit({ + logFilePath: logFilePathFirstRun, + failOn: (methodName, methodArgs) => { + // fail on esClient.bulk({ index: '.kibana_task_manager_1' }) which supposedly causes + // the .kibana_task_manager migrator to fail on the TRANSFORMED_DOCUMENTS_BULK_INDEX state + return methodName === 'bulk' && methodArgs[0].index === '.kibana_task_manager_1'; + }, + delaySeconds: 90, // give the other migrators enough time to finish before failing + }); + + try { + await firstRun(); + throw new Error('First run should have thrown an error but it did not'); + } catch (error) { + expect(error.message).toEqual( + 'Unable to complete saved object migrations for the [.kibana_task_manager] index. Error: esClient.bulk() failed unexpectedly' + ); + } + }); + }); + + describe('when the .kibana migrator fails on the REINDEX_SOURCE_TO_TEMP_INDEX_BULK state', () => { + it('is capable of successfully performing the split migration in subsequent restart', async () => { + const { runMigrations: firstRun } = await getFailingKibanaMigratorTestKit({ + logFilePath: logFilePathFirstRun, + failOn: (methodName, methodArgs) => { + // fail on esClient.bulk({ index: '.kibana_8.11.0_reindex_temp_alias' }) which supposedly causes + // the .kibana migrator to fail on the REINDEX_SOURCE_TO_TEMP_INDEX_BULK + return ( + methodName === 'bulk' && + methodArgs[0].index === `.kibana_${currentVersion}_reindex_temp_alias` + ); + }, + delaySeconds: 10, // give the .kibana_task_manager migrator enough time to finish before failing + }); + + try { + await firstRun(); + throw new Error('First run should have thrown an error but it did not'); + } catch (error) { + expect(error.message).toEqual( + 'Unable to complete saved object migrations for the [.kibana] index. Error: esClient.bulk() failed unexpectedly' + ); + } + }); + }); + + describe('when the .kibana migrator fails on the CLONE_TEMP_TO_TARGET state', () => { + it('is capable of successfully performing the split migration in subsequent restart', async () => { + const { runMigrations: firstRun } = await getFailingKibanaMigratorTestKit({ + logFilePath: logFilePathFirstRun, + failOn: (methodName, methodArgs) => { + // fail on esClient.indices.clone({ index: '.kibana_8.11.0_reindex_temp', target: ... }) which supposedly causes + // the .kibana migrator to fail on the CLONE_TEMP_TO_TARGET + return ( + methodName === 'indices.clone' && + methodArgs[0].index === `.kibana_${currentVersion}_reindex_temp` && + methodArgs[0].target === `.kibana_${currentVersion}_001` + ); + }, + delaySeconds: 15, // give the other migrators enough time to finish before failing + }); + + try { + await firstRun(); + throw new Error('First run should have thrown an error but it did not'); + } catch (error) { + expect(error.message).toEqual( + 'Unable to complete saved object migrations for the [.kibana] index. Error: esClient.indices.clone() failed unexpectedly' + ); + } + }); + }); + + describe('when the .kibana migrator fails on the UPDATE_TARGET_MAPPINGS_PROPERTIES state', () => { + it('is capable of successfully performing the split migration in subsequent restart', async () => { + const { runMigrations: firstRun } = await getFailingKibanaMigratorTestKit({ + logFilePath: logFilePathFirstRun, + failOn: (methodName, methodArgs) => { + // fail on esClient.updateByQuery({ index: '.kibana_8.11.0_001' }) which supposedly causes + // the .kibana migrator to fail on the UPDATE_TARGET_MAPPINGS_PROPERTIES (pickup mappings' changes) + return ( + methodName === 'updateByQuery' && + methodArgs[0].index === `.kibana_${currentVersion}_001` + ); + }, + delaySeconds: 10, // give the other migrators enough time to finish before failing + }); + + try { + await firstRun(); + throw new Error('First run should have thrown an error but it did not'); + } catch (error) { + expect(error.message).toEqual( + 'Unable to complete saved object migrations for the [.kibana] index. Error: esClient.updateByQuery() failed unexpectedly' + ); + } + }); + }); + + describe('when the .kibana_analytics migrator fails on the CLONE_TEMP_TO_TARGET state', () => { + it('is capable of successfully performing the split migration in subsequent restart', async () => { + const { runMigrations: firstRun } = await getFailingKibanaMigratorTestKit({ + logFilePath: logFilePathFirstRun, + failOn: (methodName, methodArgs) => { + // fail on esClient.indices.clone({ index: '.kibana_8.11.0_reindex_temp', target: ... }) which supposedly causes + // the .kibana migrator to fail on the CLONE_TEMP_TO_TARGET + return ( + methodName === 'indices.clone' && + methodArgs[0].index === `.kibana_analytics_${currentVersion}_reindex_temp` && + methodArgs[0].target === `.kibana_analytics_${currentVersion}_001` + ); + }, + delaySeconds: 15, // give the other migrators enough time to finish before failing + }); + + try { + await firstRun(); + throw new Error('First run should have thrown an error but it did not'); + } catch (error) { + expect(error.message).toEqual( + 'Unable to complete saved object migrations for the [.kibana_analytics] index. Error: esClient.indices.clone() failed unexpectedly' + ); + } + }); + }); + + describe('when the .kibana_analytics migrator fails on the UPDATE_TARGET_MAPPINGS_PROPERTIES state', () => { + it('is capable of successfully performing the split migration in subsequent restart', async () => { + const { runMigrations: firstRun } = await getFailingKibanaMigratorTestKit({ + logFilePath: logFilePathFirstRun, + failOn: (methodName, methodArgs) => { + // fail on esClient.updateByQuery({ index: '.kibana_8.11.0_001' }) which supposedly causes + // the .kibana migrator to fail on the UPDATE_TARGET_MAPPINGS_PROPERTIES (pickup mappings' changes) + return ( + methodName === 'updateByQuery' && + methodArgs[0].index === `.kibana_analytics_${currentVersion}_001` + ); + }, + delaySeconds: 10, // give the other migrators enough time to finish before failing + }); + + try { + await firstRun(); + throw new Error('First run should have thrown an error but it did not'); + } catch (error) { + expect(error.message).toEqual( + 'Unable to complete saved object migrations for the [.kibana_analytics] index. Error: esClient.updateByQuery() failed unexpectedly' + ); + } + }); + }); + + afterEach(async () => { + const { runMigrations: secondRun } = await getKibanaMigratorTestKit({ + types: typeRegistry.getAllTypes(), + logFilePath: logFilePathSecondRun, + kibanaIndex: MAIN_SAVED_OBJECT_INDEX, + defaultIndexTypesMap: DEFAULT_INDEX_TYPES_MAP, + }); + const results = await secondRun(); + expect( + results + .flat() + .every((result) => result.status === 'migrated' || result.status === 'patched') + ).toEqual(true); + + await esServer?.stop(); + await delay(2); + }); + }); +}); diff --git a/x-pack/examples/alerting_example/server/alert_types/always_firing.ts b/x-pack/examples/alerting_example/server/alert_types/always_firing.ts index 3c048702e4723..3285ca3224e48 100644 --- a/x-pack/examples/alerting_example/server/alert_types/always_firing.ts +++ b/x-pack/examples/alerting_example/server/alert_types/always_firing.ts @@ -78,6 +78,7 @@ export const alertType: RuleType< }, }; }, + category: 'kibana', producer: ALERTING_EXAMPLE_APP_ID, validate: { params: schema.object({ diff --git a/x-pack/examples/alerting_example/server/alert_types/astros.ts b/x-pack/examples/alerting_example/server/alert_types/astros.ts index 39cf751702dd7..fda111bfcf86b 100644 --- a/x-pack/examples/alerting_example/server/alert_types/astros.ts +++ b/x-pack/examples/alerting_example/server/alert_types/astros.ts @@ -81,6 +81,7 @@ export const alertType: RuleType< }, }; }, + category: 'example', producer: ALERTING_EXAMPLE_APP_ID, getViewInAppRelativeUrl({ rule }) { return `/app/${ALERTING_EXAMPLE_APP_ID}/astros/${rule.id}`; diff --git a/x-pack/examples/alerting_example/server/alert_types/pattern.ts b/x-pack/examples/alerting_example/server/alert_types/pattern.ts index 1cebdf7f3f5f5..16177fa5de7dd 100644 --- a/x-pack/examples/alerting_example/server/alert_types/pattern.ts +++ b/x-pack/examples/alerting_example/server/alert_types/pattern.ts @@ -51,6 +51,7 @@ function getPatternRuleType(): RuleType { id: 'example.pattern', name: 'Example: Creates alerts on a pattern, for testing', actionGroups: [{ id: 'default', name: 'Default' }], + category: 'kibana', producer: 'alertsFixture', defaultActionGroupId: 'default', minimumLicenseRequired: 'basic', diff --git a/x-pack/plugins/alerting/common/maintenance_window.ts b/x-pack/plugins/alerting/common/maintenance_window.ts index e5d58e6833d54..1f5bffea77081 100644 --- a/x-pack/plugins/alerting/common/maintenance_window.ts +++ b/x-pack/plugins/alerting/common/maintenance_window.ts @@ -33,6 +33,7 @@ export interface MaintenanceWindowSOProperties { expirationDate: string; events: DateRange[]; rRule: RRuleParams; + categoryIds?: string[] | null; } export type MaintenanceWindowSOAttributes = MaintenanceWindowSOProperties & diff --git a/x-pack/plugins/alerting/common/routes/maintenance_window/apis/create/schemas/v1.ts b/x-pack/plugins/alerting/common/routes/maintenance_window/apis/create/schemas/v1.ts index 6199bee1b2bfc..f10eb420963ed 100644 --- a/x-pack/plugins/alerting/common/routes/maintenance_window/apis/create/schemas/v1.ts +++ b/x-pack/plugins/alerting/common/routes/maintenance_window/apis/create/schemas/v1.ts @@ -6,10 +6,12 @@ */ import { schema } from '@kbn/config-schema'; +import { maintenanceWindowCategoryIdsSchemaV1 } from '../../../shared'; import { rRuleRequestSchemaV1 } from '../../../../r_rule'; export const createBodySchema = schema.object({ title: schema.string(), duration: schema.number(), r_rule: rRuleRequestSchemaV1, + category_ids: maintenanceWindowCategoryIdsSchemaV1, }); diff --git a/x-pack/plugins/alerting/common/routes/maintenance_window/apis/update/schemas/v1.ts b/x-pack/plugins/alerting/common/routes/maintenance_window/apis/update/schemas/v1.ts index 4322627c61e04..970cd34424576 100644 --- a/x-pack/plugins/alerting/common/routes/maintenance_window/apis/update/schemas/v1.ts +++ b/x-pack/plugins/alerting/common/routes/maintenance_window/apis/update/schemas/v1.ts @@ -6,6 +6,7 @@ */ import { schema } from '@kbn/config-schema'; +import { maintenanceWindowCategoryIdsSchemaV1 } from '../../../shared'; import { rRuleRequestSchemaV1 } from '../../../../r_rule'; export const updateParamsSchema = schema.object({ @@ -17,4 +18,5 @@ export const updateBodySchema = schema.object({ enabled: schema.maybe(schema.boolean()), duration: schema.maybe(schema.number()), r_rule: schema.maybe(rRuleRequestSchemaV1), + category_ids: maintenanceWindowCategoryIdsSchemaV1, }); diff --git a/x-pack/plugins/alerting/common/routes/maintenance_window/response/constants/latest.ts b/x-pack/plugins/alerting/common/routes/maintenance_window/response/constants/latest.ts index 25300c97a6d2e..302a1664e40bf 100644 --- a/x-pack/plugins/alerting/common/routes/maintenance_window/response/constants/latest.ts +++ b/x-pack/plugins/alerting/common/routes/maintenance_window/response/constants/latest.ts @@ -5,4 +5,6 @@ * 2.0. */ -export * from './v1'; +export type { MaintenanceWindowStatus } from './v1'; + +export { maintenanceWindowStatus } from './v1'; diff --git a/x-pack/plugins/alerting/common/routes/maintenance_window/response/schemas/v1.ts b/x-pack/plugins/alerting/common/routes/maintenance_window/response/schemas/v1.ts index 5ddfdf502789f..410a1dc1e439d 100644 --- a/x-pack/plugins/alerting/common/routes/maintenance_window/response/schemas/v1.ts +++ b/x-pack/plugins/alerting/common/routes/maintenance_window/response/schemas/v1.ts @@ -7,6 +7,7 @@ import { schema } from '@kbn/config-schema'; import { maintenanceWindowStatusV1 } from '..'; +import { maintenanceWindowCategoryIdsSchemaV1 } from '../../shared'; import { rRuleResponseSchemaV1 } from '../../../r_rule'; export const maintenanceWindowEventSchema = schema.object({ @@ -34,4 +35,5 @@ export const maintenanceWindowResponseSchema = schema.object({ schema.literal(maintenanceWindowStatusV1.FINISHED), schema.literal(maintenanceWindowStatusV1.ARCHIVED), ]), + category_ids: maintenanceWindowCategoryIdsSchemaV1, }); diff --git a/x-pack/plugins/alerting/common/routes/maintenance_window/shared/constants/latest.ts b/x-pack/plugins/alerting/common/routes/maintenance_window/shared/constants/latest.ts new file mode 100644 index 0000000000000..3d4b07e7a7845 --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/maintenance_window/shared/constants/latest.ts @@ -0,0 +1,10 @@ +/* + * 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. + */ + +export type { MaintenanceWindowCategoryIdTypes } from './v1'; + +export { maintenanceWindowCategoryIdTypes } from './v1'; diff --git a/x-pack/plugins/alerting/common/routes/maintenance_window/shared/constants/v1.ts b/x-pack/plugins/alerting/common/routes/maintenance_window/shared/constants/v1.ts new file mode 100644 index 0000000000000..2b1284ec6547e --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/maintenance_window/shared/constants/v1.ts @@ -0,0 +1,16 @@ +/* + * 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. + */ + +export const maintenanceWindowCategoryIdTypes = { + KIBANA: 'kibana', + OBSERVABILITY: 'observability', + SECURITY_SOLUTION: 'securitySolution', + MANAGEMENT: 'management', +} as const; + +export type MaintenanceWindowCategoryIdTypes = + typeof maintenanceWindowCategoryIdTypes[keyof typeof maintenanceWindowCategoryIdTypes]; diff --git a/x-pack/plugins/alerting/common/routes/maintenance_window/shared/index.ts b/x-pack/plugins/alerting/common/routes/maintenance_window/shared/index.ts new file mode 100644 index 0000000000000..ec5d9d3db826a --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/maintenance_window/shared/index.ts @@ -0,0 +1,16 @@ +/* + * 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. + */ + +export { maintenanceWindowCategoryIdTypes } from './constants/latest'; +export type { MaintenanceWindowCategoryIdTypes } from './constants/latest'; +export { maintenanceWindowCategoryIdsSchema } from './schemas/latest'; +export type { MaintenanceWindowCategoryIds } from './types/latest'; + +export { maintenanceWindowCategoryIdTypes as maintenanceWindowCategoryIdTypesV1 } from './constants/v1'; +export type { MaintenanceWindowCategoryIdTypes as MaintenanceWindowCategoryIdTypesV1 } from './constants/v1'; +export { maintenanceWindowCategoryIdsSchema as maintenanceWindowCategoryIdsSchemaV1 } from './schemas/v1'; +export type { MaintenanceWindowCategoryIds as MaintenanceWindowCategoryIdsV1 } from './types/v1'; diff --git a/x-pack/plugins/alerting/common/routes/maintenance_window/shared/schemas/latest.ts b/x-pack/plugins/alerting/common/routes/maintenance_window/shared/schemas/latest.ts new file mode 100644 index 0000000000000..f7a0dae678102 --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/maintenance_window/shared/schemas/latest.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export { maintenanceWindowCategoryIdsSchema } from './v1'; diff --git a/x-pack/plugins/alerting/common/routes/maintenance_window/shared/schemas/v1.ts b/x-pack/plugins/alerting/common/routes/maintenance_window/shared/schemas/v1.ts new file mode 100644 index 0000000000000..5319f80850710 --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/maintenance_window/shared/schemas/v1.ts @@ -0,0 +1,21 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { maintenanceWindowCategoryIdTypes as maintenanceWindowCategoryIdTypesV1 } from '../constants/v1'; + +export const maintenanceWindowCategoryIdsSchema = schema.maybe( + schema.nullable( + schema.arrayOf( + schema.oneOf([ + schema.literal(maintenanceWindowCategoryIdTypesV1.OBSERVABILITY), + schema.literal(maintenanceWindowCategoryIdTypesV1.SECURITY_SOLUTION), + schema.literal(maintenanceWindowCategoryIdTypesV1.MANAGEMENT), + ]) + ) + ) +); diff --git a/x-pack/plugins/alerting/common/routes/maintenance_window/shared/types/latest.ts b/x-pack/plugins/alerting/common/routes/maintenance_window/shared/types/latest.ts new file mode 100644 index 0000000000000..85bd37b4635f6 --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/maintenance_window/shared/types/latest.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export type { MaintenanceWindowCategoryIds } from './v1'; diff --git a/x-pack/plugins/alerting/common/routes/maintenance_window/shared/types/v1.ts b/x-pack/plugins/alerting/common/routes/maintenance_window/shared/types/v1.ts new file mode 100644 index 0000000000000..19c5ae2a133ba --- /dev/null +++ b/x-pack/plugins/alerting/common/routes/maintenance_window/shared/types/v1.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { TypeOf } from '@kbn/config-schema'; +import { maintenanceWindowCategoryIdsSchemaV1 } from '..'; + +export type MaintenanceWindowCategoryIds = TypeOf; diff --git a/x-pack/plugins/alerting/common/rule_type.ts b/x-pack/plugins/alerting/common/rule_type.ts index cce1e6f5e9542..211dc6b18223c 100644 --- a/x-pack/plugins/alerting/common/rule_type.ts +++ b/x-pack/plugins/alerting/common/rule_type.ts @@ -31,6 +31,7 @@ export interface RuleType< params: ActionVariable[]; }; defaultActionGroupId: ActionGroupIds; + category: string; producer: string; minimumLicenseRequired: LicenseType; isExportable: boolean; diff --git a/x-pack/plugins/alerting/public/alert_navigation_registry/alert_navigation_registry.test.ts b/x-pack/plugins/alerting/public/alert_navigation_registry/alert_navigation_registry.test.ts index c7bb9a4feb8ba..c95278fd477a4 100644 --- a/x-pack/plugins/alerting/public/alert_navigation_registry/alert_navigation_registry.test.ts +++ b/x-pack/plugins/alerting/public/alert_navigation_registry/alert_navigation_registry.test.ts @@ -22,6 +22,7 @@ const mockRuleType = (id: string): RuleType => ({ params: [], }, defaultActionGroupId: 'default', + category: 'test', producer: 'alerts', minimumLicenseRequired: 'basic', isExportable: true, diff --git a/x-pack/plugins/alerting/public/hooks/use_get_rule_types.ts b/x-pack/plugins/alerting/public/hooks/use_get_rule_types.ts new file mode 100644 index 0000000000000..0ff577918ba72 --- /dev/null +++ b/x-pack/plugins/alerting/public/hooks/use_get_rule_types.ts @@ -0,0 +1,41 @@ +/* + * 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 { useQuery } from '@tanstack/react-query'; +import { i18n } from '@kbn/i18n'; +import { useKibana } from '../utils/kibana_react'; +import { loadRuleTypes } from '../services/alert_api'; + +export const useGetRuleTypes = () => { + const { + http, + notifications: { toasts }, + } = useKibana().services; + + const queryFn = () => { + return loadRuleTypes({ http }); + }; + + const onError = () => { + toasts.addDanger( + i18n.translate('xpack.alerting.hooks.useGetRuleTypes.error', { + defaultMessage: 'Unable to load rule types.', + }) + ); + }; + + const { isLoading, isFetching, data } = useQuery({ + queryKey: ['useGetRuleTypes'], + queryFn, + onError, + }); + + return { + data, + isLoading: isLoading || isFetching, + }; +}; diff --git a/x-pack/plugins/alerting/public/pages/maintenance_windows/components/create_maintenance_windows_form.test.tsx b/x-pack/plugins/alerting/public/pages/maintenance_windows/components/create_maintenance_windows_form.test.tsx index 07f6048258856..dc78b4455d54b 100644 --- a/x-pack/plugins/alerting/public/pages/maintenance_windows/components/create_maintenance_windows_form.test.tsx +++ b/x-pack/plugins/alerting/public/pages/maintenance_windows/components/create_maintenance_windows_form.test.tsx @@ -6,18 +6,21 @@ */ import React from 'react'; -import { within } from '@testing-library/react'; +import { within, fireEvent, waitFor } from '@testing-library/react'; import { AppMockRenderer, createAppMockRenderer } from '../../../lib/test_utils'; import { CreateMaintenanceWindowFormProps, CreateMaintenanceWindowForm, } from './create_maintenance_windows_form'; -import { useUiSetting } from '@kbn/kibana-react-plugin/public'; -jest.mock('@kbn/kibana-react-plugin/public/ui_settings/use_ui_setting', () => ({ - useUiSetting: jest.fn(), +jest.mock('../../../utils/kibana_react'); +jest.mock('../../../services/alert_api', () => ({ + loadRuleTypes: jest.fn(), })); +const { loadRuleTypes } = jest.requireMock('../../../services/alert_api'); +const { useKibana, useUiSetting } = jest.requireMock('../../../utils/kibana_react'); + const formProps: CreateMaintenanceWindowFormProps = { onCancel: jest.fn(), onSuccess: jest.fn(), @@ -28,28 +31,59 @@ describe('CreateMaintenanceWindowForm', () => { beforeEach(() => { jest.clearAllMocks(); + loadRuleTypes.mockResolvedValue([ + { category: 'observability' }, + { category: 'management' }, + { category: 'securitySolution' }, + ]); + + useKibana.mockReturnValue({ + services: { + notifications: { + toasts: { + addSuccess: jest.fn(), + addDanger: jest.fn(), + }, + }, + }, + }); + + useUiSetting.mockReturnValue('America/New_York'); appMockRenderer = createAppMockRenderer(); - (useUiSetting as jest.Mock).mockReturnValue('America/New_York'); }); it('renders all form fields except the recurring form fields', async () => { const result = appMockRenderer.render(); + await waitFor(() => { + expect( + result.queryByTestId('maintenanceWindowCategorySelectionLoading') + ).not.toBeInTheDocument(); + }); + expect(result.getByTestId('title-field')).toBeInTheDocument(); expect(result.getByTestId('date-field')).toBeInTheDocument(); expect(result.getByTestId('recurring-field')).toBeInTheDocument(); + expect(result.getByTestId('maintenanceWindowCategorySelection')).toBeInTheDocument(); expect(result.queryByTestId('recurring-form')).not.toBeInTheDocument(); expect(result.queryByTestId('timezone-field')).not.toBeInTheDocument(); }); it('renders timezone field when the kibana setting is set to browser', async () => { - (useUiSetting as jest.Mock).mockReturnValue('Browser'); + useUiSetting.mockReturnValue('Browser'); const result = appMockRenderer.render(); + await waitFor(() => { + expect( + result.queryByTestId('maintenanceWindowCategorySelectionLoading') + ).not.toBeInTheDocument(); + }); + expect(result.getByTestId('title-field')).toBeInTheDocument(); expect(result.getByTestId('date-field')).toBeInTheDocument(); expect(result.getByTestId('recurring-field')).toBeInTheDocument(); + expect(result.getByTestId('maintenanceWindowCategorySelection')).toBeInTheDocument(); expect(result.queryByTestId('recurring-form')).not.toBeInTheDocument(); expect(result.getByTestId('timezone-field')).toBeInTheDocument(); }); @@ -71,7 +105,7 @@ describe('CreateMaintenanceWindowForm', () => { expect(recurringInput).not.toBeChecked(); }); - it('should prefill the form when provided with initialValue', () => { + it('should prefill the form when provided with initialValue', async () => { const result = appMockRenderer.render( { endDate: '2023-03-26', timezone: ['America/Los_Angeles'], recurring: true, + categoryIds: [], }} /> ); @@ -93,10 +128,101 @@ describe('CreateMaintenanceWindowForm', () => { const recurringInput = within(result.getByTestId('recurring-field')).getByTestId('input'); const timezoneInput = within(result.getByTestId('timezone-field')).getByTestId('input'); + await waitFor(() => { + expect( + result.queryByTestId('maintenanceWindowCategorySelectionLoading') + ).not.toBeInTheDocument(); + }); + + const observabilityInput = within( + result.getByTestId('maintenanceWindowCategorySelection') + ).getByTestId('checkbox-observability'); + const securityInput = within( + result.getByTestId('maintenanceWindowCategorySelection') + ).getByTestId('checkbox-securitySolution'); + const managementInput = within( + result.getByTestId('maintenanceWindowCategorySelection') + ).getByTestId('checkbox-management'); + + expect(observabilityInput).toBeChecked(); + expect(securityInput).toBeChecked(); + expect(managementInput).toBeChecked(); expect(titleInput).toHaveValue('test'); expect(dateInputs[0]).toHaveValue('03/23/2023 09:00 PM'); expect(dateInputs[1]).toHaveValue('03/25/2023 09:00 PM'); expect(recurringInput).toBeChecked(); expect(timezoneInput).toHaveTextContent('America/Los_Angeles'); }); + + it('should initialize MWs without category ids properly', async () => { + const result = appMockRenderer.render( + + ); + + await waitFor(() => { + expect( + result.queryByTestId('maintenanceWindowCategorySelectionLoading') + ).not.toBeInTheDocument(); + }); + + const observabilityInput = within( + result.getByTestId('maintenanceWindowCategorySelection') + ).getByTestId('checkbox-observability'); + const securityInput = within( + result.getByTestId('maintenanceWindowCategorySelection') + ).getByTestId('checkbox-securitySolution'); + const managementInput = within( + result.getByTestId('maintenanceWindowCategorySelection') + ).getByTestId('checkbox-management'); + + expect(observabilityInput).toBeChecked(); + expect(securityInput).toBeChecked(); + expect(managementInput).toBeChecked(); + }); + + it('can select category IDs', async () => { + const result = appMockRenderer.render(); + + await waitFor(() => { + expect( + result.queryByTestId('maintenanceWindowCategorySelectionLoading') + ).not.toBeInTheDocument(); + }); + + const observabilityInput = within( + result.getByTestId('maintenanceWindowCategorySelection') + ).getByTestId('checkbox-observability'); + const securityInput = within( + result.getByTestId('maintenanceWindowCategorySelection') + ).getByTestId('checkbox-securitySolution'); + const managementInput = within( + result.getByTestId('maintenanceWindowCategorySelection') + ).getByTestId('checkbox-management'); + + expect(observabilityInput).toBeChecked(); + expect(securityInput).toBeChecked(); + expect(managementInput).toBeChecked(); + + fireEvent.click(observabilityInput); + + expect(observabilityInput).not.toBeChecked(); + expect(securityInput).toBeChecked(); + expect(managementInput).toBeChecked(); + + fireEvent.click(securityInput); + fireEvent.click(observabilityInput); + + expect(observabilityInput).toBeChecked(); + expect(securityInput).not.toBeChecked(); + expect(managementInput).toBeChecked(); + }); }); diff --git a/x-pack/plugins/alerting/public/pages/maintenance_windows/components/create_maintenance_windows_form.tsx b/x-pack/plugins/alerting/public/pages/maintenance_windows/components/create_maintenance_windows_form.tsx index 8372e2871fb89..f51f16d972c02 100644 --- a/x-pack/plugins/alerting/public/pages/maintenance_windows/components/create_maintenance_windows_form.tsx +++ b/x-pack/plugins/alerting/public/pages/maintenance_windows/components/create_maintenance_windows_form.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState, useRef, useEffect } from 'react'; import moment from 'moment'; import { FIELD_TYPES, @@ -24,8 +24,12 @@ import { EuiFlexItem, EuiFormLabel, EuiHorizontalRule, + EuiSpacer, + EuiText, + EuiTextColor, } from '@elastic/eui'; import { TIMEZONE_OPTIONS as UI_TIMEZONE_OPTIONS } from '@kbn/core-ui-settings-common'; +import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; import { FormProps, schema } from './schema'; import * as i18n from '../translations'; @@ -34,9 +38,11 @@ import { SubmitButton } from './submit_button'; import { convertToRRule } from '../helpers/convert_to_rrule'; import { useCreateMaintenanceWindow } from '../../../hooks/use_create_maintenance_window'; import { useUpdateMaintenanceWindow } from '../../../hooks/use_update_maintenance_window'; +import { useGetRuleTypes } from '../../../hooks/use_get_rule_types'; import { useUiSetting } from '../../../utils/kibana_react'; import { DatePickerRangeField } from './fields/date_picker_range_field'; import { useArchiveMaintenanceWindow } from '../../../hooks/use_archive_maintenance_window'; +import { MaintenanceWindowCategorySelection } from './maintenance_window_category_selection'; const UseField = getUseField({ component: Field }); @@ -64,12 +70,17 @@ export const CreateMaintenanceWindowForm = React.memo(false); + const { mutate: createMaintenanceWindow, isLoading: isCreateLoading } = useCreateMaintenanceWindow(); const { mutate: updateMaintenanceWindow, isLoading: isUpdateLoading } = useUpdateMaintenanceWindow(); const { mutate: archiveMaintenanceWindow } = useArchiveMaintenanceWindow(); + const { data: ruleTypes, isLoading: isLoadingRuleTypes } = useGetRuleTypes(); + const submitMaintenanceWindow = useCallback( async (formData, isValid) => { if (isValid) { @@ -83,6 +94,7 @@ export const CreateMaintenanceWindowForm = React.memo({ + const [{ recurring, timezone, categoryIds }] = useFormData({ form, - watch: ['recurring', 'timezone'], + watch: ['recurring', 'timezone', 'categoryIds'], }); const isRecurring = recurring || false; const showTimezone = isBrowser || initialValue?.timezone !== undefined; @@ -118,6 +130,25 @@ export const CreateMaintenanceWindowForm = React.memo setIsModalVisible(false), []); const showModal = useCallback(() => setIsModalVisible(true), []); + const { setFieldValue } = form; + + const onCategoryIdsChange = useCallback( + (id: string) => { + if (!categoryIds) { + return; + } + if (categoryIds.includes(id)) { + setFieldValue( + 'categoryIds', + categoryIds.filter((category) => category !== id) + ); + return; + } + setFieldValue('categoryIds', [...categoryIds, id]); + }, + [categoryIds, setFieldValue] + ); + const modal = useMemo(() => { let m; if (isModalVisible) { @@ -144,6 +175,52 @@ export const CreateMaintenanceWindowForm = React.memo { + if (!ruleTypes) { + return []; + } + return [...new Set(ruleTypes.map((ruleType) => ruleType.category))]; + }, [ruleTypes]); + + // For create mode, we want to initialize options to the rule type category the + // user has access + useEffect(() => { + if (isEditMode) { + return; + } + if (hasSetInitialCategories.current) { + return; + } + if (!ruleTypes) { + return; + } + setFieldValue('categoryIds', [...new Set(ruleTypes.map((ruleType) => ruleType.category))]); + hasSetInitialCategories.current = true; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isEditMode, ruleTypes]); + + // For edit mode, if a maintenance window => category_ids is not an array, this means + // the maintenance window was created before the introduction of category filters. + // For backwards compat we will initialize all options for these. + useEffect(() => { + if (!isEditMode) { + return; + } + if (hasSetInitialCategories.current) { + return; + } + if (Array.isArray(categoryIds)) { + return; + } + setFieldValue('categoryIds', [ + DEFAULT_APP_CATEGORIES.observability.id, + DEFAULT_APP_CATEGORIES.security.id, + DEFAULT_APP_CATEGORIES.management.id, + ]); + hasSetInitialCategories.current = true; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isEditMode, categoryIds]); + return (
@@ -158,6 +235,17 @@ export const CreateMaintenanceWindowForm = React.memo + + + +

{i18n.CREATE_FORM_TIMEFRAME_TITLE}

+

+ + {i18n.CREATE_FORM_TIMEFRAME_DESCRIPTION} + +

+
+
@@ -229,20 +317,39 @@ export const CreateMaintenanceWindowForm = React.memo + {isRecurring && ( + + + + )} - {isRecurring ? : null} + + + {(field) => ( + error.message)} + onChange={onCategoryIdsChange} + /> + )} + + - {isEditMode ? ( - -

{i18n.ARCHIVE_SUBTITLE}

- - {i18n.ARCHIVE} - - {modal} -
- ) : null} - + {isEditMode && ( + <> + +

{i18n.ARCHIVE_SUBTITLE}

+ + {i18n.ARCHIVE} + + {modal} +
+ + + )} { + let appMockRenderer: AppMockRenderer; + + beforeEach(() => { + jest.clearAllMocks(); + appMockRenderer = createAppMockRenderer(); + }); + + it('renders correctly', async () => { + appMockRenderer.render( + + ); + + expect(screen.getByTestId('checkbox-observability')).not.toBeDisabled(); + expect(screen.getByTestId('checkbox-securitySolution')).not.toBeDisabled(); + expect(screen.getByTestId('checkbox-management')).not.toBeDisabled(); + }); + + it('should disable options if option is not in the available categories array', () => { + appMockRenderer.render( + + ); + + expect(screen.getByTestId('checkbox-observability')).toBeDisabled(); + expect(screen.getByTestId('checkbox-securitySolution')).toBeDisabled(); + expect(screen.getByTestId('checkbox-management')).toBeDisabled(); + }); + + it('can initialize checkboxes with initial values from props', async () => { + appMockRenderer.render( + + ); + + expect(screen.getByTestId('checkbox-observability')).not.toBeChecked(); + expect(screen.getByTestId('checkbox-securitySolution')).toBeChecked(); + expect(screen.getByTestId('checkbox-management')).toBeChecked(); + }); + + it('can check checkboxes', async () => { + appMockRenderer.render( + + ); + + const managementCheckbox = screen.getByTestId('checkbox-management'); + const securityCheckbox = screen.getByTestId('checkbox-securitySolution'); + + fireEvent.click(managementCheckbox); + + expect(mockOnChange).toHaveBeenLastCalledWith('management', expect.anything()); + + fireEvent.click(securityCheckbox); + + expect(mockOnChange).toHaveBeenLastCalledWith('securitySolution', expect.anything()); + }); + + it('should display loading spinner if isLoading is true', () => { + appMockRenderer.render( + + ); + expect(screen.getByTestId('maintenanceWindowCategorySelectionLoading')).toBeInTheDocument(); + }); + + it('should display error message if it exists', () => { + appMockRenderer.render( + + ); + expect(screen.getByText('test error')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/alerting/public/pages/maintenance_windows/components/maintenance_window_category_selection.tsx b/x-pack/plugins/alerting/public/pages/maintenance_windows/components/maintenance_window_category_selection.tsx new file mode 100644 index 0000000000000..79e7a35082afb --- /dev/null +++ b/x-pack/plugins/alerting/public/pages/maintenance_windows/components/maintenance_window_category_selection.tsx @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; +import { + EuiFlexGroup, + EuiText, + EuiFlexItem, + EuiFormRow, + EuiTextColor, + EuiCheckboxGroup, + EuiCheckboxGroupOption, + EuiLoadingSpinner, +} from '@elastic/eui'; + +import * as i18n from '../translations'; + +const CHECKBOX_OPTIONS: EuiCheckboxGroupOption[] = [ + { + id: DEFAULT_APP_CATEGORIES.observability.id, + label: i18n.CREATE_FORM_CATEGORY_OBSERVABILITY_RULES, + ['data-test-subj']: `checkbox-${DEFAULT_APP_CATEGORIES.observability.id}`, + }, + { + id: DEFAULT_APP_CATEGORIES.security.id, + label: i18n.CREATE_FORM_CATEGORY_SECURITY_RULES, + ['data-test-subj']: `checkbox-${DEFAULT_APP_CATEGORIES.security.id}`, + }, + { + id: DEFAULT_APP_CATEGORIES.management.id, + label: i18n.CREATE_FORM_CATEGORY_STACK_RULES, + ['data-test-subj']: `checkbox-${DEFAULT_APP_CATEGORIES.management.id}`, + }, +]; + +export interface MaintenanceWindowCategorySelectionProps { + selectedCategories: string[]; + availableCategories: string[]; + errors?: string[]; + isLoading?: boolean; + onChange: (category: string) => void; +} + +export const MaintenanceWindowCategorySelection = ( + props: MaintenanceWindowCategorySelectionProps +) => { + const { + selectedCategories, + availableCategories, + errors = [], + isLoading = false, + onChange, + } = props; + + const selectedMap = useMemo(() => { + return selectedCategories.reduce>((result, category) => { + result[category] = true; + return result; + }, {}); + }, [selectedCategories]); + + const options: EuiCheckboxGroupOption[] = useMemo(() => { + return CHECKBOX_OPTIONS.map((option) => ({ + ...option, + disabled: !availableCategories.includes(option.id), + })); + }, [availableCategories]); + + if (isLoading) { + return ( + + + + + + ); + } + + return ( + + + +

{i18n.CREATE_FORM_CATEGORY_SELECTION_TITLE}

+

+ + {i18n.CREATE_FORM_CATEGORY_SELECTION_DESCRIPTION} + +

+
+
+ + + + + +
+ ); +}; diff --git a/x-pack/plugins/alerting/public/pages/maintenance_windows/components/recurring_schedule_form/custom_recurring_schedule.test.tsx b/x-pack/plugins/alerting/public/pages/maintenance_windows/components/recurring_schedule_form/custom_recurring_schedule.test.tsx index 92e792e4523e4..9ff4f6473525b 100644 --- a/x-pack/plugins/alerting/public/pages/maintenance_windows/components/recurring_schedule_form/custom_recurring_schedule.test.tsx +++ b/x-pack/plugins/alerting/public/pages/maintenance_windows/components/recurring_schedule_form/custom_recurring_schedule.test.tsx @@ -23,6 +23,7 @@ const initialValue: FormProps = { frequency: 'CUSTOM', ends: EndsOptions.NEVER, }, + categoryIds: [], }; describe('CustomRecurringSchedule', () => { diff --git a/x-pack/plugins/alerting/public/pages/maintenance_windows/components/recurring_schedule_form/recurring_schedule.test.tsx b/x-pack/plugins/alerting/public/pages/maintenance_windows/components/recurring_schedule_form/recurring_schedule.test.tsx index 1ad208ae78f85..6422d285db7ef 100644 --- a/x-pack/plugins/alerting/public/pages/maintenance_windows/components/recurring_schedule_form/recurring_schedule.test.tsx +++ b/x-pack/plugins/alerting/public/pages/maintenance_windows/components/recurring_schedule_form/recurring_schedule.test.tsx @@ -19,6 +19,7 @@ const initialValue: FormProps = { startDate: '2023-03-24', endDate: '2023-03-26', recurring: true, + categoryIds: [], }; describe('RecurringSchedule', () => { diff --git a/x-pack/plugins/alerting/public/pages/maintenance_windows/components/schema.ts b/x-pack/plugins/alerting/public/pages/maintenance_windows/components/schema.ts index 6fbd721dc6a09..6be4a21bc3965 100644 --- a/x-pack/plugins/alerting/public/pages/maintenance_windows/components/schema.ts +++ b/x-pack/plugins/alerting/public/pages/maintenance_windows/components/schema.ts @@ -22,6 +22,7 @@ export interface FormProps { timezone?: string[]; recurring: boolean; recurringSchedule?: RecurringScheduleFormProps; + categoryIds?: string[]; } export interface RecurringScheduleFormProps { @@ -45,6 +46,13 @@ export const schema: FormSchema = { }, ], }, + categoryIds: { + validations: [ + { + validator: emptyField(i18n.CREATE_FORM_CATEGORY_IDS_REQUIRED), + }, + ], + }, startDate: {}, endDate: {}, timezone: {}, diff --git a/x-pack/plugins/alerting/public/pages/maintenance_windows/helpers/convert_from_maintenance_window_to_form.test.ts b/x-pack/plugins/alerting/public/pages/maintenance_windows/helpers/convert_from_maintenance_window_to_form.test.ts index eb187823a685b..7b40b028ab210 100644 --- a/x-pack/plugins/alerting/public/pages/maintenance_windows/helpers/convert_from_maintenance_window_to_form.test.ts +++ b/x-pack/plugins/alerting/public/pages/maintenance_windows/helpers/convert_from_maintenance_window_to_form.test.ts @@ -35,6 +35,7 @@ describe('convertFromMaintenanceWindowToForm', () => { endDate: endDate.toISOString(), timezone: ['UTC'], recurring: false, + categoryIds: [], }); }); @@ -63,6 +64,7 @@ describe('convertFromMaintenanceWindowToForm', () => { frequency: Frequency.DAILY, interval: 1, }, + categoryIds: [], }); }); @@ -95,6 +97,7 @@ describe('convertFromMaintenanceWindowToForm', () => { frequency: Frequency.DAILY, interval: 1, }, + categoryIds: [], }); }); @@ -125,6 +128,7 @@ describe('convertFromMaintenanceWindowToForm', () => { frequency: Frequency.DAILY, interval: 1, }, + categoryIds: [], }); }); @@ -153,6 +157,7 @@ describe('convertFromMaintenanceWindowToForm', () => { byweekday: { 1: false, 2: false, 3: true, 4: false, 5: false, 6: false, 7: false }, interval: 1, }, + categoryIds: [], }); }); @@ -181,6 +186,7 @@ describe('convertFromMaintenanceWindowToForm', () => { bymonth: 'weekday', interval: 1, }, + categoryIds: [], }); }); @@ -209,6 +215,7 @@ describe('convertFromMaintenanceWindowToForm', () => { frequency: Frequency.YEARLY, interval: 1, }, + categoryIds: [], }); }); @@ -236,6 +243,7 @@ describe('convertFromMaintenanceWindowToForm', () => { frequency: 'CUSTOM', interval: 1, }, + categoryIds: [], }); }); @@ -265,6 +273,7 @@ describe('convertFromMaintenanceWindowToForm', () => { frequency: 'CUSTOM', interval: 1, }, + categoryIds: [], }); }); @@ -294,6 +303,7 @@ describe('convertFromMaintenanceWindowToForm', () => { frequency: 'CUSTOM', interval: 1, }, + categoryIds: [], }); }); @@ -323,6 +333,7 @@ describe('convertFromMaintenanceWindowToForm', () => { frequency: 'CUSTOM', interval: 3, }, + categoryIds: [], }); }); }); diff --git a/x-pack/plugins/alerting/public/pages/maintenance_windows/helpers/convert_from_maintenance_window_to_form.ts b/x-pack/plugins/alerting/public/pages/maintenance_windows/helpers/convert_from_maintenance_window_to_form.ts index f29265c4c7f3a..239ff4ade0cca 100644 --- a/x-pack/plugins/alerting/public/pages/maintenance_windows/helpers/convert_from_maintenance_window_to_form.ts +++ b/x-pack/plugins/alerting/public/pages/maintenance_windows/helpers/convert_from_maintenance_window_to_form.ts @@ -27,6 +27,7 @@ export const convertFromMaintenanceWindowToForm = ( endDate: endDate.toISOString(), timezone: [maintenanceWindow.rRule.tzid], recurring, + categoryIds: maintenanceWindow.categoryIds || [], }; if (!recurring) return form; diff --git a/x-pack/plugins/alerting/public/pages/maintenance_windows/translations.ts b/x-pack/plugins/alerting/public/pages/maintenance_windows/translations.ts index 0490d3faf54cf..8e3988d64fcce 100644 --- a/x-pack/plugins/alerting/public/pages/maintenance_windows/translations.ts +++ b/x-pack/plugins/alerting/public/pages/maintenance_windows/translations.ts @@ -144,6 +144,70 @@ export const CREATE_FORM_FREQUENCY_WEEKLY = i18n.translate( } ); +export const CREATE_FORM_TIMEFRAME_TITLE = i18n.translate( + 'xpack.alerting.maintenanceWindows.createForm.frequency.title', + { + defaultMessage: 'Timeframe', + } +); + +export const CREATE_FORM_TIMEFRAME_DESCRIPTION = i18n.translate( + 'xpack.alerting.maintenanceWindows.createForm.frequency.description', + { + defaultMessage: 'Define the start and end time when events should be affected by the window.', + } +); + +export const CREATE_FORM_CATEGORY_IDS_REQUIRED = i18n.translate( + 'xpack.alerting.maintenanceWindows.createForm.categoryIds.required', + { + defaultMessage: 'A category is required.', + } +); + +export const CREATE_FORM_CATEGORY_SELECTION_TITLE = i18n.translate( + 'xpack.alerting.maintenanceWindows.createForm.categoriesSelection.title', + { + defaultMessage: 'Category specific maintenance window', + } +); + +export const CREATE_FORM_CATEGORY_SELECTION_DESCRIPTION = i18n.translate( + 'xpack.alerting.maintenanceWindows.createForm.categoriesSelection.description', + { + defaultMessage: + 'Only rules associated with the selected categories are affected by the maintenance window.', + } +); + +export const CREATE_FORM_CATEGORIES_SELECTION_CHECKBOX_GROUP_TITLE = i18n.translate( + 'xpack.alerting.maintenanceWindows.createForm.categorySelection.checkboxGroupTitle', + { + defaultMessage: 'Select the categories this should affect', + } +); + +export const CREATE_FORM_CATEGORY_OBSERVABILITY_RULES = i18n.translate( + 'xpack.alerting.maintenanceWindows.createForm.categoryIds.observabilityRules', + { + defaultMessage: 'Observability rules', + } +); + +export const CREATE_FORM_CATEGORY_SECURITY_RULES = i18n.translate( + 'xpack.alerting.maintenanceWindows.createForm.categoryIds.securityRules', + { + defaultMessage: 'Security rules', + } +); + +export const CREATE_FORM_CATEGORY_STACK_RULES = i18n.translate( + 'xpack.alerting.maintenanceWindows.createForm.categoryIds.stackRules', + { + defaultMessage: 'Stack rules', + } +); + export const CREATE_FORM_FREQUENCY_WEEKLY_ON = (dayOfWeek: string) => i18n.translate('xpack.alerting.maintenanceWindows.createForm.frequency.weeklyOnWeekday', { defaultMessage: 'Weekly on {dayOfWeek}', diff --git a/x-pack/plugins/alerting/public/pages/maintenance_windows/types.ts b/x-pack/plugins/alerting/public/pages/maintenance_windows/types.ts index 6551d3808068a..a5d838c9927d3 100644 --- a/x-pack/plugins/alerting/public/pages/maintenance_windows/types.ts +++ b/x-pack/plugins/alerting/public/pages/maintenance_windows/types.ts @@ -18,7 +18,10 @@ export const RRuleFrequencyMap = { '3': Frequency.DAILY, }; -export type MaintenanceWindow = Pick; +export type MaintenanceWindow = Pick< + MaintenanceWindowServerSide, + 'title' | 'duration' | 'rRule' | 'categoryIds' +>; export type MaintenanceWindowFindResponse = MaintenanceWindowServerSide & MaintenanceWindowModificationMetadata & { diff --git a/x-pack/plugins/alerting/public/services/alert_api.test.ts b/x-pack/plugins/alerting/public/services/alert_api.test.ts index 62cd14a3c21db..13c118bb1e78f 100644 --- a/x-pack/plugins/alerting/public/services/alert_api.test.ts +++ b/x-pack/plugins/alerting/public/services/alert_api.test.ts @@ -53,6 +53,7 @@ describe('loadRuleTypes', () => { "read": true, }, }, + "category": "management", "defaultActionGroupId": "default", "enabledInLicense": true, "id": ".index-threshold", @@ -152,6 +153,7 @@ function getApiRuleType() { return { id: '.index-threshold', name: 'Index threshold', + category: 'management', producer: 'stackAlerts', enabled_in_license: true, recovery_action_group: { @@ -202,6 +204,7 @@ function getRuleType(): RuleType { return { id: '.index-threshold', name: 'Index threshold', + category: 'management', producer: 'stackAlerts', enabledInLicense: true, recoveryActionGroup: { diff --git a/x-pack/plugins/alerting/public/services/maintenance_windows_api/archive.ts b/x-pack/plugins/alerting/public/services/maintenance_windows_api/archive.ts index fe07ebb04b38e..45c90dcc2f649 100644 --- a/x-pack/plugins/alerting/public/services/maintenance_windows_api/archive.ts +++ b/x-pack/plugins/alerting/public/services/maintenance_windows_api/archive.ts @@ -10,9 +10,14 @@ import { AsApiContract, RewriteRequestCase } from '@kbn/actions-plugin/common'; import { MaintenanceWindow } from '../../pages/maintenance_windows/types'; import { INTERNAL_BASE_ALERTING_API_PATH } from '../../../common'; -const rewriteBodyRes: RewriteRequestCase = ({ r_rule: rRule, ...rest }) => ({ +const rewriteBodyRes: RewriteRequestCase = ({ + r_rule: rRule, + category_ids: categoryIds, + ...rest +}) => ({ ...rest, rRule, + categoryIds, }); export async function archiveMaintenanceWindow({ diff --git a/x-pack/plugins/alerting/public/services/maintenance_windows_api/create.ts b/x-pack/plugins/alerting/public/services/maintenance_windows_api/create.ts index 73323b257d3e2..dfb97a5d412c4 100644 --- a/x-pack/plugins/alerting/public/services/maintenance_windows_api/create.ts +++ b/x-pack/plugins/alerting/public/services/maintenance_windows_api/create.ts @@ -10,14 +10,24 @@ import { AsApiContract, RewriteRequestCase, RewriteResponseCase } from '@kbn/act import { MaintenanceWindow } from '../../pages/maintenance_windows/types'; import { INTERNAL_BASE_ALERTING_API_PATH } from '../../../common'; -const rewriteBodyRequest: RewriteResponseCase = ({ rRule, ...res }) => ({ +const rewriteBodyRequest: RewriteResponseCase = ({ + rRule, + categoryIds, + ...res +}) => ({ ...res, r_rule: rRule, + category_ids: categoryIds, }); -const rewriteBodyRes: RewriteRequestCase = ({ r_rule: rRule, ...rest }) => ({ +const rewriteBodyRes: RewriteRequestCase = ({ + r_rule: rRule, + category_ids: categoryIds, + ...rest +}) => ({ ...rest, rRule, + categoryIds, }); export async function createMaintenanceWindow({ diff --git a/x-pack/plugins/alerting/public/services/maintenance_windows_api/finish.ts b/x-pack/plugins/alerting/public/services/maintenance_windows_api/finish.ts index 910fad0bee1c3..a9ca1ef0a3fe6 100644 --- a/x-pack/plugins/alerting/public/services/maintenance_windows_api/finish.ts +++ b/x-pack/plugins/alerting/public/services/maintenance_windows_api/finish.ts @@ -10,9 +10,14 @@ import { AsApiContract, RewriteRequestCase } from '@kbn/actions-plugin/common'; import { MaintenanceWindow } from '../../pages/maintenance_windows/types'; import { INTERNAL_BASE_ALERTING_API_PATH } from '../../../common'; -const rewriteBodyRes: RewriteRequestCase = ({ r_rule: rRule, ...rest }) => ({ +const rewriteBodyRes: RewriteRequestCase = ({ + r_rule: rRule, + category_ids: categoryIds, + ...rest +}) => ({ ...rest, rRule, + categoryIds, }); export async function finishMaintenanceWindow({ diff --git a/x-pack/plugins/alerting/public/services/maintenance_windows_api/get.ts b/x-pack/plugins/alerting/public/services/maintenance_windows_api/get.ts index d353f2b45124b..0a380e1e5b9c2 100644 --- a/x-pack/plugins/alerting/public/services/maintenance_windows_api/get.ts +++ b/x-pack/plugins/alerting/public/services/maintenance_windows_api/get.ts @@ -10,8 +10,13 @@ import { AsApiContract, RewriteRequestCase } from '@kbn/actions-plugin/common'; import { MaintenanceWindow } from '../../pages/maintenance_windows/types'; import { INTERNAL_BASE_ALERTING_API_PATH } from '../../../common'; -const rewriteBodyRes: RewriteRequestCase = ({ r_rule: rRule, ...rest }) => ({ +const rewriteBodyRes: RewriteRequestCase = ({ + r_rule: rRule, + category_ids: categoryIds, + ...rest +}) => ({ ...rest, + categoryIds, rRule, }); diff --git a/x-pack/plugins/alerting/public/services/maintenance_windows_api/update.ts b/x-pack/plugins/alerting/public/services/maintenance_windows_api/update.ts index c8328c23e825d..02e929f28e42e 100644 --- a/x-pack/plugins/alerting/public/services/maintenance_windows_api/update.ts +++ b/x-pack/plugins/alerting/public/services/maintenance_windows_api/update.ts @@ -10,14 +10,24 @@ import { AsApiContract, RewriteRequestCase, RewriteResponseCase } from '@kbn/act import { MaintenanceWindow } from '../../pages/maintenance_windows/types'; import { INTERNAL_BASE_ALERTING_API_PATH } from '../../../common'; -const rewriteBodyRequest: RewriteResponseCase = ({ rRule, ...res }) => ({ +const rewriteBodyRequest: RewriteResponseCase = ({ + rRule, + categoryIds, + ...res +}) => ({ ...res, r_rule: rRule, + category_ids: categoryIds, }); -const rewriteBodyRes: RewriteRequestCase = ({ r_rule: rRule, ...rest }) => ({ +const rewriteBodyRes: RewriteRequestCase = ({ + r_rule: rRule, + category_ids: categoryIds, + ...rest +}) => ({ ...rest, rRule, + categoryIds, }); export async function updateMaintenanceWindow({ diff --git a/x-pack/plugins/alerting/server/alerts_client/alerts_client.test.ts b/x-pack/plugins/alerting/server/alerts_client/alerts_client.test.ts index 6fc742e344758..6921ad7a99402 100644 --- a/x-pack/plugins/alerting/server/alerts_client/alerts_client.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/alerts_client.test.ts @@ -48,6 +48,7 @@ const ruleType: jest.Mocked = { isExportable: true, recoveryActionGroup: RecoveredActionGroup, executor: jest.fn(), + category: 'test', producer: 'alerts', cancelAlertsOnRuleTimeout: true, ruleTaskTimeout: '5m', diff --git a/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.test.ts b/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.test.ts index 72d222edf5593..8eb436a4f7b35 100644 --- a/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/legacy_alerts_client.test.ts @@ -93,6 +93,7 @@ const ruleType: jest.Mocked = { isExportable: true, recoveryActionGroup: RecoveredActionGroup, executor: jest.fn(), + category: 'test', producer: 'alerts', cancelAlertsOnRuleTimeout: true, ruleTaskTimeout: '5m', diff --git a/x-pack/plugins/alerting/server/alerts_client/lib/format_rule.test.ts b/x-pack/plugins/alerting/server/alerts_client/lib/format_rule.test.ts index 6e8d28de01753..bef787ad369de 100644 --- a/x-pack/plugins/alerting/server/alerts_client/lib/format_rule.test.ts +++ b/x-pack/plugins/alerting/server/alerts_client/lib/format_rule.test.ts @@ -17,6 +17,7 @@ const ruleType: jest.Mocked = { isExportable: true, recoveryActionGroup: RecoveredActionGroup, executor: jest.fn(), + category: 'test', producer: 'alerts', cancelAlertsOnRuleTimeout: true, ruleTaskTimeout: '5m', diff --git a/x-pack/plugins/alerting/server/alerts_service/alerts_service.test.ts b/x-pack/plugins/alerting/server/alerts_service/alerts_service.test.ts index 1803bdf156469..5002ecaf7e757 100644 --- a/x-pack/plugins/alerting/server/alerts_service/alerts_service.test.ts +++ b/x-pack/plugins/alerting/server/alerts_service/alerts_service.test.ts @@ -196,6 +196,7 @@ const ruleType: jest.Mocked = { isExportable: true, recoveryActionGroup: RecoveredActionGroup, executor: jest.fn(), + category: 'test', producer: 'alerts', cancelAlertsOnRuleTimeout: true, ruleTaskTimeout: '5m', diff --git a/x-pack/plugins/alerting/server/application/maintenance_window/constants.ts b/x-pack/plugins/alerting/server/application/maintenance_window/constants.ts index 2844030ad79cc..caa1616d10a7c 100644 --- a/x-pack/plugins/alerting/server/application/maintenance_window/constants.ts +++ b/x-pack/plugins/alerting/server/application/maintenance_window/constants.ts @@ -11,3 +11,10 @@ export const maintenanceWindowStatus = { FINISHED: 'finished', ARCHIVED: 'archived', } as const; + +export const maintenanceWindowCategoryIdTypes = { + KIBANA: 'kibana', + OBSERVABILITY: 'observability', + SECURITY_SOLUTION: 'securitySolution', + MANAGEMENT: 'management', +} as const; diff --git a/x-pack/plugins/alerting/server/application/maintenance_window/methods/create/create_maintenance_window.test.ts b/x-pack/plugins/alerting/server/application/maintenance_window/methods/create/create_maintenance_window.test.ts index 452c3fe5c999c..1de657bdfd6ef 100644 --- a/x-pack/plugins/alerting/server/application/maintenance_window/methods/create/create_maintenance_window.test.ts +++ b/x-pack/plugins/alerting/server/application/maintenance_window/methods/create/create_maintenance_window.test.ts @@ -15,6 +15,7 @@ import { MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE, } from '../../../../../common'; import { getMockMaintenanceWindow } from '../../../../data/maintenance_window/test_helpers'; +import type { MaintenanceWindow } from '../../types'; const savedObjectsClient = savedObjectsClientMock.create(); @@ -86,4 +87,75 @@ describe('MaintenanceWindowClient - create', () => { }) ); }); + + it('should create maintenance window with category ids', async () => { + jest.useFakeTimers().setSystemTime(new Date('2023-02-26T00:00:00.000Z')); + + const mockMaintenanceWindow = getMockMaintenanceWindow({ + expirationDate: moment(new Date()).tz('UTC').add(1, 'year').toISOString(), + }); + + savedObjectsClient.create.mockResolvedValueOnce({ + attributes: mockMaintenanceWindow, + version: '123', + id: 'test-id', + } as unknown as SavedObject); + + const result = await createMaintenanceWindow(mockContext, { + data: { + title: mockMaintenanceWindow.title, + duration: mockMaintenanceWindow.duration, + rRule: mockMaintenanceWindow.rRule as CreateMaintenanceWindowParams['data']['rRule'], + categoryIds: ['observability', 'securitySolution'], + }, + }); + + expect(savedObjectsClient.create).toHaveBeenLastCalledWith( + MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE, + expect.objectContaining({ + title: mockMaintenanceWindow.title, + duration: mockMaintenanceWindow.duration, + rRule: mockMaintenanceWindow.rRule, + enabled: true, + expirationDate: moment(new Date()).tz('UTC').add(1, 'year').toISOString(), + categoryIds: ['observability', 'securitySolution'], + ...updatedMetadata, + }), + { + id: expect.any(String), + } + ); + + expect(result).toEqual( + expect.objectContaining({ + id: 'test-id', + }) + ); + }); + + it('should throw if trying to create a maintenance window with invalid category ids', async () => { + jest.useFakeTimers().setSystemTime(new Date('2023-02-26T00:00:00.000Z')); + + const mockMaintenanceWindow = getMockMaintenanceWindow({ + expirationDate: moment(new Date()).tz('UTC').add(1, 'year').toISOString(), + }); + + await expect(async () => { + await createMaintenanceWindow(mockContext, { + data: { + title: mockMaintenanceWindow.title, + duration: mockMaintenanceWindow.duration, + rRule: mockMaintenanceWindow.rRule as CreateMaintenanceWindowParams['data']['rRule'], + categoryIds: ['invalid_id'] as unknown as MaintenanceWindow['categoryIds'], + }, + }); + }).rejects.toThrowErrorMatchingInlineSnapshot(` + "Error validating create maintenance window data - [data.categoryIds]: types that failed validation: + - [data.categoryIds.0.0]: types that failed validation: + - [data.categoryIds.0.0]: expected value to equal [observability] + - [data.categoryIds.0.1]: expected value to equal [securitySolution] + - [data.categoryIds.0.2]: expected value to equal [management] + - [data.categoryIds.1]: expected value to equal [null]" + `); + }); }); diff --git a/x-pack/plugins/alerting/server/application/maintenance_window/methods/create/create_maintenance_window.ts b/x-pack/plugins/alerting/server/application/maintenance_window/methods/create/create_maintenance_window.ts index 4f02b9038197f..b8fba87b2fc18 100644 --- a/x-pack/plugins/alerting/server/application/maintenance_window/methods/create/create_maintenance_window.ts +++ b/x-pack/plugins/alerting/server/application/maintenance_window/methods/create/create_maintenance_window.ts @@ -25,7 +25,7 @@ export async function createMaintenanceWindow( ): Promise { const { data } = params; const { savedObjectsClient, getModificationMetadata, logger } = context; - const { title, duration, rRule } = data; + const { title, duration, rRule, categoryIds } = data; try { createMaintenanceWindowParamsSchema.validate(params); @@ -42,6 +42,7 @@ export async function createMaintenanceWindow( title, enabled: true, expirationDate, + categoryIds, rRule: rRule as MaintenanceWindow['rRule'], duration, events, diff --git a/x-pack/plugins/alerting/server/application/maintenance_window/methods/create/schemas/create_maintenance_window_params_schema.ts b/x-pack/plugins/alerting/server/application/maintenance_window/methods/create/schemas/create_maintenance_window_params_schema.ts index 306b9bd8c259a..dcb9150f2385b 100644 --- a/x-pack/plugins/alerting/server/application/maintenance_window/methods/create/schemas/create_maintenance_window_params_schema.ts +++ b/x-pack/plugins/alerting/server/application/maintenance_window/methods/create/schemas/create_maintenance_window_params_schema.ts @@ -6,6 +6,7 @@ */ import { schema } from '@kbn/config-schema'; +import { maintenanceWindowCategoryIdsSchema } from '../../../schemas'; import { rRuleRequestSchema } from '../../../../r_rule/schemas'; export const createMaintenanceWindowParamsSchema = schema.object({ @@ -13,5 +14,6 @@ export const createMaintenanceWindowParamsSchema = schema.object({ title: schema.string(), duration: schema.number(), rRule: rRuleRequestSchema, + categoryIds: maintenanceWindowCategoryIdsSchema, }), }); diff --git a/x-pack/plugins/alerting/server/application/maintenance_window/methods/update/schemas/update_maintenance_window_params_schema.ts b/x-pack/plugins/alerting/server/application/maintenance_window/methods/update/schemas/update_maintenance_window_params_schema.ts index 1171855ca2819..84120a5ee8c44 100644 --- a/x-pack/plugins/alerting/server/application/maintenance_window/methods/update/schemas/update_maintenance_window_params_schema.ts +++ b/x-pack/plugins/alerting/server/application/maintenance_window/methods/update/schemas/update_maintenance_window_params_schema.ts @@ -6,6 +6,7 @@ */ import { schema } from '@kbn/config-schema'; +import { maintenanceWindowCategoryIdsSchema } from '../../../schemas'; import { rRuleRequestSchema } from '../../../../r_rule/schemas'; export const updateMaintenanceWindowParamsSchema = schema.object({ @@ -15,5 +16,6 @@ export const updateMaintenanceWindowParamsSchema = schema.object({ enabled: schema.maybe(schema.boolean()), duration: schema.maybe(schema.number()), rRule: schema.maybe(rRuleRequestSchema), + categoryIds: maintenanceWindowCategoryIdsSchema, }), }); diff --git a/x-pack/plugins/alerting/server/application/maintenance_window/methods/update/update_maintenance_window.test.ts b/x-pack/plugins/alerting/server/application/maintenance_window/methods/update/update_maintenance_window.test.ts index 27798cc4e3c57..966808fed57f2 100644 --- a/x-pack/plugins/alerting/server/application/maintenance_window/methods/update/update_maintenance_window.test.ts +++ b/x-pack/plugins/alerting/server/application/maintenance_window/methods/update/update_maintenance_window.test.ts @@ -90,6 +90,7 @@ describe('MaintenanceWindowClient - update', () => { data: { ...updatedAttributes, rRule: updatedAttributes.rRule as UpdateMaintenanceWindowParams['data']['rRule'], + categoryIds: ['observability', 'securitySolution'], }, }); @@ -110,6 +111,7 @@ describe('MaintenanceWindowClient - update', () => { createdBy: 'test-user', updatedAt: updatedMetadata.updatedAt, updatedBy: updatedMetadata.updatedBy, + categoryIds: ['observability', 'securitySolution'], }, { id: 'test-id', @@ -117,7 +119,7 @@ describe('MaintenanceWindowClient - update', () => { version: '123', } ); - // Only these 3 properties are worth asserting since the rest come from mocks + // Only these properties are worth asserting since the rest come from mocks expect(result).toEqual( expect.objectContaining({ id: 'test-id', @@ -235,4 +237,28 @@ describe('MaintenanceWindowClient - update', () => { 'Failed to update maintenance window by id: test-id, Error: Error: Cannot edit archived maintenance windows' ); }); + + it('should throw if updating a maintenance window with invalid category ids', async () => { + await expect(async () => { + await updateMaintenanceWindow(mockContext, { + id: 'test-id', + data: { + categoryIds: ['invalid_id'] as unknown as MaintenanceWindow['categoryIds'], + rRule: { + tzid: 'CET', + dtstart: '2023-03-26T00:00:00.000Z', + freq: Frequency.WEEKLY, + count: 2, + }, + }, + }); + }).rejects.toThrowErrorMatchingInlineSnapshot(` + "Error validating update maintenance window data - [data.categoryIds]: types that failed validation: + - [data.categoryIds.0.0]: types that failed validation: + - [data.categoryIds.0.0]: expected value to equal [observability] + - [data.categoryIds.0.1]: expected value to equal [securitySolution] + - [data.categoryIds.0.2]: expected value to equal [management] + - [data.categoryIds.1]: expected value to equal [null]" + `); + }); }); diff --git a/x-pack/plugins/alerting/server/application/maintenance_window/methods/update/update_maintenance_window.ts b/x-pack/plugins/alerting/server/application/maintenance_window/methods/update/update_maintenance_window.ts index 19b145a489431..f10381defc9b9 100644 --- a/x-pack/plugins/alerting/server/application/maintenance_window/methods/update/update_maintenance_window.ts +++ b/x-pack/plugins/alerting/server/application/maintenance_window/methods/update/update_maintenance_window.ts @@ -45,7 +45,7 @@ async function updateWithOCC( ): Promise { const { savedObjectsClient, getModificationMetadata, logger } = context; const { id, data } = params; - const { title, enabled, duration, rRule } = data; + const { title, enabled, duration, rRule, categoryIds } = data; try { updateMaintenanceWindowParamsSchema.validate(params); @@ -87,6 +87,7 @@ async function updateWithOCC( ...maintenanceWindow, ...(title ? { title } : {}), ...(rRule ? { rRule: rRule as MaintenanceWindow['rRule'] } : {}), + ...(categoryIds !== undefined ? { categoryIds } : {}), ...(typeof duration === 'number' ? { duration } : {}), ...(typeof enabled === 'boolean' ? { enabled } : {}), expirationDate, diff --git a/x-pack/plugins/alerting/server/application/maintenance_window/schemas/index.ts b/x-pack/plugins/alerting/server/application/maintenance_window/schemas/index.ts index 6cfbebc677b33..bb6926752f376 100644 --- a/x-pack/plugins/alerting/server/application/maintenance_window/schemas/index.ts +++ b/x-pack/plugins/alerting/server/application/maintenance_window/schemas/index.ts @@ -8,4 +8,5 @@ export { maintenanceWindowEventSchema, maintenanceWindowSchema, + maintenanceWindowCategoryIdsSchema, } from './maintenance_window_schemas'; diff --git a/x-pack/plugins/alerting/server/application/maintenance_window/schemas/maintenance_window_schemas.ts b/x-pack/plugins/alerting/server/application/maintenance_window/schemas/maintenance_window_schemas.ts index bb70e4ee82abe..080f5b1bbe676 100644 --- a/x-pack/plugins/alerting/server/application/maintenance_window/schemas/maintenance_window_schemas.ts +++ b/x-pack/plugins/alerting/server/application/maintenance_window/schemas/maintenance_window_schemas.ts @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; -import { maintenanceWindowStatus } from '../constants'; +import { maintenanceWindowStatus, maintenanceWindowCategoryIdTypes } from '../constants'; import { rRuleSchema } from '../../r_rule/schemas'; export const maintenanceWindowEventSchema = schema.object({ @@ -14,6 +14,18 @@ export const maintenanceWindowEventSchema = schema.object({ lte: schema.string(), }); +export const maintenanceWindowCategoryIdsSchema = schema.maybe( + schema.nullable( + schema.arrayOf( + schema.oneOf([ + schema.literal(maintenanceWindowCategoryIdTypes.OBSERVABILITY), + schema.literal(maintenanceWindowCategoryIdTypes.SECURITY_SOLUTION), + schema.literal(maintenanceWindowCategoryIdTypes.MANAGEMENT), + ]) + ) + ) +); + export const maintenanceWindowSchema = schema.object({ id: schema.string(), title: schema.string(), @@ -34,4 +46,5 @@ export const maintenanceWindowSchema = schema.object({ schema.literal(maintenanceWindowStatus.FINISHED), schema.literal(maintenanceWindowStatus.ARCHIVED), ]), + categoryIds: maintenanceWindowCategoryIdsSchema, }); diff --git a/x-pack/plugins/alerting/server/application/maintenance_window/transforms/transform_maintenance_window_attributes_to_maintenance_window.ts b/x-pack/plugins/alerting/server/application/maintenance_window/transforms/transform_maintenance_window_attributes_to_maintenance_window.ts index a03dbe9e4b3a8..53996f29694f4 100644 --- a/x-pack/plugins/alerting/server/application/maintenance_window/transforms/transform_maintenance_window_attributes_to_maintenance_window.ts +++ b/x-pack/plugins/alerting/server/application/maintenance_window/transforms/transform_maintenance_window_attributes_to_maintenance_window.ts @@ -39,5 +39,6 @@ export const transformMaintenanceWindowAttributesToMaintenanceWindow = ( eventStartTime, eventEndTime, status, + ...(attributes.categoryIds !== undefined ? { categoryIds: attributes.categoryIds } : {}), }; }; diff --git a/x-pack/plugins/alerting/server/application/maintenance_window/transforms/transform_maintenance_window_to_maintenance_window_attributes.ts b/x-pack/plugins/alerting/server/application/maintenance_window/transforms/transform_maintenance_window_to_maintenance_window_attributes.ts index 98b2f9fe93bce..35865f38aa9b2 100644 --- a/x-pack/plugins/alerting/server/application/maintenance_window/transforms/transform_maintenance_window_to_maintenance_window_attributes.ts +++ b/x-pack/plugins/alerting/server/application/maintenance_window/transforms/transform_maintenance_window_to_maintenance_window_attributes.ts @@ -22,5 +22,8 @@ export const transformMaintenanceWindowToMaintenanceWindowAttributes = ( updatedBy: maintenanceWindow.updatedBy, createdAt: maintenanceWindow.createdAt, updatedAt: maintenanceWindow.updatedAt, + ...(maintenanceWindow.categoryIds !== undefined + ? { categoryIds: maintenanceWindow.categoryIds } + : {}), }; }; diff --git a/x-pack/plugins/alerting/server/application/rule/methods/aggregate/aggregate_rules.test.ts b/x-pack/plugins/alerting/server/application/rule/methods/aggregate/aggregate_rules.test.ts index fd9daa3cb7356..31df2212bb5b5 100644 --- a/x-pack/plugins/alerting/server/application/rule/methods/aggregate/aggregate_rules.test.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/aggregate/aggregate_rules.test.ts @@ -80,6 +80,7 @@ describe('aggregate()', () => { recoveryActionGroup: RecoveredActionGroup, id: 'myType', name: 'myType', + category: 'test', producer: 'myApp', enabledInLicense: true, hasAlertsMappings: false, @@ -162,6 +163,7 @@ describe('aggregate()', () => { minimumLicenseRequired: 'basic', isExportable: true, recoveryActionGroup: RecoveredActionGroup, + category: 'test', producer: 'alerts', authorizedConsumers: { myApp: { read: true, all: true }, diff --git a/x-pack/plugins/alerting/server/application/rule/methods/bulk_delete/bulk_delete_rules.test.ts b/x-pack/plugins/alerting/server/application/rule/methods/bulk_delete/bulk_delete_rules.test.ts index 611aca68c81c3..037791c07effa 100644 --- a/x-pack/plugins/alerting/server/application/rule/methods/bulk_delete/bulk_delete_rules.test.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/bulk_delete/bulk_delete_rules.test.ts @@ -156,6 +156,7 @@ describe('bulkDelete', () => { async executor() { return { state: {} }; }, + category: 'test', producer: 'alerts', validate: { params: schema.any(), diff --git a/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.test.ts b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.test.ts index e5d7e0c8db43a..5eee3f4513b9e 100644 --- a/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.test.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/bulk_edit/bulk_edit_rules.test.ts @@ -241,6 +241,7 @@ describe('bulkEdit()', () => { async executor() { return { state: {} }; }, + category: 'test', producer: 'alerts', validate: { params: { validate: (params) => params }, @@ -739,6 +740,7 @@ describe('bulkEdit()', () => { async executor() { return { state: {} }; }, + category: 'test', producer: 'alerts', validate: { params: { validate: (params) => params }, @@ -2354,6 +2356,7 @@ describe('bulkEdit()', () => { async executor() { return { state: {} }; }, + category: 'test', producer: 'alerts', validLegacyConsumers: [], }); @@ -2399,6 +2402,7 @@ describe('bulkEdit()', () => { async executor() { return { state: {} }; }, + category: 'test', producer: 'alerts', validLegacyConsumers: [], }); diff --git a/x-pack/plugins/alerting/server/application/rule/methods/create/create_rule.test.ts b/x-pack/plugins/alerting/server/application/rule/methods/create/create_rule.test.ts index 41ca041e89a0b..4fcb2b0d55716 100644 --- a/x-pack/plugins/alerting/server/application/rule/methods/create/create_rule.test.ts +++ b/x-pack/plugins/alerting/server/application/rule/methods/create/create_rule.test.ts @@ -1545,6 +1545,7 @@ describe('create()', () => { async executor() { return { state: {} }; }, + category: 'test', producer: 'alerts', useSavedObjectReferences: { extractReferences: extractReferencesFn, @@ -1733,6 +1734,7 @@ describe('create()', () => { async executor() { return { state: {} }; }, + category: 'test', producer: 'alerts', useSavedObjectReferences: { extractReferences: extractReferencesFn, @@ -2560,6 +2562,7 @@ describe('create()', () => { async executor() { return { state: {} }; }, + category: 'test', producer: 'alerts', validLegacyConsumers: [], }); @@ -3028,6 +3031,7 @@ describe('create()', () => { async executor() { return { state: {} }; }, + category: 'test', producer: 'alerts', useSavedObjectReferences: { extractReferences: jest.fn(), @@ -3101,6 +3105,7 @@ describe('create()', () => { async executor() { return { state: {} }; }, + category: 'test', producer: 'alerts', useSavedObjectReferences: { extractReferences: jest.fn(), @@ -3139,6 +3144,7 @@ describe('create()', () => { async executor() { return { state: {} }; }, + category: 'test', producer: 'alerts', useSavedObjectReferences: { extractReferences: jest.fn(), @@ -3232,6 +3238,7 @@ describe('create()', () => { async executor() { return { state: {} }; }, + category: 'test', producer: 'alerts', useSavedObjectReferences: { extractReferences: jest.fn(), @@ -3282,6 +3289,7 @@ describe('create()', () => { async executor() { return { state: {} }; }, + category: 'test', producer: 'alerts', useSavedObjectReferences: { extractReferences: jest.fn(), @@ -3345,6 +3353,7 @@ describe('create()', () => { async executor() { return { state: {} }; }, + category: 'test', producer: 'alerts', useSavedObjectReferences: { extractReferences: jest.fn(), @@ -3426,6 +3435,7 @@ describe('create()', () => { async executor() { return { state: {} }; }, + category: 'test', producer: 'alerts', useSavedObjectReferences: { extractReferences: jest.fn(), @@ -3626,6 +3636,7 @@ describe('create()', () => { async executor() { return { state: {} }; }, + category: 'test', producer: 'alerts', useSavedObjectReferences: { extractReferences: jest.fn(), @@ -3684,6 +3695,7 @@ describe('create()', () => { async executor() { return { state: {} }; }, + category: 'test', producer: 'alerts', useSavedObjectReferences: { extractReferences: jest.fn(), diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts index c68532082f18a..882d3bd103531 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts @@ -197,6 +197,7 @@ beforeEach(() => { async executor() { return { state: {} }; }, + category: 'test', producer: 'myApp', validate: { params: schema.any(), @@ -761,6 +762,7 @@ describe('AlertingAuthorization', () => { recoveryActionGroup: RecoveredActionGroup, id: 'myOtherAppAlertType', name: 'myOtherAppAlertType', + category: 'test', producer: 'alerts', enabledInLicense: true, hasAlertsMappings: false, @@ -776,6 +778,7 @@ describe('AlertingAuthorization', () => { recoveryActionGroup: RecoveredActionGroup, id: 'myAppAlertType', name: 'myAppAlertType', + category: 'test', producer: 'myApp', enabledInLicense: true, hasAlertsMappings: false, @@ -791,6 +794,7 @@ describe('AlertingAuthorization', () => { recoveryActionGroup: RecoveredActionGroup, id: 'mySecondAppAlertType', name: 'mySecondAppAlertType', + category: 'test', producer: 'myApp', enabledInLicense: true, hasAlertsMappings: false, @@ -1165,6 +1169,7 @@ describe('AlertingAuthorization', () => { recoveryActionGroup: RecoveredActionGroup, id: 'myOtherAppAlertType', name: 'myOtherAppAlertType', + category: 'test', producer: 'myOtherApp', enabledInLicense: true, hasAlertsMappings: false, @@ -1180,6 +1185,7 @@ describe('AlertingAuthorization', () => { recoveryActionGroup: RecoveredActionGroup, id: 'myAppAlertType', name: 'myAppAlertType', + category: 'test', producer: 'myApp', enabledInLicense: true, hasAlertsMappings: false, @@ -1238,6 +1244,7 @@ describe('AlertingAuthorization', () => { "read": true, }, }, + "category": "test", "defaultActionGroupId": "default", "enabledInLicense": true, "hasAlertsMappings": false, @@ -1274,6 +1281,7 @@ describe('AlertingAuthorization', () => { "read": true, }, }, + "category": "test", "defaultActionGroupId": "default", "enabledInLicense": true, "hasAlertsMappings": false, @@ -1356,6 +1364,7 @@ describe('AlertingAuthorization', () => { "read": true, }, }, + "category": "test", "defaultActionGroupId": "default", "enabledInLicense": true, "hasAlertsMappings": false, @@ -1384,6 +1393,7 @@ describe('AlertingAuthorization', () => { "read": true, }, }, + "category": "test", "defaultActionGroupId": "default", "enabledInLicense": true, "hasAlertsMappings": false, @@ -1453,6 +1463,7 @@ describe('AlertingAuthorization', () => { "read": true, }, }, + "category": "test", "defaultActionGroupId": "default", "enabledInLicense": true, "hasAlertsMappings": false, @@ -1560,6 +1571,7 @@ describe('AlertingAuthorization', () => { "read": true, }, }, + "category": "test", "defaultActionGroupId": "default", "enabledInLicense": true, "hasAlertsMappings": false, @@ -1588,6 +1600,7 @@ describe('AlertingAuthorization', () => { "read": true, }, }, + "category": "test", "defaultActionGroupId": "default", "enabledInLicense": true, "hasAlertsMappings": false, @@ -1674,6 +1687,7 @@ describe('AlertingAuthorization', () => { "read": true, }, }, + "category": "test", "defaultActionGroupId": "default", "enabledInLicense": true, "hasAlertsMappings": false, @@ -1703,6 +1717,7 @@ describe('AlertingAuthorization', () => { recoveryActionGroup: RecoveredActionGroup, id: 'myOtherAppAlertType', name: 'myOtherAppAlertType', + category: 'test', producer: 'alerts', enabledInLicense: true, isExportable: true, @@ -1718,6 +1733,7 @@ describe('AlertingAuthorization', () => { recoveryActionGroup: RecoveredActionGroup, id: 'myAppAlertType', name: 'myAppAlertType', + category: 'test', producer: 'myApp', enabledInLicense: true, isExportable: true, @@ -1733,6 +1749,7 @@ describe('AlertingAuthorization', () => { recoveryActionGroup: RecoveredActionGroup, id: 'mySecondAppAlertType', name: 'mySecondAppAlertType', + category: 'test', producer: 'myApp', enabledInLicense: true, isExportable: true, @@ -1790,6 +1807,7 @@ describe('AlertingAuthorization', () => { "read": true, }, }, + "category": "test", "defaultActionGroupId": "default", "enabledInLicense": true, "hasAlertsMappings": false, @@ -1866,6 +1884,7 @@ describe('AlertingAuthorization', () => { "read": true, }, }, + "category": "test", "defaultActionGroupId": "default", "enabledInLicense": true, "hasAlertsMappings": false, @@ -1902,6 +1921,7 @@ describe('AlertingAuthorization', () => { recoveryActionGroup: RecoveredActionGroup, id: '.esQuery', name: 'ES Query', + category: 'management', producer: 'stackAlerts', enabledInLicense: true, hasAlertsMappings: false, @@ -1917,6 +1937,7 @@ describe('AlertingAuthorization', () => { recoveryActionGroup: RecoveredActionGroup, id: '.threshold-rule-o11y', name: 'New threshold 011y', + category: 'observability', producer: 'observability', enabledInLicense: true, hasAlertsMappings: false, @@ -1932,6 +1953,7 @@ describe('AlertingAuthorization', () => { recoveryActionGroup: RecoveredActionGroup, id: '.infrastructure-threshold-o11y', name: 'Metrics o11y', + category: 'observability', producer: 'infrastructure', enabledInLicense: true, hasAlertsMappings: false, @@ -1947,6 +1969,7 @@ describe('AlertingAuthorization', () => { recoveryActionGroup: RecoveredActionGroup, id: '.logs-threshold-o11y', name: 'Logs o11y', + category: 'observability', producer: 'logs', enabledInLicense: true, hasAlertsMappings: false, diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.test.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.test.ts index bfa36ccfd46c5..b433a95a77734 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.test.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization_kuery.test.ts @@ -25,6 +25,7 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { recoveryActionGroup: RecoveredActionGroup, id: 'myAppAlertType', name: 'myAppAlertType', + category: 'test', producer: 'myApp', minimumLicenseRequired: 'basic', isExportable: true, @@ -63,6 +64,7 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { recoveryActionGroup: RecoveredActionGroup, id: 'myAppAlertType', name: 'myAppAlertType', + category: 'test', producer: 'myApp', authorizedConsumers: { alerts: { read: true, all: true }, @@ -103,6 +105,7 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { recoveryActionGroup: RecoveredActionGroup, id: 'myAppAlertType', name: 'myAppAlertType', + category: 'test', producer: 'myApp', authorizedConsumers: { alerts: { read: true, all: true }, @@ -123,6 +126,7 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { recoveryActionGroup: RecoveredActionGroup, id: 'myOtherAppAlertType', name: 'myOtherAppAlertType', + category: 'test', producer: 'alerts', authorizedConsumers: { alerts: { read: true, all: true }, @@ -143,6 +147,7 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { recoveryActionGroup: RecoveredActionGroup, id: 'mySecondAppAlertType', name: 'mySecondAppAlertType', + category: 'test', producer: 'myApp', authorizedConsumers: { alerts: { read: true, all: true }, @@ -184,6 +189,7 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { recoveryActionGroup: RecoveredActionGroup, id: 'myAppAlertType', name: 'myAppAlertType', + category: 'test', producer: 'myApp', authorizedConsumers: { alerts: { read: true, all: true }, @@ -204,6 +210,7 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { recoveryActionGroup: RecoveredActionGroup, id: 'myOtherAppAlertType', name: 'myOtherAppAlertType', + category: 'test', producer: 'alerts', authorizedConsumers: { alerts: { read: true, all: true }, @@ -246,6 +253,7 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { recoveryActionGroup: RecoveredActionGroup, id: 'myAppAlertType', name: 'myAppAlertType', + category: 'test', producer: 'myApp', authorizedConsumers: { alerts: { read: true, all: true }, @@ -266,6 +274,7 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { recoveryActionGroup: RecoveredActionGroup, id: 'myOtherAppAlertType', name: 'myOtherAppAlertType', + category: 'test', producer: 'alerts', authorizedConsumers: { alerts: { read: true, all: true }, @@ -305,6 +314,7 @@ describe('asKqlFiltersByRuleTypeAndConsumer', () => { recoveryActionGroup: RecoveredActionGroup, id: 'myAppAlertType', name: 'myAppAlertType', + category: 'test', producer: 'myApp', minimumLicenseRequired: 'basic', isExportable: true, @@ -340,6 +350,7 @@ describe('asEsDslFiltersByRuleTypeAndConsumer', () => { recoveryActionGroup: RecoveredActionGroup, id: 'myAppAlertType', name: 'myAppAlertType', + category: 'test', producer: 'myApp', minimumLicenseRequired: 'basic', isExportable: true, @@ -405,6 +416,7 @@ describe('asEsDslFiltersByRuleTypeAndConsumer', () => { recoveryActionGroup: RecoveredActionGroup, id: 'myAppAlertType', name: 'myAppAlertType', + category: 'test', producer: 'myApp', authorizedConsumers: { alerts: { read: true, all: true }, @@ -477,6 +489,7 @@ describe('asEsDslFiltersByRuleTypeAndConsumer', () => { recoveryActionGroup: RecoveredActionGroup, id: 'myAppAlertType', name: 'myAppAlertType', + category: 'test', producer: 'myApp', authorizedConsumers: { alerts: { read: true, all: true }, @@ -497,6 +510,7 @@ describe('asEsDslFiltersByRuleTypeAndConsumer', () => { recoveryActionGroup: RecoveredActionGroup, id: 'myOtherAppAlertType', name: 'myOtherAppAlertType', + category: 'test', producer: 'alerts', authorizedConsumers: { alerts: { read: true, all: true }, @@ -517,6 +531,7 @@ describe('asEsDslFiltersByRuleTypeAndConsumer', () => { recoveryActionGroup: RecoveredActionGroup, id: 'mySecondAppAlertType', name: 'mySecondAppAlertType', + category: 'test', producer: 'myApp', authorizedConsumers: { alerts: { read: true, all: true }, @@ -686,6 +701,7 @@ describe('asEsDslFiltersByRuleTypeAndConsumer', () => { recoveryActionGroup: RecoveredActionGroup, id: 'myAppAlertType', name: 'myAppAlertType', + category: 'test', producer: 'myApp', minimumLicenseRequired: 'basic', isExportable: true, diff --git a/x-pack/plugins/alerting/server/data/maintenance_window/constants.ts b/x-pack/plugins/alerting/server/data/maintenance_window/constants.ts new file mode 100644 index 0000000000000..1e165d9883fb8 --- /dev/null +++ b/x-pack/plugins/alerting/server/data/maintenance_window/constants.ts @@ -0,0 +1,15 @@ +/* + * 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. + */ + +export const maintenanceWindowCategoryIdTypes = { + OBSERVABILITY: 'observability', + SECURITY_SOLUTION: 'securitySolution', + MANAGEMENT: 'management', +} as const; + +export type MaintenanceWindowCategoryIdTypes = + typeof maintenanceWindowCategoryIdTypes[keyof typeof maintenanceWindowCategoryIdTypes]; diff --git a/x-pack/plugins/alerting/server/data/maintenance_window/types/maintenance_window_attributes.ts b/x-pack/plugins/alerting/server/data/maintenance_window/types/maintenance_window_attributes.ts index 6167d94a722db..91f4e95172551 100644 --- a/x-pack/plugins/alerting/server/data/maintenance_window/types/maintenance_window_attributes.ts +++ b/x-pack/plugins/alerting/server/data/maintenance_window/types/maintenance_window_attributes.ts @@ -6,6 +6,7 @@ */ import { RRuleAttributes } from '../../r_rule/types'; +import { MaintenanceWindowCategoryIdTypes } from '../constants'; export interface MaintenanceWindowEventAttributes { gte: string; @@ -23,4 +24,5 @@ export interface MaintenanceWindowAttributes { updatedBy: string | null; createdAt: string; updatedAt: string; + categoryIds?: MaintenanceWindowCategoryIdTypes[] | null; } diff --git a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts index 609c860e4f7e9..de71e4e67f590 100644 --- a/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts +++ b/x-pack/plugins/alerting/server/lib/alerting_event_logger/alerting_event_logger.test.ts @@ -41,6 +41,7 @@ const ruleType: jest.Mocked = { isExportable: true, recoveryActionGroup: RecoveredActionGroup, executor: jest.fn(), + category: 'test', producer: 'alerts', ruleTaskTimeout: '1m', validate: { diff --git a/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.test.ts b/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.test.ts index 5aec760a1eadf..2936df04c9cdb 100644 --- a/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.test.ts +++ b/x-pack/plugins/alerting/server/lib/create_alert_event_log_record_object.test.ts @@ -22,6 +22,7 @@ describe('createAlertEventLogRecordObject', () => { isExportable: true, recoveryActionGroup: RecoveredActionGroup, executor: jest.fn(), + category: 'test', producer: 'alerts', validate: { params: schema.any(), diff --git a/x-pack/plugins/alerting/server/lib/create_get_alert_indices_alias.test.ts b/x-pack/plugins/alerting/server/lib/create_get_alert_indices_alias.test.ts index 092acf1049eca..52b9a12cc35c0 100644 --- a/x-pack/plugins/alerting/server/lib/create_get_alert_indices_alias.test.ts +++ b/x-pack/plugins/alerting/server/lib/create_get_alert_indices_alias.test.ts @@ -46,6 +46,7 @@ describe('createGetAlertIndicesAliasFn', () => { minimumLicenseRequired: 'basic', isExportable: true, executor: jest.fn(), + category: 'test', producer: 'alerts', alerts: { context: 'test', @@ -68,6 +69,7 @@ describe('createGetAlertIndicesAliasFn', () => { minimumLicenseRequired: 'basic', isExportable: true, executor: jest.fn(), + category: 'test', producer: 'alerts', alerts: { context: 'spaceAware', @@ -91,6 +93,7 @@ describe('createGetAlertIndicesAliasFn', () => { minimumLicenseRequired: 'basic', isExportable: true, executor: jest.fn(), + category: 'test', producer: 'alerts', validate: { params: schema.any(), diff --git a/x-pack/plugins/alerting/server/lib/license_state.test.ts b/x-pack/plugins/alerting/server/lib/license_state.test.ts index a0e2c31b07903..ff1fc0a835396 100644 --- a/x-pack/plugins/alerting/server/lib/license_state.test.ts +++ b/x-pack/plugins/alerting/server/lib/license_state.test.ts @@ -68,6 +68,7 @@ describe('getLicenseCheckForRuleType', () => { ], defaultActionGroupId: 'default', executor: jest.fn(), + category: 'test', producer: 'alerts', minimumLicenseRequired: 'gold', isExportable: true, @@ -206,6 +207,7 @@ describe('ensureLicenseForRuleType()', () => { ], defaultActionGroupId: 'default', executor: jest.fn(), + category: 'test', producer: 'alerts', minimumLicenseRequired: 'gold', isExportable: true, diff --git a/x-pack/plugins/alerting/server/plugin.test.ts b/x-pack/plugins/alerting/server/plugin.test.ts index 3f59d1457d57c..b77674515f6c6 100644 --- a/x-pack/plugins/alerting/server/plugin.test.ts +++ b/x-pack/plugins/alerting/server/plugin.test.ts @@ -46,6 +46,7 @@ const sampleRuleType: RuleType { context: [], state: [], }, + category: 'test', producer: 'test', enabledInLicense: true, hasAlertsMappings: false, @@ -86,6 +87,7 @@ describe('listAlertTypesRoute', () => { "state": Array [], }, "authorizedConsumers": Object {}, + "category": "test", "defaultActionGroupId": "default", "enabledInLicense": true, "hasAlertsMappings": false, @@ -141,6 +143,7 @@ describe('listAlertTypesRoute', () => { context: [], state: [], }, + category: 'test', producer: 'alerts', enabledInLicense: true, hasAlertsMappings: false, @@ -197,6 +200,7 @@ describe('listAlertTypesRoute', () => { context: [], state: [], }, + category: 'test', producer: 'alerts', enabledInLicense: true, hasAlertsMappings: false, diff --git a/x-pack/plugins/alerting/server/routes/maintenance_window/apis/create/create_maintenance_window_route.test.ts b/x-pack/plugins/alerting/server/routes/maintenance_window/apis/create/create_maintenance_window_route.test.ts index f019f8a04b76e..5437533ba8342 100644 --- a/x-pack/plugins/alerting/server/routes/maintenance_window/apis/create/create_maintenance_window_route.test.ts +++ b/x-pack/plugins/alerting/server/routes/maintenance_window/apis/create/create_maintenance_window_route.test.ts @@ -37,6 +37,7 @@ const createParams = { title: 'test-title', duration: 1000, r_rule: mockMaintenanceWindow.rRule, + category_ids: ['observability'], } as CreateMaintenanceWindowRequestBody; describe('createMaintenanceWindowRoute', () => { diff --git a/x-pack/plugins/alerting/server/routes/maintenance_window/apis/create/transforms/transform_create_body/v1.ts b/x-pack/plugins/alerting/server/routes/maintenance_window/apis/create/transforms/transform_create_body/v1.ts index 290f29d90849f..03627c258c5eb 100644 --- a/x-pack/plugins/alerting/server/routes/maintenance_window/apis/create/transforms/transform_create_body/v1.ts +++ b/x-pack/plugins/alerting/server/routes/maintenance_window/apis/create/transforms/transform_create_body/v1.ts @@ -15,5 +15,6 @@ export const transformCreateBody = ( title: createBody.title, duration: createBody.duration, rRule: createBody.r_rule, + categoryIds: createBody.category_ids, }; }; diff --git a/x-pack/plugins/alerting/server/routes/maintenance_window/apis/update/transforms/transform_update_body/v1.ts b/x-pack/plugins/alerting/server/routes/maintenance_window/apis/update/transforms/transform_update_body/v1.ts index 5e6ac58bb16f3..9da852bd6ebff 100644 --- a/x-pack/plugins/alerting/server/routes/maintenance_window/apis/update/transforms/transform_update_body/v1.ts +++ b/x-pack/plugins/alerting/server/routes/maintenance_window/apis/update/transforms/transform_update_body/v1.ts @@ -11,11 +11,12 @@ import { UpdateMaintenanceWindowParams } from '../../../../../../application/mai export const transformUpdateBody = ( updateBody: UpdateMaintenanceWindowRequestBodyV1 ): UpdateMaintenanceWindowParams['data'] => { - const { title, enabled, duration, r_rule: rRule } = updateBody; + const { title, enabled, duration, r_rule: rRule, category_ids: categoryIds } = updateBody; return { ...(title !== undefined ? { title } : {}), ...(enabled !== undefined ? { enabled } : {}), ...(duration !== undefined ? { duration } : {}), ...(rRule !== undefined ? { rRule } : {}), + ...(categoryIds !== undefined ? { categoryIds } : {}), }; }; diff --git a/x-pack/plugins/alerting/server/routes/maintenance_window/apis/update/update_maintenance_window_route.test.ts b/x-pack/plugins/alerting/server/routes/maintenance_window/apis/update/update_maintenance_window_route.test.ts index 40c6da84c551c..d7425b150d1ea 100644 --- a/x-pack/plugins/alerting/server/routes/maintenance_window/apis/update/update_maintenance_window_route.test.ts +++ b/x-pack/plugins/alerting/server/routes/maintenance_window/apis/update/update_maintenance_window_route.test.ts @@ -14,6 +14,7 @@ import { getMockMaintenanceWindow } from '../../../../data/maintenance_window/te import { MaintenanceWindowStatus } from '../../../../../common'; import { transformUpdateBody } from './transforms'; import { rewritePartialMaintenanceBodyRes } from '../../../lib'; +import { UpdateMaintenanceWindowRequestBody } from '../../../../../common/routes/maintenance_window/apis/update'; const maintenanceWindowClient = maintenanceWindowClientMock.create(); @@ -29,7 +30,7 @@ const mockMaintenanceWindow = { id: 'test-id', }; -const updateParams = { +const updateParams: UpdateMaintenanceWindowRequestBody = { title: 'new-title', duration: 5000, enabled: false, @@ -39,6 +40,7 @@ const updateParams = { freq: 2 as const, count: 10, }, + category_ids: ['observability'], }; describe('updateMaintenanceWindowRoute', () => { diff --git a/x-pack/plugins/alerting/server/routes/maintenance_window/transforms/transform_maintenance_window_to_response/v1.ts b/x-pack/plugins/alerting/server/routes/maintenance_window/transforms/transform_maintenance_window_to_response/v1.ts index cfa091a25dda1..e6e0fb9a6aa42 100644 --- a/x-pack/plugins/alerting/server/routes/maintenance_window/transforms/transform_maintenance_window_to_response/v1.ts +++ b/x-pack/plugins/alerting/server/routes/maintenance_window/transforms/transform_maintenance_window_to_response/v1.ts @@ -26,5 +26,8 @@ export const transformMaintenanceWindowToResponse = ( event_start_time: maintenanceWindow.eventStartTime, event_end_time: maintenanceWindow.eventEndTime, status: maintenanceWindow.status, + ...(maintenanceWindow.categoryIds !== undefined + ? { category_ids: maintenanceWindow.categoryIds } + : {}), }; }; diff --git a/x-pack/plugins/alerting/server/routes/rule_types.test.ts b/x-pack/plugins/alerting/server/routes/rule_types.test.ts index c52ce30e41dcd..f14c3472dcc93 100644 --- a/x-pack/plugins/alerting/server/routes/rule_types.test.ts +++ b/x-pack/plugins/alerting/server/routes/rule_types.test.ts @@ -56,6 +56,7 @@ describe('ruleTypesRoute', () => { context: [], state: [], }, + category: 'test', producer: 'test', enabledInLicense: true, defaultScheduleInterval: '10m', @@ -89,6 +90,7 @@ describe('ruleTypesRoute', () => { context: [], state: [], }, + category: 'test', producer: 'test', enabled_in_license: true, has_alerts_mappings: true, @@ -114,6 +116,7 @@ describe('ruleTypesRoute', () => { "state": Array [], }, "authorized_consumers": Object {}, + "category": "test", "default_action_group_id": "default", "default_schedule_interval": "10m", "does_set_recovery_context": false, @@ -171,6 +174,7 @@ describe('ruleTypesRoute', () => { context: [], state: [], }, + category: 'test', producer: 'alerts', enabledInLicense: true, hasAlertsMappings: false, @@ -227,6 +231,7 @@ describe('ruleTypesRoute', () => { context: [], state: [], }, + category: 'test', producer: 'alerts', enabledInLicense: true, hasAlertsMappings: false, diff --git a/x-pack/plugins/alerting/server/rule_type_registry.test.ts b/x-pack/plugins/alerting/server/rule_type_registry.test.ts index 05aa84292ed8d..1de3f7c0be400 100644 --- a/x-pack/plugins/alerting/server/rule_type_registry.test.ts +++ b/x-pack/plugins/alerting/server/rule_type_registry.test.ts @@ -63,6 +63,7 @@ describe('Create Lifecycle', () => { minimumLicenseRequired: 'basic', isExportable: true, executor: jest.fn(), + category: 'test', producer: 'alerts', validate: { params: schema.any(), @@ -87,6 +88,7 @@ describe('Create Lifecycle', () => { minimumLicenseRequired: 'basic', isExportable: true, executor: jest.fn(), + category: 'test', producer: 'alerts', validate: { params: { validate: (params) => params }, @@ -125,6 +127,7 @@ describe('Create Lifecycle', () => { minimumLicenseRequired: 'basic', isExportable: true, executor: jest.fn(), + category: 'test', producer: 'alerts', validate: { params: { validate: (params) => params }, @@ -152,6 +155,7 @@ describe('Create Lifecycle', () => { minimumLicenseRequired: 'basic', isExportable: true, executor: jest.fn(), + category: 'test', producer: 'alerts', validate: { params: { validate: (params) => params }, @@ -181,6 +185,7 @@ describe('Create Lifecycle', () => { minimumLicenseRequired: 'basic', isExportable: true, executor: jest.fn(), + category: 'test', producer: 'alerts', defaultScheduleInterval: 'foobar', validate: { @@ -210,6 +215,7 @@ describe('Create Lifecycle', () => { minimumLicenseRequired: 'basic', isExportable: true, executor: jest.fn(), + category: 'test', producer: 'alerts', defaultScheduleInterval: '10s', validate: { @@ -239,6 +245,7 @@ describe('Create Lifecycle', () => { minimumLicenseRequired: 'basic', isExportable: true, executor: jest.fn(), + category: 'test', producer: 'alerts', defaultScheduleInterval: '10s', validate: { @@ -288,6 +295,7 @@ describe('Create Lifecycle', () => { minimumLicenseRequired: 'basic', isExportable: true, executor: jest.fn(), + category: 'test', producer: 'alerts', validate: { params: { validate: (params) => params }, @@ -319,6 +327,7 @@ describe('Create Lifecycle', () => { name: 'Back To Awesome', }, executor: jest.fn(), + category: 'test', producer: 'alerts', minimumLicenseRequired: 'basic', isExportable: true, @@ -356,6 +365,7 @@ describe('Create Lifecycle', () => { defaultActionGroupId: 'default', ruleTaskTimeout: '13m', executor: jest.fn(), + category: 'test', producer: 'alerts', minimumLicenseRequired: 'basic', isExportable: true, @@ -399,6 +409,7 @@ describe('Create Lifecycle', () => { minimumLicenseRequired: 'basic', isExportable: true, executor: jest.fn(), + category: 'test', producer: 'alerts', validate: { params: { validate: (params) => params }, @@ -427,6 +438,7 @@ describe('Create Lifecycle', () => { minimumLicenseRequired: 'basic', isExportable: true, executor: jest.fn(), + category: 'test', producer: 'alerts', ruleTaskTimeout: '20m', validate: { @@ -458,6 +470,7 @@ describe('Create Lifecycle', () => { minimumLicenseRequired: 'basic', isExportable: true, executor: jest.fn(), + category: 'test', producer: 'alerts', validate: { params: { validate: (params) => params }, @@ -484,6 +497,7 @@ describe('Create Lifecycle', () => { minimumLicenseRequired: 'basic', isExportable: true, executor: jest.fn(), + category: 'test', producer: 'alerts', validate: { params: { validate: (params) => params }, @@ -503,6 +517,7 @@ describe('Create Lifecycle', () => { minimumLicenseRequired: 'basic', isExportable: true, executor: jest.fn(), + category: 'test', producer: 'alerts', validate: { params: { validate: (params) => params }, @@ -526,6 +541,7 @@ describe('Create Lifecycle', () => { minimumLicenseRequired: 'basic', isExportable: true, executor: jest.fn(), + category: 'test', producer: 'alerts', alerts: { context: 'test', @@ -557,6 +573,7 @@ describe('Create Lifecycle', () => { minimumLicenseRequired: 'basic', isExportable: true, executor: jest.fn(), + category: 'test', producer: 'alerts', validate: { params: schema.any(), @@ -583,6 +600,7 @@ describe('Create Lifecycle', () => { minimumLicenseRequired: 'basic', isExportable: true, executor: jest.fn(), + category: 'test', producer: 'alerts', validate: { params: { validate: (params) => params }, @@ -606,6 +624,7 @@ describe('Create Lifecycle', () => { "params": Array [], "state": Array [], }, + "category": "test", "defaultActionGroupId": "default", "executor": [MockFunction], "id": "test", @@ -659,6 +678,7 @@ describe('Create Lifecycle', () => { ruleTaskTimeout: '20m', minimumLicenseRequired: 'basic', executor: jest.fn(), + category: 'test', producer: 'alerts', validate: { params: schema.any(), @@ -698,6 +718,7 @@ describe('Create Lifecycle', () => { }, }, }, + "category": "test", "defaultActionGroupId": "testActionGroup", "defaultScheduleInterval": undefined, "doesSetRecoveryContext": false, @@ -779,6 +800,7 @@ describe('Create Lifecycle', () => { ruleTaskTimeout: '20m', minimumLicenseRequired: 'basic', executor: jest.fn(), + category: 'test', producer: 'alerts', validate: { params: schema.any(), @@ -805,6 +827,7 @@ describe('Create Lifecycle', () => { ], defaultActionGroupId: 'default', executor: jest.fn(), + category: 'test', producer: 'alerts', isExportable: true, minimumLicenseRequired: 'basic', @@ -855,6 +878,7 @@ function ruleTypeWithVariables( async executor() { return { state: {} }; }, + category: 'test', producer: 'alerts', validate: { params: { validate: (params) => params }, diff --git a/x-pack/plugins/alerting/server/rule_type_registry.ts b/x-pack/plugins/alerting/server/rule_type_registry.ts index 68e3e1d3d590b..1d147c2cb3d6a 100644 --- a/x-pack/plugins/alerting/server/rule_type_registry.ts +++ b/x-pack/plugins/alerting/server/rule_type_registry.ts @@ -59,6 +59,7 @@ export interface RegistryRuleType | 'recoveryActionGroup' | 'defaultActionGroupId' | 'actionVariables' + | 'category' | 'producer' | 'minimumLicenseRequired' | 'isExportable' @@ -381,6 +382,7 @@ export class RuleTypeRegistry { recoveryActionGroup, defaultActionGroupId, actionVariables, + category, producer, minimumLicenseRequired, isExportable, @@ -400,6 +402,7 @@ export class RuleTypeRegistry { recoveryActionGroup, defaultActionGroupId, actionVariables, + category, producer, minimumLicenseRequired, isExportable, diff --git a/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/migrate_legacy_actions.test.ts b/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/migrate_legacy_actions.test.ts index 5e6e958f3bbc0..2dc0d79833229 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/migrate_legacy_actions.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/lib/siem_legacy_actions/migrate_legacy_actions.test.ts @@ -40,6 +40,7 @@ const ruleType: jest.Mocked = { isExportable: true, recoveryActionGroup: RecoveredActionGroup, executor: jest.fn(), + category: 'test', producer: 'alerts', cancelAlertsOnRuleTimeout: true, ruleTaskTimeout: '5m', diff --git a/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.test.ts b/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.test.ts index d01d45abc9759..74fd3b3291d4e 100644 --- a/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/lib/validate_actions.test.ts @@ -22,6 +22,7 @@ describe('validateActions', () => { isExportable: true, recoveryActionGroup: RecoveredActionGroup, executor: jest.fn(), + category: 'test', producer: 'alerts', cancelAlertsOnRuleTimeout: true, ruleTaskTimeout: '5m', diff --git a/x-pack/plugins/alerting/server/rules_client/tests/find.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/find.test.ts index e7e737b635f28..3aea832752dfa 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/find.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/find.test.ts @@ -89,6 +89,7 @@ describe('find()', () => { isExportable: true, id: 'myType', name: 'myType', + category: 'test', producer: 'myApp', enabledInLicense: true, hasAlertsMappings: false, @@ -149,6 +150,7 @@ describe('find()', () => { defaultActionGroupId: 'default', minimumLicenseRequired: 'basic', isExportable: true, + category: 'test', producer: 'alerts', authorizedConsumers: { myApp: { read: true, all: true }, @@ -466,6 +468,7 @@ describe('find()', () => { isExportable: true, id: '123', name: 'myType', + category: 'test', producer: 'myApp', enabledInLicense: true, hasAlertsMappings: false, @@ -485,6 +488,7 @@ describe('find()', () => { async executor() { return { state: {} }; }, + category: 'test', producer: 'myApp', validate: { params: schema.any(), @@ -502,6 +506,7 @@ describe('find()', () => { async executor() { return { state: {} }; }, + category: 'test', producer: 'alerts', useSavedObjectReferences: { extractReferences: jest.fn(), @@ -677,6 +682,7 @@ describe('find()', () => { isExportable: true, id: '123', name: 'myType', + category: 'test', producer: 'myApp', enabledInLicense: true, hasAlertsMappings: false, @@ -696,6 +702,7 @@ describe('find()', () => { async executor() { return { state: {} }; }, + category: 'test', producer: 'myApp', validate: { params: schema.any(), @@ -713,6 +720,7 @@ describe('find()', () => { async executor() { return { state: {} }; }, + category: 'test', producer: 'alerts', useSavedObjectReferences: { extractReferences: jest.fn(), diff --git a/x-pack/plugins/alerting/server/rules_client/tests/get.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/get.test.ts index 5e98fcfee3721..0d29a3fc402d1 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/get.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/get.test.ts @@ -313,6 +313,7 @@ describe('get()', () => { async executor() { return { state: {} }; }, + category: 'test', producer: 'alerts', useSavedObjectReferences: { extractReferences: jest.fn(), @@ -440,6 +441,7 @@ describe('get()', () => { async executor() { return { state: {} }; }, + category: 'test', producer: 'alerts', useSavedObjectReferences: { extractReferences: jest.fn(), diff --git a/x-pack/plugins/alerting/server/rules_client/tests/get_tags.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/get_tags.test.ts index 54971f47a3b09..3941318a000d6 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/get_tags.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/get_tags.test.ts @@ -68,6 +68,7 @@ const listedTypes = new Set([ recoveryActionGroup: RecoveredActionGroup, id: 'myType', name: 'myType', + category: 'test', producer: 'myApp', enabledInLicense: true, hasAlertsMappings: false, @@ -119,6 +120,7 @@ describe('getTags()', () => { minimumLicenseRequired: 'basic', isExportable: true, recoveryActionGroup: RecoveredActionGroup, + category: 'test', producer: 'alerts', authorizedConsumers: { myApp: { read: true, all: true }, diff --git a/x-pack/plugins/alerting/server/rules_client/tests/lib.ts b/x-pack/plugins/alerting/server/rules_client/tests/lib.ts index ee84a789454ef..12da7211fdc12 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/lib.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/lib.ts @@ -118,6 +118,7 @@ export function getBeforeSetup( async executor() { return { state: {} }; }, + category: 'test', producer: 'alerts', validate: { params: { validate: (params) => params }, diff --git a/x-pack/plugins/alerting/server/rules_client/tests/list_rule_types.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/list_rule_types.test.ts index 0b95247b60adf..ef9e864ad0eca 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/list_rule_types.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/list_rule_types.test.ts @@ -74,6 +74,7 @@ describe('listRuleTypes', () => { recoveryActionGroup: RecoveredActionGroup, id: 'alertingAlertType', name: 'alertingAlertType', + category: 'test', producer: 'alerts', enabledInLicense: true, hasAlertsMappings: false, @@ -89,6 +90,7 @@ describe('listRuleTypes', () => { recoveryActionGroup: RecoveredActionGroup, id: 'myAppAlertType', name: 'myAppAlertType', + category: 'test', producer: 'myApp', enabledInLicense: true, hasAlertsMappings: false, @@ -134,6 +136,7 @@ describe('listRuleTypes', () => { recoveryActionGroup: RecoveredActionGroup, id: 'myType', name: 'myType', + category: 'test', producer: 'myApp', enabledInLicense: true, hasAlertsMappings: false, @@ -148,6 +151,7 @@ describe('listRuleTypes', () => { minimumLicenseRequired: 'basic', isExportable: true, recoveryActionGroup: RecoveredActionGroup, + category: 'test', producer: 'alerts', enabledInLicense: true, hasAlertsMappings: false, @@ -169,6 +173,7 @@ describe('listRuleTypes', () => { minimumLicenseRequired: 'basic', isExportable: true, recoveryActionGroup: RecoveredActionGroup, + category: 'test', producer: 'alerts', authorizedConsumers: { myApp: { read: true, all: true }, diff --git a/x-pack/plugins/alerting/server/rules_client/tests/resolve.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/resolve.test.ts index dd3ccaea5b67c..0cf9e792cc1bf 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/resolve.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/resolve.test.ts @@ -288,6 +288,7 @@ describe('resolve()', () => { async executor() { return { state: {} }; }, + category: 'test', producer: 'alerts', useSavedObjectReferences: { extractReferences: jest.fn(), @@ -425,6 +426,7 @@ describe('resolve()', () => { async executor() { return { state: {} }; }, + category: 'test', producer: 'alerts', useSavedObjectReferences: { extractReferences: jest.fn(), diff --git a/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts index 8b275d12cfa0a..c151e4f75e99e 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts @@ -184,6 +184,7 @@ describe('update()', () => { async executor() { return { state: {} }; }, + category: 'test', producer: 'alerts', validate: { params: { validate: (params) => params }, @@ -1003,6 +1004,7 @@ describe('update()', () => { async executor() { return { state: {} }; }, + category: 'test', producer: 'alerts', useSavedObjectReferences: { extractReferences: extractReferencesFn, @@ -1526,6 +1528,7 @@ describe('update()', () => { async executor() { return { state: {} }; }, + category: 'test', producer: 'alerts', validLegacyConsumers: [], }); @@ -1908,6 +1911,7 @@ describe('update()', () => { async executor() { return { state: {} }; }, + category: 'test', producer: 'alerts', validate: { params: { validate: (params) => params }, diff --git a/x-pack/plugins/alerting/server/rules_client_conflict_retries.test.ts b/x-pack/plugins/alerting/server/rules_client_conflict_retries.test.ts index 036fa1c98ff8f..d36c4a72d2bd2 100644 --- a/x-pack/plugins/alerting/server/rules_client_conflict_retries.test.ts +++ b/x-pack/plugins/alerting/server/rules_client_conflict_retries.test.ts @@ -371,6 +371,7 @@ beforeEach(() => { async executor() { return { state: {} }; }, + category: 'test', producer: 'alerts', validate: { params: { validate: (params) => params }, @@ -389,6 +390,7 @@ beforeEach(() => { async executor() { return { state: {} }; }, + category: 'test', producer: 'alerts', validate: { params: { validate: (params) => params }, diff --git a/x-pack/plugins/alerting/server/saved_objects/is_rule_exportable.test.ts b/x-pack/plugins/alerting/server/saved_objects/is_rule_exportable.test.ts index e517d37739337..6a799370b546f 100644 --- a/x-pack/plugins/alerting/server/saved_objects/is_rule_exportable.test.ts +++ b/x-pack/plugins/alerting/server/saved_objects/is_rule_exportable.test.ts @@ -54,6 +54,7 @@ describe('isRuleExportable', () => { minimumLicenseRequired: 'basic', isExportable: true, executor: jest.fn(), + category: 'test', producer: 'alerts', validate: { params: { validate: (params) => params }, @@ -113,6 +114,7 @@ describe('isRuleExportable', () => { minimumLicenseRequired: 'basic', isExportable: false, executor: jest.fn(), + category: 'test', producer: 'alerts', validate: { params: { validate: (params) => params }, @@ -175,6 +177,7 @@ describe('isRuleExportable', () => { minimumLicenseRequired: 'basic', isExportable: false, executor: jest.fn(), + category: 'test', producer: 'alerts', validate: { params: { validate: (params) => params }, diff --git a/x-pack/plugins/alerting/server/task_runner/execution_handler.test.ts b/x-pack/plugins/alerting/server/task_runner/execution_handler.test.ts index 2c1810b0d219e..3b3cd3a972825 100644 --- a/x-pack/plugins/alerting/server/task_runner/execution_handler.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/execution_handler.test.ts @@ -72,6 +72,7 @@ const ruleType: NormalizedRuleType< name: 'Recovered', }, executor: jest.fn(), + category: 'test', producer: 'alerts', validate: { params: schema.any(), diff --git a/x-pack/plugins/alerting/server/task_runner/fixtures.ts b/x-pack/plugins/alerting/server/task_runner/fixtures.ts index 50f26248499e1..163b9415c7e5d 100644 --- a/x-pack/plugins/alerting/server/task_runner/fixtures.ts +++ b/x-pack/plugins/alerting/server/task_runner/fixtures.ts @@ -141,6 +141,7 @@ export const ruleType: jest.Mocked = { isExportable: true, recoveryActionGroup: RecoveredActionGroup, executor: jest.fn(), + category: 'test', producer: 'alerts', cancelAlertsOnRuleTimeout: true, ruleTaskTimeout: '5m', diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index 382e0f3e608c7..03cf55f58606c 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -663,6 +663,141 @@ describe('Task Runner', () => { expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); }); + test('skips alert notification if active maintenance window contains the rule type category', async () => { + taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); + taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); + ruleType.executor.mockImplementation( + async ({ + services: executorServices, + }: RuleExecutorOptions< + RuleTypeParams, + RuleTypeState, + AlertInstanceState, + AlertInstanceContext, + string, + RuleAlertData + >) => { + executorServices.alertFactory.create('1').scheduleActions('default'); + return { state: {} }; + } + ); + const taskRunner = new TaskRunner({ + ruleType, + taskInstance: mockedTaskInstance, + context: taskRunnerFactoryInitializerParams, + inMemoryMetrics, + }); + expect(AlertingEventLogger).toHaveBeenCalledTimes(1); + rulesClient.getAlertFromRaw.mockReturnValue(mockedRuleTypeSavedObject as Rule); + maintenanceWindowClient.getActiveMaintenanceWindows.mockResolvedValueOnce([ + { + ...getMockMaintenanceWindow(), + categoryIds: ['test'] as unknown as MaintenanceWindow['categoryIds'], + id: 'test-id-1', + } as MaintenanceWindow, + ]); + + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(mockedRawRuleSO); + await taskRunner.run(); + expect(actionsClient.ephemeralEnqueuedExecution).toHaveBeenCalledTimes(0); + + const maintenanceWindowIds = ['test-id-1']; + + testAlertingEventLogCalls({ + activeAlerts: 1, + newAlerts: 1, + status: 'active', + logAlert: 2, + maintenanceWindowIds, + }); + expect(alertingEventLogger.logAlert).toHaveBeenNthCalledWith( + 1, + generateAlertOpts({ + action: EVENT_LOG_ACTIONS.newInstance, + group: 'default', + state: { start: DATE_1970, duration: '0' }, + maintenanceWindowIds, + }) + ); + expect(alertingEventLogger.logAlert).toHaveBeenNthCalledWith( + 2, + generateAlertOpts({ + action: EVENT_LOG_ACTIONS.activeInstance, + group: 'default', + state: { start: DATE_1970, duration: '0' }, + maintenanceWindowIds, + }) + ); + expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); + }); + + test('allows alert notification if active maintenance window does not contain the rule type category', async () => { + taskRunnerFactoryInitializerParams.actionsPlugin.isActionTypeEnabled.mockReturnValue(true); + taskRunnerFactoryInitializerParams.actionsPlugin.isActionExecutable.mockReturnValue(true); + ruleType.executor.mockImplementation( + async ({ + services: executorServices, + }: RuleExecutorOptions< + RuleTypeParams, + RuleTypeState, + AlertInstanceState, + AlertInstanceContext, + string, + RuleAlertData + >) => { + executorServices.alertFactory.create('1').scheduleActions('default'); + return { state: {} }; + } + ); + const taskRunner = new TaskRunner({ + ruleType, + taskInstance: mockedTaskInstance, + context: taskRunnerFactoryInitializerParams, + inMemoryMetrics, + }); + expect(AlertingEventLogger).toHaveBeenCalledTimes(1); + rulesClient.getAlertFromRaw.mockReturnValue(mockedRuleTypeSavedObject as Rule); + maintenanceWindowClient.getActiveMaintenanceWindows.mockResolvedValueOnce([ + { + ...getMockMaintenanceWindow(), + categoryIds: ['something-else'] as unknown as MaintenanceWindow['categoryIds'], + id: 'test-id-1', + } as MaintenanceWindow, + ]); + + encryptedSavedObjectsClient.getDecryptedAsInternalUser.mockResolvedValue(mockedRawRuleSO); + await taskRunner.run(); + expect(actionsClient.ephemeralEnqueuedExecution).toHaveBeenCalledTimes(0); + + testAlertingEventLogCalls({ + activeAlerts: 1, + generatedActions: 1, + newAlerts: 1, + triggeredActions: 1, + status: 'active', + logAlert: 2, + logAction: 1, + }); + + expect(alertingEventLogger.logAlert).toHaveBeenNthCalledWith( + 1, + generateAlertOpts({ + action: EVENT_LOG_ACTIONS.newInstance, + group: 'default', + state: { start: DATE_1970, duration: '0' }, + }) + ); + expect(alertingEventLogger.logAlert).toHaveBeenNthCalledWith( + 2, + generateAlertOpts({ + action: EVENT_LOG_ACTIONS.activeInstance, + group: 'default', + state: { start: DATE_1970, duration: '0' }, + }) + ); + expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); + }); + test.each(ephemeralTestParams)( 'skips firing actions for active alert if alert is muted %s', async (nameExtension, customTaskRunnerFactoryInitializerParams, enqueueFunction) => { diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index cc019b46cec51..939dfe9406f69 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -414,9 +414,20 @@ export class TaskRunner< ); } - const maintenanceWindowIds = activeMaintenanceWindows.map( - (maintenanceWindow) => maintenanceWindow.id - ); + const maintenanceWindowIds = activeMaintenanceWindows + .filter(({ categoryIds }) => { + // If category IDs array doesn't exist: allow all + if (!Array.isArray(categoryIds)) { + return true; + } + // If category IDs array exist: check category + if ((categoryIds as string[]).includes(ruleType.category)) { + return true; + } + return false; + }) + .map(({ id }) => id); + if (maintenanceWindowIds.length) { this.alertingEventLogger.setMaintenanceWindowIds(maintenanceWindowIds); } diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts index 786a0fe4a338f..8f40fe026c7a7 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_factory.test.ts @@ -59,6 +59,7 @@ const ruleType: UntypedNormalizedRuleType = { name: 'Recovered', }, executor: jest.fn(), + category: 'test', producer: 'alerts', validate: { params: schema.any(), diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index bee42c98dc075..66e2c3bfa6069 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -288,6 +288,7 @@ export interface RuleType< WithoutReservedActionGroups, AlertData >; + category: string; producer: string; actionVariables?: { context?: ActionVariable[]; diff --git a/x-pack/plugins/alerting/tsconfig.json b/x-pack/plugins/alerting/tsconfig.json index f38fbd085c7f0..d2d5aafc7fde9 100644 --- a/x-pack/plugins/alerting/tsconfig.json +++ b/x-pack/plugins/alerting/tsconfig.json @@ -58,6 +58,7 @@ "@kbn/core-http-server-mocks", "@kbn/serverless", "@kbn/core-http-router-server-mocks", + "@kbn/core-application-common", ], "exclude": ["target/**/*"] } diff --git a/x-pack/plugins/apm/common/rules/apm_rule_types.ts b/x-pack/plugins/apm/common/rules/apm_rule_types.ts index 1989885428db5..b18ea1f0c49ce 100644 --- a/x-pack/plugins/apm/common/rules/apm_rule_types.ts +++ b/x-pack/plugins/apm/common/rules/apm_rule_types.ts @@ -245,7 +245,7 @@ export const RULE_TYPES_CONFIG: Record< }, [ApmRuleType.Anomaly]: { name: i18n.translate('xpack.apm.anomalyAlert.name', { - defaultMessage: 'Anomaly', + defaultMessage: 'APM Anomaly', }), actionGroups: [THRESHOLD_MET_GROUP], defaultActionGroupId: THRESHOLD_MET_GROUP_ID, diff --git a/x-pack/plugins/apm/public/components/alerting/rule_types/register_apm_rule_types.ts b/x-pack/plugins/apm/public/components/alerting/rule_types/register_apm_rule_types.ts index 7940a4128d809..e8f2c6185ebd6 100644 --- a/x-pack/plugins/apm/public/components/alerting/rule_types/register_apm_rule_types.ts +++ b/x-pack/plugins/apm/public/components/alerting/rule_types/register_apm_rule_types.ts @@ -55,6 +55,7 @@ export function registerApmRuleTypes( }), requiresAppContext: false, defaultActionMessage: errorCountMessage, + priority: 80, }); observabilityRuleTypeRegistry.register({ @@ -92,6 +93,7 @@ export function registerApmRuleTypes( ), requiresAppContext: false, defaultActionMessage: transactionDurationMessage, + priority: 60, }); observabilityRuleTypeRegistry.register({ @@ -124,6 +126,7 @@ export function registerApmRuleTypes( }), requiresAppContext: false, defaultActionMessage: transactionErrorRateMessage, + priority: 70, }); observabilityRuleTypeRegistry.register({ @@ -153,5 +156,6 @@ export function registerApmRuleTypes( }), requiresAppContext: false, defaultActionMessage: anomalyMessage, + priority: 90, }); } diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/transaction_tabs.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/transaction_tabs.tsx index 78e6a89d30620..e424389acbb17 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/transaction_tabs.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/transaction_tabs.tsx @@ -154,7 +154,7 @@ function TimelineTabContent({ } function MetadataTabContent({ transaction }: { transaction: Transaction }) { - return ; + return ; } function LogsTabContent({ diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/index.tsx index 77a3f7d00955f..59b06577b2757 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/span_flyout/index.tsx @@ -228,7 +228,7 @@ function SpanFlyoutBody({ content: ( - + ), }, diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/transaction_flyout/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/transaction_flyout/index.tsx index 012d3006f79ac..b1ff9705ad6dd 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/transaction_flyout/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/transaction_flyout/index.tsx @@ -141,7 +141,7 @@ function TransactionFlyoutBody({ content: ( <> - + ), }, diff --git a/x-pack/plugins/apm/public/components/shared/metadata_table/error_metadata/index.tsx b/x-pack/plugins/apm/public/components/shared/metadata_table/error_metadata/index.tsx index 95b19328367c9..dfdc1d3746a0a 100644 --- a/x-pack/plugins/apm/public/components/shared/metadata_table/error_metadata/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/metadata_table/error_metadata/index.tsx @@ -27,11 +27,15 @@ export function ErrorMetadata({ error }: Props) { processorEvent: ProcessorEvent.error, id: error.error.id, }, + query: { + start: error['@timestamp'], + end: error['@timestamp'], + }, }, } ); }, - [error.error.id] + [error] ); const sections = useMemo( diff --git a/x-pack/plugins/apm/public/components/shared/metadata_table/span_metadata/index.tsx b/x-pack/plugins/apm/public/components/shared/metadata_table/span_metadata/index.tsx index d55bf82743d1d..93a10c7105cbe 100644 --- a/x-pack/plugins/apm/public/components/shared/metadata_table/span_metadata/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/metadata_table/span_metadata/index.tsx @@ -7,15 +7,16 @@ import React, { useMemo } from 'react'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; +import { Span } from '../../../../../typings/es_schemas/ui/span'; import { getSectionsFromFields } from '../helper'; import { MetadataTable } from '..'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; interface Props { - spanId: string; + span: Span; } -export function SpanMetadata({ spanId }: Props) { +export function SpanMetadata({ span }: Props) { const { data: spanEvent, status } = useFetcher( (callApmApi) => { return callApmApi( @@ -24,13 +25,17 @@ export function SpanMetadata({ spanId }: Props) { params: { path: { processorEvent: ProcessorEvent.span, - id: spanId, + id: span.span.id, + }, + query: { + start: span['@timestamp'], + end: span['@timestamp'], }, }, } ); }, - [spanId] + [span] ); const sections = useMemo( diff --git a/x-pack/plugins/apm/public/components/shared/metadata_table/transaction_metadata/index.tsx b/x-pack/plugins/apm/public/components/shared/metadata_table/transaction_metadata/index.tsx index 38352c6c7a318..92087be06f3ad 100644 --- a/x-pack/plugins/apm/public/components/shared/metadata_table/transaction_metadata/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/metadata_table/transaction_metadata/index.tsx @@ -7,15 +7,16 @@ import React, { useMemo } from 'react'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; +import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; import { getSectionsFromFields } from '../helper'; import { MetadataTable } from '..'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; interface Props { - transactionId: string; + transaction: Transaction; } -export function TransactionMetadata({ transactionId }: Props) { +export function TransactionMetadata({ transaction }: Props) { const { data: transactionEvent, status } = useFetcher( (callApmApi) => { return callApmApi( @@ -24,13 +25,17 @@ export function TransactionMetadata({ transactionId }: Props) { params: { path: { processorEvent: ProcessorEvent.transaction, - id: transactionId, + id: transaction.transaction.id, + }, + query: { + start: transaction['@timestamp'], + end: transaction['@timestamp'], }, }, } ); }, - [transactionId] + [transaction] ); const sections = useMemo( diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/anomaly/register_anomaly_rule_type.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/anomaly/register_anomaly_rule_type.ts index ed313b6f95dab..d318f6ec4a44d 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/anomaly/register_anomaly_rule_type.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/anomaly/register_anomaly_rule_type.ts @@ -7,7 +7,7 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { GetViewInAppRelativeUrlFnOpts } from '@kbn/alerting-plugin/server'; -import { KibanaRequest } from '@kbn/core/server'; +import { KibanaRequest, DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; import datemath from '@kbn/datemath'; import type { ESSearchResponse } from '@kbn/es-types'; import { @@ -93,6 +93,7 @@ export function registerAnomalyRuleType({ apmActionVariables.viewInAppUrl, ], }, + category: DEFAULT_APP_CATEGORIES.observability.id, producer: 'apm', minimumLicenseRequired: 'basic', isExportable: true, diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts index ba33057e99190..4ecade37780d9 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/error_count/register_error_count_rule_type.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; import { GetViewInAppRelativeUrlFnOpts } from '@kbn/alerting-plugin/server'; import { formatDurationFromTimeUnitChar, @@ -94,6 +95,7 @@ export function registerErrorCountRuleType({ actionVariables: { context: errorCountActionVariables, }, + category: DEFAULT_APP_CATEGORIES.observability.id, producer: APM_SERVER_FEATURE_ID, minimumLicenseRequired: 'basic', isExportable: true, diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts index 3570b93cd7039..2991a58f577be 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { GetViewInAppRelativeUrlFnOpts } from '@kbn/alerting-plugin/server'; import { @@ -106,6 +107,7 @@ export function registerTransactionDurationRuleType({ actionVariables: { context: transactionDurationActionVariables, }, + category: DEFAULT_APP_CATEGORIES.observability.id, producer: APM_SERVER_FEATURE_ID, minimumLicenseRequired: 'basic', isExportable: true, diff --git a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts index 764e251edd6a7..08385ffdec3a9 100644 --- a/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts +++ b/x-pack/plugins/apm/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; import { GetViewInAppRelativeUrlFnOpts } from '@kbn/alerting-plugin/server'; import { formatDurationFromTimeUnitChar, @@ -103,6 +104,7 @@ export function registerTransactionErrorRateRuleType({ actionVariables: { context: transactionErrorRateActionVariables, }, + category: DEFAULT_APP_CATEGORIES.observability.id, producer: APM_SERVER_FEATURE_ID, minimumLicenseRequired: 'basic', isExportable: true, diff --git a/x-pack/plugins/apm/server/routes/event_metadata/get_event_metadata.ts b/x-pack/plugins/apm/server/routes/event_metadata/get_event_metadata.ts index 7788784326389..729cffe38ea68 100644 --- a/x-pack/plugins/apm/server/routes/event_metadata/get_event_metadata.ts +++ b/x-pack/plugins/apm/server/routes/event_metadata/get_event_metadata.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; +import { rangeQuery } from '@kbn/observability-plugin/server'; import { ERROR_ID, SPAN_ID, @@ -18,35 +18,16 @@ export async function getEventMetadata({ apmEventClient, processorEvent, id, + start, + end, }: { apmEventClient: APMEventClient; processorEvent: ProcessorEvent; id: string; + start: number; + end: number; }) { - const filter: QueryDslQueryContainer[] = []; - - switch (processorEvent) { - case ProcessorEvent.error: - filter.push({ - term: { [ERROR_ID]: id }, - }); - break; - - case ProcessorEvent.transaction: - filter.push({ - term: { - [TRANSACTION_ID]: id, - }, - }); - break; - - case ProcessorEvent.span: - filter.push({ - term: { [SPAN_ID]: id }, - }); - break; - } - + const fieldName = getFieldName(processorEvent); const response = await apmEventClient.search('get_event_metadata', { apm: { events: [processorEvent], @@ -54,7 +35,9 @@ export async function getEventMetadata({ body: { track_total_hits: false, query: { - bool: { filter }, + bool: { + filter: [...rangeQuery(start, end), { term: { [fieldName]: id } }], + }, }, size: 1, _source: false, @@ -65,3 +48,19 @@ export async function getEventMetadata({ return response.hits.hits[0].fields; } + +function getFieldName(processorEvent: ProcessorEvent) { + switch (processorEvent) { + case ProcessorEvent.error: + return ERROR_ID; + + case ProcessorEvent.transaction: + return TRANSACTION_ID; + + case ProcessorEvent.span: + return SPAN_ID; + + default: + throw new Error('Unknown processor event'); + } +} diff --git a/x-pack/plugins/apm/server/routes/event_metadata/route.ts b/x-pack/plugins/apm/server/routes/event_metadata/route.ts index 02709d2c499e5..430830b4a3618 100644 --- a/x-pack/plugins/apm/server/routes/event_metadata/route.ts +++ b/x-pack/plugins/apm/server/routes/event_metadata/route.ts @@ -10,6 +10,7 @@ import { createApmServerRoute } from '../apm_routes/create_apm_server_route'; import { getEventMetadata } from './get_event_metadata'; import { processorEventRt } from '../../../common/processor_event'; import { getApmEventClient } from '../../lib/helpers/get_apm_event_client'; +import { rangeRt } from '../default_api_types'; const eventMetadataRoute = createApmServerRoute({ endpoint: 'GET /internal/apm/event_metadata/{processorEvent}/{id}', @@ -19,20 +20,22 @@ const eventMetadataRoute = createApmServerRoute({ processorEvent: processorEventRt, id: t.string, }), + query: rangeRt, }), handler: async ( resources ): Promise<{ metadata: Partial> }> => { const apmEventClient = await getApmEventClient(resources); - - const { - path: { processorEvent, id }, - } = resources.params; + const { params } = resources; + const { start, end } = params.query; + const { processorEvent, id } = params.path; const metadata = await getEventMetadata({ apmEventClient, processorEvent, id, + start, + end, }); return { diff --git a/x-pack/plugins/infra/public/alerting/inventory/index.ts b/x-pack/plugins/infra/public/alerting/inventory/index.ts index b25eaba2ba3a6..9df7f609dcbba 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/index.ts +++ b/x-pack/plugins/infra/public/alerting/inventory/index.ts @@ -66,5 +66,6 @@ export function createInventoryMetricRuleType(): ObservabilityRuleTypeModel import('./components/alert_details_app_section')), + priority: 10, }; } diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_rule_type.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_rule_type.ts index 3e129018013f8..d956b30940f4c 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_rule_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_rule_type.ts @@ -7,6 +7,7 @@ import { schema, Type } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; +import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; import { GetViewInAppRelativeUrlFnOpts, PluginSetupContract } from '@kbn/alerting-plugin/server'; import { observabilityPaths } from '@kbn/observability-plugin/common'; import { TimeUnitChar } from '@kbn/observability-plugin/common/utils/formatters/duration'; @@ -106,6 +107,7 @@ export async function registerMetricInventoryThresholdRuleType( defaultActionGroupId: FIRED_ACTIONS_ID, doesSetRecoveryContext: true, actionGroups: [FIRED_ACTIONS, WARNING_ACTIONS], + category: DEFAULT_APP_CATEGORIES.observability.id, producer: 'infrastructure', minimumLicenseRequired: 'basic', isExportable: true, diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_rule_type.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_rule_type.ts index 58fdb97281b56..40848b9a109ed 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_rule_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_rule_type.ts @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; import { GetViewInAppRelativeUrlFnOpts, PluginSetupContract } from '@kbn/alerting-plugin/server'; import { observabilityPaths } from '@kbn/observability-plugin/common'; import { O11Y_AAD_FIELDS } from '../../../../common/constants'; @@ -160,6 +161,7 @@ export async function registerLogThresholdRuleType( { name: 'tags', description: tagsActionVariableDescription }, ], }, + category: DEFAULT_APP_CATEGORIES.observability.id, producer: 'logs', useSavedObjectReferences: { extractReferences, diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/register_metric_anomaly_rule_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/register_metric_anomaly_rule_type.ts index 9e5c4c70e96d4..dc3fd1b28546c 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/register_metric_anomaly_rule_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/register_metric_anomaly_rule_type.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import { MlPluginSetup } from '@kbn/ml-plugin/server'; @@ -66,6 +67,7 @@ export const registerMetricAnomalyRuleType = ( }, defaultActionGroupId: FIRED_ACTIONS_ID, actionGroups: [FIRED_ACTIONS], + category: DEFAULT_APP_CATEGORIES.observability.id, producer: 'infrastructure', minimumLicenseRequired: 'basic', isExportable: true, diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_rule_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_rule_type.ts index 1e8904a3a729d..ad6429eb2ba0f 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_rule_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_rule_type.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import { ActionGroupIdsOf } from '@kbn/alerting-plugin/common'; @@ -188,6 +189,7 @@ export async function registerMetricThresholdRuleType( }, ], }, + category: DEFAULT_APP_CATEGORIES.observability.id, producer: 'infrastructure', alerts: MetricsRulesTypeAlertDefinition, getViewInAppRelativeUrl: ({ rule }: GetViewInAppRelativeUrlFnOpts<{}>) => diff --git a/x-pack/plugins/ml/server/lib/alerts/register_anomaly_detection_alert_type.ts b/x-pack/plugins/ml/server/lib/alerts/register_anomaly_detection_alert_type.ts index 2935643348f76..d3730d22e8e51 100644 --- a/x-pack/plugins/ml/server/lib/alerts/register_anomaly_detection_alert_type.ts +++ b/x-pack/plugins/ml/server/lib/alerts/register_anomaly_detection_alert_type.ts @@ -6,7 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import type { KibanaRequest } from '@kbn/core/server'; +import { KibanaRequest, DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; import type { ActionGroup, AlertInstanceContext, @@ -236,6 +236,7 @@ export function registerAnomalyDetectionAlertType({ }, ], }, + category: DEFAULT_APP_CATEGORIES.management.id, producer: PLUGIN_ID, minimumLicenseRequired: MINIMUM_FULL_LICENSE, isExportable: true, diff --git a/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts b/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts index e7c9c527439cd..2b53fb7a4c7c4 100644 --- a/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts +++ b/x-pack/plugins/ml/server/lib/alerts/register_jobs_monitoring_rule_type.ts @@ -6,7 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import type { KibanaRequest } from '@kbn/core/server'; +import { KibanaRequest, DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; import type { MlDatafeedState, MlJobState, @@ -137,6 +137,7 @@ export function registerJobsMonitoringRuleType({ }, ], }, + category: DEFAULT_APP_CATEGORIES.management.id, producer: PLUGIN_ID, minimumLicenseRequired: MINIMUM_FULL_LICENSE, isExportable: true, diff --git a/x-pack/plugins/monitoring/server/alerts/base_rule.ts b/x-pack/plugins/monitoring/server/alerts/base_rule.ts index 7c28080129351..609d4a2182148 100644 --- a/x-pack/plugins/monitoring/server/alerts/base_rule.ts +++ b/x-pack/plugins/monitoring/server/alerts/base_rule.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Logger, ElasticsearchClient } from '@kbn/core/server'; +import { Logger, ElasticsearchClient, DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; import { i18n } from '@kbn/i18n'; import { RuleType, @@ -99,6 +99,7 @@ export class BaseRule { state: ExecutedState; } ): Promise => this.execute(options), + category: DEFAULT_APP_CATEGORIES.management.id, producer: 'monitoring', actionVariables: { context: actionVariables, diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts.test.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts.test.tsx index 58636c3429ba4..b307c0d858f12 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts.test.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts.test.tsx @@ -173,7 +173,12 @@ describe('AlertsPage with all capabilities', () => { }); it('renders MaintenanceWindowCallout if one exists', async () => { - fetchActiveMaintenanceWindowsMock.mockResolvedValue([RUNNING_MAINTENANCE_WINDOW_1]); + fetchActiveMaintenanceWindowsMock.mockResolvedValue([ + { + ...RUNNING_MAINTENANCE_WINDOW_1, + categoryIds: ['observability'], + }, + ]); const wrapper = await setup(); await waitFor(() => { diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts.tsx index 08b89dad773ce..01a1346b73343 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts.tsx @@ -14,6 +14,7 @@ import { loadRuleAggregations } from '@kbn/triggers-actions-ui-plugin/public'; import { AlertConsumers } from '@kbn/rule-data-utils'; import { useBreadcrumbs } from '@kbn/observability-shared-plugin/public'; import { MaintenanceWindowCallout } from '@kbn/alerts-ui-shared'; +import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; import { rulesLocatorID } from '../../../common'; import { RulesParams } from '../../locators/rules'; @@ -195,7 +196,10 @@ function InternalAlertsPage() { - + { ruleTypeIndex, }); - return render(); + return render( + + + + ); } it('should render a page template', async () => { diff --git a/x-pack/plugins/observability/public/pages/rules/rules.tsx b/x-pack/plugins/observability/public/pages/rules/rules.tsx index 700f348842757..33a1a67be8359 100644 --- a/x-pack/plugins/observability/public/pages/rules/rules.tsx +++ b/x-pack/plugins/observability/public/pages/rules/rules.tsx @@ -150,7 +150,8 @@ export function RulesPage({ activeTab = RULES_TAB_NAME }: RulesPageProps) { setRefresh(new Date()); return Promise.resolve(); }} - useRuleProducer={true} + hideGrouping + useRuleProducer /> )} diff --git a/x-pack/plugins/observability/public/rules/create_observability_rule_type_registry.ts b/x-pack/plugins/observability/public/rules/create_observability_rule_type_registry.ts index ec5a5b87db3f1..aef7c5eca6ce2 100644 --- a/x-pack/plugins/observability/public/rules/create_observability_rule_type_registry.ts +++ b/x-pack/plugins/observability/public/rules/create_observability_rule_type_registry.ts @@ -21,21 +21,27 @@ export type ObservabilityRuleTypeFormatter = (options: { export interface ObservabilityRuleTypeModel extends RuleTypeModel { format: ObservabilityRuleTypeFormatter; + priority?: number; } export function createObservabilityRuleTypeRegistry(ruleTypeRegistry: RuleTypeRegistryContract) { - const formatters: Array<{ typeId: string; fn: ObservabilityRuleTypeFormatter }> = []; + const formatters: Array<{ + typeId: string; + priority: number; + fn: ObservabilityRuleTypeFormatter; + }> = []; return { register: (type: ObservabilityRuleTypeModel) => { - const { format, ...rest } = type; - formatters.push({ typeId: type.id, fn: format }); + const { format, priority, ...rest } = type; + formatters.push({ typeId: type.id, priority: priority || 0, fn: format }); ruleTypeRegistry.register(rest); }, getFormatter: (typeId: string) => { return formatters.find((formatter) => formatter.typeId === typeId)?.fn; }, - list: () => formatters.map((formatter) => formatter.typeId), + list: () => + formatters.sort((a, b) => b.priority - a.priority).map((formatter) => formatter.typeId), }; } diff --git a/x-pack/plugins/observability/public/rules/register_observability_rule_types.ts b/x-pack/plugins/observability/public/rules/register_observability_rule_types.ts index dfac0b91c4263..5d042607135ac 100644 --- a/x-pack/plugins/observability/public/rules/register_observability_rule_types.ts +++ b/x-pack/plugins/observability/public/rules/register_observability_rule_types.ts @@ -97,6 +97,7 @@ export const registerObservabilityRuleTypes = ( requiresAppContext: false, defaultActionMessage: sloBurnRateDefaultActionMessage, defaultRecoveryMessage: sloBurnRateDefaultRecoveryMessage, + priority: 100, }); if (config.unsafe.thresholdRule.enabled) { @@ -124,6 +125,7 @@ export const registerObservabilityRuleTypes = ( alertDetailsAppSection: lazy( () => import('../components/custom_threshold/components/alert_details_app_section') ), + priority: 110, }); } }; diff --git a/x-pack/plugins/observability/server/lib/rules/custom_threshold/register_custom_threshold_rule_type.ts b/x-pack/plugins/observability/server/lib/rules/custom_threshold/register_custom_threshold_rule_type.ts index a6466deb25a47..5ebe33ca88ef9 100644 --- a/x-pack/plugins/observability/server/lib/rules/custom_threshold/register_custom_threshold_rule_type.ts +++ b/x-pack/plugins/observability/server/lib/rules/custom_threshold/register_custom_threshold_rule_type.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; import { schema } from '@kbn/config-schema'; import { extractReferences, injectReferences } from '@kbn/data-plugin/common'; import { i18n } from '@kbn/i18n'; @@ -178,6 +179,7 @@ export function thresholdRuleType( }; }, }, + category: DEFAULT_APP_CATEGORIES.observability.id, producer: observabilityFeatureId, alerts: MetricsRulesTypeAlertDefinition, getViewInAppRelativeUrl: ({ rule }: GetViewInAppRelativeUrlFnOpts<{}>) => diff --git a/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/register.ts b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/register.ts index f9c8024dc55d3..c454df48e485c 100644 --- a/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/register.ts +++ b/x-pack/plugins/observability/server/lib/rules/slo_burn_rate/register.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; import { GetViewInAppRelativeUrlFnOpts } from '@kbn/alerting-plugin/server'; import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; @@ -61,6 +62,7 @@ export function sloBurnRateRuleType( }, defaultActionGroupId: ALERT_ACTION.id, actionGroups: [ALERT_ACTION, HIGH_PRIORITY_ACTION, MEDIUM_PRIORITY_ACTION, LOW_PRIORITY_ACTION], + category: DEFAULT_APP_CATEGORIES.observability.id, producer: sloFeatureId, minimumLicenseRequired: 'platinum' as LicenseType, isExportable: true, diff --git a/x-pack/plugins/observability/tsconfig.json b/x-pack/plugins/observability/tsconfig.json index ae0f47fcc38d0..a3d803b89bb97 100644 --- a/x-pack/plugins/observability/tsconfig.json +++ b/x-pack/plugins/observability/tsconfig.json @@ -88,6 +88,7 @@ "@kbn/aiops-plugin", "@kbn/content-management-plugin", "@kbn/deeplinks-observability", + "@kbn/core-application-common", "@kbn/react-kibana-mount", "@kbn/react-kibana-context-theme" ], diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts index fc5fc1b4b9547..30d17ea1bfe06 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts @@ -59,6 +59,7 @@ function createRule(shouldWriteAlerts: boolean = true) { isExportable: true, minimumLicenseRequired: 'basic', name: 'ruleTypeName', + category: 'test', producer: 'producer', validate: { params: schema.object( diff --git a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx index d92ac003cfba9..c9160c3cba1b9 100644 --- a/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx +++ b/x-pack/plugins/security_solution/public/detection_engine/rule_management_ui/pages/rule_management/index.tsx @@ -8,6 +8,7 @@ import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { MaintenanceWindowCallout } from '@kbn/alerts-ui-shared'; import React, { useCallback } from 'react'; +import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; import { APP_UI_ID } from '../../../../../common/constants'; import { SecurityPageName } from '../../../../app/types'; import { ImportDataModal } from '../../../../common/components/import_data_modal'; @@ -150,7 +151,10 @@ const RulesPageComponent: React.FC = () => { - + diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/notifications/legacy_rules_notification_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/notifications/legacy_rules_notification_alert_type.ts index 71269841bd7a4..206444e82d7aa 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/notifications/legacy_rules_notification_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions_legacy/logic/notifications/legacy_rules_notification_alert_type.ts @@ -7,6 +7,7 @@ import type { Logger } from '@kbn/core/server'; import { parseScheduleDates } from '@kbn/securitysolution-io-ts-utils'; +import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; import { DEFAULT_RULE_NOTIFICATION_QUERY_SIZE, LEGACY_NOTIFICATIONS_ID, @@ -39,6 +40,7 @@ export const legacyRulesNotificationAlertType = ({ name: 'Security Solution notification (Legacy)', actionGroups: siemRuleActionGroups, defaultActionGroupId: 'default', + category: DEFAULT_APP_CATEGORIES.security.id, producer: SERVER_APP_ID, validate: { params: legacyRulesNotificationParams, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.ts index 3a22dbf061ebd..ffdeba16e3bf2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.ts @@ -7,6 +7,7 @@ import { validateNonExact } from '@kbn/securitysolution-io-ts-utils'; import { EQL_RULE_TYPE_ID } from '@kbn/securitysolution-rules'; +import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; import { SERVER_APP_ID } from '../../../../../common/constants'; import type { EqlRuleParams } from '../../rule_schema'; @@ -59,6 +60,7 @@ export const createEqlAlertType = ( }, minimumLicenseRequired: 'basic', isExportable: false, + category: DEFAULT_APP_CATEGORIES.security.id, producer: SERVER_APP_ID, async executor(execOptions) { const { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.ts index c0e2e9ec34650..50187155e4fdd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.ts @@ -7,6 +7,8 @@ import { validateNonExact } from '@kbn/securitysolution-io-ts-utils'; import { INDICATOR_RULE_TYPE_ID } from '@kbn/securitysolution-rules'; +import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; + import { SERVER_APP_ID } from '../../../../../common/constants'; import type { ThreatRuleParams } from '../../rule_schema'; @@ -60,6 +62,7 @@ export const createIndicatorMatchAlertType = ( }, minimumLicenseRequired: 'basic', isExportable: false, + category: DEFAULT_APP_CATEGORIES.security.id, producer: SERVER_APP_ID, async executor(execOptions) { const { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.ts index 753e837f08b5f..33a87c1352359 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.ts @@ -7,6 +7,8 @@ import { validateNonExact } from '@kbn/securitysolution-io-ts-utils'; import { ML_RULE_TYPE_ID } from '@kbn/securitysolution-rules'; +import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; + import { SERVER_APP_ID } from '../../../../../common/constants'; import type { MachineLearningRuleParams } from '../../rule_schema'; @@ -47,6 +49,7 @@ export const createMlAlertType = ( }, minimumLicenseRequired: 'basic', isExportable: false, + category: DEFAULT_APP_CATEGORIES.security.id, producer: SERVER_APP_ID, async executor(execOptions) { const { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/new_terms/create_new_terms_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/new_terms/create_new_terms_alert_type.ts index e88f17ae20f2f..147314d2640a5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/new_terms/create_new_terms_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/new_terms/create_new_terms_alert_type.ts @@ -7,6 +7,7 @@ import { validateNonExact } from '@kbn/securitysolution-io-ts-utils'; import { NEW_TERMS_RULE_TYPE_ID } from '@kbn/securitysolution-rules'; +import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; import { SERVER_APP_ID } from '../../../../../common/constants'; import type { NewTermsRuleParams } from '../../rule_schema'; @@ -91,6 +92,7 @@ export const createNewTermsAlertType = ( }, minimumLicenseRequired: 'basic', isExportable: false, + category: DEFAULT_APP_CATEGORIES.security.id, producer: SERVER_APP_ID, async executor(execOptions) { const { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.ts index 1fde4b864ac9e..ae0a9187922a5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.ts @@ -6,6 +6,7 @@ */ import { validateNonExact } from '@kbn/securitysolution-io-ts-utils'; +import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; import { SERVER_APP_ID } from '../../../../../common/constants'; import type { BucketHistory } from './alert_suppression/group_and_bulk_create'; @@ -72,6 +73,7 @@ export const createQueryAlertType = ( }, minimumLicenseRequired: 'basic', isExportable: false, + category: DEFAULT_APP_CATEGORIES.security.id, producer: SERVER_APP_ID, async executor(execOptions) { const { runOpts, services, spaceId, state } = execOptions; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.ts index 75a39ae48490d..2c5855747636b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.ts @@ -7,6 +7,8 @@ import { validateNonExact } from '@kbn/securitysolution-io-ts-utils'; import { THRESHOLD_RULE_TYPE_ID } from '@kbn/securitysolution-rules'; +import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common'; + import { SERVER_APP_ID } from '../../../../../common/constants'; import type { ThresholdRuleParams } from '../../rule_schema'; @@ -60,6 +62,7 @@ export const createThresholdAlertType = ( }, minimumLicenseRequired: 'basic', isExportable: false, + category: DEFAULT_APP_CATEGORIES.security.id, producer: SERVER_APP_ID, async executor(execOptions) { const { diff --git a/x-pack/plugins/security_solution/tsconfig.json b/x-pack/plugins/security_solution/tsconfig.json index ae66ee71cfe66..19ced3697e2c9 100644 --- a/x-pack/plugins/security_solution/tsconfig.json +++ b/x-pack/plugins/security_solution/tsconfig.json @@ -174,6 +174,7 @@ "@kbn/content-management-plugin", "@kbn/discover-utils", "@kbn/subscription-tracking", + "@kbn/core-application-common", "@kbn/openapi-generator", "@kbn/es" ] diff --git a/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.ts b/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.ts index f4657e28e644a..832819e28772c 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/es_query/rule_type.ts @@ -6,7 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import { CoreSetup } from '@kbn/core/server'; +import { CoreSetup, DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; import { extractReferences, injectReferences } from '@kbn/data-plugin/common'; import { ES_QUERY_ID, STACK_ALERTS_FEATURE_ID } from '@kbn/rule-data-utils'; import { StackAlert } from '@kbn/alerts-as-data-utils'; @@ -199,6 +199,7 @@ export function getRuleType( executor: async (options: ExecutorOptions) => { return await executor(core, options); }, + category: DEFAULT_APP_CATEGORIES.management.id, producer: STACK_ALERTS_FEATURE_ID, doesSetRecoveryContext: true, alerts: STACK_ALERTS_AAD_CONFIG, diff --git a/x-pack/plugins/stack_alerts/server/rule_types/geo_containment/rule_type.ts b/x-pack/plugins/stack_alerts/server/rule_types/geo_containment/rule_type.ts index f6d1668151843..832e6499dc02b 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/geo_containment/rule_type.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/geo_containment/rule_type.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; -import { SavedObjectReference } from '@kbn/core/server'; +import { SavedObjectReference, DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; import { RuleParamsAndRefs } from '@kbn/alerting-plugin/server'; import { STACK_ALERTS_FEATURE_ID } from '@kbn/rule-data-utils'; import type { @@ -179,6 +179,7 @@ export function getRuleType(): GeoContainmentRuleType { doesSetRecoveryContext: true, defaultActionGroupId: ActionGroupId, executor, + category: DEFAULT_APP_CATEGORIES.management.id, producer: STACK_ALERTS_FEATURE_ID, validate: { params: ParamsSchema, diff --git a/x-pack/plugins/stack_alerts/server/rule_types/index_threshold/rule_type.ts b/x-pack/plugins/stack_alerts/server/rule_types/index_threshold/rule_type.ts index a2e27231c76f9..336346d8ed6f7 100644 --- a/x-pack/plugins/stack_alerts/server/rule_types/index_threshold/rule_type.ts +++ b/x-pack/plugins/stack_alerts/server/rule_types/index_threshold/rule_type.ts @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; import { TimeSeriesQuery, TIME_SERIES_BUCKET_SELECTOR_FIELD, @@ -205,6 +206,7 @@ export function getRuleType( minimumLicenseRequired: 'basic', isExportable: true, executor, + category: DEFAULT_APP_CATEGORIES.management.id, producer: STACK_ALERTS_FEATURE_ID, doesSetRecoveryContext: true, alerts: STACK_ALERTS_AAD_CONFIG, diff --git a/x-pack/plugins/synthetics/server/alert_rules/status_rule/monitor_status_rule.ts b/x-pack/plugins/synthetics/server/alert_rules/status_rule/monitor_status_rule.ts index dde24f409b0e3..2117e26e37cf7 100644 --- a/x-pack/plugins/synthetics/server/alert_rules/status_rule/monitor_status_rule.ts +++ b/x-pack/plugins/synthetics/server/alert_rules/status_rule/monitor_status_rule.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; import { isEmpty } from 'lodash'; import { ActionGroupIdsOf } from '@kbn/alerting-plugin/common'; import { GetViewInAppRelativeUrlFnOpts } from '@kbn/alerting-plugin/server'; @@ -51,6 +52,7 @@ export const registerSyntheticsStatusCheckRule = ( return createLifecycleRuleType({ id: SYNTHETICS_ALERT_RULE_TYPES.MONITOR_STATUS, + category: DEFAULT_APP_CATEGORIES.observability.id, producer: 'uptime', name: STATUS_RULE_NAME, validate: { diff --git a/x-pack/plugins/synthetics/server/alert_rules/tls_rule/tls_rule.ts b/x-pack/plugins/synthetics/server/alert_rules/tls_rule/tls_rule.ts index 67fa4b352cd1f..d4943685ee300 100644 --- a/x-pack/plugins/synthetics/server/alert_rules/tls_rule/tls_rule.ts +++ b/x-pack/plugins/synthetics/server/alert_rules/tls_rule/tls_rule.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; import { ActionGroupIdsOf } from '@kbn/alerting-plugin/common'; import { GetViewInAppRelativeUrlFnOpts } from '@kbn/alerting-plugin/server'; import { createLifecycleRuleTypeFactory, IRuleDataClient } from '@kbn/rule-registry-plugin/server'; @@ -53,6 +54,7 @@ export const registerSyntheticsTLSCheckRule = ( return createLifecycleRuleType({ id: SYNTHETICS_ALERT_RULE_TYPES.TLS, + category: DEFAULT_APP_CATEGORIES.observability.id, producer: 'uptime', name: TLS_CERTIFICATE.name, validate: { diff --git a/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/register_transform_health_rule_type.ts b/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/register_transform_health_rule_type.ts index 1be8410b84786..3eaa1db46018d 100644 --- a/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/register_transform_health_rule_type.ts +++ b/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/register_transform_health_rule_type.ts @@ -6,7 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import { Logger } from '@kbn/core/server'; +import { Logger, DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; import type { ActionGroup, AlertInstanceContext, @@ -105,6 +105,7 @@ export function getTransformHealthRuleType( }, ], }, + category: DEFAULT_APP_CATEGORIES.management.id, producer: 'stackAlerts', minimumLicenseRequired: PLUGIN.MINIMUM_LICENSE_REQUIRED, isExportable: true, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_type_compare.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_type_compare.test.ts index e9ec012707c7b..bc740e8e878c5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_type_compare.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_type_compare.test.ts @@ -6,21 +6,16 @@ */ import { RuleTypeModel } from '../../types'; -import { ruleTypeGroupCompare, ruleTypeCompare } from './rule_type_compare'; +import { + RuleTypeGroup, + ruleTypeGroupCompare, + ruleTypeCompare, + ruleTypeUngroupedCompare, +} from './rule_type_compare'; import { IsEnabledResult, IsDisabledResult } from './check_rule_type_enabled'; test('should sort groups by containing enabled rule types first and then by name', async () => { - const ruleTypes: Array< - [ - string, - Array<{ - id: string; - name: string; - checkEnabledResult: IsEnabledResult | IsDisabledResult; - ruleTypeItem: RuleTypeModel; - }> - ] - > = [ + const ruleTypes: RuleTypeGroup[] = [ [ 'abc', [ @@ -113,6 +108,102 @@ test('should sort groups by containing enabled rule types first and then by name expect(result[2]).toEqual(ruleTypes[0]); }); +describe('ruleTypeUngroupedCompare', () => { + test('should maintain the order of rules', async () => { + const ruleTypes: RuleTypeGroup[] = [ + [ + 'abc', + [ + { + id: '1', + name: 'test2', + checkEnabledResult: { isEnabled: false, message: 'gold license' }, + ruleTypeItem: { + id: 'ruleTypeItemId1', + iconClass: 'test', + description: 'Alert when testing', + documentationUrl: 'https://localhost.local/docs', + validate: () => { + return { errors: {} }; + }, + ruleParamsExpression: () => null, + requiresAppContext: false, + }, + }, + ], + ], + [ + 'bcd', + [ + { + id: '2', + name: 'abc', + checkEnabledResult: { isEnabled: false, message: 'platinum license' }, + ruleTypeItem: { + id: 'ruleTypeItemId2', + iconClass: 'test', + description: 'Alert when testing', + documentationUrl: 'https://localhost.local/docs', + validate: () => { + return { errors: {} }; + }, + ruleParamsExpression: () => null, + requiresAppContext: false, + }, + }, + { + id: '3', + name: 'cdf', + checkEnabledResult: { isEnabled: true }, + ruleTypeItem: { + id: 'ruleTypeItemId3', + iconClass: 'test', + description: 'Alert when testing', + documentationUrl: 'https://localhost.local/docs', + validate: () => { + return { errors: {} }; + }, + ruleParamsExpression: () => null, + requiresAppContext: false, + }, + }, + ], + ], + [ + 'cde', + [ + { + id: '4', + name: 'cde', + checkEnabledResult: { isEnabled: true }, + ruleTypeItem: { + id: 'ruleTypeItemId4', + iconClass: 'test', + description: 'Alert when testing', + documentationUrl: 'https://localhost.local/docs', + validate: () => { + return { errors: {} }; + }, + ruleParamsExpression: () => null, + requiresAppContext: false, + }, + }, + ], + ], + ]; + + const ruleTypesOrder = ['4', '1', '2', '3']; + + const result = [...ruleTypes].sort((left, right) => + ruleTypeUngroupedCompare(left, right, ruleTypesOrder) + ); + + expect(result[0]).toEqual(ruleTypes[2]); + expect(result[1]).toEqual(ruleTypes[1]); + expect(result[2]).toEqual(ruleTypes[0]); + }); +}); + test('should sort rule types by enabled first and then by name', async () => { const ruleTypes: Array<{ id: string; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_type_compare.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_type_compare.ts index c04574f7961e0..ad3de685463cb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_type_compare.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/rule_type_compare.ts @@ -8,25 +8,19 @@ import { RuleTypeModel } from '../../types'; import { IsEnabledResult, IsDisabledResult } from './check_rule_type_enabled'; +export type RuleTypeGroup = [ + string, + Array<{ + id: string; + name: string; + checkEnabledResult: IsEnabledResult | IsDisabledResult; + ruleTypeItem: RuleTypeModel; + }> +]; + export function ruleTypeGroupCompare( - left: [ - string, - Array<{ - id: string; - name: string; - checkEnabledResult: IsEnabledResult | IsDisabledResult; - ruleTypeItem: RuleTypeModel; - }> - ], - right: [ - string, - Array<{ - id: string; - name: string; - checkEnabledResult: IsEnabledResult | IsDisabledResult; - ruleTypeItem: RuleTypeModel; - }> - ], + left: RuleTypeGroup, + right: RuleTypeGroup, groupNames: Map | undefined ) { const groupNameA = left[0]; @@ -54,6 +48,35 @@ export function ruleTypeGroupCompare( : groupNameA.localeCompare(groupNameB); } +export function ruleTypeUngroupedCompare( + left: RuleTypeGroup, + right: RuleTypeGroup, + ruleTypes?: string[] +) { + const leftRuleTypesList = left[1]; + const rightRuleTypesList = right[1]; + + const hasEnabledRuleTypeInListLeft = + leftRuleTypesList.find((ruleTypeItem) => ruleTypeItem.checkEnabledResult.isEnabled) !== + undefined; + + const hasEnabledRuleTypeInListRight = + rightRuleTypesList.find((ruleTypeItem) => ruleTypeItem.checkEnabledResult.isEnabled) !== + undefined; + + if (hasEnabledRuleTypeInListLeft && !hasEnabledRuleTypeInListRight) { + return -1; + } + if (!hasEnabledRuleTypeInListLeft && hasEnabledRuleTypeInListRight) { + return 1; + } + + return ruleTypes + ? ruleTypes.findIndex((frtA) => leftRuleTypesList.some((aRuleType) => aRuleType.id === frtA)) - + ruleTypes.findIndex((frtB) => rightRuleTypesList.some((bRuleType) => bRuleType.id === frtB)) + : 0; +} + export function ruleTypeCompare( a: { id: string; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/apis/bulk_get_maintenance_windows.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/apis/bulk_get_maintenance_windows.ts index 3ffcce92819a7..c16dfa91d6a86 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/apis/bulk_get_maintenance_windows.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/hooks/apis/bulk_get_maintenance_windows.ts @@ -43,6 +43,7 @@ const rewriteMaintenanceWindowRes = ({ updated_at: updatedAt, event_start_time: eventStartTime, event_end_time: eventEndTime, + category_ids: categoryIds, ...rest }: AsApiContract): MaintenanceWindow => ({ ...rest, @@ -54,6 +55,7 @@ const rewriteMaintenanceWindowRes = ({ updatedAt, eventStartTime, eventEndTime, + categoryIds, }); const rewriteErrorRes = ({ diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx index 777639d962307..dede9c80d87c8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_add.tsx @@ -49,6 +49,7 @@ const RuleAdd = ({ initialValues, reloadRules, onSave, + hideGrouping, hideInterval, metadata: initialMetadata, filteredRuleTypes, @@ -286,6 +287,7 @@ const RuleAdd = ({ ruleTypeRegistry={ruleTypeRegistry} metadata={metadata} filteredRuleTypes={filteredRuleTypes} + hideGrouping={hideGrouping} hideInterval={hideInterval} onChangeMetaData={onChangeMetaData} setConsumer={setSelectedConsumer} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx index 88e3639c2dbcf..d47b2e62e4763 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/rule_form/rule_form.tsx @@ -72,7 +72,11 @@ import { useKibana } from '../../../common/lib/kibana'; import { recoveredActionGroupMessage, summaryMessage } from '../../constants'; import { IsEnabledResult, IsDisabledResult } from '../../lib/check_rule_type_enabled'; import { checkRuleTypeEnabled } from '../../lib/check_rule_type_enabled'; -import { ruleTypeCompare, ruleTypeGroupCompare } from '../../lib/rule_type_compare'; +import { + ruleTypeCompare, + ruleTypeGroupCompare, + ruleTypeUngroupedCompare, +} from '../../lib/rule_type_compare'; import { VIEW_LICENSE_OPTIONS_LINK } from '../../../common/constants'; import { MULTI_CONSUMER_RULE_TYPE_IDS } from '../../constants'; import { SectionLoading } from '../../components/section_loading'; @@ -137,6 +141,7 @@ interface RuleFormProps> { setConsumer?: (consumer: RuleCreationValidConsumer | null) => void; metadata?: MetaData; filteredRuleTypes?: string[]; + hideGrouping?: boolean; hideInterval?: boolean; connectorFeatureId?: string; validConsumers?: RuleCreationValidConsumer[]; @@ -159,6 +164,7 @@ export const RuleForm = ({ actionTypeRegistry, metadata, filteredRuleTypes: ruleTypeToFilter, + hideGrouping = false, hideInterval, connectorFeatureId = AlertingConnectorFeatureId, validConsumers, @@ -457,85 +463,93 @@ export const RuleForm = ({ {} ); - const ruleTypeNodes = Object.entries(ruleTypesByProducer) - .sort((a, b) => ruleTypeGroupCompare(a, b, solutions)) - .map(([solution, items], groupIndex) => ( - - - - - - {(kibanaFeatures - ? getProducerFeatureName(solution, kibanaFeatures) - : capitalize(solution)) ?? capitalize(solution)} - - - - - {items.length} - - - - - {items - .sort((a, b) => ruleTypeCompare(a, b)) - .map((item, index) => { - const ruleTypeListItemHtml = ( - - {item.name} - -

{item.ruleTypeItem.description}

-
-
- ); - return ( - - {ruleTypeListItemHtml} - - ) + const sortedRuleTypeNodes = hideGrouping + ? Object.entries(ruleTypesByProducer).sort((a, b) => + ruleTypeUngroupedCompare(a, b, ruleTypeToFilter) + ) + : Object.entries(ruleTypesByProducer).sort((a, b) => ruleTypeGroupCompare(a, b, solutions)); + + const ruleTypeNodes = sortedRuleTypeNodes.map(([solution, items], groupIndex) => ( + + {!hideGrouping && ( + <> + + + + + {(kibanaFeatures + ? getProducerFeatureName(solution, kibanaFeatures) + : capitalize(solution)) ?? capitalize(solution)} + + + + + {items.length} + + + + + )} + + {items + .sort((a, b) => ruleTypeCompare(a, b)) + .map((item, index) => { + const ruleTypeListItemHtml = ( + + {item.name} + +

{item.ruleTypeItem.description}

+
+
+ ); + return ( + + {ruleTypeListItemHtml} + + ) + } + isDisabled={!item.checkEnabledResult.isEnabled} + onClick={() => { + setRuleProperty('ruleTypeId', item.id); + setRuleTypeModel(item.ruleTypeItem); + setActions([]); + setRuleProperty('params', {}); + if (ruleTypeIndex && ruleTypeIndex.has(item.id)) { + setDefaultActionGroupId(ruleTypeIndex.get(item.id)!.defaultActionGroupId); } - isDisabled={!item.checkEnabledResult.isEnabled} - onClick={() => { - setRuleProperty('ruleTypeId', item.id); - setRuleTypeModel(item.ruleTypeItem); - setActions([]); - setRuleProperty('params', {}); - if (ruleTypeIndex && ruleTypeIndex.has(item.id)) { - setDefaultActionGroupId(ruleTypeIndex.get(item.id)!.defaultActionGroupId); - } - if (useRuleProducer && !MULTI_CONSUMER_RULE_TYPE_IDS.includes(item.id)) { - setConsumer(solution as RuleCreationValidConsumer); - } - }} - /> - ); - })} -
- -
- )); + if (useRuleProducer && !MULTI_CONSUMER_RULE_TYPE_IDS.includes(item.id)) { + setConsumer(solution as RuleCreationValidConsumer); + } + }} + /> + ); + })} +
+ +
+ )); const labelForRuleChecked = [ i18n.translate('xpack.triggersActionsUI.sections.ruleForm.checkFieldLabel', { diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 7716aef340406..460a621918cfe 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -453,6 +453,7 @@ export interface RuleAddProps> { initialValues?: Partial; /** @deprecated use `onSave` as a callback after an alert is saved*/ reloadRules?: () => Promise; + hideGrouping?: boolean; hideInterval?: boolean; onSave?: (metadata?: MetaData) => Promise; metadata?: MetaData; diff --git a/x-pack/plugins/triggers_actions_ui/server/routes/config.test.ts b/x-pack/plugins/triggers_actions_ui/server/routes/config.test.ts index e6a81ddbfd1ca..8918e77795520 100644 --- a/x-pack/plugins/triggers_actions_ui/server/routes/config.test.ts +++ b/x-pack/plugins/triggers_actions_ui/server/routes/config.test.ts @@ -31,6 +31,7 @@ const ruleTypes = [ context: [], state: [], }, + category: 'test', producer: 'test', enabledInLicense: true, minimumScheduleInterval: '1m', diff --git a/x-pack/plugins/uptime/server/legacy_uptime/lib/alerts/duration_anomaly.ts b/x-pack/plugins/uptime/server/legacy_uptime/lib/alerts/duration_anomaly.ts index 2a756f316731e..7de5a3b1b5327 100644 --- a/x-pack/plugins/uptime/server/legacy_uptime/lib/alerts/duration_anomaly.ts +++ b/x-pack/plugins/uptime/server/legacy_uptime/lib/alerts/duration_anomaly.ts @@ -7,7 +7,11 @@ import { GetViewInAppRelativeUrlFnOpts } from '@kbn/alerting-plugin/server'; import moment from 'moment'; -import { KibanaRequest, SavedObjectsClientContract } from '@kbn/core/server'; +import { + KibanaRequest, + SavedObjectsClientContract, + DEFAULT_APP_CATEGORIES, +} from '@kbn/core/server'; import { schema } from '@kbn/config-schema'; import { ALERT_EVALUATION_VALUE, @@ -100,6 +104,7 @@ export const durationAnomalyAlertFactory: UptimeAlertTypeFactory plugins ) => ({ id: CLIENT_ALERT_TYPES.DURATION_ANOMALY, + category: DEFAULT_APP_CATEGORIES.observability.id, producer: 'uptime', name: durationAnomalyTranslations.alertFactoryName, validate: { diff --git a/x-pack/plugins/uptime/server/legacy_uptime/lib/alerts/status_check.ts b/x-pack/plugins/uptime/server/legacy_uptime/lib/alerts/status_check.ts index 3d45641fa1406..050cf985e3a2d 100644 --- a/x-pack/plugins/uptime/server/legacy_uptime/lib/alerts/status_check.ts +++ b/x-pack/plugins/uptime/server/legacy_uptime/lib/alerts/status_check.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; import { GetViewInAppRelativeUrlFnOpts } from '@kbn/alerting-plugin/server'; import { min } from 'lodash'; import moment from 'moment'; @@ -280,6 +281,7 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory = ( plugins ) => ({ id: CLIENT_ALERT_TYPES.MONITOR_STATUS, + category: DEFAULT_APP_CATEGORIES.observability.id, producer: 'uptime', name: i18n.translate('xpack.uptime.alerts.monitorStatus', { defaultMessage: 'Uptime monitor status', diff --git a/x-pack/plugins/uptime/server/legacy_uptime/lib/alerts/tls.ts b/x-pack/plugins/uptime/server/legacy_uptime/lib/alerts/tls.ts index 2365788545775..40d16ad9a6289 100644 --- a/x-pack/plugins/uptime/server/legacy_uptime/lib/alerts/tls.ts +++ b/x-pack/plugins/uptime/server/legacy_uptime/lib/alerts/tls.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; import { GetViewInAppRelativeUrlFnOpts } from '@kbn/alerting-plugin/server'; import moment from 'moment'; import { ActionGroupIdsOf } from '@kbn/alerting-plugin/common'; @@ -117,6 +118,7 @@ export const tlsAlertFactory: UptimeAlertTypeFactory = ( plugins ) => ({ id: CLIENT_ALERT_TYPES.TLS, + category: DEFAULT_APP_CATEGORIES.observability.id, producer: 'uptime', name: tlsTranslations.alertFactoryName, validate: { diff --git a/x-pack/plugins/uptime/server/legacy_uptime/lib/alerts/tls_legacy.ts b/x-pack/plugins/uptime/server/legacy_uptime/lib/alerts/tls_legacy.ts index 9786a7a844c45..f63e392f4739f 100644 --- a/x-pack/plugins/uptime/server/legacy_uptime/lib/alerts/tls_legacy.ts +++ b/x-pack/plugins/uptime/server/legacy_uptime/lib/alerts/tls_legacy.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; import { observabilityPaths } from '@kbn/observability-plugin/common'; import moment from 'moment'; import { schema } from '@kbn/config-schema'; @@ -95,6 +96,7 @@ export const getCertSummary = ( export const tlsLegacyAlertFactory: UptimeAlertTypeFactory = (_server, libs) => ({ id: CLIENT_ALERT_TYPES.TLS_LEGACY, + category: DEFAULT_APP_CATEGORIES.observability.id, producer: 'uptime', name: tlsTranslations.legacyAlertFactoryName, validate: { diff --git a/x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts b/x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts index 72b3b7b34476f..70ae9edfffb9c 100644 --- a/x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts +++ b/x-pack/test/alerting_api_integration/common/plugins/alerts/server/alert_types.ts @@ -83,6 +83,7 @@ function getAlwaysFiringAlertType() { validate: { params: paramsSchema, }, + category: 'kibana', producer: 'alertsFixture', defaultActionGroupId: 'default', minimumLicenseRequired: 'basic', @@ -150,6 +151,7 @@ function getCumulativeFiringAlertType() { { id: 'default', name: 'Default' }, { id: 'other', name: 'Other' }, ], + category: 'kibana', producer: 'alertsFixture', defaultActionGroupId: 'default', minimumLicenseRequired: 'basic', @@ -201,6 +203,7 @@ function getNeverFiringAlertType() { validate: { params: paramsSchema, }, + category: 'kibana', producer: 'alertsFixture', defaultActionGroupId: 'default', minimumLicenseRequired: 'basic', @@ -244,6 +247,7 @@ function getFailingAlertType() { name: 'Default', }, ], + category: 'kibana', producer: 'alertsFixture', defaultActionGroupId: 'default', minimumLicenseRequired: 'basic', @@ -284,6 +288,7 @@ function getExceedsAlertLimitRuleType() { name: 'Default', }, ], + category: 'kibana', producer: 'alertsFixture', defaultActionGroupId: 'default', minimumLicenseRequired: 'basic', @@ -340,6 +345,7 @@ function getAuthorizationAlertType(core: CoreSetup) { }, ], defaultActionGroupId: 'default', + category: 'kibana', producer: 'alertsFixture', minimumLicenseRequired: 'basic', isExportable: true, @@ -429,6 +435,7 @@ function getValidationAlertType() { name: 'Default', }, ], + category: 'kibana', producer: 'alertsFixture', minimumLicenseRequired: 'basic', isExportable: true, @@ -459,6 +466,7 @@ function getPatternFiringAlertType() { id: 'test.patternFiring', name: 'Test: Firing on a Pattern', actionGroups: [{ id: 'default', name: 'Default' }], + category: 'kibana', producer: 'alertsFixture', defaultActionGroupId: 'default', minimumLicenseRequired: 'basic', @@ -543,6 +551,7 @@ function getPatternFiringAlertsAsDataRuleType() { id: 'test.patternFiringAad', name: 'Test: Firing on a Pattern and writing Alerts as Data', actionGroups: [{ id: 'default', name: 'Default' }], + category: 'kibana', producer: 'alertsFixture', defaultActionGroupId: 'default', minimumLicenseRequired: 'basic', @@ -641,6 +650,7 @@ function getPatternSuccessOrFailureAlertType() { id: 'test.patternSuccessOrFailure', name: 'Test: Succeeding or failing on a Pattern', actionGroups: [{ id: 'default', name: 'Default' }], + category: 'kibana', producer: 'alertsFixture', defaultActionGroupId: 'default', minimumLicenseRequired: 'basic', @@ -689,6 +699,7 @@ function getPatternFiringAutoRecoverFalseAlertType() { id: 'test.patternFiringAutoRecoverFalse', name: 'Test: Firing on a Pattern with Auto Recover: false', actionGroups: [{ id: 'default', name: 'Default' }], + category: 'kibana', producer: 'alertsFixture', defaultActionGroupId: 'default', minimumLicenseRequired: 'basic', @@ -765,6 +776,7 @@ function getLongRunningPatternRuleType(cancelAlertsOnRuleTimeout: boolean = true }`, name: 'Test: Run Long on a Pattern', actionGroups: [{ id: 'default', name: 'Default' }], + category: 'kibana', producer: 'alertsFixture', defaultActionGroupId: 'default', minimumLicenseRequired: 'basic', @@ -809,6 +821,7 @@ function getCancellableRuleType() { id: 'test.cancellableRule', name: 'Test: Rule That Implements Cancellation', actionGroups: [{ id: 'default', name: 'Default' }], + category: 'kibana', producer: 'alertsFixture', defaultActionGroupId: 'default', minimumLicenseRequired: 'basic', @@ -895,6 +908,7 @@ function getAlwaysFiringAlertAsDataRuleType( validate: { params: paramsSchema, }, + category: 'kibana', producer: 'alertsFixture', defaultActionGroupId: 'default', minimumLicenseRequired: 'basic', @@ -965,6 +979,7 @@ function getWaitingRuleType(logger: Logger) { id, name: 'Test: Rule that waits for a signal before finishing', actionGroups: [{ id: 'default', name: 'Default' }], + category: 'kibana', producer: 'alertsFixture', defaultActionGroupId: 'default', minimumLicenseRequired: 'basic', @@ -1080,6 +1095,7 @@ export function defineAlertTypes( id: 'test.noop', name: 'Test: Noop', actionGroups: [{ id: 'default', name: 'Default' }], + category: 'kibana', producer: 'alertsFixture', defaultActionGroupId: 'default', minimumLicenseRequired: 'basic', @@ -1095,6 +1111,7 @@ export function defineAlertTypes( id: 'test.gold.noop', name: 'Test: Noop', actionGroups: [{ id: 'default', name: 'Default' }], + category: 'kibana', producer: 'alertsFixture', defaultActionGroupId: 'default', minimumLicenseRequired: 'gold', @@ -1110,6 +1127,7 @@ export function defineAlertTypes( id: 'test.onlyContextVariables', name: 'Test: Only Context Variables', actionGroups: [{ id: 'default', name: 'Default' }], + category: 'kibana', producer: 'alertsFixture', defaultActionGroupId: 'default', minimumLicenseRequired: 'basic', @@ -1128,6 +1146,7 @@ export function defineAlertTypes( id: 'test.onlyStateVariables', name: 'Test: Only State Variables', actionGroups: [{ id: 'default', name: 'Default' }], + category: 'kibana', producer: 'alertsFixture', defaultActionGroupId: 'default', actionVariables: { @@ -1151,6 +1170,7 @@ export function defineAlertTypes( name: 'Default', }, ], + category: 'kibana', producer: 'alertsFixture', defaultActionGroupId: 'default', minimumLicenseRequired: 'basic', @@ -1177,6 +1197,7 @@ export function defineAlertTypes( name: 'Default', }, ], + category: 'kibana', producer: 'alertsFixture', defaultActionGroupId: 'default', minimumLicenseRequired: 'basic', @@ -1206,6 +1227,7 @@ export function defineAlertTypes( async executor() { return { state: {} }; }, + category: 'kibana', producer: 'alertsFixture', validate: { params: schema.any(), @@ -1228,6 +1250,7 @@ export function defineAlertTypes( name: 'Default', }, ], + category: 'kibana', producer: 'alertsFixture', defaultActionGroupId: 'default', minimumLicenseRequired: 'basic', diff --git a/x-pack/test/alerting_api_integration/common/plugins/alerts_restricted/server/alert_types.ts b/x-pack/test/alerting_api_integration/common/plugins/alerts_restricted/server/alert_types.ts index fd4dabf9c8cab..b9cbed32e09dc 100644 --- a/x-pack/test/alerting_api_integration/common/plugins/alerts_restricted/server/alert_types.ts +++ b/x-pack/test/alerting_api_integration/common/plugins/alerts_restricted/server/alert_types.ts @@ -18,6 +18,7 @@ export function defineAlertTypes( id: 'test.restricted-noop', name: 'Test: Restricted Noop', actionGroups: [{ id: 'default', name: 'Default' }], + category: 'kibana', producer: 'alertsRestrictedFixture', defaultActionGroupId: 'default', minimumLicenseRequired: 'basic', @@ -34,6 +35,7 @@ export function defineAlertTypes( id: 'test.unrestricted-noop', name: 'Test: Unrestricted Noop', actionGroups: [{ id: 'default', name: 'Default' }], + category: 'kibana', producer: 'alertsRestrictedFixture', defaultActionGroupId: 'default', minimumLicenseRequired: 'basic', diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/rule_types.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/rule_types.ts index 38a2bec3921c8..6b39947f45d68 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/rule_types.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group1/tests/alerting/rule_types.ts @@ -29,6 +29,7 @@ export default function listRuleTypes({ getService }: FtrProviderContext) { context: [], params: [], }, + category: 'kibana', producer: 'alertsFixture', minimum_license_required: 'basic', is_exportable: true, @@ -60,6 +61,7 @@ export default function listRuleTypes({ getService }: FtrProviderContext) { context: [], params: [], }, + category: 'kibana', producer: 'alertsRestrictedFixture', minimum_license_required: 'basic', is_exportable: true, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/maintenance_window/create_maintenance_window.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/maintenance_window/create_maintenance_window.ts index 424c39fc1c142..cf0e2a3dcf6a7 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/maintenance_window/create_maintenance_window.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/maintenance_window/create_maintenance_window.ts @@ -76,5 +76,31 @@ export default function createMaintenanceWindowTests({ getService }: FtrProvider }); }); } + + it('should create maintenance window with category ids', async () => { + const response = await supertest + .post(`${getUrlPrefix('space1')}/internal/alerting/rules/maintenance_window`) + .set('kbn-xsrf', 'foo') + .send({ + ...createParams, + category_ids: ['observability', 'securitySolution'], + }) + .expect(200); + + objectRemover.add('space1', response.body.id, 'rules/maintenance_window', 'alerting', true); + + expect(response.body.category_ids).eql(['observability', 'securitySolution']); + }); + + it('should throw if creating maintenance window with invalid categories', async () => { + await supertest + .post(`${getUrlPrefix('space1')}/internal/alerting/rules/maintenance_window`) + .set('kbn-xsrf', 'foo') + .send({ + ...createParams, + category_ids: ['something-else'], + }) + .expect(400); + }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/maintenance_window/update_maintenance_window.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/maintenance_window/update_maintenance_window.ts index fd4879e3e098f..c5d38a1c2a056 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/maintenance_window/update_maintenance_window.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group3/tests/maintenance_window/update_maintenance_window.ts @@ -132,6 +132,7 @@ export default function updateMaintenanceWindowTests({ getService }: FtrProvider title: 'test-maintenance-window-new', duration: 60 * 1000, r_rule: newRRule, + category_ids: ['management'], enabled: false, }) .expect(200); @@ -140,6 +141,7 @@ export default function updateMaintenanceWindowTests({ getService }: FtrProvider expect(updatedMW.duration).eql(60 * 1000); expect(updatedMW.r_rule).eql(newRRule); expect(updatedMW.title).eql('test-maintenance-window-new'); + expect(updatedMW.category_ids).eql(['management']); }); it('should update RRule correctly when removing fields', async () => { @@ -153,6 +155,7 @@ export default function updateMaintenanceWindowTests({ getService }: FtrProvider count: 1, until: moment.utc().add(1, 'week').toISOString(), }, + category_ids: ['management'], }) .expect(200); @@ -179,6 +182,7 @@ export default function updateMaintenanceWindowTests({ getService }: FtrProvider .send({ ...createParams, r_rule: updatedRRule, + category_ids: null, }) .expect(200); @@ -189,6 +193,42 @@ export default function updateMaintenanceWindowTests({ getService }: FtrProvider expect(response.body.data[0].id).to.eql(createdMaintenanceWindow.id); expect(response.body.data[0].r_rule).to.eql(updatedRRule); + expect(response.body.data[0].category_ids).to.eql(null); + }); + + it('should throw if updating maintenance window with invalid category ids', async () => { + const { body: createdMaintenanceWindow } = await supertest + .post(`${getUrlPrefix('space1')}/internal/alerting/rules/maintenance_window`) + .set('kbn-xsrf', 'foo') + .send({ + title: 'test-maintenance-window', + duration: 60 * 60 * 1000, // 1 hr + r_rule: { + dtstart: new Date().toISOString(), + tzid: 'UTC', + freq: 2, // weekly + count: 1, + }, + }) + .expect(200); + + objectRemover.add( + 'space1', + createdMaintenanceWindow.id, + 'rules/maintenance_window', + 'alerting', + true + ); + + await supertest + .post( + `${getUrlPrefix('space1')}/internal/alerting/rules/maintenance_window/${ + createdMaintenanceWindow.id + }` + ) + .set('kbn-xsrf', 'foo') + .send({ category_ids: ['something-else'] }) + .expect(400); }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/rule_types.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/rule_types.ts index 8f9b7566b7640..e722d058e8b7c 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/rule_types.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group1/rule_types.ts @@ -41,6 +41,7 @@ export default function listRuleTypes({ getService }: FtrProviderContext) { id: 'recovered', name: 'Recovered', }, + category: 'kibana', producer: 'alertsFixture', minimum_license_required: 'basic', is_exportable: true, @@ -130,6 +131,7 @@ export default function listRuleTypes({ getService }: FtrProviderContext) { id: 'recovered', name: 'Recovered', }, + category: 'kibana', producer: 'alertsFixture', minimumLicenseRequired: 'basic', isExportable: true, diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/maintenance_window_flows.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/maintenance_window_flows.ts index fd7f695c9fb62..ac92bb080f550 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/maintenance_window_flows.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/group3/maintenance_window_flows.ts @@ -157,6 +157,53 @@ export default function maintenanceWindowFlowsTests({ getService }: FtrProviderC }); }); + it('should stop alert firing actions if category ID does not match the rule type', async () => { + const pattern = { + instance: [true, true, false, true], + }; + + // Create active maintenance window + const maintenanceWindow = await createMaintenanceWindow({ + category_ids: ['observability'], + }); + const activeMaintenanceWindows = await getActiveMaintenanceWindows(); + expect(activeMaintenanceWindows[0].id).eql(maintenanceWindow.id); + expect(activeMaintenanceWindows[0].category_ids).eql(['observability']); + + // Create action and rule + const action = await createAction(); + const rule = await createRule({ actionId: action.id, pattern }); + + // Run 4 times - firing each time + await getRuleEvents({ + id: rule.id, + action: 1, + activeInstance: 1, + }); + await runSoon(rule.id); + await getRuleEvents({ + id: rule.id, + action: 2, + activeInstance: 2, + }); + + await runSoon(rule.id); + await getRuleEvents({ + id: rule.id, + action: 3, + activeInstance: 2, + recoveredInstance: 1, + }); + + await runSoon(rule.id); + await getRuleEvents({ + id: rule.id, + action: 4, + activeInstance: 3, + recoveredInstance: 1, + }); + }); + // Helper functions: async function createRule({ actionId, @@ -214,7 +261,7 @@ export default function maintenanceWindowFlowsTests({ getService }: FtrProviderC return createdAction; } - async function createMaintenanceWindow() { + async function createMaintenanceWindow(overwrites?: any) { const { body: window } = await supertest .post(`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/maintenance_window`) .set('kbn-xsrf', 'foo') @@ -227,6 +274,7 @@ export default function maintenanceWindowFlowsTests({ getService }: FtrProviderC freq: 0, // yearly count: 1, }, + ...overwrites, }) .expect(200); diff --git a/x-pack/test/apm_api_integration/tests/event_metadata/event_metadata.spec.ts b/x-pack/test/apm_api_integration/tests/event_metadata/event_metadata.spec.ts index b390df4c98eae..f89447f5f2b92 100644 --- a/x-pack/test/apm_api_integration/tests/event_metadata/event_metadata.spec.ts +++ b/x-pack/test/apm_api_integration/tests/event_metadata/event_metadata.spec.ts @@ -7,6 +7,9 @@ import expect from '@kbn/expect'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; import { PROCESSOR_EVENT } from '@kbn/apm-plugin/common/es_fields/apm'; +import { SpanRaw } from '@kbn/apm-plugin/typings/es_schemas/raw/span_raw'; +import { ErrorRaw } from '@kbn/apm-plugin/typings/es_schemas/raw/error_raw'; +import { TransactionRaw } from '@kbn/apm-plugin/typings/es_schemas/raw/transaction_raw'; import { FtrProviderContext } from '../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { @@ -14,10 +17,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { const apmApiClient = getService('apmApiClient'); const es = getService('es'); - async function getLastDocId(processorEvent: ProcessorEvent) { - const response = await es.search<{ - [key: string]: { id: string }; - }>({ + async function getMostRecentDoc(processorEvent: ProcessorEvent) { + const response = await es.search({ index: ['apm-*'], body: { query: { @@ -32,12 +33,18 @@ export default function ApiTest({ getService }: FtrProviderContext) { }, }); - return response.hits.hits[0]._source![processorEvent].id; + const doc = response.hits.hits[0]._source!; + + return { + // @ts-expect-error + id: doc[processorEvent].id as string, + timestamp: doc['@timestamp'], + }; } registry.when('Event metadata', { config: 'basic', archives: ['apm_8.0.0'] }, () => { it('fetches transaction event metadata', async () => { - const id = await getLastDocId(ProcessorEvent.transaction); + const { id, timestamp } = await getMostRecentDoc(ProcessorEvent.transaction); const { body } = await apmApiClient.readUser({ endpoint: 'GET /internal/apm/event_metadata/{processorEvent}/{id}', @@ -46,6 +53,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { processorEvent: ProcessorEvent.transaction, id, }, + query: { + start: timestamp, + end: timestamp, + }, }, }); @@ -67,7 +78,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('fetches error event metadata', async () => { - const id = await getLastDocId(ProcessorEvent.error); + const { id, timestamp } = await getMostRecentDoc(ProcessorEvent.error); const { body } = await apmApiClient.readUser({ endpoint: 'GET /internal/apm/event_metadata/{processorEvent}/{id}', @@ -76,6 +87,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { processorEvent: ProcessorEvent.error, id, }, + query: { + start: timestamp, + end: timestamp, + }, }, }); @@ -97,7 +112,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('fetches span event metadata', async () => { - const id = await getLastDocId(ProcessorEvent.span); + const { id, timestamp } = await getMostRecentDoc(ProcessorEvent.span); const { body } = await apmApiClient.readUser({ endpoint: 'GET /internal/apm/event_metadata/{processorEvent}/{id}', @@ -106,6 +121,10 @@ export default function ApiTest({ getService }: FtrProviderContext) { processorEvent: ProcessorEvent.span, id, }, + query: { + start: timestamp, + end: timestamp, + }, }, }); diff --git a/x-pack/test/functional_execution_context/plugins/alerts/server/plugin.ts b/x-pack/test/functional_execution_context/plugins/alerts/server/plugin.ts index 436452c1fdbaf..b05a650d21b20 100644 --- a/x-pack/test/functional_execution_context/plugins/alerts/server/plugin.ts +++ b/x-pack/test/functional_execution_context/plugins/alerts/server/plugin.ts @@ -73,6 +73,7 @@ export class FixturePlugin implements Plugin = { async executor() { return { state: {} }; }, + category: 'kibana', producer: 'alerts', validate: { params: { validate: (params) => params }, @@ -57,6 +58,7 @@ export const alwaysFiringAlertType: RuleType< { id: 'other', name: 'Other' }, ], defaultActionGroupId: 'default', + category: 'kibana', producer: 'alerts', minimumLicenseRequired: 'basic', isExportable: true, @@ -91,6 +93,7 @@ export const failingAlertType: RuleType { cleanKibana(); + }); + + beforeEach(() => { login(); const body: AsApiContract = { title: 'My maintenance window', duration: 60000, // 1 minute + category_ids: ['securitySolution'], r_rule: { dtstart: new Date().toISOString(), tzid: 'Europe/Amsterdam', @@ -46,13 +50,20 @@ describe( }); }); - after(() => { + afterEach(() => { // Delete a test maintenance window - cy.request({ - method: 'DELETE', - url: `${INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH}/${maintenanceWindowId}`, - headers: { 'kbn-xsrf': 'cypress-creds', 'x-elastic-internal-origin': 'security-solution' }, - }); + if (maintenanceWindowId) { + cy.request({ + method: 'DELETE', + url: `${INTERNAL_ALERTING_API_MAINTENANCE_WINDOW_PATH}/${maintenanceWindowId}`, + headers: { + 'kbn-xsrf': 'cypress-creds', + 'x-elastic-internal-origin': 'security-solution', + }, + }).then(() => { + maintenanceWindowId = ''; + }); + } }); it('Displays the callout when there are running maintenance windows', () => {