mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[RAM][Maintenance Window] Add maintenance window solution selection. (#166781)
## Summary Resolves: https://github.com/elastic/kibana/issues/166301 Adds support for solution/category filtering to maintenance windows by adding a new property: `category_ids`. Selecting one or more solutions when creating/updating a maintenance window will cause the maintenance window to only suppress rule types belonging to said solutions. In order to achieve filtering by solution/category, we are adding a new field to the rule types schema called `category`. This field should map to the feature category that the rule type belongs to (`observability`, `securitySolution` or `management`). Our initial plan was to use feature IDs or rule type IDs to accomplish this filtering, we decided against using rule type IDs because if a new rule type gets added, we need to change the API to support this new rule type. We decided against feature IDs because it's a very anti-serverless way of accomplishing this feature, as we don't want to expose feature IDs to APIs. We decided on app categories because it works well with serverless and should be much easier to maintain if new rule types are added in the future. This means the `rule_types` API has to be changed to include this new field, although it shouldn't be a breaking change since we're just adding a new field. No migrations are needed since rule types are in memory and maintenance windows are backwards compatible.  ### Error state:  ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Dima Arnautov <arnautov.dima@gmail.com>
This commit is contained in:
parent
566e086963
commit
092cc0d098
145 changed files with 1604 additions and 94 deletions
|
@ -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>): 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<MaintenanceWindow[]> => {
|
||||
const result = await http.fetch<Array<AsApiContract<MaintenanceWindow>>>(
|
||||
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`;
|
||||
|
|
|
@ -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(
|
||||
<MaintenanceWindowCallout
|
||||
kibanaServices={kibanaServicesMock}
|
||||
categories={['securitySolution']}
|
||||
/>,
|
||||
{
|
||||
wrapper: TestProviders,
|
||||
}
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('maintenanceWindowCallout')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
fetchActiveMaintenanceWindowsMock.mockResolvedValue([
|
||||
{
|
||||
...RUNNING_MAINTENANCE_WINDOW_1,
|
||||
categoryIds: ['securitySolution'],
|
||||
},
|
||||
]);
|
||||
|
||||
render(
|
||||
<MaintenanceWindowCallout
|
||||
kibanaServices={kibanaServicesMock}
|
||||
categories={['securitySolution']}
|
||||
/>,
|
||||
{
|
||||
wrapper: TestProviders,
|
||||
}
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('maintenanceWindowCallout')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
render(<MaintenanceWindowCallout kibanaServices={kibanaServicesMock} />, {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('maintenanceWindowCallout')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 (
|
||||
<EuiCallOut
|
||||
title={MAINTENANCE_WINDOW_RUNNING}
|
||||
color="warning"
|
||||
iconType="iInCircle"
|
||||
data-test-subj="maintenanceWindowCallout"
|
||||
>
|
||||
{MAINTENANCE_WINDOW_RUNNING_DESCRIPTION}
|
||||
</EuiCallOut>
|
||||
);
|
||||
if (!shouldShowMaintenanceWindowCallout) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
return (
|
||||
<EuiCallOut
|
||||
title={MAINTENANCE_WINDOW_RUNNING}
|
||||
color="warning"
|
||||
iconType="iInCircle"
|
||||
data-test-subj="maintenanceWindowCallout"
|
||||
>
|
||||
{MAINTENANCE_WINDOW_RUNNING_DESCRIPTION}
|
||||
</EuiCallOut>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ export interface MaintenanceWindowSOProperties {
|
|||
expirationDate: string;
|
||||
events: DateRange[];
|
||||
rRule: RRuleParams;
|
||||
categoryIds?: string[];
|
||||
}
|
||||
|
||||
export type MaintenanceWindowSOAttributes = MaintenanceWindowSOProperties &
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
"@kbn/core",
|
||||
"@kbn/i18n-react",
|
||||
"@kbn/alerting-plugin",
|
||||
"@kbn/rrule"
|
||||
"@kbn/rrule",
|
||||
"@kbn/actions-plugin"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -78,6 +78,7 @@ export const alertType: RuleType<
|
|||
},
|
||||
};
|
||||
},
|
||||
category: 'kibana',
|
||||
producer: ALERTING_EXAMPLE_APP_ID,
|
||||
validate: {
|
||||
params: schema.object({
|
||||
|
|
|
@ -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}`;
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -33,6 +33,7 @@ export interface MaintenanceWindowSOProperties {
|
|||
expirationDate: string;
|
||||
events: DateRange[];
|
||||
rRule: RRuleParams;
|
||||
categoryIds?: string[] | null;
|
||||
}
|
||||
|
||||
export type MaintenanceWindowSOAttributes = MaintenanceWindowSOProperties &
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -5,4 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './v1';
|
||||
export type { MaintenanceWindowStatus } from './v1';
|
||||
|
||||
export { maintenanceWindowStatus } from './v1';
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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';
|
|
@ -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];
|
|
@ -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';
|
|
@ -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';
|
|
@ -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),
|
||||
])
|
||||
)
|
||||
)
|
||||
);
|
|
@ -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';
|
|
@ -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<typeof maintenanceWindowCategoryIdsSchemaV1>;
|
|
@ -31,6 +31,7 @@ export interface RuleType<
|
|||
params: ActionVariable[];
|
||||
};
|
||||
defaultActionGroupId: ActionGroupIds;
|
||||
category: string;
|
||||
producer: string;
|
||||
minimumLicenseRequired: LicenseType;
|
||||
isExportable: boolean;
|
||||
|
|
|
@ -22,6 +22,7 @@ const mockRuleType = (id: string): RuleType => ({
|
|||
params: [],
|
||||
},
|
||||
defaultActionGroupId: 'default',
|
||||
category: 'test',
|
||||
producer: 'alerts',
|
||||
minimumLicenseRequired: 'basic',
|
||||
isExportable: true,
|
||||
|
|
41
x-pack/plugins/alerting/public/hooks/use_get_rule_types.ts
Normal file
41
x-pack/plugins/alerting/public/hooks/use_get_rule_types.ts
Normal file
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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(<CreateMaintenanceWindowForm {...formProps} />);
|
||||
|
||||
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(<CreateMaintenanceWindowForm {...formProps} />);
|
||||
|
||||
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,56 @@ 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(
|
||||
<CreateMaintenanceWindowForm
|
||||
{...formProps}
|
||||
initialValue={{
|
||||
title: 'test',
|
||||
startDate: '2023-03-24',
|
||||
endDate: '2023-03-26',
|
||||
timezone: ['America/Los_Angeles'],
|
||||
recurring: true,
|
||||
categoryIds: [],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const titleInput = within(result.getByTestId('title-field')).getByTestId('input');
|
||||
const dateInputs = within(result.getByTestId('date-field')).getAllByLabelText(
|
||||
// using the aria-label to query for the date-picker input
|
||||
'Press the down key to open a popover containing a calendar.'
|
||||
);
|
||||
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(
|
||||
<CreateMaintenanceWindowForm
|
||||
{...formProps}
|
||||
|
@ -85,18 +168,61 @@ describe('CreateMaintenanceWindowForm', () => {
|
|||
/>
|
||||
);
|
||||
|
||||
const titleInput = within(result.getByTestId('title-field')).getByTestId('input');
|
||||
const dateInputs = within(result.getByTestId('date-field')).getAllByLabelText(
|
||||
// using the aria-label to query for the date-picker input
|
||||
'Press the down key to open a popover containing a calendar.'
|
||||
);
|
||||
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();
|
||||
});
|
||||
|
||||
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');
|
||||
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(<CreateMaintenanceWindowForm {...formProps} />);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<CreateMaintenanceWindowFor
|
|||
const { defaultTimezone, isBrowser } = useDefaultTimezone();
|
||||
|
||||
const isEditMode = initialValue !== undefined && maintenanceWindowId !== undefined;
|
||||
|
||||
const hasSetInitialCategories = useRef<boolean>(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<CreateMaintenanceWindowFor
|
|||
formData.timezone ? formData.timezone[0] : defaultTimezone,
|
||||
formData.recurringSchedule
|
||||
),
|
||||
categoryIds: formData.categoryIds,
|
||||
};
|
||||
if (isEditMode) {
|
||||
updateMaintenanceWindow({ maintenanceWindowId, maintenanceWindow }, { onSuccess });
|
||||
|
@ -108,9 +120,9 @@ export const CreateMaintenanceWindowForm = React.memo<CreateMaintenanceWindowFor
|
|||
onSubmit: submitMaintenanceWindow,
|
||||
});
|
||||
|
||||
const [{ recurring, timezone }] = useFormData<FormProps>({
|
||||
const [{ recurring, timezone, categoryIds }] = useFormData<FormProps>({
|
||||
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<CreateMaintenanceWindowFor
|
|||
const closeModal = useCallback(() => 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<CreateMaintenanceWindowFor
|
|||
return m;
|
||||
}, [closeModal, archiveMaintenanceWindow, isModalVisible, maintenanceWindowId, onSuccess]);
|
||||
|
||||
const availableCategories = useMemo(() => {
|
||||
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 (
|
||||
<Form form={form}>
|
||||
<EuiFlexGroup direction="column" responsive={false}>
|
||||
|
@ -158,6 +235,17 @@ export const CreateMaintenanceWindowForm = React.memo<CreateMaintenanceWindowFor
|
|||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiSpacer size="xs" />
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s">
|
||||
<h4>{i18n.CREATE_FORM_TIMEFRAME_TITLE}</h4>
|
||||
<p>
|
||||
<EuiTextColor color="subdued">
|
||||
{i18n.CREATE_FORM_TIMEFRAME_DESCRIPTION}
|
||||
</EuiTextColor>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup alignItems="flexEnd" responsive={false}>
|
||||
<EuiFlexItem grow={3}>
|
||||
|
@ -229,20 +317,39 @@ export const CreateMaintenanceWindowForm = React.memo<CreateMaintenanceWindowFor
|
|||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{isRecurring && (
|
||||
<EuiFlexItem>
|
||||
<RecurringSchedule data-test-subj="recurring-form" />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem>
|
||||
{isRecurring ? <RecurringSchedule data-test-subj="recurring-form" /> : null}
|
||||
<EuiHorizontalRule margin="xl" />
|
||||
<UseField path="categoryIds">
|
||||
{(field) => (
|
||||
<MaintenanceWindowCategorySelection
|
||||
selectedCategories={categoryIds || []}
|
||||
availableCategories={availableCategories}
|
||||
isLoading={isLoadingRuleTypes}
|
||||
errors={field.errors.map((error) => error.message)}
|
||||
onChange={onCategoryIdsChange}
|
||||
/>
|
||||
)}
|
||||
</UseField>
|
||||
<EuiHorizontalRule margin="xl" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
{isEditMode ? (
|
||||
<EuiCallOut title={i18n.ARCHIVE_TITLE} color="danger" iconType="trash">
|
||||
<p>{i18n.ARCHIVE_SUBTITLE}</p>
|
||||
<EuiButton fill color="danger" onClick={showModal}>
|
||||
{i18n.ARCHIVE}
|
||||
</EuiButton>
|
||||
{modal}
|
||||
</EuiCallOut>
|
||||
) : null}
|
||||
<EuiHorizontalRule margin="xl" />
|
||||
{isEditMode && (
|
||||
<>
|
||||
<EuiCallOut title={i18n.ARCHIVE_TITLE} color="danger" iconType="trash">
|
||||
<p>{i18n.ARCHIVE_SUBTITLE}</p>
|
||||
<EuiButton fill color="danger" onClick={showModal}>
|
||||
{i18n.ARCHIVE}
|
||||
</EuiButton>
|
||||
{modal}
|
||||
</EuiCallOut>
|
||||
<EuiHorizontalRule margin="xl" />
|
||||
</>
|
||||
)}
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
justifyContent="flexEnd"
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { screen, fireEvent } from '@testing-library/react';
|
||||
import { MaintenanceWindowCategorySelection } from './maintenance_window_category_selection';
|
||||
import { AppMockRenderer, createAppMockRenderer } from '../../../lib/test_utils';
|
||||
|
||||
const mockOnChange = jest.fn();
|
||||
|
||||
describe('maintenanceWindowCategorySelection', () => {
|
||||
let appMockRenderer: AppMockRenderer;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
appMockRenderer = createAppMockRenderer();
|
||||
});
|
||||
|
||||
it('renders correctly', async () => {
|
||||
appMockRenderer.render(
|
||||
<MaintenanceWindowCategorySelection
|
||||
selectedCategories={[]}
|
||||
availableCategories={['observability', 'management', 'securitySolution']}
|
||||
onChange={mockOnChange}
|
||||
/>
|
||||
);
|
||||
|
||||
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(
|
||||
<MaintenanceWindowCategorySelection
|
||||
selectedCategories={[]}
|
||||
availableCategories={[]}
|
||||
onChange={mockOnChange}
|
||||
/>
|
||||
);
|
||||
|
||||
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(
|
||||
<MaintenanceWindowCategorySelection
|
||||
selectedCategories={['securitySolution', 'management']}
|
||||
availableCategories={['observability', 'management', 'securitySolution']}
|
||||
onChange={mockOnChange}
|
||||
/>
|
||||
);
|
||||
|
||||
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(
|
||||
<MaintenanceWindowCategorySelection
|
||||
selectedCategories={['observability']}
|
||||
availableCategories={['observability', 'management', 'securitySolution']}
|
||||
onChange={mockOnChange}
|
||||
/>
|
||||
);
|
||||
|
||||
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(
|
||||
<MaintenanceWindowCategorySelection
|
||||
isLoading
|
||||
selectedCategories={[]}
|
||||
availableCategories={['observability', 'management', 'securitySolution']}
|
||||
onChange={mockOnChange}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId('maintenanceWindowCategorySelectionLoading')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display error message if it exists', () => {
|
||||
appMockRenderer.render(
|
||||
<MaintenanceWindowCategorySelection
|
||||
selectedCategories={[]}
|
||||
availableCategories={['observability', 'management', 'securitySolution']}
|
||||
errors={['test error']}
|
||||
onChange={mockOnChange}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('test error')).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -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<Record<string, boolean>>((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 (
|
||||
<EuiFlexGroup
|
||||
justifyContent="spaceAround"
|
||||
data-test-subj="maintenanceWindowCategorySelectionLoading"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner size="l" />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup data-test-subj="maintenanceWindowCategorySelection">
|
||||
<EuiFlexItem>
|
||||
<EuiText size="s">
|
||||
<h4>{i18n.CREATE_FORM_CATEGORY_SELECTION_TITLE}</h4>
|
||||
<p>
|
||||
<EuiTextColor color="subdued">
|
||||
{i18n.CREATE_FORM_CATEGORY_SELECTION_DESCRIPTION}
|
||||
</EuiTextColor>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
label={i18n.CREATE_FORM_CATEGORIES_SELECTION_CHECKBOX_GROUP_TITLE}
|
||||
isInvalid={!!errors.length}
|
||||
error={errors[0]}
|
||||
>
|
||||
<EuiCheckboxGroup options={options} idToSelectedMap={selectedMap} onChange={onChange} />
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
};
|
|
@ -23,6 +23,7 @@ const initialValue: FormProps = {
|
|||
frequency: 'CUSTOM',
|
||||
ends: EndsOptions.NEVER,
|
||||
},
|
||||
categoryIds: [],
|
||||
};
|
||||
|
||||
describe('CustomRecurringSchedule', () => {
|
||||
|
|
|
@ -19,6 +19,7 @@ const initialValue: FormProps = {
|
|||
startDate: '2023-03-24',
|
||||
endDate: '2023-03-26',
|
||||
recurring: true,
|
||||
categoryIds: [],
|
||||
};
|
||||
|
||||
describe('RecurringSchedule', () => {
|
||||
|
|
|
@ -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<FormProps> = {
|
|||
},
|
||||
],
|
||||
},
|
||||
categoryIds: {
|
||||
validations: [
|
||||
{
|
||||
validator: emptyField(i18n.CREATE_FORM_CATEGORY_IDS_REQUIRED),
|
||||
},
|
||||
],
|
||||
},
|
||||
startDate: {},
|
||||
endDate: {},
|
||||
timezone: {},
|
||||
|
|
|
@ -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: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -27,6 +27,7 @@ export const convertFromMaintenanceWindowToForm = (
|
|||
endDate: endDate.toISOString(),
|
||||
timezone: [maintenanceWindow.rRule.tzid],
|
||||
recurring,
|
||||
categoryIds: maintenanceWindow.categoryIds || [],
|
||||
};
|
||||
if (!recurring) return form;
|
||||
|
||||
|
|
|
@ -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}',
|
||||
|
|
|
@ -18,7 +18,10 @@ export const RRuleFrequencyMap = {
|
|||
'3': Frequency.DAILY,
|
||||
};
|
||||
|
||||
export type MaintenanceWindow = Pick<MaintenanceWindowServerSide, 'title' | 'duration' | 'rRule'>;
|
||||
export type MaintenanceWindow = Pick<
|
||||
MaintenanceWindowServerSide,
|
||||
'title' | 'duration' | 'rRule' | 'categoryIds'
|
||||
>;
|
||||
|
||||
export type MaintenanceWindowFindResponse = MaintenanceWindowServerSide &
|
||||
MaintenanceWindowModificationMetadata & {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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<MaintenanceWindow> = ({ r_rule: rRule, ...rest }) => ({
|
||||
const rewriteBodyRes: RewriteRequestCase<MaintenanceWindow> = ({
|
||||
r_rule: rRule,
|
||||
category_ids: categoryIds,
|
||||
...rest
|
||||
}) => ({
|
||||
...rest,
|
||||
rRule,
|
||||
categoryIds,
|
||||
});
|
||||
|
||||
export async function archiveMaintenanceWindow({
|
||||
|
|
|
@ -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<MaintenanceWindow> = ({ rRule, ...res }) => ({
|
||||
const rewriteBodyRequest: RewriteResponseCase<MaintenanceWindow> = ({
|
||||
rRule,
|
||||
categoryIds,
|
||||
...res
|
||||
}) => ({
|
||||
...res,
|
||||
r_rule: rRule,
|
||||
category_ids: categoryIds,
|
||||
});
|
||||
|
||||
const rewriteBodyRes: RewriteRequestCase<MaintenanceWindow> = ({ r_rule: rRule, ...rest }) => ({
|
||||
const rewriteBodyRes: RewriteRequestCase<MaintenanceWindow> = ({
|
||||
r_rule: rRule,
|
||||
category_ids: categoryIds,
|
||||
...rest
|
||||
}) => ({
|
||||
...rest,
|
||||
rRule,
|
||||
categoryIds,
|
||||
});
|
||||
|
||||
export async function createMaintenanceWindow({
|
||||
|
|
|
@ -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<MaintenanceWindow> = ({ r_rule: rRule, ...rest }) => ({
|
||||
const rewriteBodyRes: RewriteRequestCase<MaintenanceWindow> = ({
|
||||
r_rule: rRule,
|
||||
category_ids: categoryIds,
|
||||
...rest
|
||||
}) => ({
|
||||
...rest,
|
||||
rRule,
|
||||
categoryIds,
|
||||
});
|
||||
|
||||
export async function finishMaintenanceWindow({
|
||||
|
|
|
@ -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<MaintenanceWindow> = ({ r_rule: rRule, ...rest }) => ({
|
||||
const rewriteBodyRes: RewriteRequestCase<MaintenanceWindow> = ({
|
||||
r_rule: rRule,
|
||||
category_ids: categoryIds,
|
||||
...rest
|
||||
}) => ({
|
||||
...rest,
|
||||
categoryIds,
|
||||
rRule,
|
||||
});
|
||||
|
||||
|
|
|
@ -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<MaintenanceWindow> = ({ rRule, ...res }) => ({
|
||||
const rewriteBodyRequest: RewriteResponseCase<MaintenanceWindow> = ({
|
||||
rRule,
|
||||
categoryIds,
|
||||
...res
|
||||
}) => ({
|
||||
...res,
|
||||
r_rule: rRule,
|
||||
category_ids: categoryIds,
|
||||
});
|
||||
|
||||
const rewriteBodyRes: RewriteRequestCase<MaintenanceWindow> = ({ r_rule: rRule, ...rest }) => ({
|
||||
const rewriteBodyRes: RewriteRequestCase<MaintenanceWindow> = ({
|
||||
r_rule: rRule,
|
||||
category_ids: categoryIds,
|
||||
...rest
|
||||
}) => ({
|
||||
...rest,
|
||||
rRule,
|
||||
categoryIds,
|
||||
});
|
||||
|
||||
export async function updateMaintenanceWindow({
|
||||
|
|
|
@ -48,6 +48,7 @@ const ruleType: jest.Mocked<UntypedNormalizedRuleType> = {
|
|||
isExportable: true,
|
||||
recoveryActionGroup: RecoveredActionGroup,
|
||||
executor: jest.fn(),
|
||||
category: 'test',
|
||||
producer: 'alerts',
|
||||
cancelAlertsOnRuleTimeout: true,
|
||||
ruleTaskTimeout: '5m',
|
||||
|
|
|
@ -93,6 +93,7 @@ const ruleType: jest.Mocked<UntypedNormalizedRuleType> = {
|
|||
isExportable: true,
|
||||
recoveryActionGroup: RecoveredActionGroup,
|
||||
executor: jest.fn(),
|
||||
category: 'test',
|
||||
producer: 'alerts',
|
||||
cancelAlertsOnRuleTimeout: true,
|
||||
ruleTaskTimeout: '5m',
|
||||
|
|
|
@ -17,6 +17,7 @@ const ruleType: jest.Mocked<UntypedNormalizedRuleType> = {
|
|||
isExportable: true,
|
||||
recoveryActionGroup: RecoveredActionGroup,
|
||||
executor: jest.fn(),
|
||||
category: 'test',
|
||||
producer: 'alerts',
|
||||
cancelAlertsOnRuleTimeout: true,
|
||||
ruleTaskTimeout: '5m',
|
||||
|
|
|
@ -196,6 +196,7 @@ const ruleType: jest.Mocked<UntypedNormalizedRuleType> = {
|
|||
isExportable: true,
|
||||
recoveryActionGroup: RecoveredActionGroup,
|
||||
executor: jest.fn(),
|
||||
category: 'test',
|
||||
producer: 'alerts',
|
||||
cancelAlertsOnRuleTimeout: true,
|
||||
ruleTaskTimeout: '5m',
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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]"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -25,7 +25,7 @@ export async function createMaintenanceWindow(
|
|||
): Promise<MaintenanceWindow> {
|
||||
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,
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -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]"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -45,7 +45,7 @@ async function updateWithOCC(
|
|||
): Promise<MaintenanceWindow> {
|
||||
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,
|
||||
|
|
|
@ -8,4 +8,5 @@
|
|||
export {
|
||||
maintenanceWindowEventSchema,
|
||||
maintenanceWindowSchema,
|
||||
maintenanceWindowCategoryIdsSchema,
|
||||
} from './maintenance_window_schemas';
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -39,5 +39,6 @@ export const transformMaintenanceWindowAttributesToMaintenanceWindow = (
|
|||
eventStartTime,
|
||||
eventEndTime,
|
||||
status,
|
||||
...(attributes.categoryIds !== undefined ? { categoryIds: attributes.categoryIds } : {}),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -22,5 +22,8 @@ export const transformMaintenanceWindowToMaintenanceWindowAttributes = (
|
|||
updatedBy: maintenanceWindow.updatedBy,
|
||||
createdAt: maintenanceWindow.createdAt,
|
||||
updatedAt: maintenanceWindow.updatedAt,
|
||||
...(maintenanceWindow.categoryIds !== undefined
|
||||
? { categoryIds: maintenanceWindow.categoryIds }
|
||||
: {}),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -156,6 +156,7 @@ describe('bulkDelete', () => {
|
|||
async executor() {
|
||||
return { state: {} };
|
||||
},
|
||||
category: 'test',
|
||||
producer: 'alerts',
|
||||
validate: {
|
||||
params: schema.any(),
|
||||
|
|
|
@ -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: [],
|
||||
});
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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];
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@ const ruleType: jest.Mocked<UntypedNormalizedRuleType> = {
|
|||
isExportable: true,
|
||||
recoveryActionGroup: RecoveredActionGroup,
|
||||
executor: jest.fn(),
|
||||
category: 'test',
|
||||
producer: 'alerts',
|
||||
ruleTaskTimeout: '1m',
|
||||
validate: {
|
||||
|
|
|
@ -22,6 +22,7 @@ describe('createAlertEventLogRecordObject', () => {
|
|||
isExportable: true,
|
||||
recoveryActionGroup: RecoveredActionGroup,
|
||||
executor: jest.fn(),
|
||||
category: 'test',
|
||||
producer: 'alerts',
|
||||
validate: {
|
||||
params: schema.any(),
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -46,6 +46,7 @@ const sampleRuleType: RuleType<never, never, {}, never, never, 'default', 'recov
|
|||
isExportable: true,
|
||||
actionGroups: [],
|
||||
defaultActionGroupId: 'default',
|
||||
category: 'test',
|
||||
producer: 'test',
|
||||
async executor() {
|
||||
return { state: {} };
|
||||
|
|
|
@ -45,6 +45,7 @@ const ruleTypes = [
|
|||
context: [],
|
||||
state: [],
|
||||
},
|
||||
category: 'test',
|
||||
producer: 'test',
|
||||
enabledInLicense: true,
|
||||
minimumScheduleInterval: '1m',
|
||||
|
|
|
@ -50,6 +50,7 @@ const ruleTypes = [
|
|||
context: [],
|
||||
state: [],
|
||||
},
|
||||
category: 'test',
|
||||
producer: 'test',
|
||||
enabledInLicense: true,
|
||||
defaultScheduleInterval: '10m',
|
||||
|
|
|
@ -60,6 +60,7 @@ describe('listAlertTypesRoute', () => {
|
|||
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,
|
||||
|
|
|
@ -37,6 +37,7 @@ const createParams = {
|
|||
title: 'test-title',
|
||||
duration: 1000,
|
||||
r_rule: mockMaintenanceWindow.rRule,
|
||||
category_ids: ['observability'],
|
||||
} as CreateMaintenanceWindowRequestBody;
|
||||
|
||||
describe('createMaintenanceWindowRoute', () => {
|
||||
|
|
|
@ -15,5 +15,6 @@ export const transformCreateBody = (
|
|||
title: createBody.title,
|
||||
duration: createBody.duration,
|
||||
rRule: createBody.r_rule,
|
||||
categoryIds: createBody.category_ids,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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 } : {}),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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 }
|
||||
: {}),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<ActionGroupIds extends string>(
|
|||
async executor() {
|
||||
return { state: {} };
|
||||
},
|
||||
category: 'test',
|
||||
producer: 'alerts',
|
||||
validate: {
|
||||
params: { validate: (params) => params },
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -40,6 +40,7 @@ const ruleType: jest.Mocked<UntypedNormalizedRuleType> = {
|
|||
isExportable: true,
|
||||
recoveryActionGroup: RecoveredActionGroup,
|
||||
executor: jest.fn(),
|
||||
category: 'test',
|
||||
producer: 'alerts',
|
||||
cancelAlertsOnRuleTimeout: true,
|
||||
ruleTaskTimeout: '5m',
|
||||
|
|
|
@ -22,6 +22,7 @@ describe('validateActions', () => {
|
|||
isExportable: true,
|
||||
recoveryActionGroup: RecoveredActionGroup,
|
||||
executor: jest.fn(),
|
||||
category: 'test',
|
||||
producer: 'alerts',
|
||||
cancelAlertsOnRuleTimeout: true,
|
||||
ruleTaskTimeout: '5m',
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -68,6 +68,7 @@ const listedTypes = new Set<RegistryRuleType>([
|
|||
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 },
|
||||
|
|
|
@ -118,6 +118,7 @@ export function getBeforeSetup(
|
|||
async executor() {
|
||||
return { state: {} };
|
||||
},
|
||||
category: 'test',
|
||||
producer: 'alerts',
|
||||
validate: {
|
||||
params: { validate: (params) => params },
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -72,6 +72,7 @@ const ruleType: NormalizedRuleType<
|
|||
name: 'Recovered',
|
||||
},
|
||||
executor: jest.fn(),
|
||||
category: 'test',
|
||||
producer: 'alerts',
|
||||
validate: {
|
||||
params: schema.any(),
|
||||
|
|
|
@ -141,6 +141,7 @@ export const ruleType: jest.Mocked<UntypedNormalizedRuleType> = {
|
|||
isExportable: true,
|
||||
recoveryActionGroup: RecoveredActionGroup,
|
||||
executor: jest.fn(),
|
||||
category: 'test',
|
||||
producer: 'alerts',
|
||||
cancelAlertsOnRuleTimeout: true,
|
||||
ruleTaskTimeout: '5m',
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -59,6 +59,7 @@ const ruleType: UntypedNormalizedRuleType = {
|
|||
name: 'Recovered',
|
||||
},
|
||||
executor: jest.fn(),
|
||||
category: 'test',
|
||||
producer: 'alerts',
|
||||
validate: {
|
||||
params: schema.any(),
|
||||
|
|
|
@ -288,6 +288,7 @@ export interface RuleType<
|
|||
WithoutReservedActionGroups<ActionGroupIds, RecoveryActionGroupId>,
|
||||
AlertData
|
||||
>;
|
||||
category: string;
|
||||
producer: string;
|
||||
actionVariables?: {
|
||||
context?: ActionVariable[];
|
||||
|
|
|
@ -58,6 +58,7 @@
|
|||
"@kbn/core-http-server-mocks",
|
||||
"@kbn/serverless",
|
||||
"@kbn/core-http-router-server-mocks",
|
||||
"@kbn/core-application-common",
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue